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

The Proof Tree

Chapter 2 sketched the steps behind one spend as a small graph, and promised a closer look. Chapter 19 has since built the keys, notes, and nullifiers, and chapter 20 built the anchor chain and the set polynomials. This chapter puts those objects into their places. It follows one note through its whole life, step by step, and at each step says what the step takes in, what it checks, what it emits, and who runs it.

We treat absence evidence as a black box. Chapter 21 explains how a sync service proves that a value is missing from an epoch, and here we use only the header that service hands over. Fees and stamps get the steps that produce them, and chapter 23 covers what aggregators and miners do with the result.

Status of this design

Tachyon has not activated on mainnet and its design is still changing. This chapter describes the Tachyon repository at commit 7e50254, and any later commit may differ. Its shielded protocol ZIP is an empty outline, so our sources are the book's proof tree, authorization, and fee pages, together with the tachyon crate.

These steps run against a mock of Ragu. The crate enables the mock feature of Ragu at 0725b52, not at the 3219e08 of the Ragu chapters. In that revision, the mock's fuse runs a step's body natively, discards the recorded polynomial openings, and returns a hash record in place of a proof. It does not verify the child proofs. So when we say that a step checks something, we mean that its body at this commit checks it natively, and that the design expects a circuit to enforce the same relation. We did not find real circuits for these steps.

1. Steps, headers, and parties

The vocabulary comes from chapter 2. A step declares a left input header, a right input header, a witness, and an output header, and running it with Ragu's fuse produces a new header with a proof. An unused input is the unit header (). A step with two unit inputs starts through Ragu's seed, which supplies the bootstrap proof for both. Here is one declaration, trimmed to the lines that matter:

impl Step for SpendBind {
    type Left = NoteSpendable;
    type Output = SpendHeader;
    type Right = NoteSecret;
    type Witness<'source> = (value::Trapdoor,);

This declaration says nearly everything the next sections need about a step, because the Rust types force the shape of the tree. SpendBind cannot accept anything except a proved NoteSpendable on its left and a proved NoteSecret on its right. The suffix on each header type enforces this in the proof, so two types with the same fields stay distinct.

A header does not have to be public. It is visible only to whoever receives that node's data. The NoteSecret header carries a full note opening and a master key, which is safe only because the wallet that built it never hands it to anyone. The tree therefore has private nodes low down and a public node at the top.

Three parties run the tree. A wallet holds keys and note data, a sync service holds the nullifier values a wallet shared with it along with public pool history, and an aggregator merges stamps. The book's roles table gives validators no step, and they check the final proof, although one sentence says that consensus also publishes spans. We follow the table.

The crate registers 38 steps and 16 header types. We compared the book's table of steps with the code's declarations, and the left input, right input, and output of all 38 agree.

2. The running example

We keep Alice's note from chapter 19, with value 25, created in epoch 3 and spent with target epoch 7. All of its numbers carry over, including the stand-in hash, which is SHA-256 of a tag and inputs reduced modulo 65,537. Two more stand-ins are new to this chapter.

  • Polynomials live modulo 65,539. The nullifier factors of the next section need a value with no cube root. Modulo 65,537 every element has a cube root, because 65537≡2(mod3), so we move up to the next prime, which is congruent to 1 modulo 3 and in which 2 has no cube root.
  • Set commitments are hashes of coefficients. Chapter 20 ran the Pedersen form on the toy curve, but coefficients this large do not fit the toy curve's 43 scalars. This chapter relies only on two properties of a commitment, that the same polynomial gives the same value and that a different polynomial gives a different value, and the hash provides both at this scale.

Signing keys and value commitments remain real arithmetic on the toy curve E. Here is the pool history that the note lives through. The epochs have stamps with made-up tachygrams, and the chain follows chapter 20's folds, Hst​ for a stamp and Hep​ for a boundary.

EpochStampsTachygramsEntry anchorFinal anchor
33111010819102
426630088359
513584082572
6002328123281
7394592641305

The second stamp of epoch 3 is Bob's, and its 5 tachygrams include Alice's commitment 55697. Epoch 6 published nothing, so its entry and final anchors are both 23281. Epoch 7 is still open. We write a1​,a2​,a3​ for the anchors after its three stamps, which are 58791, 25925 and 41305.

3. Deriving nullifiers

A note's lineage has to end by publishing nullifiers that nobody can choose, and along the way it has to test earlier nullifiers against each past epoch. Both needs start from the same secret, so the tree computes that secret once, in a private node that later steps can consume as often as they like.

NoteSeed

NoteSeed has two unit inputs. It witnesses the note's value and trapdoors along with the proof authorizing key pak=(ak,nk), and it emits a NoteSecret holding (cm,note,mk). The point of the step is the pairing. Deriving pk from pak pins nk, so the same nk and ψ reach both mk and cm, as chapter 19 explained. After this step, nk appears in no header.

QuantityComputationValue
witnessv, ψ, rcm, ak, nk25, 625, 61249, (27,30), 40954
pkHPkDerive​(27, 30, 40954)35507
mkHNfMaster​(625, 40954)299
cmHCmDerive​(61249, 35507, 25, 625)55697

The step does not show that this note exists, since anyone can seed an invented note. That question closes later, when a lineage has to find cm in the pool.

NullifierDerive

Later steps need many nullifiers at once, and they need to compare them with a sync service's work without revealing mk. So NullifierDerive takes the NoteSecret, derives a whole window of 16 nullifiers from 4 sponges, and emits a commitment to the window. The header it emits, NoteNullifiers, holds (cm,epoch start,commitment,epoch end), with no key material. The start epoch is a witness, but it must be a multiple of 4, and it is written on the header so that any choice labels its window honestly. For Alice's note we take the window starting at epoch 0:

Sponge input wnfw​nfw+1​nfw+2​nfw+3​
019392155692142514090
42976630935955355113
861177322583105334200
1238278131891635322206

The commitment has to hold pairs of an epoch and a nullifier, and not nullifiers alone. Section 5 shows the double spend that would otherwise follow. A natural encoding of a pair (i,m) is the linear factor m+(i+1)X, but linear factors fail.

The linear factor m+(i+1)X has the root −m/(i+1), so the pair (epoch 3, member 14090) and the pair (epoch 7, member 28180) share a root, and the two factors differ only by a constant multiple. Divisibility cannot tell them apart. Their cubic factors have different coefficients, and the leading terms alone already differ, at 64 against 512.

The code instead uses an indexed factor,

Fi,m​(X)=(m+(i+1)X)3−c,

where c has no cube root in the field. If Fi,m​ had a root x, then m+(i+1)x would be a cube root of c, and none exists. In the real field c=2.

For epoch 3 the member is nf3​=14090 and i+1=4, so working modulo 65,539,

F3,14090​(X)=(14090+4X)3−2=64X3+20930X2+60089X+45458.

Trying all 65,539 field elements finds no root, so this cubic has no linear factor, and a cubic with no linear factor is irreducible.

An irreducible factor is what makes division meaningful. Polynomials factor uniquely into irreducibles, so one product of such factors divides another exactly when its factors are among the other's, with multiplicity. Divisibility therefore tests multiset containment of (i,m) pairs. The window's sequence polynomial is the product over its members, g(X)=∏e​Fe,nfe​​(X).

Two cubic factors can still coincide when the ratio of their linear terms is a cube root of unity. Modulo 65,539 such a root is ζ=32641, so a collision needs j+1=ζ(i+1) or j+1=ζ2(i+1) for two epoch indices. None of the 240 ordered pairs in our window satisfies this. The code's comment gives i+1 below p/3​ as enough to rule out every such pair. That bound is 147 for our stand-in field, and it exceeds 2125 for the real p, while epoch indices stay below 220.

The step binds the commitment to the sponge outputs with one opening. The prover commits to g before a challenge exists, the challenge comes from a hash of that commitment, and the step compares the opening with the factors it computes itself.

The sequence has degree 3⋅16=48, and its stand-in commitment is 13205. The challenge derived from that commitment is z=18216. The opening gives g(18216)=9095, and the step gets the same value by evaluating the 16 factors directly from the sponge outputs. Suppose a prover commits instead to a sequence with the member at epoch 5 replaced by 29467. Testing every challenge shows that this false sequence agrees with the true product at 2 of the 65,539 points, which is within the degree bound of 48.

NullifierFuse

A note older than a window needs more than one. NullifierFuse joins two ranges of the same cm, requires that they be contiguous, and checks that the combined polynomial is their product.

A second window starting at epoch 16 passes the contiguity check, since 16=15+1. At the challenge z=55334 the product check reads 978=22570⋅40967 modulo 65,539. A window starting at epoch 12 would also be group-aligned, but it would repeat four epochs, and the check rejects it because 12=16.

The contiguity check keeps the product free of repeated factors. Our note needs only the first window, so the rest of the run does not use this step.

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

The code's comment gives the irreducibility and the collision bound, and the proof tree page says the factor carries its epoch. The sources do not explain why a cube is needed in place of a linear factor. The explanation through shared roots is our own, and so is the plain-factor attack in Section 5. We checked both on the stand-in field only.

4. Bootstrapping a spendable

The derivation so far could describe an invented note. The next step has to tie cm to the pool, and to give the wallet a position on the anchor chain from which a spend can proceed. The result is a NoteSpendable header, (cm,current epoch,anchor), and two steps can start one. They differ in where they leave the lineage.

A rail of anchors across epochs 3, 4 to 6, and 7, with five marked positions. SpendableInit rests just after the creation stamp. QrSpendableInit rests on the entry anchor of epoch 4. SpendableLift rests on the entry anchor of epoch 7. ActionLift rests on a1 and StampLift on a3, both inside epoch 7.

SpendableInit, for a spend in the creation epoch

SpendableInit takes cm from a NoteNullifiers header and witnesses the creation stamp's tachygrams, the anchor just before that stamp, and the epoch. It opens the set polynomial at cm and requires zero, and it folds the stamp's commitment onto the witnessed predecessor. The output anchor is the state immediately after the creation stamp.

The creation polynomial gives 0 at cm=55697, so the membership check passes. Its commitment stands in as 47595, and the witnessed predecessor is 37649, so the step computes Hst​(37649, 3, 47595)=62652 and emits (cm,e,anchor)=(55697, 3, 62652).

The predecessor and the epoch are free witnesses, which looks like a hole until we follow the anchor forward.

A wrong predecessor 37650 gives 9731, and a wrong epoch 4 gives 21577. The step accepts both, since nothing in it can tell, but neither value appears in the chain.

So a wrong witness produces a valid-looking header whose anchor consensus will not recognize when the spend is published, as chapter 20 explained. This step tests no nullifier, because the only stamp its position covers is the one that created the note. A double spend within the creation epoch runs into the duplicate window instead.

A spendable from this step can spend only inside its creation epoch. It rests after a stamp, and the next section's lift starts only from an entry anchor.

5. Maintaining a spendable

A note carried into a later epoch has to show that it was not spent in each epoch it crosses. Doing that work requires the whole epoch's tachygrams, which a wallet does not want to fetch, so the tree splits the job at a deliberate seam. A service tests values it was given without knowing whose they are, and the wallet then claims those tests for its note.

Segments from the sync service

The service's product is an ArbitraryUnspent segment, whose header holds (anchor start,epoch start,elapsed,epoch next,anchor next). It covers whole epochs, half-open on both axes, with entry anchors at both ends, and elapsed commits to one indexed factor for each covered epoch. To the service each tested value is an arbitrary field element.

Chapter 21 explains how buckets produce these segments. For this chapter one fact is enough, namely that an opening of the epoch's contents at a value gives zero if and only if the value was published. A real network routes tachygrams into many buckets by quadratic residue. To keep the numbers short we use one bucket per epoch holding everything, which is the depth-zero case.

Epoch eTested value nfe​Bucket opened at that valueAbsent
31409030223yes
429762643yes
56309316446yes
6595531yes

Three steps build segments. QrUnspentInit starts one from a bucket and a value, UnspentFuse joins two that meet on a boundary, and UnspentLift appends one more epoch. Here is a run for Alice's note, using all three:

StepRuns onOutput: anchor start, epoch start, elapsed, epoch next, anchor next
QrUnspentInitbucket of epoch 3, nf3​(10108, 3, 30057, 4, 63008)
QrUnspentInitbucket of epoch 4, nf4​(63008, 4, 34607, 5, 58408)
QrUnspentInitbucket of epoch 5, nf5​(58408, 5, 58805, 6, 23281)
UnspentFusethe two segments above(63008, 4, 21136, 6, 23281)
UnspentLiftthat segment, bucket of epoch 6, nf6​(63008, 4, 48724, 7, 45926)

The fuse checks that the anchors meet at 58408 and the epochs meet at 5, and then checks C(z)=L(z)⋅R(z) at z=37557, where it reads 8718=50038⋅36200. The lift's product check at z=45485 reads 13237=50076⋅39192. Each segment holds plain numbers, so nothing in the table mentions a note.

UnspentBind

The wallet now has to show that those plain numbers really are its nullifiers, each at its own epoch. UnspentBind does this by division. It takes a segment on the left and a NoteNullifiers on the right, ties each committed polynomial to its witnessed form, witnesses a complement, and checks

g(X)=elapsed(X)⋅complement(X).

Since the factors are irreducible, this holds only if every factor in elapsed is one of the note's genuine (epoch, nullifier) pairs. The step needs to compare no bounds, because the factors carry their epochs. It then writes the derivation's cm onto the output, a NoteUnspent, which is the first time the service's work is linked to a note.

Dividing the window polynomial, which has degree 48, by the composed elapsed of degree 9 leaves remainder 0 and a complement of degree 39, namely the 13 factors for epochs 0 to 3 and 7 to 15. At the challenge z=26729 the check reads 37717=12666⋅20861. The step outputs (55697, 63008, 4, 7, 45926), whose first entry is cm. The segment for epoch 3 binds the same way, with a complement of degree 45, and gives (55697, 10108, 3, 4, 63008).

We can now see why the factor holds the epoch.

Suppose Alice already spent the note with target 5, so epoch 5 now holds nf5​ and nf6​. An honest test at epoch 5 gives 0, and the segment cannot be built. She now tries a second spend with target 7 and tests a different one of her nullifiers at epoch 5, namely nf9​=32258, which gives 62419, so the bucket reports absence.

With plain factors X−nfe​ the tested values {nf3​,nf4​,nf9​,nf6​} all come from the window, the division leaves remainder 0, and the double spend goes through. With indexed factors, F5,32258​ does not divide the window polynomial, because the window holds F5,63093​ and F9,32258​. The division now leaves a nonzero remainder. If she commits to the honest complement anyway and hopes for a lucky challenge, then testing all 65,539 challenges shows that the check passes at exactly 2 of them.

The complement also explains how the rest of the window is used. It holds epochs below the lineage and epochs ahead of it, including 7 and 8, whose nullifiers the spend will later publish.

QrSpendableInit

A note that will outlive its creation epoch starts through QrSpendableInit. This route has to cover the creation epoch itself, since a spend later in epoch 3 would otherwise go unnoticed by every later test. So the left input is the note's NoteUnspent for that epoch, which shows that nf3​ is absent, and the right input is a sealed bucket. The step opens the bucket at cm for zero, and it requires the segment and the bucket to have the same extent, so that the bucket's stamps are the ones consensus will eventually pin. In a real network the bucket holding cm and the bucket that tested nf3​ will usually differ, and only their anchors have to agree.

For our note, the opening of epoch 3's bucket at 55697 gives 0, and the bound segment from the last section has the same extent as the bucket, running from 10108 to 63008. So the step emits (55697, 4, 63008), which rests on the entry anchor of epoch 4.

SpendableLift

SpendableLift has no witness at all. It compares a NoteSpendable with a NoteUnspent, in three equalities, and moves the lineage to the segment's far end.

CheckSpendableSegmentResult
same notecm=55697cm=55697equal
same epoche=4epoch start 4equal
same positionanchor 63008anchor start 63008equal
output(55697, 7, 45926)

Each check closes a particular gap. The epoch check stops a lineage from skipping an epoch, and the anchor check welds the segment to the lineage's position. The commitment check stops the following:

Recall the second note from chapter 19 with the same ψ. It has the same mk and so the same nullifiers, which means a segment bound through its derivation tests identical values. The cm check still separates them. With a new rcm and value 9, its commitment is 53772, and 53772 = 55697 stops the lift, so a spender cannot move absence evidence from a note worth 9 to a note worth 25, or the reverse.

The anchor check is what confines the first bootstrap to its epoch.

A spendable from SpendableInit cannot take this route. Its anchor 62652 is the state after the creation stamp and the segment for epoch 3 starts at 10108, while the segment for epoch 4 starts at 63008. Neither matches, so the third check fails.

At this commit, the book's maintenance section and the comment on the lift agree that only a lineage resting on an entry anchor can lift.

One lift can consume a segment crossing any number of epochs, so the wallet's cost does not have to grow with the note's age, apart from the windows it derives. The lineage now certifies every epoch before 7. It says nothing about epoch 7 itself, which is left to the consensus window.

6. Spending

A spend has to do two different things. It has to publish a nullifier pair and a value commitment, which requires the note's secrets, and it has to produce a randomized signing key tied to the note's owner, which requires ak. These could be one step except for a budget. The book counts a Poseidon permutation at about a seventh of a step and a scalar multiplication at about a quarter, so we can add up each step's work.

StepPermutationsScalar multiplicationsShare of one step
OutputBind220.79
OutputAction210.54
SpendBind220.79
SpendAction310.68

Doing a spend in one step would need 5 permutations and 3 scalar multiplications, which comes to 1.46 of a step before any polynomial opening. An output would need 1.32. Splitting each action in two brings every part under 1.

SpendBind

SpendBind takes the NoteSpendable on its left and the same NoteSecret from Section 3 on its right, which is an instance of the node sharing from chapter 2. The secret's commitment has to equal the lineage's, and this equality connects two facts established separately, namely that the lineage's note was minted and unspent, and that the secret's mk and value belong to that note. A phantom note reusing ψ with a larger value has a different cm and fails here.

The step then derives the pair from mk at the lineage's epoch and the next, with one sponge each, requires both to be nonzero, and commits the value. The value commitment follows the book's formula,

cv=[v]V+[rcv]R,

with v positive for a spend and negative for an output, and a fresh random trapdoor rcv. A book footnote says that this sampling may later change.

On the toy curve we take V=(6,8) and R=(11,1).

Output fieldSourceValue
cmspendable, checked equal to the secret's55697
nfcurrent​mk=299 at epoch 755113
nfnext​mk=299 at epoch 861177
anchorspendable45926
pkthe secret's note35507
cv[25]V+[38]R(30,8)

Nothing in the pair is a witness. Chapter 19 explained why a spend publishes two nullifiers.

SpendAction

SpendAction takes the SpendHeader and witnesses the action entropy θ, the key pak, and two set polynomials. It first requires pak to derive the header's pk, and this check is the only thing that ties ak to the note. Then it derives α and rk as in chapter 19, hashes (cv,rk) into an action digest, and builds the stamp's first two sets.

The sets use the root polynomials of chapter 20. A relation called enforce_poly_roots opens the witnessed polynomial at a challenge derived from its commitment, and compares the opening with the product of (z−root). The action set has one root and the tachygram set has two.

QuantityComputationValue
check on pakHPkDerive​(27, 30, 40954) against the header35507 matches
αHSpend​(4071, 55697), reduced mod 4323
rkak+[23]G(24,30)
action digest dHActionDg​(cv, rk)52188
action setX−52188, as X+13351committed as 63623
tachygram set(X−55113)(X−61177), as X2+14788X+59685committed as 49309

A prover who holds a different spend validating key (14,22), together with the right nk, computes 61293 in place of 35507, and the first row fails.

The output header type is Actions, (action commit,tachygram commit,anchor). Notice what it drops. The commitment cm, the key pk, and the epoch do not appear, and neither does anything recording the note's age. The book points out that the published pair sits at the epochs of the live range, whatever the note's age, so no later step reads a position that would reveal how long the note has existed.

7. Outputs

Creating a note needs no lineage, since nothing about the past has to be shown. It still has to look like a spend to an observer, so it also takes two steps and ends in the same header type.

OutputBind has unit inputs and witnesses the new note and rcv. It derives the commitment and the padding tachygram from one opening, requires both to be nonzero, and commits the negated value. OutputAction derives α under the Tachyon-Output domain, sets rk=[α]G, and builds the two sets. It witnesses no key, because the recipient's payment key rides inside cm, and the sender cannot be asked to prove anything about it. Suppose Alice pays 18 zatoshi to Carol:

StepQuantityComputationValue
OutputBindcmHCmDerive​(37299, 17495, 18, 43263)36798
OutputBindpadHCmOutPad​(37299, 17495, 18, 43263)56347
OutputBindcv[−18]V+[18]R(25,29)
OutputActionαHOutput​(51472, 36798), reduced mod 4317
OutputActionrk[17]G(20,6)
OutputActiondigest dHActionDg​(cv, rk)43682

The anchor in OutputAction is a free witness, because an output has no chain state to thread. The wallet usually gives it the same anchor as its spends, so that the merge in the next section needs no lift. Both action types publish a cv, an rk, a signature, and two tachygrams, and chapter 19 showed why the counts reveal nothing about the split.

8. Moving and merging actions

A transaction with several actions needs them combined into one Actions node, and combining requires a shared anchor, because the output header has a single anchor field. In our run the spend's actions rest on entry(7), which consensus may not accept as a published anchor. Under the anchor page's rule, an entry anchor counts as an end-of-block state only if the first block of its epoch carries no stamp, and a wallet would rather sit near the latest anchor in any case. The wallet therefore chooses an anchor for the bundle and moves the spend there.

ActionLift does the move. Its left input is the Actions and its right input is an AnchorSpan, the proved run of anchors from chapter 20. It requires the anchor to be the span's start or a member, by checking M(a)⋅(a−start)=0, and it outputs the span's end.

Here the spend's actions rest on entry(7)=45926, and the wallet plans to anchor its bundle at a1​=58791. The span from 45926 to 58791 has the members polynomial M(X)=X−58791. The check M(a)⋅(a−start) evaluates to (45926−58791)⋅0=0, so the lift outputs the same sets at anchor 58791. An anchor from before the span, such as 23281, gives 25959, and the step rejects it.

The same restriction from chapter 20 applies here, because no span contains a boundary digest. A wallet moves a note across an epoch with SpendableLift, before the spend, and moves its actions within the epoch with ActionLift, after it.

ActionMerge then takes two Actions headers. It requires equal anchors, ties the four witnessed input polynomials to the four header commitments, and checks that each merged set is the product of its inputs, which is the union of chapter 20. The check is enforce_poly_product, which absorbs all three commitments into the challenge so that the polynomials are fixed before the challenge exists.

Both inputs now sit at anchor 58791, so the anchor check passes. The merged action set has the roots 52188 and 43682, and at z=33689 its check reads 40527=47040⋅55546. The merged tachygram set has the roots 55113, 61177, 36798 and 56347, and at z=11199 its check reads 26949=29399⋅28926. The output header is (40362, 37081, 58791).

The code's Plan::prove follows this order. It binds and acts on each spend, lifts it along any spans to the plan's anchor, proves each output at that anchor, and merges everything.

9. Applying the fee

An Actions header cannot be published, because validators accept only a Stamp. The step between them applies the fee, and the design wants a particular freedom here. A wallet signs a maximum fee once and can choose the actual fee later, at or below that maximum, with the difference returning as change. Changing the fee then needs no further trip to a signing device.

To allow this, the bundle commits to the maximum through a max fee note, an opening (pk,maxfee,ψ,rcm) that is published only as

cvmaxfee​=[−maxfee]V+[r]R,r=PoseidonTachyon-FeeBlind​(pk, ψ, rcm).

This note is never spent. The blind binds the change note's owner into the commitment that gets signed. The fee change note (pk,maxfee−fee,ψ,rcm) takes the other fields, and it appears on chain only as a commitment among the tachygrams.

FeeFuse takes Actions on the left and a unit input on the right, and it witnesses the max fee note, the fee, and the set polynomials. It rederives r and cvmaxfee​, checks that the change lies in range, computes a fee digest and the change commitment, and multiplies each set by one more factor. The output type is Stamp, with the same fields as Actions and a different suffix.

Alice's bundle spends 25 and creates an output of 18. She reserves a maximum fee of 5, which leaves a value balance of 25−18−5=2.

QuantityComputationValue
rHFeeBlind​(35507, 44984, 1464)=24307, reduced mod 4312
cvmaxfee​[−5]V+[12]R(28,21)
feechosen by the wallet, at most 53
fee digestHFeeDg​(28, 21, 3)14071
change note value5−32
change commitmentHCmDerive​(1464, 35507, 2, 44984)16688
output action setthree roots: 52188, 43682, 14071committed as 13924
output tachygram setfive roots, 2N+B=2⋅2+1committed as 44311

The change in type is what makes the fee happen exactly once. FeeFuse accepts only Actions and emits a Stamp, so a second application has no valid input, as chapter 2's Charge example showed in miniature. A bundle with no actions has nothing to fuse, so FeeSeed starts a Stamp directly. It requires a zero maximum fee, so the fee change note is the max fee note itself, and it witnesses its anchor. Aggregators use it for an innocent aggregate.

If the wallet raises the fee to 4, the fee digest becomes 48510 and the change note, now worth 1, has the commitment 54660. So both set commitments change, to 57197 and 37899, and FeeFuse has to run again. The sighash stays 29677, and bvk stays (18,10), so no signature has to be made again.

These steps prove a local relation, which is that the change and the digest match the opening. They do not show that the fee is funded. That comes from the next section.

10. Authorization outside the proof

A bundle also needs authorization that the proof does not provide. The authorization page lists three layers, namely per-action signatures, value commitments, and a binding signature, and each covers a gap that the proof leaves open.

Action signatures

The proof shows that rk equals ak+[α]G for the ak behind the note's payment key, but a prover holding only pak can produce that proof. Spending therefore also needs a signature under rk, which requires rsk=ask+α and so requires ask. This is what separates a proving device from a custody device. The custody device receives each note and θ, recomputes cm, α, and rk for itself, and signs. Chapter 19 ran this signature on the same rk. An output's rsk is α, so no custody is needed there.

Each signature covers the transaction sighash, which includes a bundle commitment over the actions, the value balance, cvmaxfee​, and the memo. The fee is not included, and neither is the stamp, since aggregation strips it. Because α is derived from cm inside the proof, rk is itself a commitment to cm, so a signature binds its action to the note even though the action shows no tachygram.

The binding signature

No step adds up values, so the proof cannot show that a bundle balances. Value commitments allow validators to check that from public data. Summing commitments sums values and trapdoors separately,

i∑​cvi​=[i∑​vi​]V+[i∑​rcvi​]R.

A validator knows the claimed value balance, so it subtracts that term. Including the max fee note as one more output, the validator computes

bvk​=i∑​cvi​+cvmaxfee​−[value_balance]V=[i∑​vi​−maxfee−value_balance]V+[i∑​rcvi​+r]R.​

If the values balance, the first bracket vanishes and bvk=[bsk]R with bsk=∑i​rcvi​+r. The planner knows every trapdoor, so it can sign under that key, and the signature shows that it knows the discrete log of bvk to base R.

QuantityComputationValue
bsk38+18+12 mod 4325
∑cv(30,8)+(25,29)+(28,21)(28,10)
bvk(28,10)−[2]V(18,10)
check[25]R(18,10)

With the stand-in sighash 29677 and nonce point (11,30), the challenge is 25 and the response is 22, and both sides of [s]R=R+[c]bvk equal (28,21).

Cheating on the balance leaves a V term behind.

Now suppose Alice claims a value balance of 12 in place of 2, taking 10 more from the pool. The validator computes bvk′=(6,23), and the surviving term is [33]V. Of the 43 possible claims in the toy field, only the true one makes bvk equal [bsk]R. The signature fails unless Alice can find the discrete log of bvk′ to base R. On the toy curve she can, by counting, because V=[41]R gives her the key 2 for the false balance. So this protection rests on the hardness of discrete log.

The fee enters neither key. The balance equation accounts for it at the transaction level, because the pool's net outflow is ∑vin​−∑vout​−(maxfee−fee), which equals the value balance plus the fee.

For our bundle this gives 25−18−(5−3)=2+3=5, so 5 zatoshi leave the pool, and the fee change note keeps 2 inside it.

Book and code: what shows the balance

Bundle page: it lists among the things the proof establishes that "action balance effect matches pool balance effect."

Fee page, authorization page, and code at 7e50254: the fee steps prove no funding, and no step sums cv values. Validators derive bvk from public data and check the signature. The proof contributes something narrower, which is that each cv commits to the value of the note behind it.

This site follows the code. Separately, the authorization page's detailed sequence diagram carries a comment calling itself significantly outdated. It hashes the commitment's inputs in a different order from the code, and it gives an output one tachygram rather than two. We use the code's forms.

11. Publishing a stamp

Stamps keep arriving while a wallet proves, so it may lift again before publishing. StampLift has the same check as ActionLift on the other header type. The book says spans are published as an epoch progresses, and a wallet builds its own span for anything newer.

Stamps have landed since the wallet chose a1​, so it lifts along a span from a1​=58791 to a3​=41305. Two AnchorSpanSeed steps and one AnchorSpanFuse build the span, whose members polynomial has roots 25925 and 41305. The stamp's anchor 58791 is the span's start, so the product M(58791)⋅0 vanishes although M(58791)=48924, and the output anchor is 41305.

The header holds only commitments and an anchor, and the published bundle does not carry the action commitment. A validator rebuilds the header from what it can see, which is the comparison that chapter 2 required of every verifier. The crate's verification stops short of the chain, so the anchor row comes from the book and chapter 20. Here is what happens with Alice's bundle, where "in the design" marks the row that a mock cannot really perform:

Validator stepComputationResult
signaturesaction signatures under each rk, binding under bvkaccepted
arity5=2⋅2+1accepted
tachygram setrebuild from the 5 published values44311, matches the stamp
action setrebuild from 2 action and 1 fee descriptors13924
proofverify against (13924, 44311, 41305)accepted, in the design
anchorlook up 41305 in the chainfound, in epoch 7

If a miner dropped the output's descriptor, the validator would rebuild the action commitment as 57289 instead of 13924. The header would then differ from the one the proof covers, so the proof would fail.

Once the anchor checks out, it closes the free witnesses below it. The seal's lower boundary, the predecessor witnessed at each seed, and the epochs absorbed along the way all had to be genuine for the folds to reach a published value, as chapter 20 argued. An output's free anchor closes the same way, because the merge forced it to equal the spend's.

Rerandomizing before release

A proof built from private inputs could reveal a link back to them, so the book requires that every proof leaving the process that built it be rerandomized. The code calls rerandomize in Plan::prove, lift, merge, and unmerge. Intermediate nodes that stay in the wallet skip it.

Sources disagree: what rerandomizing achieves

Tachyon book: rerandomizing replaces the proof with an unrelated one that verifies against the same header, so the released proof has no correlation with its private lineage.

Ragu code at 3219e08: a comment on rerandomize says that unlinkability is the intent, that today it is not achieved, and that the remaining work is in the future.

Mock at 0725b52: rerandomize fills in a tag and keeps the rest of the record.

As in chapter 2, this site treats hiding as a goal that has not yet been met. When this chapter says a header drops a value, it means only that the header no longer carries it. It does not mean that the proof conceals it.

12. The aggregator's steps

Once published, stamps can be combined by anyone holding them. Chapter 2 followed this at the level of whole bundles, and chapter 23 takes up what it means for blocks and fees. Two steps belong here for completeness. StampMerge has the same body as ActionMerge on stamps, and StampUnmerge requires a witnessed quotient for which the dividend is the product of the divisor and the quotient, in both sets.

Suppose an aggregator holds Dave's stamp with one action, which has 2 action and fee digests and 3 tachygrams, at the same anchor 41305. StampMerge multiplies the sets, which gives degrees 5 and 8, and the merged tachygram set has the commitment 490. If a block then includes Dave's transaction alone, StampUnmerge divides by his stamp. The quotient has remainder 0 and commitment 44311, which is exactly Alice's tachygram commitment again. Dividing by a value that was never in the stamp leaves a nonzero remainder.

Aggregators need no secret, since these steps read only commitments and the polynomials rebuilt from published values, and the aggregated stamp has the same shape as any other stamp.

13. The whole tree

Here is the run, with each node labelled by the party that runs it. Rounded boxes belong to the wallet and square boxes to the sync service. Buckets appear as inputs, and chapter 21 opens them.

Counting only what the graph shows, this one spend and one output take 23 nodes, of which the wallet runs 14 and the sync service 9. The buckets add more, on the service side, and chapter 21 describes the steps that build them. The published result is one proof, whose header a validator can rebuild.

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

The book draws a similar tree, and the crate's test helper spendable_at takes the same route to a spendable. The exact mix of fuses and lifts here, the two sets of spans, and the assignment of spans and of epoch 3's segment to the sync service are our choices. The book allows a wallet or an aggregator to build spans as well, and the deep dive has the wallet prove the creation epoch's exclusion itself.

Following the fields shows where each commitment stops and where each anchor moves:

NodeHeader typecmEpochAnchor
NoteSeedNoteSecret55697
NullifierDeriveNoteNullifiers556970 to 15
UnspentBindNoteUnspent556973 to 410108 to 63008
QrSpendableInitNoteSpendable55697463008
UnspentBindNoteUnspent556974 to 763008 to 45926
SpendableLiftNoteSpendable55697745926
SpendBindSpendHeader5569745926
SpendActionActionsgone45926
ActionLiftActionsgone58791
ActionMergeActionsgone58791
FeeFuseStampgone58791
StampLiftStampgone41305

The whole registered set divides into families:

FamilyStepsWho runs themCovered in
Deriving nullifiers (3)NoteSeed, NullifierDerive, NullifierFusewalletSection 3
Holding a spendable (4)SpendableInit, QrSpendableInit, UnspentBind, SpendableLiftwalletSections 4 and 5
Spends, outputs, merging (6)SpendBind, SpendAction, OutputBind, OutputAction, ActionMerge, ActionLiftwalletSections 6 to 8
Applying the fee (2)FeeFuse, FeeSeedwallet; FeeSeed also aggregatorSection 9
Moving and combining stamps (3)StampMerge, StampLift, StampUnmergewallet and aggregator; StampUnmerge aggregator onlySections 11 and 12
Anchor spans (2)AnchorSpanSeed, AnchorSpanFusesync service, aggregator; wallet possibleSection 8
Absence segments (3)QrUnspentInit, UnspentFuse, UnspentLiftsync service; wallet possibleSection 5
Stamp summaries (2)SummarySeed, SummaryAdvancesync service; wallet possiblenamed only here; chapter 21 treats a run of stamps as one intake
Routing into buckets (7)QrSummaryIntake, QrStampIntakeSeed, QrEmptyIntakeSeed, QrIntakeMerge, QrIntakeSplit, QrSideDescend, QrBucketSealsync service; wallet possiblechapter 21
Evidence trees (6)EvidenceTreeLeafPair, EvidenceTreePairFuse, EvidenceTreeFuse, EvidenceTreeCap, EvidenceTreeDescend, EvidenceTreeOpensync service; wallet possiblechapter 21

Tachyon registers 38 steps at this commit. The book's table gives 14 of them to a wallet alone, and it assigns 20 to the sync service, none of which reads a note, although a wallet may run those itself.

14. Why cheating fails

This table collects the attempts from the chapter, and it states what each defence rests on. Every row also assumes that the proof system does not accept a false statement, and that the steps run as real circuits, which the status note at the top qualifies.

A cheater tries toWhat stops itAssumption
Spend an invented noteSpendableInit or QrSpendableInit needs cm in a stamp, and the anchor has to reach the chainPoseidon collision resistance, correct consensus lookup
Choose a second nullifier sequencenk and ψ both sit behind cm at NoteSeedPoseidon collision resistance
Commit a false nullifier windowThe opening at a challenge fixed after the commitmentFiat–Shamir, Schwartz–Zippel, commitment binding
Test another epoch's nullifier for absenceFi,m​ does not divide g at UnspentBindIrreducible factors, indices below the bound
Skip an epoch or use a detached segmentThe epoch and anchor equalities at SpendableLiftPoseidon collision resistance
Move evidence to a different note with the same ψThe cm equality at SpendableLift and SpendBindPoseidon collision resistance
Stop an epoch's evidence shortThe seal's crossing misses the published entry anchorPoseidon collision resistance (chapter 20)
Use someone else's akSpendAction rederives pk from pakPoseidon collision resistance
Spend without the owner's approvalA valid rk needs a signature under ask+αDiscrete log, signature security
Create valueThe V term survives in bvkDiscrete log between V and R
Apply a fee twice, or publish with no feeHeader suffixes separate Actions from StampSuffixes enforced by the proof system
Drop an action from a published stampThe rebuilt header differs from the proved oneCommitment binding

Most of these fall into two patterns. A value that matters is derived inside a step and never witnessed, so changing it requires a hash collision. A value that is witnessed gets carried forward until consensus or a later equality can compare it with something fixed. Reading the tree bottom-up, each step adds one equality or one identity, and the argument rests on the whole chain, so a single step's checks mean little alone.

15. Where this chapter leaves us

This chapter took the following steps:

  1. A step in Ragu's terms declares its input headers, witness, and output, and the types force the tree's shape. Low nodes hold private headers that their wallet keeps, and the published node holds only commitments and an anchor.
  2. NoteSeed pairs cm with mk, and NullifierDerive commits a window of (epoch, nullifier) pairs as a product of irreducible cubic factors, so that divisibility tests containment.
  3. A spendable starts at SpendableInit for a spend inside the creation epoch, or at QrSpendableInit, which also tests the creation epoch and rests on an entry anchor so that it can lift.
  4. A sync service builds note-blind ArbitraryUnspent segments. UnspentBind divides them out of the wallet's window and SpendableLift moves the lineage across whole epochs, with the epoch index blocking the use of a nullifier from another epoch.
  5. A spend takes two steps because of the step budget. SpendBind derives the pair and the value commitment, and SpendAction proves rk and builds the two sets. An output has the same shape, with no key and no lineage.
  6. ActionLift and ActionMerge align and combine actions, and FeeFuse turns Actions into a Stamp exactly once, with a fee chosen after signing.
  7. Signatures and value commitments sit outside the proof. Action signatures require ask, and the binding signature shows that the bundle balances, which no step shows.
  8. A validator rebuilds the header from published data, verifies the proof against it, and looks up the anchor, which closes every free witness below.
  9. All of this describes a design at one commit, running natively against a mock of Ragu, and rerandomization has not yet been shown to provide the hiding that the Tachyon book expects of it.

The tree ends at a published stamp, one per bundle unless someone combines them. The next chapter, Aggregation and Bundles, follows those stamps through aggregators and miners, and it covers stripping, the bundle format, and how fees reach the block.

Last Updated: 11/4/26, 10:49 AM
Contributors: Hanh
Prev
Oblivious Sync
Next
Aggregation and Bundles