Cardano Node Releases
A mixed-version Cardano testnet for Antithesis fault-injection testing.
Overview
This testnet exercises the node-to-node protocol across multiple cardano-node versions:
- 4 block producers: p1 (11.1.3), p2 (10.6.2), p3 (10.7.1), p4 (11.0.1) — forge blocks in a ring topology (p1↔p2↔p3↔p4)
- 3 relay nodes: relay1 (10.6.2), relay2 (10.7.1), relay3 (11.0.1) — non-producing nodes connected to all producers
Supporting services
| Service | Role |
|---|---|
| configurator | Generates genesis files, node configs, and signing keys at startup |
| tracer | cardano-tracer daemon collecting structured logs from all nodes |
| tracer-sidecar | Processes tracer logs into Antithesis assertions (chain convergence, error detection) |
| sidecar | Network health checks, the Antithesis setup signal, and the host of the chain-sync adversary driver (see Adversary). |
| log-tailer | Streams the per-pool node logs into Antithesis's log explorer for offline triage. |
| asteria-game | Single container that hosts the long-lived utxo-indexer plus three short-lived binaries (asteria-bootstrap, asteria-game, asteria-invariant) fired by composer scripts. See Why asteria-game is here below and Asteria Game. |
| tx-generator | Long-running daemon that submits well-formed ADA transfers through relay1's N2C socket. Composer drivers fire deterministic transact, refill, eventually, and finally control-socket probes. See Why tx-generator is here below and Tx-generator. |
Why asteria-game is here
Antithesis can only find bugs that the system actually exercises in the simulation. A cluster running idle — three producers minting empty blocks — explores almost nothing of the node's tx-handling code: mempool admission, script evaluation, ledger validation under contention, fork resolution while inputs are being consumed. Faults fired against an idle cluster mostly prove that the cluster restarts.
The asteria-game container is the workload generator that turns master into a busy testnet. Without it, the cluster has no realistic tx pressure. With it, the cluster sees the full envelope:
parallel_driver_asteria_player.sh— fired concurrently by the composer many times per timeline. Each invocation reads the asteria UTxO from chain via the long-lived utxo-indexer, plans a move, and for player-1 builds + signs + submits aspawnShipPlutus transaction (consume asteria → mint aSHIP*token → write to the spacetime address → produce the next asteria UTxO withship_counter += 1). Player-2 and player-3 run the same observe- plan path without acting, exercising the read side.
serial_driver_asteria_bootstrap.sh— exclusive-access driver that idempotently re-deploys the validators and admin NFT each invocation. Re-runs detect the existing state via the indexer and skip the mint, exercising the "is already deployed" path under fault injection.anytime_asteria_admin_singleton.sh— periodic invariant probe. Asserts (viasdk_always) that exactly oneasteriaAdminNFT exists at the asteria spend address — the one-shot mint policy must hold under any combination of forks, kills, and partitions.eventually_asteria_consistency.sh— post-workload consistency snapshot.ship_counteron the asteria UTxO must equal the count ofSHIP*tokens at the spacetime address.eventually_alive.sh— short liveness probe against the indexer'sreadyendpoint.
What this gives Antithesis to score:
- Tx-build pressure under faults. The player driver builds, signs, and submits real Plutus transactions concurrently with fault injection. Validation: a 1h validation run on [PR #128][pr-128] saw 22,990 player invocations, 2,599 successful spawn-ship transactions landing on chain, and 149 observed fork switches during the run.
- Plutus-script ledger paths. The validators (asteria.spend, spacetime.spend, shipyard.mint) execute on every ship spawn — exercises ref-input handling, datum decoding, redeemer validation, exec-budget accounting.
- Multi-version interop. Producers p1/p2/p3/p4 run four different cardano-node versions; the same Plutus tx must validate identically across versions or the cluster forks.
- Invariants that survive faults.
asteria_admin_singleton(sdkAlways) andasteria_state_consistent(sdkSometimes) provide oracle properties Antithesis can falsify if a fault produces a divergent view, a double-mint, or a lost ship.
The container is the load + oracle of the test. Its image is
pinned by SHA in docker-compose.yaml; building it locally goes
through components/asteria-game/. The composer scripts that drive
it live alongside the binaries in components/asteria-game/composer/asteria-game/
and are baked into the image at build time.
For the architectural detail of how the binaries integrate with
cardano-node-clients (provider, submitter, indexer, TxBuild DSL,
fee bisection), see Asteria Game.
Why tx-generator is here
asteria-game gives master Plutus-heavy script traffic. The
tx-generator adds a complementary ADA-only workload: many simple,
well-formed UTxO fan-out transactions that put pressure on mempool
admission, ledger validation, rollback handling, and node-to-client
submission without depending on Plutus execution.
The service is deliberately composer-driven. The daemon is idle until Antithesis fires a short command:
parallel_driver_transact.sh- requests one deterministic fan-out transaction from an existing population address.parallel_driver_refill.sh- requests one faucet-funded top-up into a fresh population address.eventually_population_grew.sh- snapshots the daemon state and asserts that a populated address set was observed.eventually_pressure_summary.sh- emits a post-workload snapshot with population size, UTxO value percentiles, tip slot, and last tx id.
Why this belongs in the production-baseline testnet:
- Independent transaction pressure. It continues driving ordinary ADA transfers even when the Plutus workload is faulted, not yet bootstrapped, or waiting on an invariant window.
- Replayable pressure. Every command carries its own seed; with the same persistent state and request payload, the daemon rebuilds the same transaction. That makes Antithesis examples reproducible.
- N2C resilience surface. The daemon maintains one multiplexed N2C bearer to relay1 for ChainSync, LocalStateQuery, and LocalTxSubmission. Relay restarts and network faults exercise the reconnect and duplicate-submit recovery logic.
- Oracle properties. Hard failures surface as
tx_generator_*_submit_rejected; liveness and pressure are scored throughtx_generator_population_grew,tx_generator_refill_landed, andtx_generator_pressure_summary.
Image provenance promotion (September 2026)
The scheduled cardano-node-releases profile consumes the tx-generator and
tracer-sidecar images built from source revision
36756d0efcf73f412df42a352afd7d6b0ac3c33d, pinned by immutable registry digest.
Both images record source creation time 2026-09-08T10:52:25Z and OCI
revision, source, version, and creation labels.
These exact images passed a one-hour, faults-enabled Antithesis run on the
otherwise identical cardano_node_provenance_241 profile: 69/69 properties
Passing, including Recent software version provided. The
validation workflow
used consumer commit ed603743e31149709ac2baa002ebbfd21136cc82.
Antithesis identified both images among its freshness examples.
The scheduled profile must select the published digests to consume the harness fix; merging the image-building code alone does not update existing pins. Creation time follows the source revision, so rebuilding unchanged source does not renew the 72-hour freshness window. New source images need validation and pin promotion as they are published.
Original workload promotion (May 2026)
The original workload promotion used tx-generator:69bf815, referenced from
downstream commit 4687a09. It was validated on a sibling
cardano_node_tx_generator testnet before promotion (since removed;
the workload now lives directly in this testnet).
| Evidence | Value |
|---|---|
| Antithesis test run | 9352ad089c67523bc2ba2c14d1d18b5b39b24f49797c640214eaf375abf74944 |
| Triage report id | OSIShLbA8Ixh6uRxqDdjWdtr |
| Started | 2026-05-05 12:48 UTC |
| Properties | 40/40 passed |
| Findings | 0 new, 0 ongoing, 0 resolved, 0 rare |
Shared images were checked against the nearest completed
cardano_node_master run (8ca5bdd583..., started
2026-05-05 12:42 UTC). The configurator, log-tailer,
sidecar, tracer-sidecar, cardano-node, and cardano-tracer
images matched. The expected workload-specific differences were
tx-generator:69bf815 in the tx-generator validation run and
asteria-game:f7ce4a2 in master.
The validation run also cleared the four previous tx-generator composer zero-exit findings:
| Composer command | Passing hits | Failing hits |
|---|---|---|
tx-generator/parallel_driver_transact.sh |
5,288 |
0 |
tx-generator/parallel_driver_refill.sh |
3,506 |
0 |
tx-generator/eventually_population_grew.sh |
1,448 |
0 |
tx-generator/finally_pressure_summary.sh |
413 |
0 |
Measured pressure in that run:
| Measurement | Value |
|---|---|
| Transact-driver starts | 5,709 |
| Refill-driver starts | 4,597 |
| Max concurrent composer commands | 12 |
| Total virtual hours | 335.301 |
| Virtual seconds per input | 0.743665 |
tx_generator_population_grew hits |
166 |
tx_generator_refill_landed hits |
43 |
tx_generator_pressure_summary hits |
413 |
Network topology
p1 (11.1.3) ←→ p2 (10.6.2) ←→ p3 (10.7.1) ←→ p4 (11.0.1) ←→ p1
↑ ↑ ↑ ↑
relay1 (10.6.2) ----+---------------+---------------+
relay2 (10.7.1) ----+---------------+---------------+
relay3 (11.0.1) ----+---------------+---------------+
Producers form a ring. Relays connect to all producers.
Configuration
testnet.yaml: Genesis parameters (poolCount, networkMagic 42, epoch length, protocol version)docker-compose.yaml: Full topology definition with YAML anchors for producers and relaysrelay-topology.json: Shared topology for relay nodes (connects to all producers)tracer-config.yaml: cardano-tracer log forwarding configuration
ExperimentalHardForksEnabled and ExperimentalProtocolsEnabled are false.
With them on, the 11.x nodes stamp protocol version 12 in block headers, which
10.7.1 rejects (HeaderProtVerTooHigh {supplied: Version 12, expected: Version
11}), and the version matrix forks. A cross-version testnet cannot run a
protocol version its oldest member does not support.
Fast variant
testnets/cardano-node-releases-fast is the same cluster with a fast genesis,
so a run crosses epoch boundaries. With a 24 h epoch, the releases testnet
never reaches one.
| parameter | releases | releases-fast |
|---|---|---|
securityParam (k) |
432 | 200 |
activeSlotsCoeff (f) |
0.05 | 0.2 |
slotLength |
1 s | 0.5 s |
epochLength |
86400 slots (24 h) | 10000 slots (5000 s) |
Both keep epochLength at 10k/f, the Praos sizing that fits the stability
window (3k/f) and the randomness stabilisation window (4k/f) inside one epoch.
The fast variant pays with a shorter fork tolerance: k blocks are about 500 s
of producer divergence, so a network split survives at most about 1/5 of an
epoch. A node never rolls back more than k blocks, so a longer split forks the
cluster for good. With k = 20 (50 s) Antithesis network faults did that in
every run, and k = 100 did it in a one-hour run; k = 200 held. The
tracer-sidecar receives SECURITY_PARAM: "200".
The scheduled cardano-node.yaml runs releases at 01:05 UTC and
releases-fast at 13:05 UTC, 3 h each, so a releases-fast run crosses two epoch
boundaries.
Running locally
# Start the testnet
just up
# Check container status
just ps
# View tracer-sidecar logs (chain progress)
just logs tracer-sidecar
# Stop and clean up
just down
Version strategy
Older node versions get fewer instances, newer versions get more. Relay nodes always run recent versions since they are the public-facing nodes that dApps and ecosystem tools connect to.