02 - Zcash protocol foundations
1. Why this chapter exists
Before any cryptography, a reader needs a working model of what a
Zcash node and wallet actually do: where value lives, how the
transaction format is structured, what a "network upgrade" implies
for parser dispatch, and how the four value pools relate. Without
this model, the type Transaction in
zcash_primitives/src/transaction/mod.rs
is just an opaque tag-union. By the end of this chapter you will be
able to map every field of that struct onto a consensus rule and
explain why
txid.rs
does not hash the wire bytes directly.
2. Definitions
Definition (UTXO). An unspent transaction output. In Zcash the
transparent pool inherits Bitcoin's UTXO model verbatim: an output
is an amount plus a scriptPubKey, and an input references an
earlier output by (txid, vout).
Definition (shielded value pool). A pool in which outputs are cryptographic commitments rather than plaintext addresses and amounts. Membership is tested through a Merkle inclusion proof against a per-pool note commitment tree; spending is proven in zero knowledge.
Definition (zatoshi). The base unit of Zcash value:
. The total supply ceiling
is zatoshi
(MAX_MONEY).
Definition (network upgrade). A change to consensus rules
identified by a BranchId (a 32-bit constant) and an activation
height. The BranchId value is hashed into every sighash and TxId,
so the same wire bytes produce different identifiers under different
network upgrades. This is two-way replay protection (ZIP 200).
Definition (anchor). A historical root of a note-commitment tree. A shielded spend references an anchor instead of a specific output; the zero-knowledge proof certifies that some note in the tree with that root is being spent without disclosing which one.
Definition (nullifier). A deterministic, per-note tag revealed on spend. The mapping note nullifier is a PRF keyed by the spending key, so an outside observer cannot link the two; consensus rejects any duplicate nullifier.
Invariant (transaction balance). A Zcash transaction is valid only if
with the surplus going to the miner as a fee. Per-pool balances and are public scalars in the transaction header and are enforced both by ZK proofs (inside the bundle) and by binding signatures (so that the published balance matches the implicit balance inside the proofs). Chapter 04 derives the math.
Definition (TxId). The transaction identifier. Since v5 (NU5) it is the root of a small BLAKE2b tree of personalised digests, not a flat hash of the serialised bytes. See Section 3.3.
3. The code
3.1 The four value pools
| Pool | Mechanism | Active since |
|---|---|---|
| Transparent | Bitcoin-style UTXOs | Genesis |
| Sprout | Original Zerocash, BCTV14 SNARKs, Groth16 after Sapling | Genesis |
| Sapling | Jubjub + BLS12-381 + Groth16 | Sapling NU (Oct 2018) |
| Orchard | Pallas / Vesta + Halo 2 | NU5 (May 2022) |
Sprout has been disabled for new outputs since NU5: Sprout-to-any is allowed, but nothing can be sent into Sprout. Sapling and Orchard coexist and are the active shielded pools.
3.2 The transaction shape
A v5 Zcash transaction carries:
- Header: version (5), version group ID, consensus branch ID, lock time, expiry height.
- Transparent bundle: vector of
TxIn(each a(prevout, scriptSig, sequence)) and vector ofTxOut(each a(value, scriptPubKey)). Bitcoin-style. - Sapling bundle: vector of Spend descriptions, vector of Output descriptions, a value-balance scalar , and a binding signature.
- Orchard bundle: vector of Action descriptions, a value-balance , flags, anchor, proof, and binding signature. Each Action description bundles one spend and one output; output-only and spend-only Actions use dummy notes.
The Sprout bundle is empty in practice for new transactions but the
format still permits up to one JsDescription (JoinSplit
description). v4 also exists and is still used for some
Sapling-only transactions.
Where this lives in the code:
zcash_primitives/src/transaction/mod.rsdefinesTxVersion,TransactionData, and theAuthorizationtrait family.zcash_primitives/src/transaction/components/sapling.rsdefines the Sapling serialization format and the v4/v5 split:
loading...
zcash_primitives/src/transaction/components/orchard.rsis the analogous file for Orchard.zcash_transparent/src/bundle.rsdefines the transparent bundle.
3.3 TxId is not a hash of the wire bytes
The TxId is computed by
to_txid
from per-bundle BLAKE2b sub-digests:
loading...
loading...
Concretely:
where each sub-digest is itself a BLAKE2b hash with a
domain-separator personalisation
(ZTxIdHeadersHash, ZTxIdTranspaHash, ZTxIdSaplingHash,
ZTxIdOrchardHash). The personalisation includes the consensus
branch ID, so the same wire bytes have different TxIds in different
network upgrades.
The motivation is twofold:
- Replay protection across forks.
- Cheap sighashes: a sighash is computed by replacing one sub-leaf of the TxId tree with a per-input commitment, far cheaper than recomputing a flat hash. See ZIP 244.
3.4 Network upgrades and BranchId
The canonical types live in
components/zcash_protocol/src/consensus.rs:
loading...
loading...
Almost every consensus rule is parameterised by BranchId. Sighash
code forks on it. Block-header commitment rules fork on it. When you
read
sighash_v4
versus
sighash_v5,
the branch ID is the discriminator. The match arms in
Transaction::read route to different parsers based on
(version, version_group_id), which encode the branch.
3.5 Anchors, Merkle trees, nullifiers
The set of unspent shielded outputs is not a UTXO set in the transparent sense. It is the set of commitments included in a global note-commitment tree (one per shielded pool):
- Sprout tree: depth 29, SHA-256 hashes.
- Sapling tree: depth 32, Pedersen-hash over Jubjub (chapter 04).
- Orchard tree: depth 32, Sinsemilla over Pallas (chapter 05).
A Spend in a shielded transaction does not point to a specific commitment; it points to an anchor, the root of the commitment tree as of some past block. The ZK proof certifies that there exists a leaf in the tree whose path leads to that anchor and whose spending key is known.
Spending a shielded note produces a nullifier that is revealed in the transaction. The mapping note nullifier is deterministic in the note and the spending key, but a third party cannot link them. Two valid spends of the same note would produce the same nullifier, so consensus simply requires that no nullifier appears twice.
Concretely, for Sapling:
where is the nullifier-deriving key derived from the spending key and is a unique per-note value derived from the commitment position. Orchard is similar but uses a Poseidon-based PRF.
The incremental Merkle tree library lives in
incrementalmerkletree
(a separate crate). The wallet uses
shardtree
for efficient checkpointed state.
3.6 How a wallet "sees" its money
Because outputs are commitments and recipients are not directly tagged, wallets must trial-decrypt every shielded output to find the ones addressed to them. The note encryption scheme (chapter 08) is designed so that trial decryption is fast and only succeeds for the intended recipient.
zcash_client_backend::scanning is the loop that does this. For
each block, for each shielded output, the wallet attempts
decryption with each of its incoming viewing keys. On success it
has the cleartext note (value, recipient diversifier, ,
randomness), which it can use to construct a spend later.
3.7 PCZT in one paragraph
Constructing a shielded transaction combines capabilities that may
live in different trust domains: key custody, proving parameters,
chain view, and the user-facing UI. The
pczt
crate generalises Bitcoin's PSBT to Zcash. A partially constructed
transaction flows through roles: Creator, Constructor, IO
Finalizer, Prover, Signer, Spend Authoriser, Combiner, Transaction
Extractor. Each role only needs its own slice of the data; this is
why hardware wallets, threshold signing, air-gapped signers, and
MPC are all supported.
4. Failure modes
A contributor changing transaction parsing or sighash code without understanding the layering can produce subtle, hard-to-catch bugs:
- Wrong
BranchIdrouting. Treating a v5 transaction with the SaplingBranchIdas if it were under NU5 leads to silently wrong sighashes; signatures verify nowhere and transactions get rejected after broadcast. The test vectors inzcash_primitives/src/transaction/tests.rsexercise every supported branch; run them before touchingTransaction::readorto_txid. - Flat-hash TxId. Re-introducing a hash of the wire bytes removes the personalisation-based replay protection. ZIP 244 is the law here; the TxId tree is not a refactoring opportunity.
- Mismatched value-balance signs.
v_balanceis signed; the sign convention is "input minus output". Builders that flip the sign produce bundles that pass internal checks but fail the binding signature at the consensus node. Seezcash_primitives/src/transaction/builder.rsfor the canonical sign handling, and the historical Sprout counterfeiting bug (CVE-2019-7167) documented in chapter 12 for the most expensive consequence of getting balance math wrong. - Anchor staleness. The wallet must pick a recent-enough anchor
(currently 10 blocks deep for Sapling/Orchard) but not so recent
that a reorg can invalidate it. Tests in
zcash_client_backend/src/data_apicover the boundary.
5. Spec pointers
- ZIP 200: network upgrade
mechanism, definition of
BranchId, two-way replay protection. - ZIP 225: the v5 transaction format introduced at NU5.
- ZIP 244: the TxId / sighash tree
whose layout
txid.rsimplements. Read it before changing any sub-digest. - ZIP 317: the fee algorithm. The
default fee is plus per-action increments;
see
zcash_primitives/src/transaction/fees.rs. - Zcash Protocol Specification, sections 3 and 4:
abstract protocol covering transactions, blocks, value pools,
trees, and nullifiers; the source from which
zcash_protocolandzcash_primitivesare derived.
6. Exercises
- Identify the parser dispatch. Read
Transaction::readinzcash_primitives/src/transaction/mod.rsand list every(version, version_group_id)pair the function accepts. State whichBranchIdeach maps to. - Trace a sighash. Pick
signature_hashinzcash_primitives/src/transaction/sighash.rsand follow the call chain until you reach the BLAKE2b personalisation constant used. Confirm that swapping the constant changes the sighash but not the wire format. - Modify and test. In a checkout, add a new debug-only
print!statement at the top ofto_txidthat logs the incomingconsensus_branch_id. Runcargo test -p zcash_primitives txidand confirm at least one test vector exercises a non-default branch. Remove the print before committing.
Answers in the code
- TxId construction:
zcash_primitives/src/transaction/txid.rs#L435-L452. - Sighash dispatch by version:
zcash_primitives/src/transaction/sighash.rs. BranchIdenum:components/zcash_protocol/src/consensus.rs#L701-L728.
7. Further reading
- chapter 07: the builder API and how it wires together every bundle.
- chapter 12: the Sprout counterfeiting CVE, dummy-note misuse, and Sapling InternalH; each is a real example of how getting Section 4 wrong cost real money.