Local Testnet

A 3-node loopback testnet for verifying dugite block production and diffusion against the Haskell reference implementation. One dugite block producer, one dugite relay, and one cardano-node validator (relay role), all on the same machine, all on the loopback interface — the cardano-node connects through the dugite relay.

What this is

graph LR
  dbp[dugite-bp<br/>N2N 3001<br/>metrics 12798<br/>pool1 — sole forger] <--> dr[dugite-relay<br/>N2N 3002<br/>metrics 12799<br/>hub]
  dr <--> cbp[cardano-node<br/>N2N 3003<br/>validator only]

dugite-bp is the sole block producer. cardano-node runs as a passive validator (no forging keys passed) and chainsync+blockfetches every block forged by dugite-bp, applying each one through the Haskell ledger. This gives us byte-exact cross-validation of dugite's forged blocks against the reference implementation with zero risk of asymmetric forks (no two forgers competing for the same height).

The chain boots into Conway PV10 from a fresh genesis with a single forging pool (pool1, 100% of active stake; pool2's keys exist but its stake is 0), 1-second slots, activeSlotsCoeff = 0.2, epochLength = 200, and securityParam = 10. Blocks are minted every ~5 seconds on average and an epoch elapses every ~3.3 minutes, so the 30-minute soak crosses ~9 epoch boundaries (enough for reward distribution to complete before the soak ends).

Historical note: prior to 2026-05-18, cardano-node also ran as a forger (pool2, equal stake) for symmetric multi-forger cross-validation. That topology produced an asymmetric-fork class that no per-tx test could survive: the slower side's leader-slot timer fired before propagation completed and each pool ended up on its own short chain permanently (first-seen tiebreaker, equal lengths). Making cardano-node a validator eliminates that class entirely while preserving the cross-validation we actually rely on (does cardano-node accept every dugite block byte-for-byte?).

Prerequisites

  • cardano-node >= 11.0.1 and cardano-cli >= 11.0.0 on $PATH
  • target/release/dugite-node built (cargo build --release from the repo root)
  • jq for JSON manipulation
  • ~2 GB of free disk for the soak
  • macOS: the system-builtin caffeinate (used to suppress App Nap during the soak). This dependency is macOS-only; on Linux the soak runs unwrapped.

The setup script's prereq check will refuse to run if any of these are missing.

One-time setup

./testnet/local-devnet/setup.sh

This generates a fresh genesis (4 files: byron, shelley, alonzo, conway), key sets for two stake pools, four stake delegators, three genesis keys, and one UTxO funding key. All output lands under testnet/local-devnet/genesis/, testnet/local-devnet/keys/, and testnet/local-devnet/config/ (rendered configs). Generated keys and genesis files are gitignored.

Expected output ends with:

[INFO]  All configs + topologies rendered to testnet/local-devnet/config/
[INFO]  Setup complete. Next: ./run.sh

The genesis start time is set to "now + 30 seconds." Re-run setup.sh if more than ~5 minutes pass before you call run.sh (the start-time freshness check will refuse to start a stale chain).

Running the network

./testnet/local-devnet/run.sh

This starts the three nodes in the background (caffeinate-wrapped on macOS), records PIDs to state/<node>.pid, sends logs to logs/<node>.log, and exposes N2C sockets at:

  • testnet/local-devnet/state/dugite-relay.sock
  • testnet/local-devnet/state/dugite-bp.sock
  • testnet/local-devnet/state/cardano-bp.sock

You can query each socket with cardano-cli (or dugite-cli, which speaks the same N2C protocol):

cardano-cli query tip \
  --testnet-magic 42 \
  --socket-path testnet/local-devnet/state/dugite-bp.sock

To stop the network:

./testnet/local-devnet/stop.sh

This sends SIGTERM, waits 5 seconds, then SIGKILL if needed. DBs and logs are preserved in state/ and logs/.

Running the soak test

./testnet/local-devnet/soak.sh         # default: 1800s (30 minutes)
./testnet/local-devnet/soak.sh 300     # 5-minute smoke test

The soak runs three concurrent samplers while it's alive:

  • tip-sampler — every 5 seconds, queries tip on each socket and appends (ts, node, slot, block_no, hash, era) rows to tip-samples.csv.
  • block-recorder — tails the three node logs and writes one row per first- sight of each block, with observer, forge/recv flag, slot, hash, and (for forge events) the issuer's vkey.
  • tx-injector — at T+2 min, T+10 min, and T+20 min, submits 5 self-transfer payment transactions to each of the 3 sockets (15 per wave, 45 total) and records each submission's txid and return code.

Evidence lands in testnet/local-devnet/evidence/<timestamp>/. A heartbeat line is printed every 30 seconds with current tips from all three nodes.

Verifying results

After the soak finishes, soak.sh does not automatically run verify — run it manually:

./testnet/local-devnet/verify.sh testnet/local-devnet/evidence/<timestamp>/

The verifier evaluates four pass/fail predicates and writes evidence/<timestamp>/report.md. Predicates:

#PredicatePass condition
1Block forge cross-checkEvery confirmed (slot, hash) pair is seen by all three observers in blocks.csv. (Most-recent 10 blocks are excluded from the check to allow rollback grace.)
2Per-BP forge attributiondugite-bp forged >= 3 blocks; cardano-node forged 0 (it's a validator). Expected ~360 by dugite-bp at f=0.2, σ=1.0 — failure at 3 is a real wiring bug, not a slot-lottery flake. Any forge events attributed to a non-dugite-bp issuer are a setup error.
3Transaction inclusion round-tripEvery submitted tx has submit_rc=0 and (when run with the devnet up) appears in all three nodes' UTxO sets at the genesis payment address.
4Tip parity over timeAt >=95% of 5-second ticks (excluding the first 60s warmup), all three nodes report tips within 2 blocks of each other.

The report.md includes a metadata snapshot (versions, genesis hashes, magic), counts (block events, tx submissions, tip samples), a forge-attribution breakdown, and a per-predicate result table.

You can also self-test the verifier (without a real soak) using committed test fixtures:

./testnet/local-devnet/verify.sh --self-test

Topology & port reference

ProcessN2NMetricsSocketConfigTopology
dugite-bp (pool1, sole forger)300112798dugite-bp.sockdugite-bp.config.jsondugite-bp.topology.json
dugite-relay (hub)300212799dugite-relay.sockdugite-relay.config.jsondugite-relay.topology.json
cardano-node (validator)3003cardano-bp.sockcardano-bp.config.jsoncardano-bp.topology.json

The devnet uses the standard Cardano N2N port (3001) for dugite-bp and single-digit increments for the relay (3002) and the Haskell BP (3003). The metrics ports follow the same convention: dugite-bp keeps the well-known Prometheus default (12798), the relay exposes 12799. If a public-network soak is running on the same host it must be stopped before the devnet boots, since both processes would otherwise bind 3001/12798.

Monitoring with dugite-monitor

Because dugite-bp runs on the default metrics port (12798), the bundled TUI monitor connects with no overrides. In a separate terminal once the devnet is up:

./target/release/dugite-monitor

To inspect the relay instead of the BP, point the monitor at its metrics port:

./target/release/dugite-monitor --metrics-url http://localhost:12799/metrics

The cardano-node validator does not expose a Prometheus endpoint in this devnet (its EKG/Prometheus exporters are disabled to keep the configuration minimal and avoid port collisions).

Configuration reference

The genesis is generated by cardano-cli conway genesis create-testnet-data with two override fragments committed under config/spec/. Only fields that differ from cardano-cli's defaults are listed below.

config/spec/shelley-spec.json:

fieldvaluepurpose
slotLength1.01-second slot duration
activeSlotsCoeff0.2f = 0.2; ~5s expected block time
epochLength200~3.3 minutes per epoch -> ~9 epoch transitions in the 30-min soak. The Praos lower bound is 3k/f = 150 slots; we stay safely above that while keeping reward/governance cycles short enough for tx-zoo.
securityParam10small k -> fast immutability (3k/f ~= 150 slots)
updateQuorum2matches the 3 genesis keys (2-of-3)
maxLovelaceSupply60_000_000_000_000_00060 B ADA, mainnet-shaped
networkMagic42local devnet magic

config/spec/conway-spec.json carries Conway governance parameters; for a 30-min run with no proposals only the protocol version matters. PV is set to 10.0 so the chain boots straight into Conway.

Troubleshooting

  • ERROR: cardano-cli x.y.z < 11.0.0 required — install a newer cardano-cli; see prerequisites.
  • ERROR: Port 3001 is in use — another devnet (or the public soak rig) is using a port. Run ./stop.sh or lsof -iTCP:3001.
  • Genesis is N seconds old (>300s). Re-run ./setup.sh — the start time has drifted; re-run setup.
  • Tips not advancing after run.sh — check logs/<node>.log for the failing node. Most common cause is a KES key path mismatch (re-run setup.sh).
  • Soak hangs on macOS — confirm caffeinate is wrapping the dugite processes via ps auxw | grep caffeinate. App Nap can freeze dugite for tens of minutes without it.
  • dugite-monitor shows no data — confirm the BP is exposing metrics on 12798 with curl -s localhost:12798/metrics | head. If empty, the BP either failed to start or its config doesn't have the Prometheus exporter enabled (the devnet template enables it by default).

What this validates

  • Dugite block production end-to-end (forge -> adopt -> diffuse)
  • Dugite relay's ChainSync/BlockFetch serving (Rust -> Haskell validator)
  • Dugite N2N peer connection lifecycle on loopback
  • Dugite N2C local-socket tx submission, query tip, query utxo
  • Byte-exact acceptance of every dugite-forged block by cardano-node 11.0.1+ (the Haskell ledger applies each block; any header / body / Conway predicate mismatch surfaces as a ChainDB.AddBlockValidation.InvalidBlock trace in logs/cardano-bp.log)

What this does NOT validate

  • Byron-era code paths (chain boots in Conway)
  • Hard-fork combinator era transitions (none occur during the soak)
  • Multi-relay diffusion topologies (single hub)
  • Plutus phase-2 / governance enactment (no proposals or scripts in the soak; the tx-zoo covers governance under a separate driver)
  • Symmetric multi-forger chain selection (cardano-node is a validator here, not a forger — see "Historical note" at top)
  • Mainnet-scale peer counts, NAT/firewall behaviour, or BGP-level routing
  • Mithril snapshot import (covered by other tests)

Transaction validation (tx-zoo)

In addition to the soak (which exercises block production + diffusion under a steady-state load of identical self-payments), the devnet ships with a transaction zoo that exercises the full Conway tx surface against the same 3-node hub. Use this when you want to verify that the dugite mempool, forger, and validator accept every transaction class that mainnet allows.

./testnet/local-devnet/tx-zoo/run-all.sh --setup    # one-time: keys + Plutus binaries
./testnet/local-devnet/tx-zoo/run-all.sh            # run everything
./testnet/local-devnet/tx-zoo/run-all.sh 03-plutus  # one category
./testnet/local-devnet/tx-zoo/run-all.sh --summary  # totals from the last run

The zoo covers 8 categories (59 scripts total): bookkeeping (simple-pay, multi-output, metadata CIP-20/CIP-25, validity intervals, required signers, treasury donation, tx chaining), native scripts (all/any/atLeast policies, time-locked, burns, pay-to-script, spend-from-script), Plutus V1/V2/V3 (spend, mint, inline datums, reference scripts, reference inputs, datum-hash reveal, collateral consumption), stake operations (register, delegate, combined certs, deregister, pool register/retire, reward withdrawal), governance certificates (DRep register/update/deregister, vote-delegation to DRep + always-abstain + always-no-confidence, CC hot-key authorisation, CC resignation), proposals (Info, ParameterChange, HardForkInitiation, TreasuryWithdrawal, NoConfidence, UpdateCommittee, NewConstitution), voting (DRep + SPO + CC yes/no/abstain), and negative paths (min-utxo violation, fee-too-low, expired TTL, insufficient collateral). Each script submits through the dugite relay socket and waits for inclusion at that same socket to guarantee diffusion + validation on the dugite path before recording PASS/FAIL/SKIP into tx-zoo/state/results.csv.

Requirements

  • The devnet must be up via ./run.sh (the zoo refuses to start without all 3 sockets).
  • The Conway genesis must boot at protocol version >= 10 so Plutus V1/V2/V3 and Conway-only certs are admissible. The committed shelley-spec.json already sets protocolVersion.major = 10; if you fork the spec to a lower version every script that touches Plutus or governance will record FAIL with "ScriptFailed: requires protocol >= N" from cardano-cli or the dugite mempool.
  • The zoo uses vendored always-true Plutus binaries for V1/V2/V3 scripts. Three categories (collateral, ref-scripts, inline-datum) run with these vendored versions; only the negative-path "insufficient collateral" needs a real V2 always-true script (also vendored).

Caveats

  • not-included failures: the zoo uses a single shared funding wallet (the genesis UTxO key) and runs scripts in lexical order. Each test consumes the largest UTxO at that wallet, so concurrent runs against the same socket will race. If you see intermittent not-included in results.csv for a tx that was added to the mempool, check that no other client is submitting to the same socket — the zoo expects exclusive access.
  • Rewards warmup: reward-withdrawal cannot succeed until the delegated stake earns rewards, which requires at least two epoch boundaries after delegation. On the 200-slot epoch this means the script SKIPs for the first ~7 minutes after a fresh devnet boot. The script records SKIP with reason no-rewards rather than failing.

Seated CC member

cardano-cli conway genesis create-testnet-data emits an empty Constitutional Committee (members={}, threshold=0), which would make the CC hot-key auth, CC resignation, and CC voting scripts non-runnable. To provide full governance coverage out of the box, setup.sh generates a CC cold/hot key pair at keys/cc-1/ and post-processes the generated conway-genesis.json to seat that member with a 1-of-1 threshold and a long expiry term (epoch 1000). tx-zoo/lib/keygen.sh then reuses those same keys, so 05g-cc-hot-key-authorization, 05h-cc-resign, 07f-cc-vote-yes, and 07g-cc-vote-no operate on a genuine seated member rather than an orphan keypair.

If you fork setup.sh or generate genesis through any other path, ensure the resulting conway-genesis.json has a non-empty committee.members map and a threshold matching that membership; otherwise those four scripts will auto-SKIP with reason empty-committee / cc-not-authorized.

Results are appended to tx-zoo/state/results.csv (one row per script with ts, name, status, txid, detail) and the per-script stderr lives under tx-zoo/state/logs/<name>.log. After a failed run, re-run a single category or script directly:

./testnet/local-devnet/tx-zoo/03-plutus/03a-spend-v1.sh

Tracking: see the GitHub issue linked from docs/superpowers/specs/2026-05-16-local-testnet-design.md.