ATAN Post-KIP-21 Archive Verification¶
Verify that the post-KIP-21 (post-Toccata) ATAN finality-period archives (<score>.pb) stored by a
node are valid, by running the offline kaspa-atan-verify tool against the kaspad data volume.
For each finality period the tool recomputes the seq_commit scalar chain, replays the IGRA lane, and
confirms they match the stored values, reporting PASS / FAIL / MISSING per period.
Safe to run against a live node
kaspa-atan-verify is strictly read-only and never opens the atan_db RocksDB, so it does not
contend with the running kaspad's database lock. Always mount the data volume read-only (:ro).
Committed .pb files are written atomically, so the period kaspad is currently writing is invisible to
the tool — cap --to at the last fully-imported finality score.
When to use¶
- After importing historical ATAN data, to confirm the imported post-KIP-21 periods are valid.
- As a periodic integrity check of the archive on an ATAN/uploader node.
Only periods at or after Toccata activation exercise the four-group validator; earlier periods are
validated through the legacy path. The per-period log prints pre-kip21 / post-kip21 block counts so a
pre-activation (legacy-only) run is obvious.
Prerequisites¶
The verifier ships inside the published kaspad image (igranetwork/kaspad) as /app/kaspa-atan-verify
— there is no separate image to build or pull. You only need a KASPAD_VERSION whose image bundles the
verifier (a build at or after the one that added kaspa-atan-verify; the ZK-hardening handling described
below requires a build at or after that fix).
The image already runs as UID 1000, matching the kaspa user that owns the kaspad data volume, so it can
read a :ro-mounted volume without a --user override.
1. Identify the container, volume, and archive path¶
The kaspad container and the Compose-prefixed volume name vary by deployment, so discover them first:
# Container name (e.g. `kaspad`, or `kaspad-atan` under docker-compose.atan.yml):
docker ps --format '{{.Names}}\t{{.Image}}' | grep kaspad
# Volume name (Compose prefixes it: <project>_kaspad_data):
docker volume ls | grep kaspad
The archive lives under the node's DATADIR, in atan/<TX_ID_PREFIX>/chain_block_lists. For Galleon
testnet-10 (NETWORK=testnet-10, TX_ID_PREFIX=97b4) that is:
List the imported periods (read-only alpine; the temp var avoids the head/tail-share-a-pipe pitfall):
VOL=<volume-from-above> # e.g. atan-testnet-10_kaspad_data
docker run --rm -v "$VOL":/app/data:ro alpine sh -c '
d=/app/data/kaspa-testnet-10/datadir/atan/97b4/chain_block_lists
scores=$(ls "$d" | sed "s/\.pb//" | sort -n)
echo "first=$(echo "$scores" | head -1) last=$(echo "$scores" | tail -1) count=$(echo "$scores" | wc -l)"
'
2. Boundary proof (verify one period first)¶
Verify the activation-boundary period on its own before the full sweep. A PASS confirms the recomputed
seq_commit matches the stored chain; the variant counts show whether the period is mixed pre/post or pure
post-KIP-21.
VOL=<volume-from-above>
docker run --rm -v "$VOL":/app/data:ro \
-e RUST_LOG=warn,kaspa_atan_verify=info \
--entrypoint /app/kaspa-atan-verify igranetwork/kaspad:${KASPAD_VERSION} \
--chain-block-lists /app/data/kaspa-testnet-10/datadir/atan/97b4/chain_block_lists \
--from 1057 --to 1057 --zk-hardening-activation 476232000
Quiet the logs
kaspa-atan-verify honours RUST_LOG. The deployed node sets RUST_LOG to trace for the ATAN
crates, which makes the validator log one line per block (hundreds of thousands per period). Always
pass -e RUST_LOG=warn,kaspa_atan_verify=info so you get just the per-period PASS/FAIL lines. This
matters most for docker exec (which inherits the live container's RUST_LOG).
Quick alternative: exec into the running node
Because the verifier is part of the kaspad image, you can also run it inside the live container (the
archive is already mounted at /app/data). Use the container name from step 1 and the same RUST_LOG
override:
docker exec -e RUST_LOG=warn,kaspa_atan_verify=info kaspad /app/kaspa-atan-verify \
--chain-block-lists /app/data/kaspa-testnet-10/datadir/atan/97b4/chain_block_lists \
--from 1057 --to 1057 --zk-hardening-activation 476232000
This shares the running container's resources; for the full sweep prefer the isolated docker run form.
--tx-prefix (default 97b4) and --igra-lane-id (default 97b10000) default correctly for testnet-10 and
can be omitted, but must match the node's TX_ID_PREFIX / IGRA_LANE_ID (.env; see
Environment Reference) — a mismatch is the most common false FAIL.
--zk-hardening-activation has no default (the verifier is born-hardened by default); pass it
explicitly as 476232000 for the TN10 historical archive (see the next section), or periods 1057–1077
will fail.
3. Verify the full range¶
Once the boundary proof passes, widen to the full post-KIP-21 range (set --to to the last imported period
from step 1):
VOL=<volume-from-above>
docker run --rm -v "$VOL":/app/data:ro \
-e RUST_LOG=warn,kaspa_atan_verify=info \
--entrypoint /app/kaspa-atan-verify igranetwork/kaspad:${KASPAD_VERSION} \
--chain-block-lists /app/data/kaspa-testnet-10/datadir/atan/97b4/chain_block_lists \
--from 1057 --to 1091 --zk-hardening-activation 476232000
ZK hardening vs Toccata (TN10)¶
On TN10 the two consensus activations happened at different times, before the core team later collapsed them into one ("born-hardened"):
| Activation | DAA score | ≈ finality period |
|---|---|---|
| Toccata (KIP-21 / post-KIP-21 blocks begin) | 467_579_632 |
1057 |
ZK hardening (inactivity_shortcut required) |
476_232_000 |
1078 |
So in the historical archive:
- 1057–1077 are post-Toccata but pre-ZK-hardening: their blocks carry no
inactivity_shortcut, and the verifier validates them via the identity activity root. - 1078–1091 are ZK-hardened: their blocks carry the
inactivity_shortcut.
The verifier handles both via --zk-hardening-activation 476232000. The flag has no default (the verifier
is born-hardened by default), so it must be passed explicitly; with a current build and the flag the full
1057..=1091 sweep passes. If you omit it — or the image predates the ZK-hardening fix — every post-Toccata
period fails with:
... `inactivity_shortcut` is inconsistent with hardening activation: post-toccata block must carry an
inactivity_shortcut (born-hardened)
In that case, pass --zk-hardening-activation 476232000 and use a current image.
Interpreting the output¶
A healthy full run (testnet-10, with --zk-hardening-activation 476232000):
atan-verify: network=testnet-10 toccata_activation_daa=467579632 zk_hardening_activation=476232000 tx_prefix=97b4 igra_lane_id=97b10000 range=1057..=1091
period 1057: PASS (format=V2, pre-kip21=159477, post-kip21=...)
period 1078: PASS (format=V2, pre-kip21=0, post-kip21=192681)
period 1091: PASS (format=V2, pre-kip21=0, post-kip21=161882)
atan-verify: summary 35 passed / 0 failed / 0 missing of 35 period(s)
The startup line echoes the resolved config; zk_hardening_activation=none(born-hardened) means the flag was
not passed.
PASS— the period'sseq_commitchain and IGRA-lane replay match the stored values.FAIL— a mismatch; theAtanErrornames the offending block and the expected/actual values.MISSING— no.pbfor that score in the directory (e.g.--toexceeds what is imported).pre-kip21/post-kip21— block-variant counts; a non-zeropre-kip21marks an activation-boundary period (e.g. 1057) or a pre-activation period.- The container exit code is non-zero if any period failed or was missing — usable in scripts/CI.
Notes¶
- Mount the volume read-only (
:ro). Do not run the verifier with a writable mount. - Always pass
-e RUST_LOG=warn,kaspa_atan_verify=info(see the warning above). - For the TN10 historical archive, always pass
--zk-hardening-activation 476232000(no default). - Cap
--toat the last fully-imported finality score; the period currently being written is intentionally not visible to the tool. - A wrong
--tx-prefix/--igra-lane-id(not matching the node) is the most likely cause of a falseFAIL; confirm them against.envbefore investigating data. - See
atan/verify/README.mdinrusty-kaspa-privatefor the tool's full reference.