How Zylem works
Zylem has three parts. Ring files carry Zcash history, a shared header proof stands in for every historical header, and the client checks both before its wallet scans anything. This page explains each part and what the client has to trust.
The chain of checks
Zcash headers anchor the commitments Zylem uses to authenticate wallet data. Zylem walks from the header down to the notes, so nothing a wallet uses is taken on the server's word.
Equihash, the target, the per-block difficulty adjustment and the time rules, checked from a trusted checkpoint.
Each ID is rebuilt from the compact data the wallet already gets, plus a few hundred bytes of digests, then matched to the header's Merkle root.
The history tree in each next header authenticates every block's Sapling, Orchard and Ironwood roots.
A Ring's note commitments, appended to its start, must reproduce those roots. That authenticates every tree state inside it.
A tampered block fails at the first check it touches, with a typed reason such as TxIdMismatch or TreeRootMismatch. The wallet only ever sees blocks that passed.
Rings
A Ring holds one epoch of 4,096 blocks: compact blocks, the digests that rebuild their transaction IDs, and the state to start checking from. Rings are named by their hash and rebuilt byte for byte by any builder, so a CDN, a mirror or a friend's server can host them. Hosts cannot alter authenticated data undetected, but they can withhold it or replay older valid data.
A builder turns blocks from any node into sealed, self-checking files.
a new Ring about every 28 hHeader proofs
Headers are 1,487 bytes each, gigabytes since NU5. Zylem proves the header chain in the SP1 zkVM instead: leaves of 512 headers each check every rule with full Equihash, and recursive aggregation joins them into one proof that commits to each block's record. A wallet checks that proof and the Rings it downloads, and never fetches a historical header.
Measured on an RTX 5090 and an M2 Pro laptop. The mainnet cost is a projection from those runs.
GPU provers replace every historical header with one proof.
a new leaf about every 3.6 hVerification
Built by anyone, independently checked by clients. The wallet, or the Zylem proxy beside it, starts from what it ships with and works outward.
The wallet, or the proxy beside it, checks everything before scanning it.
every syncAnyone can run a builder or a prover. A wrong Ring or a wrong proof fails verification on the wallet's side, so nobody has to vouch for them.
Builds are deterministic. Two independent builders publish the same Ring hashes, and any difference is visible at once.
Static files on any CDN. Serving more wallets adds bandwidth, not nodes, databases or per-user servers.
Proving every mainnet header since NU5 is projected at about $323 of GPU time. Keeping up is about $0.30 for a short GPU run every 12 hours.
One proof of about 0.8 MB replaces 1,487 bytes of header per block, gigabytes since NU5. Wallets download only the Rings they need.
Proofs cover only blocks at least 100 deep. Ordinary reorganizations touch only the newest blocks, which the wallet checks itself.
Rings are built and proved on testnet today, and the live demo verifies them in your browser. Mainnet costs are projections from these runs.
Trust model
Servers and hosts can still withhold or delay data, or replay old data that is valid. The tip policy limits that: the newest blocks must carry enough work and agree across independent sources, or the wallet sees an error instead of a weak chain.
Measurements
Every figure on these pages comes from a run recorded in the Zylem repository. Mainnet costs are projections from testnet runs.
Build it yourself
Anyone can run each step, and no client has to trust whoever did. Testnet commands below; mainnet takes --network main. The full guide is docs/build.md in the repository.
Needs Rust and a Zakura or zcashd node's JSON-RPC. A pruned Zakura node works: the first build bootstraps once from a light-wallet server for blocks the node has pruned. Rings land in data/testnet as plain files for any static host, and two builders produce byte-identical files.
cargo build --release
./target/release/zylem-build backfill --rpc http://127.0.0.1:18232/ \
--lwd https://testnet.zec.rocks:443 --out data/testnet --finality 100
./target/release/zylem-build verify data/testnet ./target/release/zylem-build serve --out data/testnet --rpc http://127.0.0.1:18232/ --finality 100
Needs SP1 6.9.0 (sp1up) and, for real proofs, an NVIDIA GPU with about 26 GB of memory: an RTX 5090 proves a leaf of 512 headers with Equihash in about 5 minutes. Leaves are independent, so any number of GPUs share the work, and every step is safe to rerun.
./target/release/zylem-build jobs --rpc http://127.0.0.1:18232/ \
--activation 4465026 --shards 4 --out data/jobs/nu7
spikes/header-proof/pipeline/pack.sh data/jobs/nu7
zylem/spikes/header-proof/pipeline/setup.sh zylem/spikes/header-proof/pipeline/run-shard.sh 0
zylem/spikes/header-proof/pipeline/join.sh ~/out ~/out/shard-0.proof ~/out/shard-1.proof \
~/out/shard-2.proof ~/out/shard-3.proof --groth16
spikes/header-proof/sp1/target/release/export-proof ~/out/history.proof data/proofs/history
./target/release/zylem-build publish --jobs data/jobs/nu7 --history data/proofs/history \
--vks ~/out/vks.json --rings data/testnet --groth16 ~/out/history.groth16.bytes --out data/publish/nu7
With the program registry (generation 1), set PROGRAMS=programs/testnet before the pod scripts and publish with --registry programs/testnet. Without a GPU, SP1_PROVER=mock runs the same flow and checks every claim.
| Where | Use | Checks |
|---|---|---|
| An existing wallet | zylem-proxy, a drop-in light-wallet server | Every Ring from the checkpoint; the wallet only changes its server address |
| Browsers and phones | The WebAssembly client in spikes/header-proof/wasm-verify (1.8 MB) | RingFile.verify() for a whole Ring; verify_history or verify_history_v2 for the 1.27 MB proof; verify_groth16 for the 356-byte proof; check_all_records and verify_binding to match a Ring to a proof |
| Contracts on EVM chains | SP1's Groth16 verifier for circuit v6.1.0: SP1VerifierGroth16.sol from succinctlabs/sp1-contracts | verifyProof(programKey, publicValues, proof) with Zylem's aggregation key: 303,762 gas in a local EVM. Not deployed on a chain yet |
./target/release/zylem-proxy --rings http://127.0.0.1:8090 \
--headers-rpc http://user:pass@127.0.0.1:18232/ \
--upstream https://testnet.zec.rocks:443 \
--checkpoint "$(jq -r .checkpoint checkpoints/testnet.json)"
zcash-devtool wallet -w my-wallet sync --server 127.0.0.1:9067
cd spikes/header-proof/wasm-verify wasm-pack build --release --target web --out-dir pkg-web
cd spikes/header-proof/onchain && forge test -vv
Program keys: aggregation 0x007d30f1…876f34cb, leaf 0x00b1908c…85b8daa0 (generation 0); generation 1 in programs/testnet/registry.json. Each path trusts the cryptography, a checkpoint and honest-majority hashpower; proofs add SP1 and these programs, and the 356-byte form adds SP1's Groth16 setup.
Technical details
Valid proof of work alone is not enough: Zcash adjusts difficulty every block, so a private fork can grow cheaply. The proxy presents a tip only when the chain after the checkpoint carries the shipped minimum work, at least two independent sources confirm it, and no verified source shows more blocks beyond it or a heavier fork. Headers dated more than two hours ahead are rejected, and a long silence raises a stale-tip alarm.
The proof commits to every block's record, including its Merkle root, so a transaction's inclusion can be shown with its Merkle path, its record's path and the history proof. Absence is checkable too: a verified Ring authenticates every transaction in each block, so a client holding the Rings for a range can confirm a transaction or nullifier did not appear there. What does not exist yet is a compact proof of absence that could be handed to a contract.
The first piece works: SP1's v6.1.0 verifier contract accepts Zylem's 356-byte Groth16 history proof in a local EVM for 303,762 gas, and an example contract reads the claim and stores the proven block hash for 334,361 gas (spikes/header-proof/onchain). Nothing is deployed on a chain yet. The rest is still to build. A contract cannot ask independent sources for the tip, so it needs a freshness rule, such as accepting only facts a set depth below a proven tip that meets a minimum work, and someone who keeps the on-chain client updated. Wrapping runs on the GPU prover, once per history proof. Checking v5 and v6 transaction IDs on-chain means ZIP 244's BLAKE2b hashing. Facts about a shielded payment's amount or recipient need selective disclosure.