Adversary
A node-to-node downstream client that connects to producer nodes, asks for a chain sync from a randomly-chosen intersection point, pulls a bounded number of blocks, and disconnects. The composer fires it repeatedly under fault injection so producers see concurrent connection churn while their chain is being rolled back, paused, or partitioned.
What it does today
The component ships a single Haskell binary, adversary, run inside
the sidecar container.
- Initiator-only N2N client (
NodeToNodeV_14,InitiatorOnlyDiffusionMode). - Implements the chain-sync mini-protocol only (mini-protocol number 2).
- Reads a list of chain points from
CHAINPOINT_FILEPATH. The file is harvested bytracer-sidecarfrom real cardano-tracer logs at run time; one chain point per line, plus the implicitoriginpoint. - Spawns
NCONNSconcurrent connections fanned out over the producer hostnames inNODES. Each connection picks a random starting point, sendsMsgFindIntersect [point], then loopsMsgRequestNextuntilLIMITblocks have been pulled or the producer's tip is reached, then disconnects. - All connections terminate. The driver script exits 0 on completion.
- Randomness comes from
newStdGen(host RNG entropy). It is not wired to the Antithesis hypervisor's random source today.
This exercises:
- Producer mux / connection-management code under N concurrent N2N initiators.
- The intersection-finding logic, with both well-known and historic / rolled-back points.
- ChainSync server state-machine handling under concurrent peers.
This does not exercise:
- Block-fetch, tx-submission, keep-alive, or handshake mini-protocols.
- Upstream-peer (server) behaviour — the adversary never serves a chain.
- Mid-protocol misbehaviour — the loop only ever sends well-formed requests.
- Steered fault injection — Antithesis cannot bias the adversary's choices.
The full long-term plan is in adversary-roadmap.md.
Composer driver
parallel_driver_flaky_chain_sync.sh lives at
components/sidecar/composer/chain-sync-client/ and is mounted into
the sidecar container at
/opt/antithesis/test/v1/chain-sync-client/. It:
- Resolves the producer hostnames from
POOLS(and optionalEXTRA_NODES). - Validates that
CHAINPOINT_FILEPATHexists. If the file is missing the script exits 0, so an early-test invocation whiletracer-sidecaris still warming up does not fail the run (see #9). - Execs the
adversarybinary with the resolved arguments.
A duplicate of the same script lives at
components/adversary/composer/chain-sync-client/ for the component's
local test loop; the canonical copy is the one in
components/sidecar/.
Environment variables
| Variable | Default | Meaning |
|---|---|---|
POOLS |
(required) | Number of producer pools; the script generates p1, p2, ..., p$POOLS as target hostnames |
EXTRA_NODES |
empty | Whitespace-separated extra hostnames to append (e.g. relays) |
PORT |
3001 |
Producer N2N port |
NETWORKMAGIC |
42 |
Cardano network magic for handshake |
LIMIT |
100 |
Maximum number of blocks pulled per connection before disconnecting |
NCONNS |
100 |
Number of concurrent connections per invocation |
CHAINPOINT_FILEPATH |
(required) | File written by tracer-sidecar listing harvested chain points |
Wiring on cardano_node_master
The adversary runs inside the sidecar service. The relevant compose
fragment in testnets/cardano_node_master/docker-compose.yaml:
sidecar:
image: ghcr.io/cardano-foundation/cardano-node-antithesis/sidecar@sha256:...
environment:
NETWORKMAGIC: 42
PORT: 3001
LIMIT: 100
POOLS: "3"
NCONNS: 1
CHAINPOINT_FILEPATH: "/opt/cardano-tracer/chainPoints.log"
volumes:
- tracer:/opt/cardano-tracer
Note NCONNS: 1 on the deployed testnet — the value is intentionally
conservative until we have signal on whether higher fan-out causes
spurious findings.
Build the image
The adversary is published as part of the sidecar image (see
components/sidecar/). It is also buildable
standalone for local development.
Nix
cd components/adversary
nix build .#docker-image
docker load < ./result
Non-Nix
cd components/adversary
docker build \
-t ghcr.io/cardano-foundation/cardano-node-antithesis/adversary:dev \
-f ./Dockerfile .
Local test loop
just up
docker compose -f testnets/cardano_node_master/docker-compose.yaml \
exec sidecar bash
/opt/antithesis/test/v1/chain-sync-client/parallel_driver_flaky_chain_sync.sh
Source layout
components/adversary/
├── adversary.cabal
├── app/Main.hs — argv parsing, point file load
├── src/Adversary.hs — reexports + chain-point parse
├── src/Adversary/Application.hs — chain-sync state machine,
│ repeatedAdversaryApplication
├── src/Adversary/ChainSync/
│ ├── Codec.hs — codec for Header/Point/Tip
│ └── Connection.hs — connectToNode + Ouroboros
├── composer/chain-sync-client/
│ └── parallel_driver_flaky_chain_sync.sh
├── test/ — AdversarySpec.hs unit tests
├── flake.nix
└── Dockerfile
See also
- Adversary roadmap — long-term plan: long-running daemon, parallel-driver fan-out, full tier list of misbehaviour archetypes, planned home in
lambdasistemi/cardano-node-clients. - #9 — the chainpoint-file race that the driver tolerates.
- Network spec PDF — chain-sync mini-protocol on p. 21.
- ouroboros-network — the upstream library we depend on.
- Other projects by HAL
- Other projects by the Cardano Foundation
- About Cardano