Ragu & Tachyon
Home
  • Scaling Zcash
  • An Introduction to Proof-Carrying Data
  • Statements and Challenges
  • Arithmetization
  • QAP and Polynomial Encoding
  • Polynomial Commitments
  • The Inner Product Argument
  • Batched Openings
  • The Complete Proof
  • Recursive Proofs
  • Accumulation
  • Split Accumulation
  • Curve Cycles
  • Revdot Folding
  • Wiring and the Registry
  • Endoscalars
  • Staging
  • Recursion in Ragu
  • Notes, Nullifiers, and Tachygrams
  • Pool State and Anchors
  • Oblivious Sync
  • The Proof Tree
  • Aggregation and Bundles
Final Recap
  • Notation
  • Glossary
  • Curves
Home
  • Scaling Zcash
  • An Introduction to Proof-Carrying Data
  • Statements and Challenges
  • Arithmetization
  • QAP and Polynomial Encoding
  • Polynomial Commitments
  • The Inner Product Argument
  • Batched Openings
  • The Complete Proof
  • Recursive Proofs
  • Accumulation
  • Split Accumulation
  • Curve Cycles
  • Revdot Folding
  • Wiring and the Registry
  • Endoscalars
  • Staging
  • Recursion in Ragu
  • Notes, Nullifiers, and Tachygrams
  • Pool State and Anchors
  • Oblivious Sync
  • The Proof Tree
  • Aggregation and Bundles
Final Recap
  • Notation
  • Glossary
  • Curves
  • Scaling Zcash
  • An Introduction to Proof-Carrying Data
  • Statements and Challenges
  • Arithmetization
  • QAP and Polynomial Encoding
  • Polynomial Commitments
  • The Inner Product Argument
  • Batched Openings
  • The Complete Proof
  • Recursive Proofs
  • Accumulation
  • Split Accumulation
  • Curve Cycles
  • Revdot Folding
  • Wiring and the Registry
  • Endoscalars
  • Staging
  • Recursion in Ragu
  • Notes, Nullifiers, and Tachygrams
  • Pool State and Anchors
  • Oblivious Sync
  • The Proof Tree
  • Aggregation and Bundles
  • Final Recap
  • Appendix

    • Notation
    • Glossary
    • Curves

Notes, Nullifiers, and Tachygrams

The earlier parts of this site built a proof system that can absorb other proofs. This chapter returns to the application that system serves. Chapter 1 described Tachyon in outline, with epochs, evolving nullifiers, and a window of retained values. Here we work out the objects themselves: which keys a wallet holds, what a note contains, how a note's nullifiers are derived for each epoch, why a spend publishes two of them, and why every action publishes exactly two values of a single kind.

We do not cover how a proof refers to the pool's history or how a spender shows absence from old epochs. The next chapter builds the pool state, chapter 21 covers absence proofs, and chapter 22 walks through the steps that prove all of this.

Status of these designs

Tachyon has not activated on mainnet and its design is still changing. Everything here describes the Tachyon repository at commit 7e50254, whose shielded protocol ZIP is still an empty outline. The book's own key hierarchy page carries a note calling itself significantly outdated, and the book's analysis of nullifiers presents its choice as a recommendation with an alternative. Where the sources differ, we show both and follow the code unless we say otherwise. Tachyon also pins Ragu at 0725b52, not at the 3219e08 that the earlier chapters read.

1. Fields and stand-ins

Tachyon's objects live in two fields. Pallas, the curve used for spend authorization, has coordinates in Fp​ and scalars in Fq​. Poseidon's inputs and outputs, and therefore all tachygrams, are elements of Fp​, and signing keys are elements of Fq​. The toy curve E from the curves appendix has the same shape, with coordinates in F31​ and scalars in F43​, so we use it for every signing key below.

The hashes need a different treatment. Tachyon uses Poseidon throughout, which chapter 3 introduced, and each use has its own domain. In a field of 31 elements, unrelated hash outputs would collide constantly, and that would hide the behaviour we want to see. So the examples use a stand-in. We write HTag​(…) for SHA-256 of the tag and inputs, reduced modulo 65,537, and a stand-in sponge produces four such values from one call. These numbers are not Poseidon outputs and are not elements of the toy field. They only show the structure, and the small range is useful later, because it lets us count a cheater's chances exhaustively.

2. The spending key and its two children

Spending a note requires two separate abilities. The owner has to authorize the spend with a signature, and the owner has to produce the note's nullifiers, which nobody else should be able to compute. The deep dive observes that these are the only two abilities the shielded protocol needs from its keys. Everything else in Orchard's key tree, including viewing keys and diversified addresses, supports delivering notes to recipients and detecting them, and chapter 1 explained that delivery has moved to a separate payment protocol.

The root is a spending key sk of 32 random bytes, and each ability gets one child derived from it. The two derivations differ only in a one-byte domain, through BLAKE2b-512 with the Zcash expansion personalization:

ask=ToScalar(PRFskexpand​(0x21)),nk=ToBase(PRFskexpand​(0x22)).

The spend authorizing key ask is a scalar in Fq​, and its public point is the spend validating key ak=[ask]G. The nullifier key nk is an element of Fp​, and it has no public counterpart.

One detail comes from RedPallas, the signature scheme that Orchard and Tachyon share. It requires ak to have a cleared sign bit in its compressed encoding. A point and its negative have the same x and opposite signs of y, so if the derived point has the wrong sign, the code negates ask, since [−ask]G=−[ask]G. On the toy curve we use the parity of y as the sign.

Our generator G is the toy point (1,2), and the wallet's sk is the text alice-wallet-seed-7, which stands in for 32 random bytes.

KeyDerivationStand-in value
ask, before normalizingexpand with 0x21, in F43​19
its public point[19]G(27,1), with y odd
ask, normalizednegated, since y was odd24
ak[24]G(27,30), with y even
nkexpand with 0x2240954

The derivations from sk are not part of what proofs check. The deep dive says that the circuits use ak and nk as witnesses and never constrain how they arose. A wallet standard chooses the path, and the protocol only needs the results to look like randomly sampled keys.

3. The payment key

A sender has to name the owner of a new note. Publishing (ak,nk) to the sender would give away the nullifier key, so the note carries a commitment to the pair. The payment key is a hash of the coordinates of ak together with nk:

pk=PoseidonTachyon-PkDerive​(akx​, aky​, nk).

With our keys, and the stand-in in place of Poseidon, this gives:

pk=HPkDerive​(27, 30, 40954)=35507.

If the same wallet walks to the next index for a second sender, it obtains ak=(14,22) and nk=35396, which give pk=3270. Nothing in the two payment keys shows that they belong to one wallet.

Two facts about this hash shape the rest of the design. First, pk is a deterministic function of the pair, so one payment key handed to two senders would let them compare notes and link the payments. Since a hash admits no tweak, unlinkability comes from minting a fresh address per sender. The index that makes keys differ sits above sk, which is why our second address started from a second seed. The book gives two reasons for preferring a hash over Orchard's diversified base. That trick has no known secure analogue under lattice assumptions, and handing ak to senders would let one who kept it attack it later with a quantum computer.

Second, the hash binds nk to the note's owner field, and Section 8 shows that this is what stops a spender from inventing a second nullifier sequence.

The wallet can also separate proving from signing. The proof authorizing key pak=(ak,nk) gives a device what it needs to build proofs for every note paid to the matching payment key, and it cannot sign. A hardware wallet can therefore hold ask while another device does the proving. The key hierarchy page and a code comment call nk wallet-wide, so that one pak covers all of a wallet's notes. That does not fit a fresh (ak,nk) pair per sender, and the sources do not reconcile the two. The next table lists what each key allows, with the per-sender reading.

KeySecret?What its holder can do
skyeseverything below
askyessign for spends, after randomizing (Section 5)
nkyescompute nullifiers given a note's ψ, and so see when the note is spent
pak=(ak,nk)yesbuild proofs for every note paid to this pk, without signing
pkshared with sendersreceive notes, with no other ability

Book and code: narrower delegation

Book: the key hierarchy page, marked as outdated, lists a per-note bundle (ak,mk) that would let a prover work on one note only, where mk is the per-note key of Section 6.

Code at 7e50254: NoteSeed and SpendAction both witness a full pak and rederive pk from it, so a prover holding only (ak,mk) could not run them. A comment on the type also says that proof delegation is yet to be specified.

This site follows the code, and treats per-note proving delegation as unsettled. A sync service, by contrast, receives only values and no key material.

4. Notes and commitments

A note has to record who owns it and what it is worth, and it needs enough randomness to stay hidden and to have nullifiers of its own. Tachyon's note has exactly four fields:

Note=(pk, v, ψ, rcm).

Here pk is the payment key, and v is a value in zatoshi between 0 and 2.1×1015. The nullifier trapdoor ψ gives the note its identity for nullifier derivation, and the commitment trapdoor rcm hides the note inside its commitment. Comparing with Orchard's note from chapter 1, the diversifier and transmission key have become pk, and ρ has gone. Section 9 returns to what removing ρ costs.

The sender chooses ψ and rcm and so knows all four fields, as the threat model assumes. The recipient has to learn them as well, through whatever payment protocol the two wallets use. How to generate them is unsettled. The deep dive says they should come from a standardized derivation off a wallet key, the code comments say they can come from a key negotiated out of band, and we found no standard at this commit. Suppose Bob pays Alice 25 zatoshi using the payment key above:

FieldValueWho knows it
pk35507Alice gave it to Bob
v25Bob and Alice
ψ625Bob and Alice
rcm61249Bob and Alice

Bob cannot publish these values, so he publishes a note commitment that fixes all four fields:

cm=PoseidonTachyon-CmDerive​(rcm, pk, v, ψ).

Sapling and Orchard use variants of the Pedersen commitment, which rely on discrete log, and Tachyon uses a hash alone. Each Tachyon use of Poseidon starts its sponge with a zero rate and with the 16-byte domain name as a number in the capacity cell. Different domains therefore behave as unrelated hash functions, even on identical inputs, and Section 11 relies on this. For our note:

cm=HCmDerive​(61249, 35507, 25, 625)=55697.

Changing only rcm to 10197 gives the commitment 15883, which looks unrelated to 55697 although the note has the same owner, value, and ψ.

A random rcm is what lets the commitment hide two notes with the same owner and value from each other, and hiding here assumes that Poseidon behaves like a pseudorandom function.

5. Randomizing the signing key for each action

A signature has to be checkable by everyone, so its verification key must be public. If that key were ak, then every spend under that key would carry the same public key, and an observer could link them all. Spend authorization therefore works as in Orchard. Each action picks a scalar α and signs with a randomized key,

rsk=ask+α,rk=ak+[α]G=[ask+α]G.

The second equation holds because scalar multiplication distributes over addition, so rk is exactly the public key of rsk. The action publishes rk, and the proof shows that it came from the ak inside the note's payment key.

Tachyon derives α from per-action entropy θ and the note commitment, with α=PoseidonTachyon-Spend​(θ, cm) read as a scalar, which is possible because p<q. This ties the key to the note, and it lets a signing device recompute α from the note and θ without having to trust a value handed to it. Our stand-in output is far larger than 43, so here we reduce it modulo 43, which the real construction does not do.

QuantityComputationValue
θchosen by the signer4071
αHSpend​(4071, 55697)=1614, reduced mod 4323
rsk24+23 mod 434
rk(27,30)+[23]G(24,30)
check[4]G(24,30)

A signature follows the Schnorr pattern. With nonce 9, the signer computes R=[9]G=(9,22), derives a challenge c=18 from a hash of R, rk, and the message, and returns s=9+18⋅4=38 modulo 43. The verifier checks [s]G=R+[c]rk, and both sides equal (26,23). The verifier never used ak.

The public rk reveals nothing about ak when α is uniform, because adding a uniform multiple of the generator produces a uniform point whatever the starting key.

We can test this exhaustively on the toy curve. Of the 42 points other than the identity, 21 have even y and could be a normalized ak. For each of those 21 candidates, exactly one α in F43​ turns it into (24,30). The published key is therefore equally consistent with every owner, and a second action by Alice with α=32 publishes the unrelated-looking (6,23).

An output action has nothing to authorize, since creating a note spends nobody's money. It still has to look like a spend, so it also publishes a key and a signature, with rsk=α and rk=[α]G, using the domain Tachyon-Output in place of Tachyon-Spend. Both shapes produce a uniform point. Each action also publishes a value commitment cv, which the chapter on the proof tree covers.

6. Deriving a note's nullifiers

Chapter 1 set out what an epoch nullifier has to do. A note needs a different nullifier in each epoch, these values must look unrelated to anyone without the owner's secret, and each epoch must have exactly one valid value. A wallet might also need many of them at once, because a proof about an old note covers every epoch since its creation. These needs lead to a derivation in two stages.

The first stage combines the two secrets that make a nullifier belong to this owner and this note. It produces the note's master key:

mk=PoseidonTachyon-NfMaster​(ψ, nk).

The sender knows ψ and not nk, and the wallet holds nk for many notes, so only the owner of this specific note has both. Doing this once also keeps nk out of later headers. In the proof tree, NoteSeed computes mk and passes it on, and no later header carries nk, although SpendAction witnesses pak again to check the payment key. Our note gives:

mk=HNfMaster​(625, 40954)=299.

The second stage turns mk and an epoch into nullifiers. One Poseidon permutation yields a whole rate's worth of output, so four values cost the same as one. Wasting three of them would be a poor use of a limited circuit, and Tachyon takes all four and assigns them to four consecutive epochs. To find the value for epoch e, round down to the group's start w=e−(emod4), run one sponge over (mk,w), and take the entry at position e−w:

PoseidonTachyon-NfDerive​(mk, w)=(nfw​, nfw+1​, nfw+2​, nfw+3​).

For e=7 we get w=7−(7mod4)=4, so nf7​ is the entry at position 3 of the group that starts at 4.

Here is the running note's sequence, created in epoch 3 and spent with target epoch 7, the same example as the figure in chapter 1. We use "target epoch" for the epoch e that a spend's proof reaches. The sources also call it the anchor's epoch or the present epoch.

Epoch eGroup start wPosition e−wnfe​Role for this spend
00019392before the note existed
10115569before the note existed
20221425before the note existed
30314090tested for absence
4402976tested for absence
54163093tested for absence
64259553tested for absence
74355113published by the spend
88061177published by the spend
98132258never revealed
108231053never revealed
118334200never revealed

A value depends only on (mk,e), because each group absorbs its own w. Two derivations that overlap therefore agree on the epochs they share, and the script asserts this for our note.

Both inputs matter. A second note to Alice with ψ=58561 has mk=1991 and nf7​=45091, so her two notes do not share nullifiers. Bob knows the first note's ψ, but his own key 44055 gives mk=2019 and nf7​=55576, where the true value is 55113. So the sender of a note cannot watch for its spend.

The privacy argument assumes that Poseidon, keyed by mk, behaves as a pseudorandom function. Revealing some outputs then says nothing about the others, including the pair revealed at spend time.

The code allows epoch indices up to 1,048,575, since a block height has 32 bits and an epoch has 4,096 blocks. At 75 seconds per block that range lasts about 10,207 years.

Book and code: the nullifier's inputs

Deep dive: mk=Poseidonmk(nk,ψ) and nfe​=Poseidonnf.Permute(mk,⌊e/Rate⌋)[emodRate], calling this the leading candidate.

Nullifier page and code at 7e50254: mk absorbs ψ first, and the sponge absorbs w, the group-aligned epoch.

For e=6 the deep dive absorbs ⌊6/4⌋=1, while the code absorbs w=4. In our stand-in the two choices give 35925 and 59553, so they are different functions, although each gives every epoch one fixed value.

This site follows the code. Comments in the code also lag behind it. One still gives nf=Fnk​(Ψ∥e), another mentions a GGM tree, and a third calls the commitment scheme undecided while the function below it uses Poseidon.

7. Why the wallet derives its own nullifiers

A reader might expect a design in which the wallet hands a sync service a key for a range of epochs, and the service computes the nullifiers itself. An earlier Tachyon design did this, and the book keeps it as an archived page. The reason it was replaced is proof cost, which the following comparison makes concrete.

The archived design used the GGM construction, which treats an epoch's bits as directions down a binary tree. A seed sits at the root, each node has two children given by hashes G0​ and G1​, and each leaf is a nullifier. Hashes are one-way, so holding a node gives access to the leaves below it and to nothing else, which is exactly a key for a range of epochs.

Take a 3-bit epoch space and the seed 13625, with a stand-in hash for each Gb​. To delegate epochs 2 and 3, a wallet hands over only the node at prefix 01, which is k01​=G1​(G0​(13625))=23515. Walking one more level gives G0​(23515)=26563 and G1​(23515)=19988, which are exactly the nullifiers for those epochs, and one-way hashing keeps the holder away from all the others. The range of epochs 0 to 6 needs 3 prefixes (0, 10, 110). Over all ranges in this small space the worst case needs 4, reached by epochs 1 to 6, so the amount of delegated key material depends on the range.

The trouble is that each level of the tree costs one hash inside a proof, and proof steps are small. The analysis counts it this way.

A step has 211=2,048 multiplication gates, and one Poseidon permutation costs 288 of them, so a step fits ⌊2,048/288⌋=7 permutations.

A binary tree over an epoch space of 214 days, about 45 years, has depth 14, so one walk takes 14 permutations and overflows a step. A tree with higher arity near the root can cut the depth to 7, which just fits one walk.

The analysis wants at least two nullifiers in one step, since a spend publishes two, and a tree walk cannot provide that. It then tries other constructions and recommends using a Poseidon permutation directly, with its rate-4 output. The analysis calls this Attempt 3, and it is the design of Section 6:

DesignPermutations for one nullifierNullifiers per step of 7 permutations
GGM, binary tree of depth 1414none, since one walk overflows a step
GGM, mixed arity, depth 771
One hash per nullifier (Attempt 3 before batching)17
Rate-4 squeeze (Attempt 3)1/4 of a permutation7⋅4=28

This construction gives up the range key, because a plain hash has no partial key to hand out. The trade is that wallets derive and prove their own nullifiers, and they send the service explicit values. The service then holds a finite list and can compute nothing beyond it, and chapter 21 shows how the wallet later ties the service's work to its note.

Book and code: nullifiers per step

Analysis page: 7⋅4=28 nullifier derivations per step. The same page's conclusion offers a MiMC-based alternative at about 50 to 68 per step, for those willing to accept the added dependency.

Code at 7e50254: NF_DERIVATION_WIDTH is 16, so NullifierDerive runs 4 sponges per window, and the step also binds the window to a committed sequence. The sources do not explain the choice of 16.

This site follows the code, and treats 28 as the analysis's upper count.

8. How the commitment pins the nullifiers

Publishing a nullifier only prevents double spends if a spender has no choice about its value. If a spender could produce two nullifier sequences for one note, the second sequence would pass both the exclusion proof and the window, since neither has ever seen those values. The note commitment has to rule this out, and it does so through a chain of hashes:

The proof's NoteSeed step computes pk from the witnessed pak, builds the note, and derives both mk and cm from the same ψ and nk. A seed can name any note it likes, so this step alone proves little. The claim closes later, when SpendableInit requires cm to be a member of a creation stamp's tachygrams. The spender is therefore held to a commitment that already exists in the pool, and both inputs to mk sit behind that commitment.

To cheat, a spender would keep cm fixed while changing what lies behind it. Our small hash range allows us to try every possible change to one field at a time:

Field changedWrong values triedValues that keep cm the sameWhat a match would give the cheater
nk (through pk)65,5361a second nullifier sequence
ψ65,5361a second nullifier sequence
v65,5362a different value for the same note
rcm65,5362nothing useful, since the note is otherwise the same

A random function into 65,537 values should produce about one match per row. The table shows 6 matches in total, and each of them is a real collision. With nk=56813, for example, the payment key still comes out as 35507, so the commitment stays 55697, while the master key 18682 yields nf7​=55179 in place of 55113. On this toy scale a cheater could double spend.

The attack succeeds here only because the range is small. The protection comes entirely from the assumption that nobody can find such a collision, so it needs a hash with a far larger range. Real Poseidon outputs have about 255 bits, and a search of the kind we just ran would be hopeless. Two further assumptions complete the argument. The proof system must not allow a false statement to verify, and the proof must really compute these hashes, which Section 13 qualifies, since these steps do not yet run as real circuits.

The remaining pieces follow the same pattern. SpendBind computes the published pair from the threaded mk and the epoch, so no nullifier is a free witness. It also compares the secret's commitment with the one the spendable lineage carries, and the proof tree page notes what this rejects. A phantom note that reuses ψ with a larger value has a different cm, and that commitment was never published.

9. Reusing a trapdoor

Nothing in the protocol forces ψ to be unique, because the pool holds an unordered set of values and gives notes no position to bind to. Orchard avoided this by setting a new note's ρ to the nullifier of a spent note, and consensus already forbids repeated nullifiers. Tachyon cannot enforce that binding, and a malicious sender can exploit this:

Suppose Bob sends Alice a second note with value 9, a new rcm=4977, and the same ψ=625. Its commitment 53772 differs from 55697, so it looks like a separate note. Since pk fixes the same nk, both notes have mk=299 and the same nf7​=55113 at every epoch. Once Alice spends one of them, the other can never be spent, because its nullifiers have already been published. A wallet detects this on receipt by computing one nullifier at a fixed reference epoch for each incoming note. With reference epoch 0, both notes give 19392, and the wallet rejects the second.

This is the faerie gold attack, in which a recipient holds money that turns out not to be spendable. It harms the recipient and creates no money, and Tachyon moves the defence out of consensus. A wallet's own note set is small, which keeps the comparison cheap.

Book and code: faerie gold

Deep dive: the attack remains possible in principle, and resisting it becomes the responsibility of the payment protocol, through the wallet check above.

Code comments at 7e50254: they call the attack moot under out-of-band payments, and say that nullifiers need no collision resistance. The nullifier page separately warns that notes should not reuse ψ.

We did not find a wallet check in the crate. This site follows the deep dive here, since the example above shows that the attack works, and out-of-band delivery does not change the arithmetic.

10. Why a spend publishes two nullifiers

We can now return to the rule that chapter 1 stated and tested with labels. A spend with target epoch e proves that nfj​ is absent from epoch j, for every closed epoch j from the note's creation up to e−1, and validators keep the tachygrams of the current and preceding epochs. Validators also accept a stamp for epoch e during epoch e or epoch e+1, because a transaction may still be waiting when a new epoch begins, and neither a miner nor a sync service can update the proof on the wallet's behalf.

A later proof looks for each nullifier only in the epoch with the same number. So the mechanism works only if a spend leaves a value where such a test will find it. The grace period threatens exactly this, because a spend with target e may land in epoch e+1.

Suppose instead that a spend published only nfe​. A first spend with target 7 waits in the mempool and gets accepted in epoch 8, so 55113 lands in epoch 8. A second spend with target 8 now tests nf7​ against epoch 7, where it is absent, and it publishes nf8​=61177, which nobody has published. Both checks pass and the note is spent twice. Across all 111 scenarios, 48 get through in this way.

Publishing nfe+1​ as well closes this gap. Write a for the epoch in which a spend is accepted, so that a is either e or e+1. Both possibilities are covered by the pair, so a spend accepted in epoch a always publishes nfa​ during epoch a. The figure shows the three situations.

A grid of epochs 6 to 9 with a row of tests, each asking whether the nullifier with that epoch's number is absent from that epoch. A pair accepted in epoch 7 places nf7 in epoch 7, where the test fails. A pair accepted in epoch 8 places nf8 in epoch 8, where the test fails. A lone nf7 accepted in epoch 8 matches no test.

From this one fact, the double-spend argument takes two cases. Let a first spend have target t1​ and acceptance epoch a1​, and let a second spend of the same note have target t2​ and acceptance epoch a2​≥a1​.

Case t2​>a1​. Epoch a1​ is closed and lies before the second target, so the second proof has to show nfa1​​ absent from epoch a1​. The first spend put it there, so the proof cannot be built.

Case t2​≤a1​. Combining a1​≤a2​ with a2​≤t2​+1 gives t2​≤a1​≤t2​+1, so a1​ is t2​ or t2​+1. The second spend publishes both nft2​​ and nft2​+1​, so it repeats nfa1​​. The same inequalities give a2​−a1​≤1, so epoch a1​ is still retained, and validators reject the duplicate. This is also why the window needs two epochs.

Take two spends with target 7, the first accepted in epoch 7 and the second in epoch 8. The colliding values 55113 and 61177 entered the pool during epoch 7, so a validator that kept only the current epoch would miss them when the second spend arrives.

The argument assumes that a spender cannot choose nullifiers, which Section 8 covered, and that proofs cannot be forged. Here is every outcome for a first spend with target 7, using our note's values:

First spend: targetSecond spend: targetCaught byReason
7, accepted in 76, accepted in 7windowboth publish nf7​=55113
7, accepted in 77, accepted in 7windowboth publish nf7​=55113
7, accepted in 77, accepted in 8windowboth publish nf7​=55113
7, accepted in 78, accepted in 8exclusion proofnf7​=55113 is present in epoch 7
7, accepted in 78, accepted in 9exclusion proofnf7​=55113 is present in epoch 7
7, accepted in 79, accepted in 9exclusion proofnf7​=55113 is present in epoch 7
7, accepted in 79, accepted in 10exclusion proofnf7​=55113 is present in epoch 7
7, accepted in 87, accepted in 8windowboth publish nf7​=55113
7, accepted in 88, accepted in 8windowboth publish nf8​=61177
7, accepted in 88, accepted in 9windowboth publish nf8​=61177
7, accepted in 89, accepted in 9exclusion proofnf8​=61177 is present in epoch 8
7, accepted in 89, accepted in 10exclusion proofnf8​=61177 is present in epoch 8

The script checks this on every pair of spends of our note, with targets from epoch 3 to epoch 9 and each spend accepted in its target epoch or the next. There are 111 such scenarios. In 72 of them t2​>a1​ and the second proof cannot be built, and in the other 39 we have t2​≤a1​ and consensus finds a shared value in its window. No scenario escapes. The two checks also overlap in 6 scenarios, where the first spend was accepted on time and the second targets and lands in the very next epoch, so the window would have caught it too.

Reconstructed from code at commit 7e50254, not confirmed by the authors

The rules come from the deep dive's consensus section, which argues by listing the safe cases for each acceptance time. The single fact about nfa​ and the two-case argument are our restatement. The SpendBind step derives the pair as described, and a comment on the tachygram type states the window rule, but we did not find the window scan itself in the crate. The tachygram page adds a second reason for scanning a whole epoch, which the next chapter explains, since a stamp's anchor can advance within its epoch.

Publishing the pair has a cost in the circuit. The two values do not always come from one sponge.

Our note's pair shows why. The value nf7​ comes from the group at w=4, while nf8​ comes from the group at w=8, so this pair needs two sponges. A spend with target 5 would find both nf5​ and nf6​ in the group at w=4.

A circuit's shape cannot depend on secret values, as chapter 4 explained, and SpendBind runs two sponges in every case. The pair also needs a following epoch, so the code refuses a spend in the final epoch.

11. Tachygrams and output padding

Earlier Zcash pools keep commitments in a Merkle tree and nullifiers in a separate set. Tachyon instead treats both as one kind of value, a tachygram, which is a 32-byte element of Fp​. A single structure can answer both questions a spend asks, namely whether a commitment is present and whether a nullifier is absent, and consensus does not need to know which kind a value is. This also helps privacy, as long as the published values give no other hint of their kinds.

Counting gives such a hint. A bundle already reveals its number of actions n, because each action has one signature. A spend publishes two nullifiers and an output has only one commitment, so without further care the total t would reveal the split, with s=t−n spends.

Spends sOutputs oTachygrams without paddingObserver computes s=t−nTachygrams with padding
0444−4=08
1355−4=18
2266−4=28
3177−4=38
4088−4=48

The deep dive notes that the leak is only about the count, since tachygrams never sit beside a particular action. To remove it, each action publishes exactly two. A spend publishes its pair. An output publishes its commitment and a padding tachygram, so the last column of the table is constant.

The pad cannot be any easily computed function of the commitment. To see what an observer could do if it were, start from a bundle's published list:

Take one bundle (B=1) with N=4 actions, two of each kind. It publishes 2⋅4+1=9 tachygrams, and the stamp lists them in sorted order.

Published tachygramKnown only to the parties involved
13229output 2, pad
24937output 2, cm
30698spend 2, nf8​
31831output 1, cm
40944spend 2, nf7​
46656output 1, pad
55113spend 1, nf7​
55356fee change note, cm
61177spend 1, nf8​

Suppose outputs derived the pad as H(cm). An observer holding the 9 published values hashes each one and looks for the result in the list, which takes 72 ordered comparisons. In our bundle with that design, the test finds exactly 2 matches, (31831, 52667) and (24937, 38763), and so the observer learns the output count. Run the same test on the values in the table above, where each pad comes from the note opening, and it finds no matches. To test the real relation, the observer would need the note's rcm, pk, v, and ψ.

So Tachyon derives the pad from the note opening, with the same four inputs as the commitment and a different domain:

tg⊥​=PoseidonTachyon-CmOutPad​(rcm, pk, v, ψ).

The hiding that the commitment gets from rcm also hides any relation between the two values, and the domain tag from Section 4 is the only thing separating the pad from the commitment.

Book and code: the padding tachygram

Deep dive: tg⊥​=Hcm⊥​(r⊥​), where r⊥​ is an arbitrary preimage, and it calls this a dummy.

Tachygram page and code at 7e50254: the pad commits to the note opening, and OutputBind computes both values from the note.

Both forms resist the pairing test, so the disagreement is about construction. This site follows the code.

Sources differ: tachygrams per action

The bundle page says an action is bound to one tachygram, and the deep dive's accumulator section defines one per action, while its transaction section and the code use two. As in chapter 1, this site follows the code, with two per action and one more per bundle.

One value in the table falls outside the pairs. Each bundle's fee change note publishes only its commitment, even when its value is zero, and so a stamp covering N actions in B bundles has 2N+B tachygrams. The extra one appears once per bundle whatever the bundle does, so it adds nothing about the split. The aggregation chapter covers fees.

A stamp holds its tachygrams as a flat list, and the wire format requires them to be unique and sorted by their byte encoding. Sorting erases the order in which actions contributed them, and in the code a spend's step commits its pair as a set. Every published value is also required to be nonzero, which keeps each factor of the polynomial in the next section from degenerating. The proof tree page gives a different reason, a collision with the note's own commitment, and we did not establish that one independently.

These privacy statements cover what the published values reveal. As chapter 2 records, Ragu's book and code do not yet establish zero knowledge, so a separate question remains about what a proof itself reveals.

12. One structure for both tests

We can now see why one kind of value is enough. A stamp's set of tachygrams corresponds to the polynomial whose roots are its members, f(X)=∏i​(X−tgi​). A value is present exactly when f vanishes there, and absent when it does not, and neither test cares about the kind of value.

For our bundle, working modulo 65,537, f(X)=∏i​(X−tgi​) gives f(31831)=0 for output 1's commitment and f(55113)=0 for spend 1's nf7​. The test has no need to know which kind each root is. The value nf9​=32258 was not published, and f(32258)=43890.

A spender uses the first kind of test to show that its commitment was published, and the second to show that an old nullifier was not. The next chapter develops these polynomials and their commitments.

13. Where these derivations run

Each object in this chapter is computed inside a step of the proof tree. Chapter 22 walks through them in order, and this table locates what we have covered:

StepWho runs itWhat it does here
NoteSeedwalletderives pk from pak, then mk and cm
NullifierDerivewalletderives a window of nullifiers from mk
SpendBindwalletderives nfe​ and nfe+1​, and checks they are nonzero
SpendActionwalletchecks pak against pk, derives α and rk, and commits the pair
OutputBindwalletderives cm and the pad from a note

Reconstructed from code at commit 7e50254, not confirmed by the authors

At this commit, Tachyon builds Ragu with its mock feature, and these steps compute hashes natively. Comments mark the known gaps. A real circuit needs a low-bit decomposition of each epoch to find w, since any field element can be written as 4q, and a bit decomposition of α to use it as a scalar. So our statements about what a proof forces describe the design that the steps declare, and we did not find real circuits enforcing them in this crate. The gate counts of Section 7 come from the analysis page, and they are estimates for those circuits.

14. Where this chapter leaves us

This chapter took the following steps:

  1. A wallet derives two children from sk. The scalar ask authorizes spends through its public point ak, and nk derives nullifiers. No proof constrains these derivations.
  2. The payment key is a hash of ak and nk, so it reveals neither of them, and wallets mint a fresh one per sender since a hash cannot be tweaked.
  3. A note is (pk,v,ψ,rcm), its commitment is a domain-tagged Poseidon hash of those fields, and the sender knows all four fields but not nk.
  4. Each action signs with a key randomized by α, so a published rk is consistent with every possible owner, and outputs use the same form with no spending authority.
  5. A note's master key is mk=Poseidon(ψ,nk), and one sponge over mk and a group start w gives four consecutive epoch nullifiers. Cost in the proof is the reason this replaced the archived GGM tree.
  6. Since ψ and nk both sit behind cm, a spender held to a published commitment has one nullifier sequence, assuming Poseidon resists collisions. Reused ψ values remain a danger that the wallet must detect.
  7. A spend accepted in epoch a always publishes nfa​ in epoch a. This single fact, the exclusion proof, and a two-epoch window together catch every double spend.
  8. Commitments and nullifiers are published as one kind of value. Each action publishes two tachygrams and each bundle one more, derived so that an observer learns the number of actions and not the split between spends and outputs.

All of this is a design at one commit of a proposal that has not activated, and the steps above still run against a mock of Ragu. We have spoken of values being published in the pool without saying how a proof can refer to history that validators have forgotten. The next chapter builds that structure, starting from the hash chain of anchors.

Last Updated: 11/4/26, 10:49 AM
Contributors: Hanh
Prev
Recursion in Ragu
Next
Pool State and Anchors