Architecture
Anchors and chain data
A verifier accepts a proof only if its two roots are in the verifier’s anchor table. This page explains how those roots are built from public data, checked, published and re-derived.
On this page
#What an anchor is
An anchor is a finalised height and two roots: the Ironwood note-commitment tree root and the root of the indexed Merkle tree of every Ironwood nullifier revealed up to that height. Records look like this:
{
"network": "mainnet",
"height": 3493000,
"blockHash": "00000000000477d021dac9bc28de61cac47a915add870a5716e3865234229b0c",
"ncRoot": "1780a43b9054d8f45de3763df336ec4f443697b2762d397b014255d202b56a3c",
"nfRoot": "34da5ef38d024c99b8ec7fc464c78cda5543b06ba412c775ca9dfcb26453c336"
}#How an anchor is built
- Connect to lightwalletd and read the tip. The anchor height is
floor((tip − 100) / 1000) × 1000, so it is finalised and on a checkpoint. - Stream every compact block from Ironwood activation (mainnet 3,428,143, testnet 4,134,000). Each Ironwood action carries a nullifier, a note commitment, an ephemeral key and a 52-byte compact ciphertext.
- Build the note tree from every commitment and the dense IMT from every nullifier.
- Check the tree root, its size and the block hash against lightwalletd’s own tree state. A mismatch stops the scan.
- Write the snapshot (
.vsnp) with a checkpoint every 1,000 blocks, and merge the record intoanchors.<network>.json. Publishing refuses a different record at a height already published.
#Snapshots
A snapshot is everything a prover needs, from public data only: every action’s nullifier, commitment and encrypted output up to a height, and (version 2) a checkpoint table every 1,000 blocks plus the last block. Provers trial-decrypt it to find their notes, and take the tree at an earlier checkpoint for “unmoved since” proofs. Reading is bounds-checked and never panics. The mainnet snapshot at 3,493,000 is about 84 MB.
#Published anchors
The table served at /api/anchors merges the mainnet, testnet and demo tables. Earlier testnet heights are there for “unmoved since” proofs.
| Network | Height | nc_root | nf_root |
|---|---|---|---|
| demo | 3,481,000 | 7cd9a74820ce97cb… | 689d7c0c743b83eb… |
| demo | 3,491,040 | da7e8a67b49fa924… | 689d7c0c743b83eb… |
| mainnet | 3,493,000 | 1780a43b9054d8f4… | 34da5ef38d024c99… |
| testnet | 4,410,000 | dc487630542d8caa… | 322fcff2c72d6acb… |
| testnet | 4,459,000 | 94f218a998772a28… | f9468d5ccaef361e… |
| testnet | 4,460,391 | d83151878dc1976e… | 157230149dc27092… |
#Re-derive any entry
pof-anchor scan --to 3493000 # mainnet, from zec.rocks
pof-anchor scan --network testnet --to 4410000
pof-anchor scan --network testnet --to 4459000 --also-at 4410000 # earlier checkpoints of the same scan
pof-anchor check --snapshot target/snapshots/testnet-4459000.vsnp --at 4410000A wrong entry cannot make a false proof pass: the roots must match the proof’s own public inputs. It can only make honest proofs fail. That is why an anchor table is trusted for availability, not for truth.
#Testnet and mainnet
The network is recorded in the anchor table, not in the proof or the attestation. pof-anchor refuses a lightwalletd that reports a different chain than --network, so a testnet anchor cannot be published as mainnet, and verdicts on a testnet anchor say so. A verifier that only wants mainnet should load only mainnet anchors.