A self-contained, two-pool private Cardano network for end-to-end testing of
Dingo as a block producer alongside the upstream cardano-node. The whole
thing runs locally under Docker Compose: a configurator container generates
fresh genesis files and pool keys on each start, then three nodes come up and
forge blocks against each other while a txpump sidecar continuously
submits payment transactions into Dingo's mempool.
The Go test harness lives alongside this directory: internal/test/devnet/
(helpers and config loader at the top level, runnable scenarios under
internal/test/devnet/scenarios/). The layout mirrors
internal/test/antithesis/. All test files are gated by the devnet build
tag.
| Service | Role | Default host port | Container IP |
|---|---|---|---|
configurator |
One-shot: generates keys + genesis | — | — |
dingo-producer |
Dingo, forging with pool 1 keys | 3010 → 3001 |
172.20.0.10 |
cardano-producer |
cardano-node, forging with pool 2 |
3011 → 3001 |
172.20.0.11 |
cardano-relay |
cardano-node relay (no forging) |
3012 → 3001 |
172.20.0.12 |
txpump |
Submits payment txs into Dingo | — | 172.20.0.20 |
Pool 1 and pool 2 are wired into a ring topology by configurator.sh. The
relay peers with both producers so block diffusion is exercised in addition to
direct producer-to-producer sync.
The txpump container is a load generator: it talks Ouroboros NtC over
Dingo's UNIX socket and submits randomised payment transactions in batches
(10–50 txs per round, 5–15s cooldown), funded from the genesis UTxO keys
generated by the configurator. It exists to keep the mempool exercised while
the consensus tests run, so block bodies are non-empty and tx-submission /
mempool paths are continuously hit. The image is built from
internal/test/antithesis/ (Dockerfile.txpump, cmd/txpump/).
Driven by testnet.yaml. Notable values:
networkMagic: 42epochLength: 500slots,slotLength: 1s(~8 min epochs)activeSlotsCoeff: 0.4,securityParam (k): 40- All hard forks at epoch 0 — the network starts in Conway with protocol version 10.0.
systemStartis set tonow + 30sby the configurator after key generation, so nodes have time to come up before slot 0.
- Docker with the Compose plugin (
docker compose versionmust work). - Go 1.25+ on the host (matching
go.mod) to run the integration tests. - Outbound network access on first run to pull
ghcr.io/blinklabs-io/cardano-node:11.0.1and to clonecardano-foundation/testnet-generation-toolinside the configurator image. - Local build of Dingo via the repo root
Dockerfile— Compose builds it automatically onup/run-tests.sh. - Local build of
txpumpfrom../antithesis/— also built automatically by Compose.
You do not need to make build Dingo locally — Compose builds the
dingo-producer image directly from the repo by referencing the top-level
Dockerfile (build.context: ../../.. in docker-compose.yml). The build
runs make build inside ghcr.io/blinklabs-io/go:1.26.1-1 against your
working tree, so any uncommitted local changes are included.
Force a rebuild after editing Dingo source:
# from this directory (internal/test/devnet)
docker compose -f docker-compose.yml build dingo-producer
# or from the repo root
docker compose -f internal/test/devnet/docker-compose.yml build dingo-producerrun-tests.sh always rebuilds dingo-producer before starting. start.sh
does not — it only runs up -d, so add an explicit build (or pass
--build to up) when you want fresh code in the running container.
The configurator image (Dockerfile.configurator) is built from this
directory and pinned to upstream testnet-generation-tool@v0.1.0; it rarely
needs rebuilding once cached.
start.sh, stop.sh, and run-tests.sh all resolve their own location via
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)", so they work
from any current working directory:
# from this directory
./start.sh
# from the repo root
./internal/test/devnet/start.sh
# from anywhere
/home/me/dingo/internal/test/devnet/run-tests.sh -run TestBasicPlain docker compose invocations are not location-independent — pass
-f <path>/docker-compose.yml if you are not in this directory, since the
compose file's build.context paths (. and ../../..) are resolved
relative to the compose file itself, not your shell's CWD.
The scripts must, however, stay at this path inside the repo: the compose
file references ../../.. to pick up the Dingo source tree for its build
context, and the topology/*.json and testnet.yaml files are mounted by
relative path. Don't copy the directory elsewhere expecting it to work
standalone.
./start.sh # docker compose up -d (configurator runs first, then nodes)
./stop.sh # docker compose down -v (also removes volumes)Tail logs while it runs:
docker compose -f docker-compose.yml logs -f
docker compose -f docker-compose.yml logs -f dingo-producerOpen a shell inside a node:
docker exec -it dingo-producer sh
docker exec -it cardano-producer bashThe Dingo node socket is on the dingo-producer-ipc volume at
/ipc/dingo.socket; the cardano-node sockets live on each node's
*-ipc volume at /ipc/node.socket.
run-tests.sh is the entry point used both locally and in CI:
./run-tests.sh # bring up the network, run all devnet tests, tear down
./run-tests.sh -run TestBasicBlockForging # forward -run (and other flags) to `go test`
./run-tests.sh --keep-up # leave the network running on success (for poking around)What it does:
- Builds the
dingo-producerimage. docker compose up -dand waits up to 120s for all three nodes to reporthealthy. (Thetxpumpcontainer has no healthcheck — it depends ondingo-producerbeing healthy and starts pumping from there.)- From the repo root, runs
go test -tags devnet -count=1 -v ./internal/test/devnet/.... - Tears the network down. On failure it dumps the last 100 lines of compose logs first.
The harness reads endpoint addresses from DEVNET_DINGO_ADDR,
DEVNET_CARDANO_ADDR, and DEVNET_RELAY_ADDR, which run-tests.sh sets
based on the host port mappings. Tests also re-parse testnet.yaml at
runtime via devnet.LoadDevNetConfig(), so changes to network parameters
flow through without code edits — override the path with
DEVNET_TESTNET_YAML if you stage a non-default spec.
All scenarios live in internal/test/devnet/scenarios/ and use the harness
in internal/test/devnet/:
| Test | What it verifies |
|---|---|
TestBasicBlockForging |
Dingo forges at least one block and all three nodes converge within securityParam |
TestDingoChainAdvances |
Dingo's tip slot advances over a short observation window |
TestCardanoProducerChainAdvances |
cardano-producer's tip advances (sanity for the reference node) |
TestChainGrowthRate |
Block production rate is within the expected range derived from activeSlotsCoeff and slotLength |
TestRelayPropagation |
Blocks reach the non-forging relay |
TestSustainedConsensus |
All nodes stay in agreement across multiple sampling intervals |
TestEpochBoundaryConsensus |
Dingo and cardano-node remain in consensus across at least one epoch boundary (exercises candidate-nonce freeze, lab nonce roll, and new-epoch VRF verification) |
If the defaults clash with something already on the host, override the port
mappings via environment or a local .env file next to docker-compose.yml:
| Variable | Default | Used by |
|---|---|---|
DEVNET_DINGO_PORT |
3010 |
docker-compose host port for Dingo |
DEVNET_CARDANO_PORT |
3011 |
docker-compose host port for cardano |
DEVNET_RELAY_PORT |
3012 |
docker-compose host port for relay |
DINGO_PORT |
falls back to DEVNET_DINGO_PORT |
run-tests.sh only |
CARDANO_PORT |
falls back to DEVNET_CARDANO_PORT |
run-tests.sh only |
RELAY_PORT |
falls back to DEVNET_RELAY_PORT |
run-tests.sh only |
TEST_TIMEOUT |
5m |
go test -timeout in run-tests.sh |
run-tests.sh derives DEVNET_DINGO_ADDR etc. from these so the test
harness and the compose port mappings always agree.
| File | Purpose |
|---|---|
docker-compose.yml |
Service, volume, and network definitions |
Dockerfile.configurator |
Builds the genesis/key generator image (cardano-foundation/testnet-generation-tool v0.1.0) |
configurator.sh |
Runs inside the configurator: drives genesis-cli.py, builds ring topology, sets systemStart, relaxes key permissions for non-root node containers |
testnet.yaml |
Network spec consumed by genesis-cli.py (pool count, slot params, hard fork schedule) |
topology/dingo-producer.json, cardano-producer.json, relay.json |
Static peer lists mounted into each node |
start.sh / stop.sh |
Convenience wrappers around docker compose up -d / down -v |
run-tests.sh |
Full bring-up → test → tear-down cycle used by CI |
../antithesis/Dockerfile.txpump, ../antithesis/cmd/txpump/ |
Source for the txpump load generator image |
harness.go, config.go |
Go test harness package: Ouroboros NtN client, tip queries, consensus checks, testnet.yaml loader (build tag devnet) |
harness_test.go |
Tests for the harness itself (build tag devnet) |
scenarios/ |
Devnet test scenarios (one or more Test* per file, all //go:build devnet) |
stop.sh and the run-tests.sh trap both run docker compose down -v,
which removes the per-pool config volumes (p1-configs, p2-configs,
utxo-keys) and node databases. Genesis is regenerated on every start, so
this is the desired default — there's no state worth preserving between
runs. If a previous run left orphaned containers or volumes around:
docker compose -f docker-compose.yml down -v --remove-orphans
docker volume ls | grep devnet # inspect leftovers- Nodes don't reach
healthywithin 120s.run-tests.shdumpsdocker compose psand the last 100 log lines on timeout. Most often this is a build issue with the Dingo image or genesis-time skew (the configurator schedulessystemStart30s in the future; if your machine's clock is far off, regenerate by tearing down and starting again). port is already allocated. Set theDEVNET_*_PORToverrides above.- Configurator fails on first run. It clones
cardano-foundation/testnet-generation-toolat build time; check outbound network access and re-rundocker compose build configurator. - Dingo image is stale.
run-tests.shrebuildsdingo-produceron every invocation; forstart.shruns, force a rebuild withdocker compose -f docker-compose.yml build dingo-producer.