Skip to main content

Orchard Transaction Format

1. Why this chapter exists

Knowing the halo2 chips that build Orchard is half the story. The other half is what those chips' outputs become on chain. The Orchard transaction format is a deliberate redesign of Sapling: it merges spends and outputs into symmetric "actions", pulls the anchor up to the bundle level, and replaces the per-description Groth16 proofs with a single halo2 proof for the entire bundle. These choices are visible at the byte level (the V5 transaction serialization in ZIP 225) and at the consensus level (one zk-proof verification per bundle instead of one per description).

This chapter is the field-by-field reference for that format and the diff against Sapling and Sprout. If you ever need to write parser code, debug a transaction that fails to deserialize, or reason about which fields the halo2 proof actually binds, this is the page to come back to.

The protocol-context chapter (read it first) introduced the term action; this chapter is its expansion.

2. Definitions

Definition (Bundle). The per-pool subset of a transaction. A V5 transaction carries up to three bundles: transparent (legacy Bitcoin-style inputs/outputs), Sapling (SaplingBundle), and Orchard (OrchardBundle). Each pool's bundle is self-contained: its proofs, anchors, and binding signatures verify independently of the others. Pool-to-pool value transfer is the only coupling, and it goes through the per-bundle valueBalance field.

Definition (Action). Orchard's unit of work. An action combines exactly one note spend and one note output, both possibly dummies (with flags signalling which sides are real). The symmetric structure means a wallet that wants to make a 1-input, 3-output payment uses 3 actions with one real spend in the first action and dummy spends in the other two; the dummy notes are indistinguishable from real ones at the consensus layer. Defined in ZIP 224 section "Actions and Bundles".

Definition (Dummy note). A note generated by the prover for balance / unlinkability padding. Its value commitment is real (otherwise valueBalance would not sum), but its underlying note value is zero and its address is randomly generated. Dummies are indistinguishable from real notes on chain. Used to round out the number of actions to whatever the wallet wants for privacy reasons.

Definition (enableSpends, enableOutputs). Two bits in the bundle-level flagsOrchard byte. If enableSpends is 0, every action's spend side is forced to be a dummy (the bundle is "output-only", useful for shielding). If enableOutputs is 0, every action's output side is forced to be a dummy ("deshielding-only"). Both bits set is the normal case. Both clear means an Orchard bundle that does nothing, which is forbidden by consensus.

Definition (valueBalanceOrchard). Signed 64-bit integer giving the net value moving out of the Orchard pool. Positive means Orchard value flows to the transparent or Sapling pools; negative means the reverse. The binding signature certifies that the sum of input-action value commitments minus output-action value commitments equals [valueBalanceOrchard]VOrchard[\mathsf{valueBalanceOrchard}] \mathcal{V}^{\mathsf{Orchard}}, which forces value conservation across the pool boundary without revealing per-action amounts.

3. The code

3.1 Bundle and action structure (in zcash/orchard)

The Orchard bundle is a Rust type in zcash/orchard/src/bundle.rs; the per-action description is in zcash/orchard/src/action.rs. Both are outside this repository; halo2 only provides the chips that the bundle's proof field certifies.

In the spec (and on the wire), the Orchard bundle has these fields:

OrchardBundle = {
actions: vector of ActionDescription (>= 1 if present),
flags: 1 byte (enableSpends | enableOutputs | reserved),
valueBalance: int64 (signed, in zatoshi),
anchorOrchard: 32 bytes (root of the note commitment tree),
sizeProofsOrchard: CompactSize,
proofsOrchard: halo2 proof (variable length),
vSpendAuthSigsOrchard: vector of 64-byte RedPallas signatures,
one per action,
bindingSigOrchard: 64 bytes (RedPallas binding signature),
}

Each ActionDescription is exactly 820 bytes serialized:

ActionDescription = {
cv: 32 bytes (value commitment, point compressed to x),
nullifier: 32 bytes (Pallas base field, the nullifier nf),
rk: 32 bytes (randomized spend-auth verification key),
cmx: 32 bytes (x-coord of new note commitment cm_new),
ephemeralKey: 32 bytes (DH public key for note encryption),
encCiphertext: 580 bytes (ChaCha20-Poly1305 of the new note + memo),
outCiphertext: 80 bytes (recipient-detection ciphertext for the OVK),
}

spendAuthSig[i] and bindingSigOrchard live at the bundle level, not in the action. The proofsOrchard field is one halo2 proof that simultaneously certifies all of the actions' predicates.

3.2 What the halo2 proof binds

The Orchard circuit's public input vector has one block per action plus a small bundle-level block. The action block, in order:

FieldSourceField typeWhat constrains it
anchorbundle-levelPallas base fieldshared across all actions; one anchor per bundle
cvper actionPallas base field (x of P)cv=[v]V+[rcv]R\mathsf{cv} = [v]\mathcal{V} + [\mathsf{rcv}]\mathcal{R}
nf_oldper actionPallas base fieldnf=ExtractP([f(nk,ρ,ψ)]K+cmold)\mathsf{nf} = \mathrm{Extract}_{\mathbb{P}}([f(\mathsf{nk},\rho,\psi)] K + \mathsf{cm}_{\mathsf{old}})
rkper actionPallas base field (x of P)rk=ak+[α]GSpendAuth\mathsf{rk} = \mathsf{ak} + [\alpha] G_{\mathrm{SpendAuth}}
cmx_newper actionPallas base fieldcmx=ExtractP(NoteCommitrcmOrchard())\mathsf{cmx} = \mathrm{Extract}_{\mathbb{P}}(\mathsf{NoteCommit}^{\mathsf{Orchard}}_{\mathsf{rcm}}(\dots))
enableSpendbundle-level{0,1}\{0, 1\}forces the spend side to a dummy when 0
enableOutputbundle-level{0,1}\{0, 1\}forces the output side to a dummy when 0

The mapping from chip to predicate:

  • cv and the value-conservation equation: ECC chip (mul_fixed_short for the value, mul_fixed for the randomness, add), see chapter 14 section 3.7.
  • nf_old: Poseidon for the scalar, then ECC for the curve arithmetic, then Extract_P. Chapter 15 section 3.7.
  • cmx_new: Sinsemilla NoteCommit, then Extract_P. Chapter 15.
  • rk: ECC mul (variable-base) plus add.
  • enableSpend / enableOutput: bound to the dummy-handling selectors inside the Orchard circuit.

Everything else in ActionDescription (the ciphertexts, the ephemeral key, the spend-auth signature) is not a public input to the halo2 proof: it is verified by separate consensus rules (decryption, signature verification, format checks). The proof only certifies the in-circuit predicates above.

3.3 Bundle-level proof verification

A V5 transaction with an Orchard bundle is accepted only if all of the following hold (Zcash Protocol Spec section 5.4.7 "RedDSA" and section 7.1.2 "Transaction Consensus Rules"):

  1. The single halo2 proof in proofsOrchard verifies against the per-action public-input blocks and the bundle-level public inputs. This is one call to halo2_proofs::plonk::verify_proof, no matter how many actions are in the bundle.
  2. Every spendAuthSig[i] is a valid RedPallas signature over the SIGHASH transaction digest with public key actions[i].rk.
  3. bindingSigOrchard is a valid RedPallas signature over the same digest, with a public key derived from the sum of actions[i].cv minus [valueBalance]VOrchard[\mathsf{valueBalance}] \mathcal{V}^{\mathsf{Orchard}}.
  4. anchorOrchard matches a historical note-commitment-tree root at the consensus-required age window.
  5. Every actions[i].nullifier is absent from the historical nullifier set (no double-spend).

The fact that a single halo2 proof covers all actions is the single largest verifier-cost saving over Sapling.

3.4 The differences, field by field

The most useful comparison is to lay the three pool formats side by side.

Per-input description

ConceptSprout (JoinSplit)Sapling (SpendDescription)Orchard (ActionDescription)
Inputs per description211 (spend side)
Outputs per description201 (output side)
Anchorper JoinSplitper spend (every spend carries its own)per bundle (one shared anchor)
Nullifierper input (2)oneone (or dummy)
Value commitmentn/aoneone (covers spend - output)
Randomized auth pubkeyn/arkrk
Output commitment2 (in same)(in separate OutputDescription)cmx (x-coord only)
Encrypted note + memo2 (in same)(separate)encCiphertext (580 B)
Per-description zk-proofyes (BCTV14/Groth16)yes (Groth16)no (one proof per bundle)
Spend-auth signaturen/aper spendper action
Serialized size (typical)~1700 B384 B + 948 B output820 B (combined)

Per-bundle metadata

ConceptSprout (vJoinSplit array)SaplingOrchard
Value balanceper JoinSplit (vpub_old/new)per bundle (valueBalanceSapling)per bundle (valueBalanceOrchard)
Binding signatureper JoinSplit (signs randomSeed)per bundle (Pedersen-based)per bundle (RedPallas)
Single proof per bundle?no (one per JoinSplit)no (one per Spend, one per Output)yes (one halo2 proof)
Pool-level proof systemBCTV14/Groth16 (BN254)Groth16 (BLS12-381)halo2 + IPA (Pallas)
Trusted setup neededyesyesno
Enable spends/outputs flagsn/an/a (always both)bundle flag byte

Why these choices

  • Per-bundle anchor (Orchard) vs per-spend anchor (Sapling). In Sapling, each spend independently selects which historical anchor to use, which complicates wallet logic and bloats the spend description. Orchard sharing a single anchor across the whole bundle is simpler, slightly smaller on the wire, and no less private (the wallet can always split into multiple bundles if it wants different anchors).
  • Symmetric actions (Orchard) vs split spend/output (Sapling). Sapling's separation leaked the spend / output count of a transaction; Orchard's symmetric structure with mandatory dummies hides it. The wallet always pads to the max(inputs, outputs).
  • Single proof (Orchard) vs many proofs (Sapling). Each Groth16 verification is ~3-4 pairings. Sapling transactions with 5 spends and 5 outputs cost ~10 verifier units; an Orchard bundle with 5 actions costs one halo2 verification (with IPA-batched MSM). The verifier-cost gap grows linearly with the number of descriptions.
  • No trusted setup (Orchard) vs MPC ceremony (Sapling). halo2's IPA does not need a structured reference string. This was the original motivation for the Pasta cycle and for halo2 itself (the Halo paper).

3.5 Real-world examples (mainnet)

Two real V5 transactions, both mined in mainnet block 3357450, make the format above concrete. Both happen to have exactly two actions in their Orchard bundle. The full raw JSON for each is archived under onboarding/static/orchard-tx-examples/ so the examples remain reproducible even if the explorer is unavailable.

Example A: pure shielded transfer (d92c8b0d...a25f)

Bundle-level fields:

FieldValue
version5
vin / vout count0 / 0 (no transparent component)
vShieldedSpend / Output0 / 0 (no Sapling component)
actions count2
flagsOrchardenableSpends = true, enableOutputs = true
valueBalanceOrchard+10000 zat (+0.0001 ZEC, the fee leaving the Orchard pool)
anchorOrchard5255c4c7e2fbb24ae185caae08177cfdb097d496d8d81f95a5c9cdfcde81f416
proofsOrchard length7264 bytes (14528 hex chars) for the single halo2 proof

First action's serialized fields:

FieldBytesValue
cv32d493c81198c6a7e2c8b2d2570780bea2656b5d68078f9f655117c21e250cbb15
nullifier32d07befbcf98f4c0e5da783e1ff29d892bd7073faea0d44f87317c23ae875093a
rk32c1095f96b593f06a8d9720e5acec432194885677928eb182501139037ca674bc
cmx3243e1807748ad1f27c44bbfd8544545c3ccecba2269ae20fe95f1c5a6a43a8f3a
ephemeralKey32a142f7870ceec45203a78f4eedf3a09f95e28e3195a0bc5e0a864060661cddbd
encCiphertext58094d5dedc... (Note + memo, ChaCha20-Poly1305)
outCiphertext80e20d73b6... (OVK-encrypted decryption material)

Each action contributes 32+32+32+32+32+580+80=82032 + 32 + 32 + 32 + 32 + 580 + 80 = 820 bytes to the bundle, matching the spec exactly. The spendAuthSig for each action (64 bytes) lives at the bundle level in vSpendAuthSigsOrchard.

What this transaction proves: valueBalanceOrchard = +10000 says "10,000 zat is moving out of the Orchard pool" — and since the transaction has no transparent or Sapling outputs, that 10,000 zat is the miner fee. The two actions otherwise net to zero (one spend equals one output), so the bundle is consistent with "spend two Orchard notes, create two new Orchard notes, total value equals input minus 10000".

Example B: shielding (T->Z) transaction (714c7b48...7685)

Same block 3357450.

FieldValue
version5
vin count1 (transparent input t1a4eKkm768xAH6PKmC1goHys3rh7MiZcea, value 0.12006222 ZEC = 12 006 222 zat)
vout count0
actions count2
flagsOrchardenableSpends = true, enableOutputs = true
valueBalanceOrchard-11991222 zat (negative = value flowing into the Orchard pool)
anchorOrchardae2935f1dfd8a24aed7c70df7de3a668eb7a49b1319880dde2bbd9031ae5d82f

First action:

FieldValue
cve59a67c425fba74cccc3a2d1e1716c971cbcefa1209933889c363612211c69ac
nullifier7c55b6fc9a9061cf1e0a657434b1a0e8e4cc9bc478744f23a996800e37a94321
rk3cd24d54bc8f6f64d89b32d7125f05f0e952858d88fecd0d50143d1e0b855487
cmxb80db92198058a36273dab3543819bf648fe8f7a08c1a824613ff68b1b957c3b
ephemeralKeyee006dc5904320c8c446ef898b9473c0313e3e8851e44c9f671dd01dc366ed08

Consensus-balance check, in zat:

transparent_in = 12 006 222
transparent_out = 0
valueBalanceOrchard = -11 991 222 (negative: pool absorbs value)
fee = transparent_in - transparent_out - (- valueBalanceOrchard)
= 12 006 222 - 0 - 11 991 222
= 15 000 zat (0.00015 ZEC)

enableSpends = true even though there are no real Orchard inputs: the two actions still have dummy spend sides. The prover witnesses those dummies and proves the dummy predicate (value zero, random key); on the wire the bundle is indistinguishable from one that spends two real notes. This is how a transaction that is "only" a shielding still produces spends and nullifiers indistinguishable from real activity.

Example C: t->z consolidation, four transparent inputs (b79955d9...9969)

Block 3357100.

FieldValue
version5
vin count4 (consolidating four prior transparent UTXOs)
vout count1 (transparent change to t1Ku2KLyndDPsR32jwnrTMd3yvi9tfFP8ML)
actions count2
valueBalanceOrchard-1 309 449 925 zat (negative: pool absorbs ~13.09 ZEC)
anchorOrchardae2935f1dfd8a24aed7c70df7de3a668eb7a49b1319880dde2bbd9031ae5d82f

The four transparent inputs have values 1.5001, 1.5412, 2.28319145 and 232.68922473 ZEC, totalling 238.01371318 ZEC. The single transparent output returns 224.91891693 ZEC to the same wallet. The remaining ~13.09 ZEC enters the Orchard pool as two new shielded notes.

Why this example matters: it shows that the Orchard bundle size is independent of how many transparent inputs the transaction has. The transparent-side fan-in is whatever Bitcoin-style script logic dictates; the Orchard side stays exactly two actions (which is the wallet's privacy-padding choice, not a consequence of the transparent count).

Action 0:

FieldValue
cv428b36c3c06f08d9470b98aef3ab4e105971183a228367d938daeb2be2d1f584
nullifier616ef3b62330c64cb378bf245868714e857d9a8d5687ee23e4d7d580e6f6ba36
rk7ef26ff3327584acdae27f87e9c77e055b3016e04347b1ccb369f2e8fdb2b51d
cmxf698fa99b0135def37f4eda1c7705859301d246dd482d477d9fba2cb8494aa15
ephemeralKey61a50a832f09721ff03cea426804d1013eb3b727c38135fce055ec8236d2b39e

Note that anchorOrchard here matches example B's anchor exactly: both bundles selected the same historical note-commitment-tree root. This is allowed by consensus (any anchor within the rolling age window is accepted) and is mildly useful as a verifier-side cache hit, but it carries no cryptographic significance.

Example D: z->t deshielding, four actions (8ca808cd...1cbe)

Block 3357000.

FieldValue
version5
vin count0
vout count1 (transparent output t1ZQEGDvsfecozh3LvWGdUrcL8idozig9K9, 3.2495003 ZEC)
actions count4
valueBalanceOrchard+324 975 030 zat (~+3.25 ZEC leaving the pool)
anchorOrchardaad846cc74c19db033b625533056ab47bcda1adebac77d5060caf922fb24782c
proofsOrchard length11808 bytes (vs 7264 bytes for 2-action bundles)

Action 0:

FieldValue
cv1429d2ff83c5a938d21f0c678ca9006ff61940b148a3744b9ddfb2aacde1d49f
nullifiercfce1e109fa2902b9a1942a07e890fa530ff39ae50815e696614ff6a1e03ab19
rk128afa79f1fd183706eebd53e7972131d580e950338a2affa8bab95d6658c726
cmx302af08932772b3113f134947393f2f2fe907aafd1f4330720e97451a4b6290e
ephemeralKey09ec3c3c6fd34f4c58d2051dda0e2c99348e433672df52ddabcc6e2fa13706b8

This bundle has four actions and pulls 3.25 ZEC out of the pool into a single transparent address. Four actions means the wallet spent something like 2 real notes (the others are dummies, or it spent 3 real notes with one dummy output; the bundle does not distinguish). The output side similarly hides whether the 3.25 ZEC came from one note, two notes, or four.

Example E: pure shielded transfer with three actions (538553e4...fd6a)

Block 3357150.

FieldValue
version5
vin / vout count0 / 0
actions count3
valueBalanceOrchard+15 000 zat (the fee)
proofsOrchard length9536 bytes

Same shape as example A (pure z->z, no transparent or Sapling component) but with three actions instead of two. The wallet that built this transaction picked three for unlinkability reasons (probably one spend plus two outputs, or one real plus two dummies). The on-chain observer sees three nullifiers and three commitments without learning how many of each are real.

Proof-size scaling

Pulling the proof length and action count from all five examples:

ExampleActionsproofsOrchard (bytes)Per-action delta vs base
A, B, C27264baseline
E39536+2272
D411808+2272 (relative to E)

Each additional action adds exactly 2272 bytes to the halo2 proof on mainnet (at this protocol version). This is the per-action evaluation and commitment overhead the prover writes to the transcript; the constant 7264-byte base covers the gate, permutation, lookup, and IPA tail. Note that this is the proof size, not the verifier cost: the per-action verifier work is dominated by the multi-scalar multiplication, which IPA can amortize when batched (see chapter 09).

What these five examples confirm about the format

  • Per-action body is exactly 820 bytes (32 + 32 + 32 + 32 + 32 + 580 + 80), matching the table in section 3.1.
  • The Orchard bundle size is independent of how many transparent inputs and outputs the surrounding transaction has (example C has 4 vin, example A has 0).
  • A single halo2 proof covers all actions in the bundle; the proof grows by ~2272 bytes per additional action.
  • An equivalent Sapling bundle would carry one ~192-byte Groth16 proof per spend and per output, plus per-description overheads, so the cross-over point in proof size between Orchard and Sapling is around 30+ descriptions; below that Sapling is smaller, but every action's verifier cost is one pairing-set in Sapling versus an amortizable MSM in Orchard.
  • The anchor is bundle-scoped: examples B and C share the same anchor across different bundles, which is allowed and expected.
  • enableSpends and enableOutputs are both true in every example above; their existence in the byte format is what makes output-only and spend-only bundles encodable at all, but mainnet wallets do not seem to use that capability in practice.

3.6 Where to look in the code

The transaction format lives in zcash/librustzcash, not in this repository. Pointers:

For halo2-side code that the format ultimately reaches:

halo2_gadgets/src/sinsemilla/merkle.rs (the anchor's hash personalization)

/// SWU hash-to-curve personalization for the Merkle CRH generator
pub const MERKLE_CRH_PERSONALIZATION: &str = "z.cash:Orchard-MerkleCRH";

/// Instructions to check the validity of a Merkle path of a given `PATH_LENGTH`.

MERKLE_DEPTH_ORCHARD = 32 (in halo2_gadgets/src/sinsemilla/merkle.rs) is exactly the depth that anchorOrchard is the root of. The two anchor values quoted in section 3.5 (5255c4c7... and ae2935f1...) are each 32-byte outputs of MerkleCRH^Orchard applied 32 times up from a leaf produced by this chip.

4. Failure modes

  • Mixing bundle flags with action contents. enableSpends = 0 does not allow you to skip the spend-side witness in the action; the witness is still required (and is for a dummy note). The flag only changes the predicate the proof must satisfy. Wallet code that conflates the two will produce unprovable bundles.
  • Per-action anchors. Porting a Sapling parser to Orchard and assuming each action carries its own anchor is a frequent source of bugs. Anchor is bundle-level; each action shares it.
  • Forgetting that cv is a Pallas point, not a value. cv is the value commitment, not the value; sum of cvs minus [valueBalance] V should be a commitment to zero modulo the binding-signature pubkey transformation. Treating cv as an int64 is the classic format-parsing mistake.
  • Encrypting before signing. SIGHASH-style coverage in V5 transactions includes the ciphertexts; signing first and encrypting afterwards produces invalid signatures.
  • Re-using the bundle proof in another bundle. The proof is bound to the exact public-input vector, which includes the per-action cv, nf, cmx, and rk. Reuse is detected by consensus, but if you are writing batching infrastructure on top, do not share proofs across bundles.

5. Spec pointers

6. Exercises

  1. From the table in section 3.4, identify the three differences between a Sapling SpendDescription and an Orchard ActionDescription that change the on-wire serialized size. Compute the per-input size delta in bytes.
  2. Read zcash/orchard/src/action.rs and identify which Rust struct field corresponds to each row of the action format table in section 3.1.
  3. Suppose a wallet wants to make a 0-input, 1-output Orchard transaction (a pure shielding from the transparent pool). How many actions does the bundle need, and what does the flagsOrchard byte contain?
  4. Why is anchorOrchard 32 bytes (instead of, say, a varint)? Hint: it is the output of Extract_P applied to a Pallas point.
  5. The halo2 proof in proofsOrchard is the only zk-proof in the Orchard bundle. List the fields in ActionDescription that the proof does not bind (i.e. that are checked by separate consensus rules).

Answers in the code

  • Exercise 3: one action, with enableSpends = 0 and enableOutputs = 1. The spend side of the action is a dummy; the output side is the real new note. valueBalanceOrchard equals the negation of the transparent value being shielded (it is signed: negative means value flowing into the pool).
  • Exercise 4: Extract_P returns a Pallas base-field element, which fits in a fixed 32-byte little-endian encoding (the field modulus is ~255 bits).
  • Exercise 5: ephemeralKey, encCiphertext, outCiphertext, and spendAuthSig are checked outside the halo2 proof; only anchor, cv, nf, rk, cmx, enableSpend, enableOutput are public inputs to the circuit.

7. Further reading