Skip to main content

From Actions to Shielded Transfers

1. Why This Chapter Exists

The previous chapters dissected one Action: its public inputs, its witness, its constraints, the keys it derives. A shielded transfer, however, is a chain-level state transition that consumes a set of input notes and produces a set of output notes, possibly with transparent ingress or egress. The transfer is not in any one file: it is the joint effect of the Halo 2 proof, the binding signature, the per-Action spend authorisations, the nullifier update, and the note-tree update. After this chapter the reader can trace a payment from "wallet calls Builder::add_output" to "the recipient's wallet sees an incoming note" and identify every cryptographic check that licenses that transition.

2. Definitions

Definition 2.1 (Orchard State)

At block height hh, the Orchard state is the triple

Stateh=(CMTreeh,NfSeth,Poolh),\mathsf{State}_h = \big(\mathsf{CMTree}_h,\, \mathsf{NfSet}_h,\, \mathsf{Pool}_h\big),

where CMTreeh\mathsf{CMTree}_h is the Sinsemilla-hashed Merkle tree of all note commitments inserted up to height hh (see Chapter 11), NfSethFp\mathsf{NfSet}_h \subset \mathbb{F}_p is the set of revealed nullifiers, and PoolhZ\mathsf{Pool}_h \in \mathbb{Z} is the running net value held inside the Orchard pool (sum of all value_balance\mathsf{value\_balance} values applied so far).

Definition 2.2 (Bundle Transfer Semantics)

A valid Bundle BB at height hh defines a state transition StatehStateh+1\mathsf{State}_h \to \mathsf{State}_{h+1}:

CMTreeh+1=CMTreeh{cmi}i=1N,NfSeth+1=NfSeth{nfi}i=1N,Poolh+1=Poolhvalue_balance(B).\begin{aligned} \mathsf{CMTree}_{h+1} &= \mathsf{CMTree}_h \cup \{\mathsf{cm}^\star_i\}_{i=1}^N, \\ \mathsf{NfSet}_{h+1} &= \mathsf{NfSet}_h \cup \{\mathsf{nf}_i\}_{i=1}^N, \\ \mathsf{Pool}_{h+1} &= \mathsf{Pool}_h - \mathsf{value\_balance}(B). \end{aligned}

The transition is valid only if all five conditions below hold; otherwise the transaction containing BB is rejected by every honest validator.

Definition 2.3 (Bundle Validity)

A Bundle B=({Ai},π,σbind,{σiauth},value_balance)B = (\{A_i\}, \pi, \sigma^{\mathsf{bind}}, \{\sigma_i^{\mathsf{auth}}\}, \mathsf{value\_balance}) is valid at height hh iff:

  1. Anchor freshness: every Action's anchor anchori\mathsf{anchor}_i is the root of CMTreeh\mathsf{CMTree}_{h'} for some h{hanchorAge,,h}h' \in \{h - \mathsf{anchorAge}, \dots, h\}.
  2. Nullifier freshness: the multiset {nfi}\{\mathsf{nf}_i\} is disjoint from NfSeth\mathsf{NfSet}_h and contains no internal duplicates.
  3. Proof validity: Halo2.Verify(vk,{Insti}i=1N,π)=accept\mathsf{Halo2.Verify}(\mathsf{vk},\, \{\mathsf{Inst}_i\}_{i=1}^N,\, \pi) = \mathsf{accept}.
  4. Binding signature: RedPallas.VerifyBinding(bvk(B),SighashBundle(B),σbind)=accept\mathsf{RedPallas.Verify}_{\mathsf{Binding}}\big(\mathsf{bvk}(B),\, \mathsf{SighashBundle}(B),\, \sigma^{\mathsf{bind}}\big) = \mathsf{accept}, where bvk(B)=icvinet[value_balance]V\mathsf{bvk}(B) = \sum_i \mathsf{cv}^{\mathsf{net}}_i - [\mathsf{value\_balance}]\, \mathcal{V}.
  5. Per-Action authorisation: for every non-dummy Action, RedPallas.VerifySpendAuth(rki,SighashTx,σiauth)=accept\mathsf{RedPallas.Verify}_{\mathsf{SpendAuth}}\big(\mathsf{rk}_i,\, \mathsf{SighashTx},\, \sigma_i^{\mathsf{auth}}\big) = \mathsf{accept}.

Invariant 2.4 (Rho Chaining within a Bundle)

The bundle's Actions are linked by the rho-chain: for each Action, the new note's ρ\rho is bound to the old note's nullifier of the same Action. This couples each fresh note to a specific spend within the same bundle, which is what prevents two distinct bundles from minting two notes that share a ρ\rho unless they share a nullifier (and would therefore both be rejected by Condition 2.3.2).

3. The Code

3.1 The Mental Model

Read the lifecycle below as a sequence of state transitions. Every transition is licensed by a cryptographic check; if any check fails, the transition is reverted before the next one runs.

[wallet] [chain]
|
| pick spendable notes + paths anchor = current_root
| pick output recipients + values
| pad to power-of-two Actions
| shuffle pairings
| sample alpha_i, rcv_i, esk_i
| compute rho_new_i := nf_old_i (per Action)
|
| Builder::build ----------------> Unauthorized Bundle
| Bundle::create_proof -----------> proof pi
| Bundle::prepare ----------------> typestate transition
| Bundle::apply_signatures -------> Authorized Bundle
|
|---- broadcast(tx with Bundle) --->
| consensus check:
| anchor in {recent roots}?
| nullifiers disjoint?
| proof verifies?
| bind sig verifies?
| auth sigs verify?
| if accept:
| insert cm*_i into tree
| add nf_i to nullifier set
| apply value_balance
|
[recipient]
| scan every shielded output:
| K_enc = KDF(epk, [ivk] epk)
| P = AEAD.Decrypt(K_enc, C_enc)
| if AEAD tag valid -> recover Note

3.2 The Code Path of a Transfer

A real run of the integration test in tests/builder.rs exercises every step:

3.3 What an On-Chain Observer Sees

Per bundle, on the public ledger:

  • The list of Actions, each carrying (cvinet,nfi,rki,cmi,epki,Cienc,Ciout,enableSpends,enableOutputs)(\mathsf{cv}^{\mathsf{net}}_i,\, \mathsf{nf}_i,\, \mathsf{rk}_i,\, \mathsf{cm}^\star_i,\, \mathsf{epk}_i,\, C^{\mathsf{enc}}_i,\, C^{\mathsf{out}}_i,\, \mathsf{enableSpends},\, \mathsf{enableOutputs}).
  • The Halo 2 proof π\pi (one per bundle, not per Action).
  • The binding signature σbind\sigma^{\mathsf{bind}}.
  • The per-Action spend-authorising signatures {σiauth}\{\sigma_i^{\mathsf{auth}}\}.
  • The declared value_balance\mathsf{value\_balance}.

What the observer does not see, even with unlimited computation, under the protocol's security assumptions:

  • The mapping between Actions and the senders / receivers.
  • Whether a given Action is a real spend, real output, dummy spend with dummy output, or any combination. The Halo 2 proof attests that if enabled, the spend / output is well-formed, but the flags are public and the dummies look identical on the wire to real Actions.
  • The values of any individual notes; only the net value_balance\mathsf{value\_balance} of the whole bundle.
  • The identity of the diversified address inside any Action.

3.4 The Joint Effect: a Transfer

Combining the above:

  • Sender's wallet loses access to the input notes (their nullifiers are now public, the next attempt to spend them is rejected by Condition 2.3.2).
  • Recipient's wallet, on the next trial-decryption pass, sees the output notes and adds them to its spendable set.
  • The Orchard pool's net value shifts by value_balance-\mathsf{value\_balance}: if value_balance>0\mathsf{value\_balance} > 0, that amount leaves the pool (typically to a transparent recipient or a Sapling output); if <0< 0, that amount enters the pool from a transparent source.
  • The state of every full node converges on the same updated Stateh+1\mathsf{State}_{h+1}.

The transfer is the joint application of these effects. No single file in orchard is the "transfer"; the transfer is what the verifier's acceptance of a bundle licenses on chain.

3.5 Replay, Linkability, and Censorship Resistance

  • Replay is prevented by Condition 2.3.2 (nullifier freshness) plus the transaction-level uniqueness enforced by the SIGHASH binding to the transaction's other fields (ZIP 244).
  • Cross-Action linkability within a bundle is prevented by the shuffle in Builder::build and by the per-Action randomiser αi\alpha_i that decouples rki\mathsf{rk}_i from ak\mathsf{ak}.
  • Cross-bundle linkability between two of the same wallet's bundles is prevented by the same αi\alpha_i argument: every bundle's rki\mathsf{rk}_i is independent under the re-randomisation security definition of RedDSA.
  • Censorship resistance at the cryptographic layer: a node can refuse to mine a transaction, but cannot prove it was censored. This is a property of the broader chain, not of Orchard specifically.

3.6 Composition with Other Pools

A Zcash transaction may contain transparent inputs and outputs, a Sapling bundle, and an Orchard bundle simultaneously. The SIGHASH (ZIP 244) binds them together: the per-Action spend-auth signatures sign the transaction-level digest, so a transaction cannot be split into a valid sub-transaction. The Orchard pool accounting (Pool_h above) is one of three pool variables that the chain maintains; the consensus check verifies all three.

4. Failure Modes

  • Anchor staleness. The anchor must be a known recent root. A reorg can invalidate it; wallets that build offline and broadcast late may produce bundles that are rejected. The builder does not detect this; it is a wallet UX issue.
  • Nullifier-set collision under concurrency. Two transactions that spend the same note get mined in the same block; only one can win. The losing wallet sees its bundle invalidated. Any wallet must serialise spends of the same note.
  • value_balance overflow. The accounting is over a signed 63-bit quantity; reckless arithmetic in a wallet can wrap. The value.rs module is documented to be defensive but callers must respect the documented range.
  • Memo length leak via padding. The 512-byte memo padding is fixed; a wallet that emits a smaller ciphertext (a buggy encoder) leaks the memo length category.
  • Side-channel on trial decryption. The recipient's wallet attempts to decrypt every output; a timing-side-channel that distinguishes "AEAD tag failed at byte X" from "AEAD tag failed at byte Y" can leak the receiver. The Domain implementation in zcash_note_encryption uses constant-time AEAD via chacha20poly1305; a custom decryptor must do the same.

5. Spec Pointers

6. Exercises

  1. Reading task. Open tests/builder.rs. Annotate the file with the Bundle Validity conditions (2.3.1 to 2.3.5) at each verify-equivalent call. Identify which line of test code corresponds to each consensus check.
  2. Reading task. Pick a transaction on a public block explorer and list its on-chain Orchard fields. Mark which of the Definition 2.3 checks each field feeds into.
  3. Code task. In a unit test, build a Bundle, then mutate one byte of an Action's cm\mathsf{cm}^\star before calling verify_proof. Identify the verification step that catches the mutation and report its error type.
  4. Code task. Build a Bundle with two Actions that share the same input note (a deliberately invalid configuration). The test should fail before consensus; identify whether it fails at the builder or at the prover, and why.
  5. Research task. Read src/bundle/commitments.rs alongside ZIP 244. Determine which transaction fields are bound by the spend-auth signature and which are not. Identify one field whose absence from the SIGHASH would allow a non-trivial replay.

7. Further Reading

  • The librustzcash transaction-builder source, which composes Orchard with the transparent and Sapling pools in production.
  • The Sapling protocol paper, Section 3, for a parallel treatment of pool semantics in the earlier protocol.
  • Reorgs and finality in Zcash, Section 3.7, for the anchor-staleness window and reorg recovery rules.