Almost every blockchain you have ever read the source of solves the same problem: how do strangers agree on who owns what, without a referee? Bitcoin answered it by making ownership public. Every output, every amount, every link between them is a matter of permanent record, and the security of the system leans on exactly that visibility — anyone can add up the coins and check the books.
Zcash asks a harder question. Can a network verify a payment it is not allowed to see?
The answer is yes, and the machinery that makes it yes is one of the most interesting pieces of applied cryptography in production anywhere. This post is a ground-up tour of that machinery. We will start from why the problem matters, build the cryptographic toolbox one primitive at a time, lay out every data structure the protocol actually puts on disk and on the wire, and then walk two transactions end to end: one transparent, one shielded, from the moment a wallet is opened to the moment a recipient's balance updates.
I have tried to make this readable in order. Each section assumes only what came before it. If you already know Bitcoin's UTXO model, you can skim §1; if you already know commitment schemes and Merkle trees, skim §3. The two lifecycle sections (§7 and §8) are the heart of the post and are written to be followed line by line.
A note on timing. Zcash moved fast in 2026. A critical soundness bug was found in the Orchard circuit in May, patched in June, and answered in July with an entirely new shielded pool called Ironwood. A further upgrade, NU7, is targeted for November. This post describes the protocol as of September 2026, with the history included where it explains the design. §9 is a dedicated postmortem of the bug, because it is the single best case study in the ecosystem for what a zero-knowledge circuit actually guarantees — and what it does not.
Table of contents
- The problem: transparency is a design choice
- The philosophy: why encryption and not obfuscation
- The cryptographic toolbox
- Architecture: pools, turnstiles, and addresses
- The data structures
- Network infrastructure
- Lifecycle I: a transparent transaction, end to end
- Lifecycle II: a shielded transaction, end to end
- Case study: the Orchard soundness bug
- The road ahead
- Appendix: constants, glossary, sources
1. The problem: transparency is a design choice
1.1 Bitcoin did not fail at privacy — it never tried
It is worth being precise about this, because the usual framing ("Bitcoin is private-ish, Zcash is more private") gets the engineering backwards.
Bitcoin's validation rule is a readability rule. To check a transaction, a node must:
- look up each input's previous output in the UTXO set and confirm it exists and is unspent,
- run the input's script against the output's script and confirm it succeeds,
- add up input values, add up output values, and confirm
sum(inputs) >= sum(outputs).
Every one of those steps is a read of plaintext. The UTXO set is a plaintext key-value store. The amounts are 8-byte little-endian integers sitting in the clear. The scripts name public key hashes. Validation is reading, and so a design in which validation is possible for everyone is necessarily a design in which reading is possible for everyone.
Satoshi noticed the consequence and wrote a mitigation into section 10 of the Bitcoin whitepaper: use a fresh address for every transaction. That mitigation does not survive contact with the UTXO model. The moment you spend two outputs in one transaction, you have published the claim "the same entity controls both of these," and the entire heuristic edifice of chain analysis — common-input-ownership, change-output detection, round-number heuristics, temporal clustering, exchange deposit tagging — is built on that single leak. By 2014 this was a commercial product category.
1.2 Pseudonymity is a decaying asset
Here is the part that matters for anyone designing a system meant to last:
A transparent ledger is a permanent, append-only corpus that adversaries get to analyse forever, with tools that only improve. The data is written once; the analysis is rewritten every year. A transaction graph published in 2016 is being analysed in 2026 with techniques — and compute budgets — that did not exist when it was published. Nothing about your 2016 privacy improves with time; everything about the adversary's capability does.
This is a one-way ratchet, and it means a privacy property that is statistical has a shelf life. If your anonymity comes from "there were sixteen plausible senders," then the sixteen only need to be narrowed once, retroactively, and every transaction you ever made under that assumption is re-exposed. If your anonymity comes from "the amount is encrypted under a key nobody has," there is nothing to narrow.
1.3 What a shielded ledger has to accomplish
So the design target is a ledger where a node can still answer all three validation questions —
- Does the input exist?
- Has it been spent before?
- Does the value balance?
— while the answers to which input, whose input, and how much are cryptographically unavailable to that node.
Stated that plainly it sounds impossible. It is not. The trick, in one sentence, is: replace each plaintext read with a zero-knowledge proof about a commitment. The rest of this post is the detail of how.
2. The philosophy: why encryption and not obfuscation
Zcash's architecture is downstream of a specific philosophical position. It is worth spelling out, because it is the reason the protocol looks the way it does rather than like a mixer, a ring signature scheme, or a compliance-gated pool.
2.1 Privacy is not secrecy
Secrecy is nobody may know. Privacy is I choose who knows.
That distinction is load-bearing in Zcash's design, not decorative. The protocol ships a key hierarchy (§5.4) in which viewing authority and spending authority are separate, and viewing authority is further split into incoming and outgoing. You can hand an auditor a full viewing key and they can reconstruct your entire history — every amount, every counterparty, every memo — without being able to move a single zatoshi. You can hand a payment processor an incoming viewing key and they can see money arrive without learning where you send it.
This is why "if you have nothing to hide" misses. The system is not built to make disclosure impossible; it is built to make disclosure yours to authorise. Default-open with opt-in secrecy and default-closed with opt-in disclosure are not the same system with the defaults flipped — they have different failure modes, and only one of them fails safe.
2.2 Privacy is binary, so partial privacy is a countdown
Information does not un-leak. That has a sharp consequence: a system that leaks a small, bounded amount per transaction does not provide "most" privacy, it provides delayed exposure.
Think of it as an entropy budget. Suppose your identity, among plausible candidates, carries $H_0$ bits of uncertainty from an observer's perspective. Each transaction leaks some $\delta_i$ bits — a timing correlation, an amount pattern, a fee fingerprint, a network-layer address. Exposure is not a question of whether any single $\delta_i$ is small; it is the question of whether
$$H_0 - \sum_i \delta_i > 0$$
still holds after $n$ transactions and $t$ years of improved analysis. Since $n$ grows with use and the effective $\delta_i$ grow with adversary capability, any system with $\delta_i > 0$ has a finite lifetime. The design goal has to be $\delta_i = 0$ for the data that matters.
2.3 Obfuscation vs. encryption
This gives a clean taxonomy of every privacy technology in the space:
| Approach | Mechanism | Anonymity set | Degrades over time? |
|---|---|---|---|
| Fresh addresses | Naming discipline | 1 (graph still public) | Already broken |
| Mixers / tumblers | Break the link by pooling | Pool depth at time of use | Yes — timing & amount analysis |
| Ring signatures | Hide the real input among $k$ decoys | $k$ (e.g. 16) | Yes — decoys can be eliminated |
| Encryption + ZK proofs | The data is absent, not hidden | Every note ever created | No |
The row that matters is the last one. In a decoy-based scheme, privacy is a search problem for the adversary: the real spend is in there, and eliminating candidates is progress. In Zcash's shielded pools, the spend is proven to be one of the entire commitment tree with no hint of which, so there is no elimination procedure to run — an observer who learns that some note in a tree of tens of millions was spent has learned a fact with no exploitable structure.
The security argument changes category with it. Mixer privacy is economic ("it costs more to deanonymise than it's worth"). Encryption privacy is mathematical ("recovering it is equivalent to breaking a hardness assumption"). Economic security erodes as compute gets cheaper. Mathematical security does not, until the assumption itself falls — and when the assumption is at risk, you can see it coming and migrate, which is exactly what §10.3 is about.
2.4 The auditability problem, and the honest answer to it
Here is the strongest objection to encrypted money, and it deserves a real answer rather than a dismissal:
If balances are hidden, how does anyone know the supply is correct? A transparent chain lets you add up every coin. An encrypted one does not. If a bug let someone mint from nothing, you would never see it.
This is not a hypothetical, and Zcash has now lived through it twice (§9). The protocol's answer is the turnstile (ZIP 209), and it is a genuinely elegant piece of design.
Each value pool keeps a running, consensus-enforced balance: the total value that has entered minus the total that has left. Crossing between pools is always a public, plaintext amount — when you shield 5 ZEC, the chain records "+5 into the shielded pool" in the clear, and when you unshield, it records the withdrawal. Consensus then enforces the invariant:
$$\text{ChainValuePoolBalance}_{\text{pool}} \ge 0 \quad \text{at every block, for every pool}$$
So counterfeiting inside a pool is possible in principle if the cryptography breaks, but cashing it out is not. The forged value is trapped behind an accounting gate that no proof can talk its way past, because the gate does not check a proof — it checks arithmetic on public numbers. The blast radius of a total cryptographic failure in a shielded pool is bounded by what legitimately entered that pool.
That is the honest answer: Zcash does not claim its cryptography cannot fail. It claims the failure cannot inflate the money supply beyond a publicly computable bound. Which, as we will see in §9, is precisely what saved it.
2.5 Criticisms worth taking seriously
Good engineering writing does not skip the objections. Four that hold up:
- Trusted setup (historical). Sprout and Sapling used Groth16, which needs a structured reference string. If every participant in the ceremony colluded and kept their toxic waste, they could forge proofs. The ceremonies were elaborate and multi-party, but "believe at least one of $N$ was honest" is a real trust assumption. Orchard removed this: Halo 2 needs no trusted setup, and both Orchard and Ironwood inherit that property.
- Adoption asymmetry. A privacy set is only as good as the population using it. For years most ZEC sat transparent because shielding was slow and exchanges required t-addresses. Orchard plus modern wallets that shield by default changed the economics; the shielded fraction grew several-fold across 2025. But the criticism was correct when it was made.
- Protocol funding. 20% of the block subsidy is directed by consensus rules to development funding. Whether that is prudent long-termism or a tax is a genuine political disagreement, not a technical one. What is not true is the "premine" claim — no coins existed before the genesis block; the allocation is a split of ongoing issuance on the same schedule as miner rewards.
- Complexity is attack surface. This one got expensive. Orchard's circuit is thousands of constraints, each of which must be exactly right; a single missing equality constraint went unnoticed for four years through multiple professional audits (§9). Encryption buys you privacy that does not decay, and pays for it in a much larger and much subtler trusted computing base.
3. The cryptographic toolbox
Before any data structure makes sense, you need five primitives. I will introduce each with the property that Zcash actually relies on, because the property is what the protocol's security argument uses — not the name of the algorithm.
3.1 Hash functions and PRFs
A cryptographic hash $H : \{0,1\}^* \to \{0,1\}^n$ gives us three properties Zcash leans on:
- Preimage resistance — given $H(x)$, finding $x$ is infeasible. (This is what makes a commitment hiding, given enough entropy in $x$.)
- Collision resistance — finding $x \ne x'$ with $H(x) = H(x')$ is infeasible. (This is what makes the Merkle tree binding.)
- Pseudorandomness under a key — $\text{PRF}_k(\cdot)$ is indistinguishable from a random function to anyone without $k$. (This is what makes nullifiers unlinkable.)
Zcash uses several, each chosen for a different reason:
| Function | Used for | Why this one |
|---|---|---|
| BLAKE2b-512 / BLAKE2s | PRF^expand, txid and sighash digests, key derivation | Fast, personalisable (16-byte domain separator baked into the compression function), well analysed |
| SHA-256d | Transparent txids, block header hashing, Equihash internals | Bitcoin inheritance |
| Sinsemilla | Note commitments, Commit^ivk, MerkleCRH | Cheap inside a PLONKish circuit — collision resistance reduces to discrete log on Pallas |
| Poseidon | PRF^nf (the nullifier PRF) | Algebraic; extremely few constraints in-circuit |
| BLAKE3 | H^qk in quantum-recoverable key derivation (ZIP 2005) | Modern, fast, tree-hash structure |
That split — one family for out-of-circuit work, another for in-circuit work — is the single most important implementation detail to internalise. Constraint count is the currency of ZK system design. SHA-256 inside a circuit costs tens of thousands of constraints; Sinsemilla and Poseidon cost tens to hundreds. Zcash pays for that efficiency with a weaker assumption (discrete log rather than "it's a good hash"), and §10.3 explains why that bill eventually comes due.
PRF^expand deserves a specific mention because it shows up everywhere in §8. It is:
$$\text{PRF}^{\text{expand}}_{sk}(t) = \text{BLAKE2b-512}(\texttt{"Zcash\_ExpandSeed"},\; sk \,\|\, t)$$
where t is a domain-separation tag beginning with a distinguishing byte. Every distinct derivation from a seed uses a distinct first byte — 0x06 for ask, 0x07 for nk, 0x08 for rivk, 0x04 for esk, 0x09 for ψ, 0x0B for the quantum-recoverable rcm — so all of these are independent random-looking outputs of the same seed. This is how one 32-byte secret expands into an entire key tree deterministically.
3.2 Commitments
A commitment scheme lets you publish a value that pins down a secret without revealing it. Formally, $\text{Com}(m; r)$ for message $m$ and randomness $r$, with two properties:
- Hiding — $\text{Com}(m; r)$ reveals nothing about $m$ when $r$ is uniform and secret.
- Binding — you cannot find $(m, r) \ne (m', r')$ with $\text{Com}(m; r) = \text{Com}(m'; r')$.
You cannot have both unconditionally; one is computational. Zcash's note commitments are perfectly hiding, computationally binding, which is the right way round: a broken binding assumption means someone can forge (bounded by the turnstile), while a broken hiding assumption would retroactively deanonymise everyone. You want the property that cannot be repaired to be the unconditional one.
Sinsemilla is the workhorse. It is a Pedersen-style hash specialised for lookup-argument-friendly arithmetisation:
$$\text{SinsemillaHashToPoint}(D, M) = \text{a point built by incomplete additions over 10-bit chunks of } M$$
and the commitment form adds a randomisation term:
$$\text{SinsemillaCommit}_r(D, M) = \text{SinsemillaHashToPoint}(D, M) + [r]\,\mathcal{R}$$
Because it is Pedersen-shaped, its collision resistance is exactly the hardness of finding a nontrivial discrete-log relation among the Sinsemilla bases on Pallas. Remember that sentence; it comes back in §10.3.
Value commitments are a separate, simpler thing, and they are homomorphic on purpose:
$$\text{ValueCommit}_{rcv}(v) = [v]\,\mathcal{V} + [rcv]\,\mathcal{R}$$
Homomorphic means $\text{Com}(v_1) + \text{Com}(v_2) = \text{Com}(v_1 + v_2)$ (with randomness adding too). That is the entire trick behind how Zcash proves balance without a proof: a verifier can add up the value commitments of every action and check the sum against a public net value, all in the clear, on encrypted amounts. §8, Step 7 does this arithmetic concretely.
3.3 Merkle trees, and the incremental variant
A Merkle tree turns a set into a single 32-byte root such that membership in the set can be proven with $\log_2 N$ hashes.
root
/ \
H0 H1
/ \ / \
cm0 cm1 cm2 cm3
To prove cm1 is in the tree you publish the authentication path [cm0, H1], and the verifier recomputes H(cm0, cm1) = H0, then H(H0, H1) and checks it equals the root. The verifier never learns cm2 or cm3.
Zcash uses a fixed-depth, append-only, incremental Merkle tree: depth 32 (capacity $2^{32} \approx 4.29$ billion notes), positions filled left to right, unfilled positions holding a fixed "uncommitted" value. Appending is $O(\log N)$ and needs only the tree's frontier — the rightmost path — not the whole tree. The root at a given block is called an anchor.
The ZK version of the above adds one crucial twist: the path is a private input. The circuit takes (leaf, position, path) as witness and the root as a public input, recomputes the root inside the circuit, and constrains it to equal the public anchor. The verifier learns that some leaf is in the tree at the given anchor, and nothing more.
Coming from Ethereum? The closest analogue is a stateless-validation witness — a Merkle path proving state membership that travels with the transaction. The difference is that here the path is never revealed: it goes into the witness of a SNARK, and the block carries the proof rather than the path. That is roughly the endpoint that stateless Ethereum designs aim at when they talk about proof-carrying blocks.
3.4 The Pallas/Vesta curve cycle
Orchard (and therefore Ironwood) is built over two curves:
| Base field modulus | Scalar field (group order) | |
|---|---|---|
| Pallas | $q_\mathbb{P}$ = 0x4000...224698fc094cf91b992d30ed00000001 | $r_\mathbb{P}$ = 0x4000...224698fc0994a8dd8c46eb2100000001 |
| Vesta | $r_\mathbb{P}$ | $q_\mathbb{P}$ |
Both primes are 255 bits. The two curves form a 2-cycle: Pallas's base field is Vesta's scalar field and vice versa. Neither is pairing-friendly, which is exactly why Halo 2 — which does not need pairings — can use them.
Why a cycle? Because a circuit computes over its scalar field while curve arithmetic happens over the base field. If you want to verify a proof inside another proof (recursion), the two fields must line up, and a cycle makes them line up perfectly with no expensive non-native field emulation. Recursion is what Halo 2's accumulation scheme uses to amortise away the trusted setup, and it is the foundation Tachyon's proof-carrying data design (§10.2) builds on.
Application-layer objects live on Pallas: note commitments are Pallas points, ak and nk are Pallas field elements, addresses are Pallas points. The proof system's internal polynomial commitments live on Vesta.
3.5 zk-SNARKs: what is actually being promised
A zk-SNARK for a relation $\mathcal{R}$ lets a prover convince a verifier that they know a witness $w$ such that $(x, w) \in \mathcal{R}$, for public input $x$, with three properties:
- Completeness — an honest prover with a valid witness always convinces the verifier.
- Knowledge soundness — a prover who convinces the verifier must know a valid witness. (This is the property that failed in §9.)
- Zero knowledge — the proof reveals nothing about $w$ beyond the truth of the statement.
Plus the "SN" part: succinct (proof is small and verification is fast, sub-linear in the computation) and non-interactive (one message, no back-and-forth).
| Sapling | Orchard / Ironwood | |
|---|---|---|
| System | Groth16 | Halo 2 |
| Arithmetisation | R1CS | PLONKish (with custom gates + lookups) |
| Curve | BLS12-381 | Pallas / Vesta |
| Trusted setup | Required (Powers of Tau + circuit-specific) | None |
| Proof size | 192 bytes per Spend/Output | 2720 + 2272·n bytes for n actions |
| Verification | Pairing check, ~ms | Inner-product argument, ~ms |
Two things to note. First, Halo 2's proof is larger than Groth16's but requires no ceremony — a deliberate trade of bytes for the elimination of a trust assumption. Second, the Orchard proof size formula is sub-linear per action in the constant: a 2-action bundle costs 7,264 bytes but the marginal action is only 2,272, because a single aggregated proof covers the whole bundle.
The critical mental model for §9: a SNARK proves exactly what the circuit constrains, and nothing about what the circuit meant to constrain. A circuit is a specification written in equations, and a missing equation is a missing rule — the proof system will happily and correctly prove the weaker statement forever.
3.6 Signatures
Three signature schemes appear:
- ECDSA over secp256k1 — transparent inputs, inherited from Bitcoin. Same as everywhere else.
- RedPallas (a RedDSA instantiation over Pallas) — shielded spend authorisation and binding signatures.
- RedJubjub — the Sapling equivalent, over the Jubjub curve.
RedDSA has a property Zcash exploits heavily: key re-randomisation. Given a public key $ak$ and a random $\alpha$, you can compute
$$rk = ak + [\alpha]\,\mathcal{G}$$
and produce a signature valid under $rk$ using $ask + \alpha$. The signature verifies under $rk$, which is what goes on chain — and $rk$ is statistically independent of $ak$. So the chain sees a fresh, unlinkable verification key on every spend, while the circuit proves that $rk$ was derived from the right $ak$ for the note. Authorisation without identity.
The binding signature is stranger and cleverer: it is a signature whose verification key is computed by the verifier from the transaction's own value commitments. If the amounts balance, the key the verifier derives is $[\sum rcv]\,\mathcal{R}$, for which the prover knows the discrete log (it is the sum of randomness they chose). If the amounts do not balance, the derived key has an unknown discrete log and no signature can exist. It is a proof of balance dressed up as a signature — and it costs zero circuit constraints, because it happens entirely outside the proof.
4. Architecture: pools, turnstiles, and addresses
4.1 Two ledgers sharing a currency
Zcash is best understood as two ledgers that share a unit of account and a consensus layer:
┌──────────────────────────────────────┐
│ ZCASH BLOCK │
└──────────────────────────────────────┘
│
┌───────────────────────┴───────────────────────┐
│ │
╔══════════▼═══════════╗ ╔═══════════▼══════════╗
║ TRANSPARENT POOL ║ ║ SHIELDED POOLS ║
║ ║ ◄── turnstile ──► ║ ║
║ UTXO set ║ (public amounts) ║ note commitment tree║
║ t1.../t3... addrs ║ ║ nullifier set ║
║ Bitcoin scripts ║ ║ u1... addrs ║
║ amounts in cleartext║ ║ everything encrypted║
╚══════════════════════╝ ╚══════════════════════╝
The transparent side is Bitcoin, essentially unmodified: a UTXO set, Script, P2PKH and P2SH, 8-byte values in the clear. The shielded side is a completely different machine: no UTXO set, no addresses on chain, no amounts on chain — instead, a growing Merkle tree of note commitments and a growing set of nullifiers.
A vital point, frequently gotten wrong: the two ledgers are cryptographically independent. The privacy of a shielded transaction does not depend on how much ZEC sits transparent. Your anonymity set is "every note ever created in that pool", a quantity that has nothing to do with the transparent UTXO set's size. If 99% of ZEC were transparent, the shielded 1% would have exactly the anonymity set it has today.
4.2 Pool lineage
There have been four shielded pools. They are not versions of one thing; each is a distinct value pool with its own tree, its own nullifier set, and its own accounting.
| Pool | Since | Proof system | Curve | Trusted setup | Status today |
|---|---|---|---|---|---|
| Sprout | 2016 (genesis) | BCTV14 → Groth16 | BN-254 / BLS12-381 | Yes | Exit-only since ZIP 211; ~40s to make a tx |
| Sapling | 2018 (Sapling upgrade) | Groth16 | BLS12-381 (+Jubjub) | Yes | Supported, being phased out |
| Orchard | 2022 (NU5) | Halo 2 | Pallas / Vesta | No | Sealed at NU6.3 — exit-only |
| Ironwood | 2026 (NU6.3) | Halo 2 | Pallas / Vesta | No | Current destination for all new shielded value |
The 2018→2022 arc is a straightforward performance and trust story: Sprout needed ~40 seconds and gigabytes of RAM per transaction; Sapling cut that to a few seconds and tens of megabytes, making mobile shielded payments viable; Orchard removed the trusted setup entirely and unified spends and outputs into a single action structure.
Ironwood is different in kind. It is not a new cryptographic protocol — it reuses Orchard's Action encoding, Halo 2 proof system, note structure, key structure, and (with one small change) circuit. What makes it a separate pool is purely state: its own note commitment tree, its own nullifier set, its own chain value pool balance, its own chain-history metadata. It was created as the answer to the Orchard bug (§9): a pool that starts empty and provably clean, so that supply integrity becomes independently verifiable again.
4.3 The turnstile in practice
Every value pool has a consensus-tracked balance. Moving value between pools is always a plaintext event:
shield: transparent ──── +5.0 ZEC (public) ────► Ironwood
unshield: Ironwood ──── −2.0 ZEC (public) ────► transparent
migrate: Orchard ──── +1.0 ZEC (public) ────► Ironwood
internal: Ironwood ──── (nothing public) ────► Ironwood
In transaction terms, this is what valueBalanceOrchard / valueBalanceIronwood / valueBalanceSapling encode: the net value the pool loses to the rest of the transaction. Positive means value is leaving the pool; negative means value is entering. Consensus sums these across a block, updates the pool balances, and rejects any block that would push any pool balance below zero.
This is worth sitting with, because it is the entire auditability story:
- Inside a pool, nobody can see anything.
- Across a pool boundary, everybody sees the exact amount.
- Therefore total ZEC in any pool has a public upper bound, forever.
Ironwood weaponised this. At NU6.3, the Orchard pool was sealed: no new value may enter, and the circuit now forbids ordinary payments within Orchard (§9.4). Everything in Orchard may only leave via the turnstile, which caps total Orchard outflow at total historical Orchard inflow. Any counterfeit ZEC that might hypothetically exist in Orchard is therefore permanently trapped — it can never reach Ironwood, Sapling, or the transparent pool.
4.4 Addresses
| Type | Prefix | Encoding | What it is |
|---|---|---|---|
| Transparent P2PKH | t1... | Base58Check, version 0x1CB8 | HASH160(pubkey) |
| Transparent P2SH | t3... | Base58Check, version 0x1CBD | HASH160(redeemScript) |
| Sapling | zs1... | Bech32 | (d, pk_d) on Jubjub |
| Unified | u1... | Bech32m (ZIP 316) | A bundle of receivers |
| TEX | tex1... | Bech32m (ZIP 320) | t-addr that only accepts transparent sends |
Modern Zcash uses unified addresses almost exclusively. A u1... address is not one address; it is a typed, length-prefixed list of receivers — say an Orchard receiver plus a Sapling receiver plus a transparent receiver — wrapped in an F4Jumble permutation (so that truncating the string corrupts it globally rather than leaking a prefix) and Bech32m-encoded.
The sender's wallet picks the best receiver it supports. This is the mechanism that made the Ironwood migration painless: existing u1... addresses keep working unchanged, and upgraded wallets simply route new payments to the Ironwood pool behind the same address string. Users were never asked to rotate addresses.
Within a pool, addresses are diversified. From one full viewing key you can derive billions of distinct-looking addresses (§5.5) that all receive to the same wallet and are mutually unlinkable to anyone without your viewing key. Give a different one to your employer, your exchange, and each client, and they cannot collude by comparing strings.
5. The data structures
This is the reference section. Everything in §7 and §8 refers back here.
5.1 The note
A note is the shielded analogue of a UTXO: a discrete, immutable chunk of value. Notes are never modified — only created and destroyed.
An Orchard/Ironwood note is the tuple $(\mathsf{addr}, v, \rho, \psi, \mathsf{rcm})$:
{
"addr": "u1alice...", // recipient address = (d, pk_d)
"v": 300000000, // value in zatoshi (3.0 ZEC)
"rho": "0x7a8b9c...", // uniqueness seed — see §5.3
"psi": "0x1d2e3f...", // derived randomness, feeds the nullifier
"rcm": "0x4a5b6c...", // commitment randomness (the blinding factor)
"position": 847291, // leaf index in the note commitment tree
"cmx": "0x9f8e7d..." // the on-chain commitment (derived, not stored in the note)
}
The fields split into three roles:
addrandvare the payment semantics: who and how much.rcmis the blinding factor that makes the commitment hiding. Two notes paying Alice 5 ZEC produce completely different commitments becausercmdiffers.rhoandpsiexist for nullifier purposes.rhoguarantees uniqueness (§5.3);psiis derived randomness that enters both the commitment and the nullifier.
Only addr, v, and the seed material actually travel; rcm and psi are derived from a 32-byte rseed that is what gets encrypted, which is why the note plaintext is compact.
5.2 Note commitment
The commitment is what goes on chain. For Orchard and Ironwood:
$$\mathsf{cm} = \mathsf{NoteCommit}^{\mathsf{Orchard}}_{\mathsf{rcm}}\!\left(\mathsf{g}^\star_\mathsf{d},\ \mathsf{pk}^\star_\mathsf{d},\ v,\ \rho,\ \psi\right)$$
$$\mathsf{cm_x} = \mathsf{Extract}_\mathbb{P}(\mathsf{cm})$$
NoteCommit is a Sinsemilla commitment, so cm is a Pallas point; Extract takes its $x$-coordinate, giving the 32-byte cmx that becomes a leaf of the tree. (Two points share an $x$-coordinate, which is harmless here and is accounted for in the security proofs.)
The two properties we need, restated in protocol terms:
- Hiding —
cmxreveals nothing about the recipient, value, or any other field. - Binding — you cannot produce two distinct notes with the same
cmx, so a commitment pins down exactly one note.
rcm derivation is where Ironwood differs from Orchard, and it is the whole of ZIP 2005. Both derive rcm from the note's rseed, but:
Orchard (lead byte 0x02): rcm = ToScalar( PRF^expand_rseed( [0x05] || ρ ) )
Ironwood (lead byte 0x03): rcm = ToScalar( PRF^expand_rseed( pre_rcm ) )
where pre_rcm = [0x0B] || g*_d || pk*_d || v || ρ || ψ
In Orchard, rcm depends only on rseed and ρ. In Ironwood, it depends on every field of the note. Why that matters is subtle and beautiful: a Pedersen-style commitment is only computationally binding, and an adversary who could compute discrete logs could open the same cm to a different note. But if rcm is a hash of all the note's fields, then changing any field changes rcm, which changes cm — and no discrete-log ability helps you, because BLAKE2b is not an elliptic curve. This makes the commitment scheme post-quantum binding, which is the foundation of the future Recovery Protocol (§10.3).
5.3 Nullifiers
Commitments solve "does this note exist?". Nullifiers solve "has it been spent?".
The naive approach — point at the commitment you are spending — destroys privacy completely: it publicly links the spend to the creation. So instead, each note has a derived value that only its owner can compute:
$$\mathsf{nf} = \mathsf{DeriveNullifier}_{\mathsf{nk}}(\rho, \psi, \mathsf{cm}) = \mathsf{Extract}_\mathbb{P}\!\left(\left[\left(\mathsf{PRF}^{\mathsf{nf}}_{\mathsf{nk}}(\rho) + \psi\right) \bmod q_\mathbb{P}\right]\mathcal{K} + \mathsf{cm}\right)$$
where nk is the nullifier deriving key (a secret, §5.4), $\mathcal{K}$ is a fixed independent generator, and $\mathsf{PRF^{nf}}$ is Poseidon.
Read the properties off the formula:
- Deterministic. Given the same note and the same
nk, you always get the samenf. So spending twice publishes the same value twice, and the second is rejected. This is the double-spend defence. - Unlinkable. Without
nk, the map fromcmtonfis a PRF output — indistinguishable from random. Nobody can compute your note's nullifier, and nobody can invert a published nullifier back to a commitment. - Unique per note, not per key.
cmis inside the formula, so two distinct notes never collide even under one key.
And ρ is the crucial anti-Faerie-Gold defence. In Orchard and Ironwood, the ρ of each output note is set to the nullifier of the note spent in the same action:
$$\rho_{\text{new}} := \mathsf{nf}_{\text{old}}$$
Since nullifiers are globally unique (they can each appear on chain only once), every note in history gets a unique ρ, for free, with no randomness to get wrong. This closes an attack class where a malicious sender creates two notes that collide in a way that lets them block the recipient from spending.
The two sets have opposite shapes, and it matters operationally:
| Note commitment tree | Nullifier set | |
|---|---|---|
| Structure | Append-only Merkle tree, depth 32 | Unordered set |
| Grows by | one leaf per output | one element per spend |
| Shrinks | never | never |
| Purpose | prove existence | prove non-reuse |
| Queried by | Merkle path (in circuit) | membership check (in the clear) |
Both grow forever. The nullifier set in particular is a state-growth problem exactly analogous to Ethereum's — every validator must retain every nullifier ever revealed, indefinitely, or it cannot detect a double spend. At 100 shielded transactions per second that is roughly a gigabyte of new permanent state per day. Fixing it is Tachyon's headline motivation (§10.2).
5.4 The key tree
Bitcoin has one private key and one public key. Zcash has a hierarchy, because it separates spending, viewing incoming, viewing outgoing, and marking spent.
spending key (sk) ← 32 bytes; the master secret
│
├─► ask (spend authorizing key) ← signs spend authorisations
│ └─► ak = Extract(DerivePublic(ask)) (spend validating key)
│
├─► nk (nullifier deriving key) ← computes nullifiers
│
└─► rivk (Commit^ivk randomness)
│
└─► ivk = Commit^ivk_rivk(ak, nk) (incoming viewing key)
│
├─► dk (diversifier key)
│ └─► d ──► g_d = DiversifyHash(d)
│
└─► pk_d = [ivk] · g_d ← the diversified transmission key
│
└─► address = (d, pk_d)
full viewing key (fvk) = (ak, nk, rivk) ← sees everything, spends nothing
outgoing viewing key (ovk) ⊂ fvk ← decrypts what you sent
What each level can do:
| Key | Detect incoming | Decrypt incoming | See outgoing | Compute nullifiers | Spend |
|---|---|---|---|---|---|
ivk | ✅ | ✅ | ❌ | ❌ | ❌ |
ovk | ❌ | ❌ | ✅ | ❌ | ❌ |
fvk = (ak, nk, rivk) | ✅ | ✅ | ✅ | ✅ | ❌ |
sk | ✅ | ✅ | ✅ | ✅ | ✅ |
The fvk row is the "selective disclosure" of §2.1 made concrete. An auditor with your fvk can see every note you have received and computes every nullifier, so they can tell which notes you have spent and reconstruct your complete financial history — and they still cannot move a coin, because spending requires ask, which fvk does not contain.
Note the relation $\mathsf{pk_d} = [\mathsf{ivk}]\,\mathsf{g_d}$. Every address you own is your one ivk applied to a different base point. That single equation is what binds an address to a viewing key — and it is precisely the equation the Orchard circuit failed to enforce (§9).
5.5 Diversified addresses
$\mathsf{g_d} = \mathsf{DiversifyHash}(d)$ maps an 11-byte diversifier to a Pallas point. In Orchard (unlike Sapling), every 11-byte string produces a valid point, so diversifiers can simply be enumerated: $d_0, d_1, d_2, \dots$ typically derived from dk via a PRF.
d = 0x00000000000000000000 → g_d0 → pk_d0 = [ivk]·g_d0 → u1employer...
d = 0x00000000000000000001 → g_d1 → pk_d1 = [ivk]·g_d1 → u1client...
d = 0x00000000000000000002 → g_d2 → pk_d2 = [ivk]·g_d2 → u1exchange...
All three receive to the same wallet. Given only two addresses, deciding whether they share an ivk is a decisional Diffie–Hellman problem — infeasible. Your employer and your exchange cannot tell they are paying the same person.
5.6 The Action
Orchard's headline structural innovation. Sapling had separate SpendDescription and OutputDescription arrays, so the shape of a transaction (3 spends, 1 output) leaked information. Orchard forces a 1:1 pairing: every action contains exactly one spend and exactly one output, either of which may be a dummy — a zero-valued note that exists only to fill the slot and is indistinguishable on chain from a real one.
A 2-in-1-out payment becomes a 2-action bundle where one output is a dummy. A 1-in-2-out payment becomes a 2-action bundle where one spend is a dummy. An observer sees "two actions" and learns nothing else.
The wire format is exactly 820 bytes:
| Field | Bytes | Meaning |
|---|---|---|
cv | 32 | Value commitment for this action's net value |
nullifier | 32 | Nullifier of the note being spent |
rk | 32 | Re-randomised spend-authorisation validating key |
cmx | 32 | Commitment of the note being created |
ephemeralKey | 32 | $\mathsf{epk}$ for note encryption |
encCiphertext | 580 | Encrypted note plaintext (for the recipient) |
outCiphertext | 80 | Encrypted recovery data (for the sender) |
| total | 820 |
The 580 breaks down as a 564-byte plaintext plus a 16-byte Poly1305 tag, and the plaintext is:
1 byte leadByte 0x02 = Orchard, 0x03 = Ironwood (quantum-recoverable)
11 bytes d diversifier
8 bytes v value, little-endian
32 bytes rseed seed from which rcm, psi, esk are derived
512 bytes memo arbitrary payload (ZIP 302 conventions)
─────────
564 bytes
Ironwood notes are only ever lead byte 0x03; Orchard notes are never 0x03. That one byte is the sole note-level difference between the two pools.
5.7 Transactions: v5 and v6
A v5 transaction (ZIP 225, live since NU5) has four zones: common header, transparent, Sapling, Orchard.
┌─ common ────────────────────────────────────────────────┐
│ header (4) nVersionGroupId (4) nConsensusBranchId (4) │
│ lock_time (4) nExpiryHeight (4) │
├─ transparent ───────────────────────────────────────────┤
│ tx_in_count tx_in[] tx_out_count tx_out[] │
├─ Sapling ───────────────────────────────────────────────┤
│ nSpendsSapling vSpendsSapling[96 each] │
│ nOutputsSapling vOutputsSapling[756 each] │
│ valueBalanceSapling (8) anchorSapling (32) │
│ vSpendProofsSapling[192] vSpendAuthSigsSapling[64] │
│ vOutputProofsSapling[192] bindingSigSapling (64) │
├─ Orchard ───────────────────────────────────────────────┤
│ nActionsOrchard vActionsOrchard[820 each] │
│ flagsOrchard (1) valueBalanceOrchard (8) │
│ anchorOrchard (32) │
│ sizeProofsOrchard proofsOrchard[2720 + 2272·n] │
│ vSpendAuthSigsOrchard[64 each] bindingSigOrchard (64) │
└──────────────────────────────────────────────────────────┘
v6 (ZIP 229, live since NU6.3) appends a fifth zone — the Ironwood component — with byte-identical structure to the Orchard one, and makes two further changes:
flagsOrchardandflagsIronwoodgain a third bit,enableCrossAddress(bit 2). Bits 0 and 1 remainenableSpendsandenableOutputs.- Anchors move from effecting data to authorizing data. In v5, the anchor is committed by the txid and by the signature hash, so choosing a different anchor means re-signing. In v6, the anchor lives in the auth digest instead, so a transaction can be signed once and then have its anchor and proof updated later without changing the txid.
That second change sounds like plumbing and is not. It is what makes the privacy-preserving Orchard→Ironwood migration (§10.1) implementable at all: a wallet can pre-sign an entire schedule of migration transactions in one signing session, then re-anchor and re-prove each one at broadcast time hours later. Penumbra is credited in the ZIP for demonstrating the idea.
Concrete sizes, computed from the formulas above:
| Transaction | Size |
|---|---|
| Transparent, 1-in 2-out P2PKH | ~240 bytes |
| Sapling, 2-in 2-out | ~2,729 bytes |
| Orchard/Ironwood, 2 actions | 9,165 bytes (of which 7,264 is proof) |
| Orchard/Ironwood, 4 actions | 15,477 bytes |
| v6 migration (2 Orchard + 1 Ironwood action) | 15,150 bytes |
A minimal shielded transaction is roughly 38× the size of a minimal transparent one. That cost is the price of the privacy, and shrinking it is the third leg of Tachyon.
5.8 Transaction identifiers (ZIP 244)
A subtlety that trips people coming from Bitcoin: in v5+ Zcash, the txid is not the hash of the serialised transaction. It is the root of a tree of digests, deliberately split so that the txid commits only to effecting data (what the transaction does) and a separate auth digest commits to authorizing data (proofs and signatures).
txid_digest_v6 = BLAKE2b-256("ZcashTxHash_" || consensusBranchId,
header_digest ‖ transparent_digest ‖ sapling_digest_v6
‖ orchard_digest_v6 ‖ ironwood_digest_v6)
auth_digest_v6 = BLAKE2b-256("ZTxAuthHash_" || consensusBranchId,
transparent_auth_digest ‖ sapling_auth_digest_v6
‖ orchard_auth_digest_v6 ‖ ironwood_auth_digest_v6)
Three consequences:
- Malleability is dead. You cannot change a signature to change a txid, because signatures are not in the txid. This is what SegWit achieved for Bitcoin, done structurally rather than as a patch.
- The consensus branch ID is baked into the personalisation. A transaction signed for one network upgrade is invalid on any other — automatic, universal replay protection across forks.
- Blocks commit to both.
hashAuthDataRootin the block header's commitment field covers the auth digests, so the proofs are still committed to; they are just committed to separately.
5.9 Blocks
The block header is 1,487 bytes:
| Field | Bytes |
|---|---|
nVersion | 4 |
hashPrevBlock | 32 |
hashMerkleRoot | 32 |
hashBlockCommitments | 32 |
nTime | 4 |
nBits | 4 |
nNonce | 32 |
solutionSize + solution | 3 + 1344 |
Two Zcash-specific fields. nNonce is 32 bytes (not 4), and solution holds the 1,344-byte Equihash solution (§6.1). And hashBlockCommitments (ZIP 221 + ZIP 244) is a hash of:
$$\mathsf{hashBlockCommitments} = \mathsf{BLAKE2b}(\mathsf{hashChainHistoryRoot} \,\|\, \mathsf{hashAuthDataRoot} \,\|\, \texttt{0}^{32})$$
The chain history root is the root of an MMR (Merkle Mountain Range) over all blocks since Heartwood, which is what enables FlyClient-style logarithmic light-client proofs of chain work. The auth data root is the Merkle root of every transaction's auth digest.
6. Network infrastructure
6.1 Proof of work: Equihash
Zcash uses Equihash with parameters $(n, k) = (200, 9)$, not SHA-256d.
Equihash is a memory-hard generalised birthday problem. You are given a list of $2^{n/(k+1)+1}$ strings derived from the block header, and must find $2^k = 512$ of them that XOR to zero, subject to an algorithm-binding ordering constraint. The best known algorithm (Wagner's) needs to hold large sorted tables in RAM, so the cost is dominated by memory bandwidth rather than raw hashing throughput.
The solution encoding: 512 indices × 21 bits each = 10,752 bits = 1,344 bytes, which is why the header carries a 1,347-byte solution field and Zcash block headers are 1,487 bytes rather than Bitcoin's 80.
The original goal was ASIC resistance — keep mining accessible to GPU owners and thus decentralised. That goal was not achieved; Equihash ASICs shipped in 2018 and dominate today. The parameters remain because changing them is a hard fork with no clear benefit now that the ASIC market exists and is competitive.
Validity is the familiar rule, with the Equihash check added:
$$\mathsf{SHA256d}(\mathsf{header}) < \mathsf{target}(\mathsf{nBits}) \quad\wedge\quad \mathsf{EquihashValid}_{200,9}(\mathsf{header})$$
6.2 Difficulty adjustment
Bitcoin retargets every 2,016 blocks. Zcash retargets every block, using a DigiShield v3 variant with a 17-block averaging window, median-time-past endpoints, and a damping factor of 4:
AWT = PoWAveragingWindow × PoWTargetSpacing # 17 × 75 = 1275 s
actual = MedianTime(last 11 blocks) − MedianTime(11 blocks ending 17 back)
damped = AWT + (actual − AWT) / PoWDampingFactor # PoWDampingFactor = 4
damped = clamp(damped, 0.84 × AWT, 1.32 × AWT) # AdjustUp 16%, AdjustDown 32%
target = MeanTarget(last 17 blocks) × damped / AWT
Three details are doing the work. The damping factor of 4 means only a quarter of any observed deviation is applied per block, so noise does not propagate. The median-time endpoints stop a miner from manipulating difficulty by lying about timestamps. And the clamps are deliberately asymmetric — difficulty may fall by up to 32% in a step but rise by at most 16% — so that if hashrate flees, the chain recovers its block rate quickly rather than stalling.
Per-block retargeting matters far more for a 75-second chain than for a 10-minute one: a hashrate swing that Bitcoin absorbs over two weeks would, without this, strand a 75-second chain for hours.
6.3 Issuance and funding
Zcash has Bitcoin's monetary policy with different block timing:
- Supply cap: 21,000,000 ZEC; 1 ZEC = $10^8$ zatoshi.
- Target block spacing: 75 seconds since Blossom (was 150s); 25 seconds proposed in NU7.
- Halving interval: 1,680,000 blocks since Blossom (
PostBlossomHalvingInterval = 840,000 × 2).
The subsidy formula for a post-Blossom height:
$$\mathsf{Halving}(h) = \left\lfloor \frac{\mathsf{Blossom} - \mathsf{SlowStartShift}}{840000} + \frac{h - \mathsf{Blossom}}{1680000} \right\rfloor$$
$$\mathsf{BlockSubsidy}(h) = \left\lfloor \frac{12.5\ \text{ZEC}}{2 \cdot 2^{\mathsf{Halving}(h)}} \right\rfloor$$
SLOW_START_SHIFT, BLOSSOM = 10_000, 653_600
PRE_HALVING, POST_HALVING = 840_000, 1_680_000
MAX_SUBSIDY = 12_50000000 # 12.5 ZEC in zatoshi
def halving(h):
return int((BLOSSOM - SLOW_START_SHIFT) / PRE_HALVING
+ (h - BLOSSOM) / POST_HALVING)
def subsidy(h): # post-Blossom
return MAX_SUBSIDY // (2 * 2 ** halving(h))
subsidy(1_046_400) / 1e8 # 3.125 — 1st halving, Canopy, Nov 2020
subsidy(2_726_400) / 1e8 # 1.5625 — 2nd halving, NU6, Nov 2024
subsidy(3_450_000) / 1e8 # 1.5625 — today
# 3rd halving lands at height 4,406,400 → 0.78125 ZEC
So the current block subsidy is 1.5625 ZEC, split by consensus (ZIP 1016, active since NU6.1):
| Recipient | Share | ZEC/block |
|---|---|---|
| Miner | 80% | 1.25 |
| Zcash Community Grants | 8% | 0.125 |
| Coinholder-controlled lockbox | 12% | 0.1875 |
The lockbox is a protocol-owned pot that accumulates on chain and is disbursed by a governance process rather than paid directly to an organisation — an attempt to decentralise funding decisions rather than pre-assigning them to incumbents.
6.4 What a node actually stores
This is the piece most overviews skip, and it is the part that determines whether the design scales.
CHAIN STATE (per node)
├── Block index + headers
├── Transparent UTXO set ~ Bitcoin-shaped
├── Sapling note commitment tree (frontier + recent roots)
├── Orchard note commitment tree (frontier + recent roots) ← frozen at NU6.3
├── Ironwood note commitment tree (frontier + recent roots) ← growing
├── Sprout nullifier set ─┐
├── Sapling nullifier set │ append-only,
├── Orchard nullifier set │ never pruned,
├── Ironwood nullifier set ─┘ grows forever
├── Chain value pool balances (transparent, Sprout, Sapling, Orchard, Ironwood)
└── Chain history MMR (ZIP 221)
Two observations. First, a node does not store the full commitment trees — only the frontier (the rightmost path, enough to append) plus a window of recent roots so it can validate anchors. Wallets keep their own paths for their own notes. Second, the nullifier sets are the unbounded liability: they cannot be pruned, because pruning one would re-enable a double spend of an arbitrarily old note.
Coming from Ethereum? The nullifier set is Zcash's state-growth problem in the same way the account trie is Ethereum's — except worse, because there is no notion of an inactive account that could be expired or rented, and no way to prove a nullifier absent from a set you have pruned. This is exactly the asymmetry Tachyon's proof-carrying wallets attack: shift the burden from "validator remembers everything" to "spender proves they have not spent before."
6.5 Node software: the Z3 stack
The implementation landscape changed completely in 2026.
zcashd— the original C++ node forked from Bitcoin Core. End of life. Its hard-coded end-of-support halt triggered at block 3,417,100 on 18 July 2026, and everyzcashd6.20.0 node shut itself down and refuses to restart. It does not support NU6.3.- Zebra — a from-scratch Rust full node by the Zcash Foundation, now the sole consensus implementation on mainnet. Async, modular, no wallet.
- Zaino — a Rust indexer (Zingo Labs) sitting in front of Zebra, serving light clients and block explorers.
- Zallet — a Rust wallet (ZODL) that replaces the wallet that used to live inside
zcashd, includingwallet.datmigration.
The deprecation was deliberately sequenced to precede NU6.3: the halt height was chosen so that every un-upgraded C++ node was already offline before Ironwood activated, guaranteeing a Zebra-only network at the fork. The acceleration was a direct consequence of the Orchard vulnerability — the judgement being that keeping a second, less-maintained consensus implementation alive through a security crisis was a bigger risk than the migration.
There is an irony worth naming: Zcash spent years arguing that two independent node implementations protect against a bug in either. It now runs on one. The tradeoff was consciously made, and the counterweight is the formal-verification work that shipped with Ironwood.
6.6 P2P, mempool, and fees
The peer layer is Bitcoin-descended: version/verack handshake, inv/getdata gossip, addrv2 peer exchange (ZIP 155). ZIP 239 added v5-transaction relay message types.
Two policy layers matter for transaction authors:
ZIP 401 — mempool cost limiting. Each transaction gets a "cost" (at least its size, floored at 4,000) and an "eviction weight" that adds a penalty if it pays below the conventional fee. When the mempool exceeds its cap, transactions are evicted at random weighted by eviction weight. Random weighted eviction rather than strict fee-ordering is a deliberate anti-fingerprinting choice.
ZIP 317 — the fee formula. Zcash does not price by byte. It prices by logical action:
$$\mathit{conventional\_fee} = \mathit{marginal\_fee} \cdot \max(\mathit{grace\_actions},\ \mathit{logical\_actions})$$
with marginal_fee = 5000 zatoshi and grace_actions = 2, and
$$ \begin{aligned} \mathit{logical\_actions} =\ & \max\!\left(\left\lceil \tfrac{\mathit{tx\_in\_size}}{150} \right\rceil, \left\lceil \tfrac{\mathit{tx\_out\_size}}{34} \right\rceil\right) \\ & + 2 \cdot \mathit{nJoinSplit} + \max(\mathit{nSpendsSapling}, \mathit{nOutputsSapling}) \\ & + \mathit{nActionsOrchard} + \mathit{nActionsIronwood} \end{aligned} $$
The Ironwood term was added as Revision 1 of the ZIP at NU6.3.
import math
MARGINAL_FEE, GRACE = 5000, 2
def zip317(tx_in_sz=0, tx_out_sz=0, nspend_s=0, nout_s=0, n_orch=0, n_iron=0):
transparent = max(math.ceil(tx_in_sz / 150), math.ceil(tx_out_sz / 34))
la = transparent + max(nspend_s, nout_s) + n_orch + n_iron
return MARGINAL_FEE * max(GRACE, la)
zip317(tx_in_sz=150, tx_out_sz=68) # 10_000 zat — 1-in 2-out transparent
zip317(n_orch=2) # 10_000 zat — minimal shielded
zip317(n_orch=2, n_iron=1) # 15_000 zat — a migration transaction
zip317(tx_in_sz=1500, tx_out_sz=68) # 50_000 zat — 10 transparent inputs
zip317(n_orch=50) # 250_000 zat = 0.0025 ZEC
Three design choices are embedded here, and all three are privacy choices:
- Per-action, not per-input-plus-output. An Orchard action bundles a spend and an output, so charging
inputs + outputswould have made Orchard systematically more expensive than Sapling and pushed users toward the weaker pool. - A grace window of 2. Wallets pad to two actions to hide whether a transaction really had one input or one output. If padding cost extra, wallets would stop padding, and the arity would leak.
- The fee is computable from public data only. It must be, because the fee is visible on chain. If the fee depended on any private quantity, it would be a side channel.
The block template algorithm is likewise anti-fingerprinting: rather than sorting by fee rate, miners are recommended to select transactions at random, weighted by $\min(\mathit{fee}/\mathit{conventional\_fee},\ 4)$, with a per-block budget of at most 50 "unpaid actions" for under-paying transactions. Paying 10× the conventional fee buys you at most 4× the selection probability.
6.7 Light clients and the scanning problem
This is the shielded pool's fundamental UX tax, and it deserves its own treatment.
A transparent wallet asks a server "what are the UTXOs for address X?" A shielded wallet cannot ask that question without destroying its own privacy — the question is the leak. So instead:
- The wallet downloads compact blocks: a stripped representation containing, per output, only
cmu/cmx(32 bytes),ephemeralKey(32 bytes), and the first 52 bytes ofencCiphertext— 116 bytes per output. That 52 is not arbitrary: it is exactlyleadByte(1) + d(11) + v(8) + rseed(32), i.e. the whole note minus the 512-byte memo. Everything needed to reconstruct and spend the note, nothing else. - For every output in every block, the wallet performs trial decryption with its
ivk: derive the shared secret $\mathsf{KA.Agree}(\mathsf{ivk}, \mathsf{epk})$, run the KDF, attempt ChaCha20-Poly1305 decryption. Failure means "not mine" — which is the overwhelmingly common case. - Successful decryptions yield notes; the wallet records their positions and maintains Merkle paths for them.
The cost is linear in total network activity, not in your own activity. Concretely: at 10 shielded outputs per block over ~1.15M blocks, a from-scratch sync is ~1.33 GB of compact data and roughly 11.5 million attempted decryptions — versus ~9.4 GB if full Actions were downloaded. Compact blocks are a 7× saving and still leave a wallet that has been offline for months with minutes-to-hours of catch-up work.
per-output work: 1 Pallas scalar mult + 1 BLAKE2b KDF + 1 ChaCha20-Poly1305 (52 B)
per-output data: 32 (cmx) + 32 (epk) + 52 (ct prefix) = 116 bytes
Every serious scaling proposal for Zcash is, at bottom, an attack on this loop. Tachyon's answer is to remove the ciphertexts from the chain entirely (out-of-band note delivery) and to replace scanning with an oblivious sync service that does the work without learning what it is looking for.
7. Lifecycle I: a transparent transaction, end to end
We will trace one payment from key generation to confirmation. Tobi is sending 1.0 ZEC to Ada, both using transparent addresses.
The transparent pool is Bitcoin with the serial numbers filed off, so this section doubles as the baseline against which §8's complexity can be measured. Read them as a pair: nearly every step below has a shielded counterpart that does the same job with a proof instead of a plaintext read.
Step 1 — Keys and address
1. seed → BIP-32/ZIP-32 derivation → private key k (32 bytes, secp256k1 scalar)
2. K = [k]·G → public key, compressed SEC1 (33 bytes)
3. h = RIPEMD160(SHA256(K)) → 20-byte pubkey hash
4. payload = 0x1CB8 ‖ h → mainnet P2PKH version prefix
5. checksum = SHA256d(payload)[0..4]
6. address = Base58(payload ‖ checksum) → "t1..."
import hashlib
def b58(b):
A = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
n = int.from_bytes(b, "big"); out = ""
while n: n, r = divmod(n, 58); out = A[r] + out
return "1" * (len(b) - len(b.lstrip(b"\x00"))) + out
def t_addr(pubkey: bytes) -> str:
h = hashlib.new("ripemd160", hashlib.sha256(pubkey).digest()).digest()
payload = bytes.fromhex("1cb8") + h # t1 = P2PKH mainnet
return b58(payload + hashlib.sha256(
hashlib.sha256(payload).digest()).digest()[:4])
The 20-byte hash is all that appears on chain; the public key itself is only revealed when the output is spent. (That detail matters for §10.3: an unspent P2PKH output is not exposed to a quantum adversary, but a spent-from address is.)
Step 2 — Coin selection
Tobi's wallet queries its view of the UTXO set for outputs paying his addresses:
UTXO #1: txid a1b2… vout 0 0.7 ZEC
UTXO #2: txid c3d4… vout 1 0.9 ZEC
UTXO #3: txid e5f6… vout 0 5.0 ZEC (coinbase, height 3_449_900)
He needs 1.0 ZEC plus fee. Selecting #1 and #2 gives 1.6 ZEC in, 1.0 out to Ada, and change back to himself.
Two Zcash-specific constraints apply to UTXO #3 and are worth flagging:
- Coinbase maturity. A transparent coinbase output cannot be spent until 100 blocks after the block that created it.
- The coinbase shielding rule. A transaction that spends a transparent coinbase output must have an empty
vout— it may only pay to shielded outputs. This is a Zcash original with no Bitcoin analogue; it forces newly mined coins through the shielded pool before they can circulate transparently.
Step 3 — Fee
By ZIP 317, with one P2PKH input (150 bytes) and two P2PKH outputs (34 bytes each):
$$\mathit{logical\_actions} = \max\!\left(\left\lceil\tfrac{150}{150}\right\rceil, \left\lceil\tfrac{68}{34}\right\rceil\right) = \max(1, 2) = 2$$
$$\mathit{fee} = 5000 \cdot \max(2, 2) = 10{,}000\ \text{zatoshi} = 0.0001\ \text{ZEC}$$
So: in 1.6, out 1.0 + 0.5999 change, fee 0.0001.
Unlike the shielded case, the fee here is implicit: it is sum(inputs) − sum(outputs), never written down. Nodes compute it.
Step 4 — Build the unsigned transaction
{
"header": 0x80000005, // fOverwintered | version 5
"nVersionGroupId": "0x26A7270A",
"nConsensusBranchId": "<NU6.3 branch id>",
"lock_time": 0,
"nExpiryHeight": 3450040, // current height + 40 (ZIP 203 default)
"vin": [
{ "prevout": {"hash":"a1b2…","n":0}, "scriptSig": "", "nSequence": 0xFFFFFFFF },
{ "prevout": {"hash":"c3d4…","n":1}, "scriptSig": "", "nSequence": 0xFFFFFFFF }
],
"vout": [
{ "value": 100000000, "scriptPubKey": "OP_DUP OP_HASH160 <ada_h160> OP_EQUALVERIFY OP_CHECKSIG" },
{ "value": 59990000, "scriptPubKey": "OP_DUP OP_HASH160 <tobi_h160> OP_EQUALVERIFY OP_CHECKSIG" }
]
}
nExpiryHeight is a Zcash addition (ZIP 203): the transaction becomes permanently invalid at that height, so a transaction that never gets mined expires rather than lingering indefinitely in mempools.
Step 5 — The signature hash
Here v5 diverges from Bitcoin's legacy algorithm in a way that matters. ZIP 244 defines the signature digest as a tree, and for a transparent input it commits to the value and scriptPubKey of the specific input being signed — the BIP-143 property, made structural.
signature_digest = BLAKE2b-256(
personal = "ZcashTxHash_" ‖ consensusBranchId,
input = header_digest ‖ transparent_sig_digest ‖ sapling_digest
‖ orchard_digest ‖ ironwood_digest )
transparent_sig_digest (for SIGHASH_ALL, signing input i) =
hash_type ‖ prevouts_digest ‖ amounts_digest ‖ scriptpubkeys_digest
‖ sequence_digest ‖ outputs_digest ‖ txin_sig_digest(i)
Why this matters practically: a hardware wallet can now learn the amount it is authorising from the signed data itself, rather than trusting the host to report it truthfully. The legacy Bitcoin algorithm's failure to sign input amounts was a real vulnerability class, and Zcash closed it at the format level.
Step 6 — Sign
For each input: ECDSA over secp256k1 with the corresponding private key, DER-encoded, with the one-byte hash type appended.
scriptSig = <sig ‖ SIGHASH_ALL> <pubkey>
Now the public key is on chain. Then serialise, and compute the txid:
$$\mathsf{txid} = \mathsf{txid\_digest\_v6}(\text{effecting data only})$$
Note again: the signatures just added do not change the txid; they live in the auth digest.
Step 7 — Broadcast and mempool
The wallet opens a connection to one or more peers and sends inv(MSG_TX, txid); peers respond getdata; the wallet sends the transaction. Each receiving node validates before relaying:
| Check | What it does |
|---|---|
| Structural | Field lengths, encodings, version group ID matches version, non-empty vin or vout |
| Consensus branch | nConsensusBranchId matches this node's active upgrade |
| Expiry | nExpiryHeight > current height |
| Inputs exist | Every prevout is in the UTXO set (and, if coinbase, mature) |
| Scripts | For each input, run scriptSig ‖ scriptPubKey and require a true result |
| Balance | sum(inputs) ≥ sum(outputs); the remainder is the fee |
| Coinbase rule | If any input is a transparent coinbase, vout must be empty |
| Policy | Fee vs. ZIP 317 conventional fee, ZIP 401 mempool cost |
Every one of these is a plaintext read. That is the point of this section: the transparent validator learns everything.
The transaction then sits in each node's mempool — which is local, not global, and not synchronised. Different nodes hold different views; consensus only cares about what ends up in blocks.
Step 8 — Mining
A miner builds a block template by the ZIP 317 weighted-random algorithm, constructs a header, and grinds Equihash solutions until one satisfies the target. At 75-second spacing, expected wait for a fee-paying transaction is a block or two.
Step 9 — Block validation and state update
When the block propagates, every node re-validates it and applies:
for each input i: UTXO_SET.remove(i.prevout)
for each output o: UTXO_SET.insert(new_outpoint(txid, index), o)
transparent_pool_balance += (coinbase subsidy) − (net value shielded)
Tobi's two UTXOs are gone; two new ones exist. Ada's wallet, which is watching for outputs paying her h160, sees hers immediately — no scanning, no trial decryption, no key agreement. She just reads the chain.
Step 10 — Confirmation
Zcash has no cryptographic finality; the heaviest-work chain wins. At 75-second blocks, six confirmations take about 7.5 minutes rather than Bitcoin's hour — but each block represents proportionally less work, so "six confirmations" is not equivalent security. Against an adversary with majority hashrate, no confirmation count is safe on any pure-PoW chain; what confirmations buy is an economic guarantee, not a mathematical one.
What an observer learned
Everything:
- Tobi controls two specific prior outputs (common-input-ownership: they belong to the same entity).
- His public keys, now revealed.
- Exact amounts: 0.7, 0.9 in; 1.0, 0.5999 out; 0.0001 fee.
- Ada's address, and that she received 1.0 ZEC.
- Which output is almost certainly change (the non-round one paying a never-before-seen address).
- Timing, and — if they were watching the network layer — plausibly Tobi's IP.
This is a complete, permanent, machine-readable record of a payment. Now let us do the same payment with the data absent.
8. Lifecycle II: a shielded transaction, end to end
Same payment, shielded. Tobi sends 1.0 ZEC to Ada, both using unified addresses, both wallets Ironwood-capable. Tobi holds two Ironwood notes worth 0.7 and 0.9 ZEC.
I am going to walk this at full detail, because every step exists for a reason and the reasons interlock. Here is the shape of the whole thing before we start:
┌── WALLET (private) ────────────────────────────────────────────────────┐
│ 0. sync & decrypt → the wallet's own note set │
│ 1. select notes, compute fee │
│ 2. fetch Merkle paths, choose anchor │
│ 3. compute nullifiers of the notes being spent │
│ 4. build output notes (ρ ← nf, ψ, rcm from rseed) │
│ 5. compute note commitments │
│ 6. encrypt notes (encCiphertext, outCiphertext) │
│ 7. compute value commitments, choose rcv │
│ 8. PROVE ── the Halo 2 circuit ──────────────────────────────────► │
│ 9. sign: spend-auth sigs (randomised) + binding signature │
│ 10. assemble v6 transaction, compute txid │
└────────────────────────────────────────────────────────────────────────┘
│ broadcast
┌── NETWORK (public) ─────────────▼──────────────────────────────────────┐
│ 11. node validation: proof, anchor, nullifiers, binding sig, structure │
│ 12. mempool → block → state update (tree grows, nullifier set grows) │
└────────────────────────────────────────────────────────────────────────┘
│
┌── RECIPIENT ────────────────────▼──────────────────────────────────────┐
│ 13. trial-decrypt every output; recover note; verify commitment │
└────────────────────────────────────────────────────────────────────────┘
Step 0 — Wallet state
Before Tobi can spend anything, his wallet must know what he owns. There is no "balance" to query. The wallet has, from §6.7, previously trial-decrypted every shielded output on the chain with its ivk and kept the ones that decrypted:
Note A = { "addr":"u1tobi…", "v": 70000000, "rho":"0x7a8b9c…",
"psi":"0x1d2e3f…", "rcm":"0x4a5b6c…", "position": 847291 }
Note B = { "addr":"u1tobi…", "v": 90000000, "rho":"0x3e4f5a…",
"psi":"0xc7d8e9…", "rcm":"0x1a2b3c…", "position": 851004 }
position is not decorative. It is the leaf index in the Ironwood commitment tree, and without it the wallet cannot construct a Merkle path. A wallet that loses positions has lost the ability to spend, even with the keys.
Step 1 — Note selection and fee
Tobi needs 1.0 ZEC. Neither note covers it, so both are spent: 1.6 ZEC in.
Orchard/Ironwood actions pair one spend with one output, so two spends force two actions, hence two outputs. That is exactly what we need: 1.0 to Ada, and change back to Tobi.
$$\mathit{logical\_actions} = \mathit{nActionsIronwood} = 2 \;\Rightarrow\; \mathit{fee} = 5000 \cdot \max(2,2) = 10{,}000\ \text{zatoshi}$$
So change = $1.6 - 1.0 - 0.0001 = 0.5999$ ZEC.
Why padding is mandatory, not optional. If Tobi had needed only one input and one output, the wallet would still build two actions, filling the spare slots with dummies: a zero-valued output note sent to a random address nobody controls, or a dummy spend of a fabricated note. The action count is public; the real arity must not be. ZIP 317's grace window of 2 exists precisely so this padding is free.
Step 2 — Anchor and Merkle paths
For each note, the wallet retrieves the authentication path from the note's position to a root:
merkle_path_A = [ sibling@level0, sibling@level1, …, sibling@level31 ] # 32 × 32 bytes
anchor = <root of the Ironwood commitment tree at the chosen height>
Anchor choice is a live privacy decision, not an implementation detail:
- Too recent and the anchor narrows you to the set of wallets that had synced that far — which, immediately after a new block, is small.
- Too old and you stand out from the crowd, and risk the node rejecting it (nodes keep only a window of recent roots).
Standard practice is a few blocks back from the tip. ZIP 318's migration flow goes further and draws anchors from shared, network-wide boundary heights (every 144 blocks ≈ 3 hours) so that many wallets deliberately share the same anchor, forming what the ZIP calls a cohort.
The path is a witness, never published. Nobody will see position, and nobody will see which leaf was proven.
Step 3 — Nullifiers
$$\mathsf{nf}_A = \mathsf{Extract}_\mathbb{P}\!\left(\left[\left(\mathsf{PRF}^{\mathsf{nf}}_{\mathsf{nk}}(\rho_A) + \psi_A\right) \bmod q_\mathbb{P}\right]\mathcal{K} + \mathsf{cm}_A\right) = \texttt{0x2c3d4e5f…}$$
$$\mathsf{nf}_B = \dots = \texttt{0x8f7a9b2c…}$$
These are the only trace of Tobi's spending that will appear on chain: two opaque 32-byte values. They reveal nothing about which commitments they correspond to, and nobody but Tobi could have produced them, because nk is his.
Step 4 — Output notes
Now the wallet constructs the two new notes. The order of derivation matters and is worth spelling out, because it is a chain of dependencies:
For the action spending Note A (which will carry Ada's output):
ρ_ada := nf_A ← the nullifier we just computed
rseed := 32 random bytes
ψ_ada := ToBase( PRF^expand_rseed( [0x09] ‖ ρ_ada ) )
esk := ToScalar( PRF^expand_rseed( [0x04] ‖ ρ_ada ) )
g*_d, pk*_d := from Ada's Ironwood receiver
pre_rcm := [0x0B] ‖ g*_d ‖ pk*_d ‖ v ‖ ρ_ada ‖ ψ_ada ← Ironwood (lead 0x03)
rcm_ada := ToScalar( PRF^expand_rseed( pre_rcm ) )
Three things to notice:
ρcomes from the nullifier. Uniqueness is inherited, not generated. This is what prevents Faerie Gold attacks.- Everything is derived from one 32-byte
rseed. That is what makes the 564-byte note plaintext sufficient for the recipient to reconstruct all the note's secrets. pre_rcmincludes every note field — this is the ZIP 2005 Ironwood change from §5.2, and the reason the derivation order had to change (rcmnow depends onψ, soψmust be computed first).
The change note is built identically with $\rho_{\text{change}} := \mathsf{nf}_B$ and Tobi's own internal (change) address.
Step 5 — Commitments
$$\mathsf{cm_x}^{\text{ada}} = \mathsf{Extract}_\mathbb{P}\!\left(\mathsf{NoteCommit}_{\mathsf{rcm_{ada}}}(\mathsf{g}^\star_\mathsf{d}, \mathsf{pk}^\star_\mathsf{d}, 100000000, \rho_{\text{ada}}, \psi_{\text{ada}})\right) = \texttt{0x8a9b0c1d…}$$
$$\mathsf{cm_x}^{\text{chg}} = \dots = \texttt{0x2d3e4f5a…}$$
Two opaque 32-byte values that will become leaves of the tree. They commit to the recipient, amount, and all randomness — and reveal none of it.
Step 6 — Note encryption
The commitment proves the note exists; it does not tell Ada what her note is. Without v, ρ, ψ, and rcm she can neither verify the commitment nor ever compute the nullifier to spend it. So the note plaintext has to reach her.
In-band secret distribution, per output:
esk ← derived from rseed in Step 4
epk = [esk] · g_d ← the ephemeral public key, goes on chain
shared_secret = KA.Agree(esk, pk_d) = [esk·ivk] g_d
K_enc = KDF(shared_secret, epk) ← BLAKE2b-256
encCiphertext = ChaCha20Poly1305(K_enc, note_plaintext) # 564 + 16 = 580 bytes
Ada recovers the same shared secret from the other side: $\mathsf{KA.Agree}(\mathsf{ivk}, \mathsf{epk}) = [\mathsf{ivk} \cdot \mathsf{esk}]\,\mathsf{g_d}$. Standard Diffie–Hellman — the novelty is entirely in the fact that pk_d was never linked to any identity and epk is fresh per output.
There is a second ciphertext, and it exists for a reason people often miss:
ock = PRF^ock(ovk, cv, cmx, epk)
outCiphertext = ChaCha20Poly1305(ock, pk_d ‖ esk) # 64 + 16 = 80 bytes
outCiphertext is encrypted to Tobi's outgoing viewing key. Without it, Tobi's own wallet would have no record of what he sent: the output note is Ada's, decryptable only with her ivk. This is how "sent" history exists at all — and it is also why sharing your ovk with an auditor reveals your payments, while sharing only ivk does not.
A subtlety worth internalising. Encryption is not part of what the zk-SNARK proves. The proof covers note well-formedness and balance; the ciphertext is a separate transport layer. If Tobi encrypted garbage, the transaction would still be perfectly valid and would still burn his 1.0 ZEC into a note Ada can never find. Consensus does not care. This is a wallet-correctness concern, not a protocol one — and it is exactly the dependency that ZIP 2004 ("remove the dependency of consensus on note encryption") and Tachyon's out-of-band delivery are pulling apart.
Step 7 — Value commitments
Per action, the wallet commits to that action's net value with fresh randomness:
$$\mathsf{cv}_i = \mathsf{ValueCommit}_{\mathsf{rcv}_i}(v_i^{\text{old}} - v_i^{\text{new}}) = [v_i^{\text{old}} - v_i^{\text{new}}]\,\mathcal{V} + [\mathsf{rcv}_i]\,\mathcal{R}$$
For our transaction:
Action 0: spends 0.7, creates 1.0 → net = −0.3
Action 1: spends 0.9, creates 0.5999 → net = +0.3001
sum = +0.0001 = the fee
Note that the pairing inside an action is arbitrary — Action 0 does not mean "Note A became Ada's note." The values do not have to match per action; only the global sum must. That is the whole point of the action abstraction: it destroys the correspondence between particular inputs and particular outputs.
Because ValueCommit is homomorphic, a verifier can sum the commitments in the clear:
$$\sum_i \mathsf{cv}_i - [\mathit{valueBalanceIronwood}]\,\mathcal{V} = \left[\sum_i \mathsf{rcv}_i\right]\mathcal{R}$$
Here valueBalanceIronwood = 10,000 zatoshi — the net value leaving the pool, which is exactly the fee. If the amounts balance, the left-hand side is a point whose discrete log with respect to $\mathcal{R}$ is $\sum \mathsf{rcv}_i$, which only Tobi knows. If they do not balance, it is a point with a $\mathcal{V}$ component and nobody knows its discrete log.
Step 8 — The proof
This is the load-bearing step. Tobi's wallet runs the Action circuit and produces a single Halo 2 proof covering both actions.
The circuit's public inputs (what the verifier sees):
anchor the Merkle root
nf_i the nullifier of each action's spend
rk_i the re-randomised spend-auth validating key
cmx_i the commitment of each action's output
cv_i the value commitment of each action
enableSpends, enableOutputs, enableCrossAddress
The private inputs (the witness):
the spent note (g_d, pk_d, v_old, ρ_old, ψ_old, rcm_old)
its Merkle path + position
ak, nk, rivk, ivk the key material
α the spend-auth randomiser
the output note (g_d', pk_d', v_new, ρ_new, ψ_new, rcm_new)
rcv value commitment randomness
And the statement it enforces, per action:
| # | Constraint | Why it exists |
|---|---|---|
| 1 | cm_old = NoteCommit_rcm(g_d, pk_d, v_old, ρ_old, ψ_old) | The claimed note is well-formed |
| 2 | Merkle path from Extract(cm_old) at position reaches anchor | The note exists in the tree |
| 3 | ivk = Commit^ivk_rivk(ak, nk) | The viewing key is derived from the key tuple |
| 4 | pk_d = [ivk] · g_d | The spender owns the address the note pays |
| 5 | nf = DeriveNullifier_nk(ρ_old, ψ_old, cm_old) | The revealed nullifier is the right one for this note |
| 6 | rk = ak + [α]·G | The published verification key comes from the real ak |
| 7 | cm_new = NoteCommit_rcm'(...), cmx_new = Extract(cm_new) | The output commitment is well-formed |
| 8 | ρ_new = nf | Output uniqueness inherits from the spend |
| 9 | cv = ValueCommit_rcv(v_old − v_new) | The value commitment matches the real values |
| 10 | v_old, v_new ∈ [0, 2^64) | Range proof — no negative values, no wraparound |
| 11 | if dummy spend: v_old = 0 and the Merkle check is bypassed | Padding is sound |
| 12 | (NU6.3) if enableCrossAddress = 0: output address = spend address | Orchard is exit-only (§9.4) |
Constraint 4 is the one to remember. It is the constraint that was not actually enforced for four years. §9 is about exactly this line.
Constraint 10 is the other one people underestimate. Amounts are field elements mod a 255-bit prime; without an explicit range check, "−1 ZEC" is a perfectly good field element that wraps to something enormous, and the balance equation would happily accept it.
What the circuit does not prove: anything about the ciphertexts, anything about the fee being reasonable, anything about who Ada is. Those are not consensus concerns.
The output is roughly 7.3 KB for a 2-action bundle ($2720 + 2272 \times 2$). Proving takes on the order of a second or two per action on a modern phone; verification takes milliseconds. That asymmetry — expensive once for the sender, cheap forever for every verifier — is what makes the whole design economically viable.
Step 9 — Signatures
Spend authorisation, per action. For randomiser $\alpha_i$ (already witnessed in the circuit), sign the sighash with $ask + \alpha_i$:
$$\mathsf{spendAuthSig}_i = \mathsf{RedPallas.Sign}(ask + \alpha_i,\ \mathsf{SigHash})$$
which verifies under the public $rk_i = ak + [\alpha_i]\mathcal{G}$. The chain sees a different rk every single time; only the circuit knows they all come from one ak.
Binding signature, once per bundle. Sign with $\sum_i \mathsf{rcv}_i$ as the private key:
$$\mathsf{bindingSig} = \mathsf{RedPallas.Sign}\!\left(\sum_i \mathsf{rcv}_i,\ \mathsf{SigHash}\right)$$
The verifier does not receive the key; it derives it as $\sum_i \mathsf{cv}_i - [\mathit{valueBalance}]\mathcal{V}$ (Step 7). A valid signature therefore proves the amounts balance, at the cost of one signature verification and zero circuit constraints.
Step 10 — Assembly
{
"header": "0x80000006", "nVersionGroupId": "0xD884B698",
"nConsensusBranchId": "<NU6.3>", "lock_time": 0, "nExpiryHeight": 3450040,
"vin": [], "vout": [],
"nSpendsSapling": 0, "nOutputsSapling": 0,
"nActionsOrchard": 0,
"nActionsIronwood": 2,
"vActionsIronwood": [
{ "cv":"0x9a8b7c6d…", "nullifier":"0x2c3d4e5f…", "rk":"0x5e6f7a8b…",
"cmx":"0x8a9b0c1d…", "ephemeralKey":"0x6b7c8d9e…",
"encCiphertext":"…580 bytes…", "outCiphertext":"…80 bytes…" },
{ "cv":"0x1b2c3d4e…", "nullifier":"0x8f7a9b2c…", "rk":"0x4d5e6f7a…",
"cmx":"0x2d3e4f5a…", "ephemeralKey":"0x8c9d0e1f…",
"encCiphertext":"…580 bytes…", "outCiphertext":"…80 bytes…" }
],
"flagsIronwood": "0b00000111", // enableSpends | enableOutputs | enableCrossAddress
"valueBalanceIronwood": 10000, // the fee, in the clear
"anchorIronwood": "0x7f8e9d0c…",
"sizeProofsIronwood": 7264,
"proofsIronwood": "…7264 bytes…",
"vSpendAuthSigsIronwood": ["…64 bytes…", "…64 bytes…"],
"bindingSigIronwood": "…64 bytes…"
}
Total: 9,165 bytes. The txid is computed over effecting data only; the proof and both signature arrays go into the auth digest, and — new in v6 — so does anchorIronwood, which is why this transaction could have been signed before the anchor was chosen.
Step 11 — Node validation
A node receiving this runs a completely different set of checks from §7:
| # | Check | Cost |
|---|---|---|
| 1 | Structural: lengths, flags reserved bits zero, sizeProofs matches formula | µs |
| 2 | nConsensusBranchId matches active upgrade; version/version-group consistent | µs |
| 3 | nExpiryHeight not passed | µs |
| 4 | Anchor: anchorIronwood is a known recent root of the Ironwood tree | lookup |
| 5 | Nullifiers: neither nf is already in the Ironwood nullifier set, and they differ from each other | 2 lookups |
| 6 | Proof: Halo 2 verification against the fixed Ironwood verifying key | ~ms |
| 7 | Spend auth sigs: each verifies under its rk | ~µs each |
| 8 | Binding sig: verifies under $\sum \mathsf{cv}_i - [\mathit{valueBalance}]\mathcal{V}$ | ~µs |
| 9 | Pool accounting: applying this transaction keeps every chain value pool balance ≥ 0 | arithmetic |
| 10 | Coinbase rules, flag rules (e.g. enableSpendsIronwood = 0 in coinbase) | µs |
Notice what is absent: no UTXO lookup, no script execution, no amount comparison. Checks 4–9 collectively do the job that §7's plaintext reads did, and the node ends up knowing exactly three facts — a valid proof exists, two specific nullifiers are now used, and the pool's net balance changed by 10,000 zatoshi.
Step 12 — Mining and state update
The transaction is mined. Every node applies:
IRONWOOD_TREE.append(0x8a9b0c1d…) # Ada's commitment, position N
IRONWOOD_TREE.append(0x2d3e4f5a…) # Tobi's change, position N+1
IRONWOOD_NULLIFIERS.insert(0x2c3d4e5f…)
IRONWOOD_NULLIFIERS.insert(0x8f7a9b2c…)
ironwood_pool_balance −= 10000 # the fee left the pool
new_anchor = IRONWOOD_TREE.root()
The update is deterministic: every node computes the same new root, holds the same nullifier set, and agrees on the pool balance. Global consistency on data nobody can read.
Tobi's old notes are not deleted — their commitments stay in the tree forever, which is exactly what keeps the anonymity set monotonically growing. They are merely unspendable, because their nullifiers are now used.
Step 13 — Ada finds her money
Ada's wallet does not know anything happened. It syncs, and for every output in every new block it tries:
for output in compact_block.outputs: # cmx, epk, ct[0:52]
shared = KA.Agree(ivk, output.epk)
k_enc = KDF(shared, output.epk)
pt = chacha20poly1305_decrypt(k_enc, output.ct_prefix)
if pt is None:
continue # not hers — the common case
# decrypted: recover d, v, rseed
d, v, rseed = parse(pt)
g_d = DiversifyHash(d)
pk_d = [ivk] · g_d
psi = ToBase(PRF_expand(rseed, [0x09] ‖ rho))
rcm = ToScalar(PRF_expand(rseed, pre_rcm(g_d, pk_d, v, rho, psi)))
cmx_check = Extract(NoteCommit_rcm(g_d, pk_d, v, rho, psi))
assert cmx_check == output.cmx # ← the critical verification
wallet.add_note(v, rho, psi, rcm, position=output.position)
Two details matter here.
ρ is not in the ciphertext. Ada takes it from the nullifier field of the same action — that is what "ρ_new = nf" (circuit constraint 8) buys her: the value she needs is already public, in the right place, for free.
The commitment re-check is not optional, and it is doing more work than it looks. Two reasons. First, a malicious sender could encrypt a plaintext that does not match the commitment they published; Ada would believe she had been paid, but her note would be unspendable, because the commitment in the tree would not match anything she could build a proof for. Second — and this is the subtle one — when Ada is scanning compact blocks she only has 52 of the 580 ciphertext bytes, which means she cannot verify the Poly1305 tag (it authenticates all 564 plaintext bytes). Compact-block decryption is therefore unauthenticated, and recomputing cmx from the recovered fields is what replaces the missing tag check. Get this wrong and a wallet will happily display money that does not exist.
When it matches, Ada has a spendable note. To spend it she performs exactly the process Tobi just performed, and her spend's nullifier becomes the ρ of her recipient's note. The cycle closes.
What an observer learned
| Observable | Value |
|---|---|
| Transaction exists | yes |
| Pool used | Ironwood |
| Number of actions | 2 |
| Nullifiers revealed | 0x2c3d4e5f…, 0x8f7a9b2c… |
| New commitments | 0x8a9b0c1d…, 0x2d3e4f5a… |
| Net pool value change | −10,000 zatoshi (the fee) |
| Anchor | a recent Ironwood root |
| Sender | unknown |
| Recipient | unknown |
| Amount | unknown |
| Which notes were spent | unknown — could be any two of the millions in the tree |
| Whether either output is change | unknown |
| Whether the 2 actions are 2 real spends, or 1 real + 1 dummy | unknown |
Compare with the §7 list. That is the entire thesis of the protocol in one table.
What still leaks — the honest accounting
Zero-knowledge on chain does not mean zero information overall. Four residual channels, all real:
- The fee and action count. Both public. A 50-action transaction is visibly large; a nonstandard fee fingerprints your wallet software. This is why ZIP 317 defines a conventional fee that everyone is expected to pay.
- Pool crossings. Shielding, unshielding, and Orchard→Ironwood migration all publish an exact amount in the clear. This is the turnstile's price. ZIP 318's migration design — canonical denominations of the form $n \times 10^k$ with $n \in \{1,2,5\}$, Poisson-scheduled broadcast, shared boundary anchors — is an entire engineering effort aimed at making these amounts collide across wallets rather than fingerprint them.
- Timing and network metadata. If your wallet broadcasts from a stable IP the moment you open the app, an observer correlating broadcasts with your light-wallet-server sync requests can learn a great deal. Hence Tor/Nym routing, and ZIP 318's rule that a background task may sync or broadcast in a given window, never both.
- Your counterparties. The protocol hides you from the chain. It does not hide you from the person you paid, who knows perfectly well what they received.
Designing well means knowing which of these you are exposed to. The protocol closes the ones it can close at the consensus layer; the rest are wallet and network-layer problems, and they are where most of the remaining work is.
9. Case study: the Orchard soundness bug
In May 2026, a researcher found that the Orchard circuit had not been enforcing constraint 4 from §8's table — the one binding an address to a viewing key. The bug had been live since Orchard's activation in May 2022, through multiple professional audits.
I am giving this its own section because it is the most instructive thing that has happened to a production ZK system, and because understanding it requires exactly the machinery we have just built.
9.1 What was supposed to be true
Recall the binding chain from §5.4:
$$sk \longrightarrow (ak, nk, rivk) \longrightarrow ivk = \mathsf{Commit}^{ivk}_{rivk}(ak, nk) \longrightarrow \mathsf{pk_d} = [ivk]\,\mathsf{g_d}$$
and the nullifier:
$$\mathsf{nf} = \mathsf{DeriveNullifier}_{\mathsf{nk}}(\rho, \psi, \mathsf{cm})$$
The security argument for no-double-spending runs backwards along that chain:
- A note commits to a specific
pk_d. pk_d = [ivk]·g_dpins down exactly oneivk(discrete log is unique).ivk = Commit^ivk_rivk(ak, nk)pins downnk.nkpins down the nullifier.
∴ each note has exactly one valid nullifier. Publish it and the note is spent, once, forever.
Break any link and the chain fails. Break link 2 and a spender can pick an arbitrary nk, compute the matching ivk, and derive a different nullifier for the same note — on demand, as many times as they like.
9.2 The actual defect
The constraint pk_d = [ivk]·g_d is a variable-base scalar multiplication, implemented in halo2_gadgets by double-and-add over the bits of ivk. In a circuit you do not run the algorithm; you constrain every intermediate step. One of the things you must constrain is that the base point used inside the loop is the base point that was passed in.
In halo2_gadgets/src/ecc/chip/mul/incomplete.rs, the loop wrote the base point coordinates with assign_advice where it needed copy_advice:
for (row, k) in bits.iter().enumerate() {
// …
// BUG: assigns the value, but creates NO equality constraint
region.assign_advice(|| "x_p", self.double_and_add.x_p, row + offset, || x_p)?;
region.assign_advice(|| "y_p", self.y_p, row + offset, || y_p)?;
// …
}
The distinction is the entire bug. In halo2:
assign_advicewrites a value into a cell. The prover chooses it. Nothing constrains it.copy_advicewrites the value and adds a copy (equality) constraint tying that cell to another cell.
A separate constraint did ensure the base was consistent across loop iterations — the prover could not use a different base each round. But nothing anchored that consistent base to the actual g_d computed upstream. So a malicious prover could substitute one arbitrary base point for the whole loop, and the circuit would verify: the scalar multiplication was internally self-consistent, just not the scalar multiplication anyone intended.
The fix adds the anchor at the first iteration and lets the existing inter-iteration constraint propagate it:
if row == 0 {
// anchor the loop's base point to the real `base` from outside
base.x().copy_advice(|| "x_p", &mut region, self.double_and_add.x_p, offset)?;
base.y().copy_advice(|| "y_p", &mut region, self.y_p, offset)?;
} else {
region.assign_advice(|| "x_p", self.double_and_add.x_p, row + offset, || x_p)?;
region.assign_advice(|| "y_p", self.y_p, row + offset, || y_p)?;
}
Two lines.
9.3 The exploit path
With link 2 unenforced, the attack is mechanical:
Attacker owns note N (committed to pk_d, in the tree).
Spend #1: pick nk₁ → ivk₁ = Commit^ivk_rivk(ak, nk₁)
→ prove [ivk₁]·(arbitrary base) = pk_d ← circuit accepts
→ nf₁ = DeriveNullifier_{nk₁}(ρ, ψ, cm)
Spend #2: pick nk₂ → ivk₂ = …
→ nf₂ = DeriveNullifier_{nk₂}(ρ, ψ, cm) nf₂ ≠ nf₁
Spend #3: …
Every spend produces a fresh, valid-looking nullifier. Consensus only checks "has this nullifier appeared before?" — and it never had. The same note could be spent without limit, with no cryptographic evidence on chain, because the proofs are zero-knowledge and the fraudulent nullifiers are statistically indistinguishable from honest ones.
This is the defining property of a soundness bug in a privacy system: the privacy that protects honest users also erases the forensic trail. There is no way to audit the chain and determine whether it happened.
9.4 What actually contained it
The turnstile (§4.3). ZIP 209's rule — chain value pool balance ≥ 0 — meant that however much counterfeit ZEC might exist inside Orchard, the total that could ever leave Orchard was capped at the total that legitimately entered. The supply of ZEC outside the pool could not be inflated.
That bound is real, but it is weaker than it first sounds, and the ZIP's own analysis says so: the turnstile only trips when cumulative outflow exceeds cumulative inflow. An attacker withdrawing modest amounts over time never trips it; the shortfall would only surface if legitimate users collectively tried to withdraw more than the pool actually held. The turnstile bounds the damage; it does not detect the attack.
The response ran in three stages:
| Date | Action |
|---|---|
| 29 May 2026 | Discovered by Taylor Hornby (contracted by Shielded Labs) during an AI-assisted circuit audit; reported same day |
| 2 June 2026 | Emergency soft fork at height 3,363,426 — all Orchard-containing transactions rejected outright |
| 3 June 2026 | NU6.2 hard fork at height 3,364,600 — corrected circuit, new verifying key, Orchard re-enabled |
| 28 July 2026 | NU6.3 / Ironwood at height 3,428,143 — new pool, Orchard sealed exit-only |
Sapling and transparent transactions were unaffected throughout. ZEC fell over 40% on public disclosure.
Why a new pool and not just a fixed circuit? Because the fix makes the future sound and does nothing about the past. Since it is mathematically impossible to prove from shielded data that no counterfeiting occurred between 2022 and 2026, the only way to restore independently verifiable supply integrity was to start a pool that is provably clean from block zero and force everything to migrate through the accounting gate. Ironwood begins empty; every coin in it demonstrably crossed a turnstile that could not pass more than entered Orchard. Whatever might be in Orchard stays there.
That is also what enableCrossAddress is for. Post-NU6.3, the circuit constrains Orchard actions so that an output must go to the same protocol-level address as the spend — meaning Orchard supports change but not payments. Combined with the coinbase ban on Orchard components, the old pool is a one-way exit.
9.5 What to take from it
Five things, and they generalise well beyond Zcash:
- A circuit proves what it constrains, not what it means. There is no compiler error for a missing constraint. The proof system will correctly and eternally prove a weaker statement. This failure mode has no analogue in ordinary software — the code worked.
- Under-constrained arithmetic is the dominant ZK bug class. Not overflow, not reentrancy — missing equality constraints. Everything built on shared gadget libraries inherits that risk, and
halo2_gadgetsis used well beyond Zcash. - Audits are sampling, not proof. Four years, multiple expert reviews, one missing
copy_advice. The answer the ecosystem reached for was formal verification, which is what shipped alongside Ironwood. - Defence in depth pays for itself. The turnstile was designed years earlier for exactly this scenario and was the only reason the incident was survivable rather than existential. Build the accounting bound even when you are confident in the cryptography — especially then.
- Privacy and incident response are in tension, and you should plan for it. In a transparent system you check whether the bug was exploited. Here you cannot, ever. That has to shape the response: you design for containment, not for forensics.
10. The road ahead
Three efforts define Zcash's near future, and they map onto the three things this post has shown are expensive.
10.1 Finishing the migration
The Orchard pool held roughly 3.66 million ZEC when it was sealed. Moving it is a user-driven process with no deadline, and each crossing publishes an amount — so doing it naively would hand observers a census of wallet balances.
ZIP 318 is one of the more interesting privacy-engineering documents in the ecosystem. Its rules for a wallet performing the migration:
- Canonical denominations. Decompose the balance into amounts of the form $n \times 10^k$ with $n \in \{1,2,5\}$ — so
123.45 ZEC → [100, 20, 2, 1, 0.2, 0.2, 0.05]. The reasoning inverts the usual intuition: privacy for a public amount comes from collision, not entropy. A random amount collides with nobody and is a fingerprint; a canonical one is emitted by thousands of wallets. - Note preparation. Fully shielded send-to-self transactions, padded to exactly 16 actions, that reshape the balance into notes that exactly fund each planned transfer. On chain, splitting and consolidating are indistinguishable.
- Poisson scheduling. Inter-transfer delays drawn from an exponential distribution (mean 66 blocks), so observing one broadcast tells you nothing about the next.
- Boundary anchors. Anchors drawn from shared network-wide heights every 144 blocks, recency-weighted, never the most recent — so an anchor identifies a large cohort rather than a wallet that just synced.
- Canonical transaction shape. Exactly one Orchard spend padded to two actions, exactly one single-action Ironwood output, canonical fee,
lock_time = 0. Every migration transaction looks identical to every other and to a manual Orchard→Ironwood spend. - Sync/broadcast decoupling. A background task may synchronise or broadcast, never both in the same window.
It is a good illustration of a general truth: once an amount must be public, the remaining privacy levers are shape and timing, and both have to be engineered deliberately.
10.2 Tachyon: attacking the three bottlenecks
§6.7 and §5.7 identified the costs: scanning is linear in network activity, the nullifier set grows forever, and a shielded transaction is ~38× a transparent one. Project Tachyon attacks all three.
Out-of-band note delivery. Remove encCiphertext from the chain entirely and deliver note plaintexts through a side channel (payment URIs, QR codes, messaging). That deletes 580 of the 820 bytes per action and deletes the scanning problem's input data. It also has a post-quantum benefit: there is no ciphertext to harvest now and decrypt later.
Oblivious synchronization. Rather than a wallet scanning, an untrusted service incrementally maintains a proof that the wallet's notes remain unspent — without learning which notes. The protocol forces nullifiers to evolve in an unlinkable way over time, so the service cannot correlate requests into a wallet profile. The wallet carries a self-updating proof of its own state instead of a scanned view of everyone else's.
Proof-carrying data. Instead of every validator storing every nullifier forever, spenders prove — recursively — that they have not spent before. Validators then verify a proof rather than consult unbounded state, which removes the asymptotic state-growth barrier. PCD also lets block producers aggregate shielded transactions without coordinating with users, cutting marginal size and verification cost.
Coming from Ethereum? This is recognisably the same family as recursive-proof rollups and stateless clients: move state from the verifier to the prover, compress verification recursively, and let a block carry a proof instead of the data. The difference is the privacy constraint, which rules out the usual trick of just letting a prover service see everything.
The Pallas/Vesta cycle from §3.4 is what makes the recursion practical. The curve choice made in 2020 was made with this in mind.
10.3 Quantum readiness — staged, and honestly described
This gets overstated in both directions, so here is the precise position.
What is already fine. Nullifiers are built on keyed PRFs (symmetric), which quantum computers do not break. Note commitments are perfectly hiding, so a note commitment sitting on chain today reveals nothing to any adversary, ever, regardless of compute. Recipient addresses are not published on chain. So the "harvest now, decrypt later" exposure is narrower than for most chains — but it is not zero, because in-band ciphertexts are harvestable and could be decrypted against a known address once DH breaks.
What is not fine. Everything resting on discrete log: Halo 2 soundness, Groth16 soundness, RedPallas and RedJubjub signatures, Sinsemilla's collision resistance, the binding of Pedersen-style commitments, and transparent ECDSA. An adversary able to compute a single discrete log on Pallas could forge balance proofs or steal shielded funds. This is a total break, and it is why ZIP 2005 states plainly that the pre-quantum protocols must be switched off before cryptographically relevant quantum computers exist.
The staged plan. Stage one shipped with Ironwood, and it is recoverability, not resistance:
- Ironwood notes derive
rcmfrom a hash of all note fields (§5.2), making the commitment scheme post-quantum binding. Commit^ivkgets the same treatment:rivkis derived either fromskviaH^rivk_legacy, or — for FROST threshold setups and hardware wallets — from a new "quantum spending key"qskthroughqk = BLAKE3.derive_key(…, qsk).- A future Recovery Protocol would then prove, in a post-quantum proof system over a rehashed commitment tree, that the note was correctly derived — letting users evacuate funds into a post-quantum pool even after the elliptic-curve layer is disabled.
The honest framing, in the ZIP's own words: this "does not by itself make the protocol secure against quantum adversaries." It makes Ironwood funds recoverable. Funds left in Sprout, Sapling, or Orchard when those protocols are switched off would be permanently inaccessible. That is why ZIP 2005 tells wallets to move everything — transparent, Sprout, and Sapling included — into Ironwood recoverable notes.
10.4 NU7
Targeted for 5 November 2026 (testnet 6 October, final go/no-go 20 October), carrying:
- ZIP 218 — block target spacing from 75s to 25s, tripling block production. Subsidy per block adjusts so the halving schedule and 21M cap are preserved.
- ZIP 2003 — v4 transactions disabled.
- ZIP 234 + 235 — the Network Sustainability Mechanism: 60% of transaction fees removed from circulation, reintroduced into block rewards starting February 2031. This is Zcash's answer to Bitcoin's long-run security-budget problem — fees fund future issuance rather than the cap being broken or miners being left with nothing.
Notably, NU7 introduces no new transaction formats. After a year of emergency upgrades, that is the point.
11. Appendix: constants, glossary, sources
11.1 Network upgrades
| Upgrade | Height | Date | Branch ID | Brought |
|---|---|---|---|---|
| Sprout | genesis | 2016-10-28 | 0x00000000 | The original shielded pool |
| Overwinter | 347,500 | 2018-06-25 | 0x5ba81b19 | v3 tx, expiry heights, replay protection |
| Sapling | 419,200 | 2018-10-28 | 0x76b809bb | Sapling pool, v4 tx, viewing keys |
| Blossom | 653,600 | 2019-12-11 | 0x2bb40e60 | 75-second blocks |
| Heartwood | 903,000 | 2020-07-16 | 0xf5b9230b | Shielded coinbase, ZIP 221 MMR |
| Canopy | 1,046,400 | 2020-11-18 | 0xe9ff75a6 | 1st halving, funding streams |
| NU5 | 1,687,104 | 2022-05-31 | 0xc2d6d0b4 | Orchard, v5 tx, ZIP 244, UAs |
| NU6 | 2,726,400 | 2024-11-23 | 0xc8e71055 | 2nd halving, new funding streams |
| NU6.1 | 3,146,400 | 2025-11-24 | 0x4dec4df0 | ZIP 1016 funding model, lockbox |
| NU6.2 | 3,364,600 | 2026-06-03 | — | Orchard circuit fix (emergency) |
| NU6.3 | 3,428,143 | 2026-07-28 | — | Ironwood, v6 tx, quantum recoverability |
| NU7 | TBD | ~2026-11-05 | — | 25s blocks, NSM, v4 disabled |
11.2 Constants
| Constant | Value |
|---|---|
| Supply cap | 21,000,000 ZEC |
| 1 ZEC | $10^8$ zatoshi |
| Block spacing | 75 s (25 s proposed, NU7) |
| Halving interval | 1,680,000 blocks (post-Blossom) |
| Current block subsidy | 1.5625 ZEC (80/8/12 miner / ZCG / lockbox) |
| Next halving | height 4,406,400 → 0.78125 ZEC |
| PoW | Equihash (200, 9); 1,344-byte solution |
| Block header | 1,487 bytes |
| Difficulty | per-block DigiShield v3; 17-block window, damping 4, clamps 0.84×/1.32× |
| Merkle depth (Sapling/Orchard/Ironwood) | 32 → $2^{32}$ notes |
| Action | 820 bytes |
| Orchard/Ironwood proof | $2720 + 2272n$ bytes |
| Note plaintext | 564 bytes (+16 tag = 580) |
| Memo | 512 bytes |
marginal_fee / grace_actions | 5,000 zat / 2 |
| Coinbase maturity | 100 blocks (transparent only) |
| Default expiry delta | 40 blocks |
| Curves | Pallas / Vesta (2-cycle), 255-bit fields |
11.3 Glossary
action — the Orchard/Ironwood unit pairing one spend with one output, either possibly a dummy. anchor — a Merkle root of a note commitment tree that a transaction proves membership against. auth digest — the commitment to a transaction's proofs and signatures, separate from the txid. binding signature — a signature under the summed value-commitment randomness; proves balance. diversifier (d) — 11 bytes producing a distinct address from the same viewing key. dummy — a zero-valued note used as padding, indistinguishable on chain from a real one. note — a shielded unit of value, $(\mathsf{addr}, v, \rho, \psi, \mathsf{rcm})$. nullifier — the value revealed on spend; deterministic per note, unlinkable to its commitment. rk — a re-randomised spend-authorisation validating key, fresh on every spend. turnstile — the consensus rule that no pool's cumulative outflow may exceed its inflow. zatoshi — $10^{-8}$ ZEC.
11.4 Sources
Protocol and specifications:
- Zcash Protocol Specification — Hopwood, Bowe, Hornby, Wilcox
- ZIP index — all ZIPs below
- ZIP 224: Orchard Shielded Protocol · ZIP 225: v5 Transaction Format · ZIP 229: v6 Transaction Format
- ZIP 209: Chain Value Pool Balances (the turnstile) · ZIP 213: Shielded Coinbase
- ZIP 244: Transaction Identifier Non-Malleability · ZIP 316: Unified Addresses
- ZIP 317: Proportional Transfer Fee Mechanism · ZIP 208: Shorter Block Target Spacing
- ZIP 2005: Ironwood Quantum Recoverability · ZIP 318: Orchard to Ironwood Migration
Incident and upgrades:
- Ironwood — Shielded Labs
- Zcash Orchard Soundness Bug Analysis — BlockSec
- The Orchard counterfeiting vulnerability and next steps — Zcash Community Forum
- zcashd End of Life
- Zcash targets November for NU7 mainnet upgrade — Cointelegraph
Scaling:
Implementations:
Closing
The thing worth carrying away from all of this is not any particular formula. It is the shape of the trade.
Bitcoin's validation rule is a reading rule, so its security and its surveillance are the same property — you cannot keep one and discard the other by changing wallet habits. Zcash replaces each read with a proof about a commitment, and in doing so gets a ledger that can be checked without being seen. The price is a much larger trusted computing base: a circuit whose every constraint must be exactly right, a scanning cost linear in everyone else's activity, transactions 38× the size, and — as June 2026 demonstrated — failure modes that cannot be audited after the fact.
Zcash's answer to that price is not to claim the cryptography is infallible. It is to bound what a failure can cost. The turnstile is a plaintext accounting rule sitting underneath the encryption, and when the cryptography did fail, that rule was the difference between a bad quarter and a dead protocol. Ironwood is the same instinct applied a second time: when you cannot prove the past was clean, build a present that provably is, and gate the door between them.
That is the engineering lesson worth more than the formulas. Encryption buys you privacy that does not decay. Plaintext invariants buy you damage bounds that do not depend on the encryption being right. A system that wants to last needs both.