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

Pool State and Anchors

Chapter 1 ended its account of validator state with a promise. Validators forget old tachygrams, yet a spender still has to prove facts about them, so a proof needs some way to refer to history that validators no longer hold. This chapter builds that reference. Each stamp's tachygrams become a polynomial, the polynomial becomes one curve point, and the curve point goes into a hash chain whose current state is called the anchor.

We also cover the rule that consensus applies around this chain, which is the two-epoch window for duplicate tachygrams. Chapter 19 explains what a tachygram is and why a spend publishes two nullifiers, so here we treat tachygrams as opaque field elements. Chapter 21 then uses this chapter's objects to prove that a nullifier is absent from a whole epoch.

Status of these rules

Tachyon has not activated on mainnet, and this part of its design is still changing. Everything here describes Tachyon commit 7e50254, which pins Ragu at 0725b52 rather than the 3219e08 used in the Ragu chapters. The accumulator ZIP is an empty outline, so no normative specification exists yet. Our sources are the book's anchor and tachygrams pages, its deep dive, and the tachyon crate.

The crate implements the fold functions and the proof steps, but we did not find a validator in it. The window and the anchor lookup appear only in the book and in doc comments, and the crate's own comment calls its proof system the mock Ragu application. Later sections mark the places where the sources disagree.

1. What the pool state has to do

Start from the Orchard design in chapter 1. There an anchor is a Merkle root over note commitments, and a spender proves that a commitment sits at some leaf under a recent root. Tachyon cannot reuse this directly, for two reasons. Its published values include nullifiers as well as commitments, and spends have to prove absence as well as presence, which a Merkle path over unsorted leaves does not provide. It also assigns notes no canonical position, since a stamp publishes an unordered set.

The replacement has to meet four requirements:

  1. A proof must be able to test whether a value belongs to a published batch, in either direction, without validators holding the batch.
  2. A proof must be able to name a point in history with one short value, and validators must be able to check that the point is real.
  3. That value must also fix which epoch the point belongs to, because each epoch has its own nullifiers.
  4. A validator's work per stamp must stay small, or the savings from aggregation would vanish here.

The first requirement leads to a polynomial whose roots are the batch. The second and third lead to a hash chain that absorbs a short commitment to each polynomial along with an epoch number. The fourth explains why the chain absorbs a commitment the stamp already carries, instead of asking validators to rebuild anything larger. Sections 2 to 5 take the polynomial, and Sections 6 to 10 take the chain.

The deep dive calls the first object the "tachygram accumulator." We avoid that name, because the accumulator of chapter 11 is a different object, and say set commitment as the code's TachygramSetCommit does.

2. Encoding a set as a polynomial

A stamp publishes a batch of tachygrams. As the tachygrams page counts, a stamp covering N actions in B bundles holds 2N+B of them, so a single-action autonome holds three.

We want to test membership by computing something short. A polynomial offers this through its roots, because a value x is a root of a product of linear factors exactly when one factor vanishes at x. So the set polynomial of a stamp is

tachygram_acc(X)=i∏​(X−tgi​).

Tachygrams are elements of Fp​, the base field of Pallas, and this polynomial has coefficients in the same field. In the toy setting we use F43​. Real tachygrams are hash outputs, as chapter 19 explains, and the small values here stand in for them.

Take a stamp A with the three tachygrams 22, 24, 40. Multiplying the factors one at a time, with coefficients reduced modulo 43, gives

(X−22)(X−24)(X2+40X+12)(X−40)​=X2−46X+528=X2+40X+12,=X3+3X+36.​

Membership now becomes one evaluation, and it answers both ways. A zero means the value is in the set, and a nonzero result means it is not.

Value xIn the settachygram_acc(x)
22yes0
24yes0
40yes0
20no12
6no12

Running through all 43 field elements, the polynomial is zero at exactly 3 of them, which are the three tachygrams. All 6 orderings of the three factors expand to the same coefficients, so the polynomial records the set and no order.

Two properties of this encoding matter later. It ignores order, as the count above shows, which fits a stamp whose tachygrams carry no positions. It also holds a multiset rather than a set, because a repeated value becomes a repeated factor while a membership test sees only whether the value is a root.

For example, the multiset {22,22,24} gives X3+18X2+35X+37. It has a double root at 22, and evaluating it there still gives 0, the same answer as a single occurrence.

3. Committing to the set

The polynomial has as many coefficients as the stamp has tachygrams, plus one, so it does not fit in a hash chain that should take a short input. The Pedersen commitment of chapter 6 compresses it to one curve point, and it has the additional advantage that the proof system can open such a commitment at a point. The Tachyon code commits to the coefficients directly with Ragu's fixed generators on Vesta:

C=i∑​ci​⋅Gi​,tachygram_acc(X)=i∑​ci​Xi.

Vesta's scalar field is Fp​, so the coefficients act directly as scalars. Here is the same computation with the toy curve's four generators.

Coefficient ofValueGeneratorTermRunning sum
X036G0​(25,2)(25,2)
X13G1​(24,30)(22,7)
X20G2​O(22,7)
X31G3​(17,7)(23,24)

The commitment is CA​=(23,24). Coefficients live in F43​, like the tachygrams, and the coordinates of CA​ live in F31​.

The code labels this commitment "deterministic (untrapdoored)," which means it has no blinding term r⋅H. A blinding term would stop anyone else from reproducing the point, and validators have to reproduce it. The tachygrams are published in the stamp in any case, so a blind would hide nothing.

The commitment gets two rejections that the polynomial itself does not suggest. Anchor::next_stamp refuses to absorb the identity point, and it also refuses the commitment to an empty set.

On the toy curve, the empty set has the constant polynomial 1, so its commitment is 1⋅G0​=(1,2). That point is a valid curve point, and it differs from O, so the code needs a separate check for it.

A stamp with no tachygrams would advance the chain without publishing anything, and these errors exclude that case explicitly.

4. Combining and dividing sets

Aggregators merge stamps, and chapter 2 showed the StampMerge step doing this at the level of headers. We can now see what the merge does to the polynomials. The union of two multisets is the product of their polynomials, because the roots of a product are the roots of its factors with multiplicities added.

Point addition cannot compute this. A Pedersen commitment is additive, so CA​+CB​ commits to the sum fA​+fB​ and not to the product. The merge step therefore has to establish the product some other way. It witnesses all three polynomials and tests the identity p(r)=fA​(r)fB​(r) at a challenge r derived after all three commitments are fixed.

Stamp B has the tachygrams 20, 23, 41, so fB​(X)=X3+2X2+30X+17. Writing fA​ for the polynomial of stamp A above, the union is

p(X)=fA​(X)fB​(X)=X6+2X5+33X4+16X3+33X2+13X+10.

A product of degree 6 has 7 coefficients, which needs more than our four generators, so this example adds three more curve points, G4​=(4,6), G5​=(5,2) and G6​=(7,6). The commitment to the union is CAB​=(17,24).

At the challenge r=11, the check reads p(11)=27 against fA​(11)⋅fB​(11)=24⋅28=27, and the two sides agree.

The order of events is what gives the test its force. If p=fA​fB​, the difference is a nonzero polynomial whose degree is at most the larger of the two degrees, so Schwartz–Zippel from chapter 3 bounds the number of challenges that pass. Here is that bound in a cheating attempt.

Suppose a prover claims a union p′ with the root 40 replaced by 31. Then p′(40)=5, so 40 would look absent from the merged set, and p′(31)=0, so 31 would look present. Trying every challenge in F43​, the false product passes at exactly 5 of the 43 values, namely 20, 22, 23, 24, 41. The degree bound allows at most 6, since p′−fA​fB​ has degree at most 6 and each passing challenge is one of its roots. The chance 5/43 is large here only because the field is tiny. On a field of about 2254 elements, the same bound is negligible.

Division is the reverse. A quotient exists with zero remainder exactly when the divisor's multiset is contained in the dividend's, and this is how StampUnmerge from chapter 2 removes a contributor.

Dividing p by fB​ returns fA​ with remainder zero, which removes stamp B from the union. Dividing p by X−13 leaves the nonzero remainder 3, because 13 is not in the set.

The same product gives one polynomial for a whole epoch, which matters for exclusion proofs. Testing a nullifier against every stamp of an epoch would take one query per stamp, so the deep dive multiplies all the stamp polynomials of the epoch into an epoch polynomial e(X). A nullifier is absent exactly when e is nonzero there.

Section 7 builds a chain whose epoch 0 holds stamps A, B and C, where stamp C has the tachygrams 13, 19 and 21. Their product e0​(X) has degree 9. It gives e0​(41)=0 for a value published in stamp B, and e0​(4)=37 for a value that does not appear in the epoch.

The degree of e(X) grows with the traffic of the epoch, which is why chapter 21 splits it into buckets. An empty epoch has e(X)=1, which has no roots.

5. Checking a commitment against the published list

A stamp publishes both the list of tachygrams and the set commitment. Binding only tells us that the commitment opens to one polynomial, and it does not say that this polynomial has the published list as its roots. A prover who commits to a different polynomial could add a root or remove one, and the deep dive stresses this point. So someone has to compare them, and the sources describe two ways of doing it.

Suppose stamp A publishes the list 22, 24, 40 but carries a commitment to the polynomial with roots 22, 24, 31. That commitment is (16,10).

  • Recomputation. The validator builds fA​ from the list and commits to it, which gives (23,24). The two points differ, so it rejects the stamp.
  • Evaluation at a random point. The validator computes (11−22)(11−24)(11−40)=24 from the list. The polynomial behind the carried commitment gives 21 at r=11, so an opening to the value 24 fails. The two polynomials agree at 2 of the 43 challenges, which is the prover's chance of getting past this test.

The sources differ on how the commitment is checked

The deep dive has the verifier evaluate the product of (r−tgi​) at a random point and check a commitment opening, and it presents this as avoiding the group operations of a recomputation. The bundle page instead says full validation recomputes the commitment from the list. The code does the same, by building the polynomial, committing, and comparing points. This site follows the code. The proof itself is verified against the carried commitment and does not read the list, so the comparison must happen separately in either design.

Once this check has passed, later work can rely on the point alone, and this is why the stamp carries it. Advancing the chain reads the carried point and never touches the tachygrams again.

6. Folding stamps into a hash chain

We now need one short value that names a point in history. Consider what a validator holds after pruning, which is no tachygrams from old epochs. A proof made today has to convince it that some old set commitment was really published, and the validator cannot look that up.

A hash chain solves this with very little state. Each new state is a hash of the previous state and the new data, so one state fixes the whole sequence behind it, assuming the hash resists collisions. A validator needs to compare only the final value, while a prover can recompute any part of the sequence inside a proof by supplying the inputs. Poseidon suits this, because chapter 3 chose it for being cheap inside circuits.

The anchor page calls each step a fold. This word has nothing to do with the folding of proofs in earlier chapters, since here it means one hash. A stamp in a block of epoch e gives the stamp absorption

anchor′=PoseidonTachyon-AnchorSt​(anchor, e, tglo​, tghi​),

and we write this as Hst​ for short. Each input is there for a reason, so we take them in turn.

The epoch. The epoch is the epoch of the block containing the stamp, which a validator reads from the height. Absorbing it at every step lets a proof learn the epoch of a chain segment from the segment itself, with no outside help. Keep this separate from the anchor that the stamp itself carries, which names an earlier state and can lie in the preceding epoch, as Section 11 shows.

The limbs. The last two inputs encode the set commitment. Poseidon works over Fp​, while the coordinates of a Vesta point lie in the other Pasta field, as chapter 13 explained. A coordinate may not fit in Fp​ as an element, and the point also needs a bit for the sign of y. The code therefore takes the 32-byte compressed encoding and reads each half as a 128-bit integer. Both integers lie far below the modulus, and the pair determines the point.

The toy curve has the same shape, with coordinates in F31​ and a hash working in F43​, so we split a six-bit encoding into two three-bit limbs.

For CA​=(23,24), the coordinate 23 fits in five bits, and we place the parity of y=24 in a sixth bit. The encoding is 23+32⋅0=23. Splitting it into a low and a high part of three bits each gives tglo​=7 and tghi​=2, since 23=7+8⋅2. Checking all 42 affine points of the toy curve shows that each one gets a different encoding, so the pair of limbs determines the point.

The domain. The string Tachyon-AnchorSt is a tag placed in the sponge's capacity, as with every Tachyon Poseidon hash. Section 7 needs it to tell two kinds of fold apart.

The deep dive writes this fold more loosely as H(anchorold​∥i∥tgacc), and we use the code's form.

Real Poseidon values are numbers of about 254 bits, which are hard to follow on a page, so the rest of this chapter uses a stand-in. Hst​ and Hep​ below are SHA-256 with the domain string as a prefix, reduced modulo 43. They copy only the structure of the chain, and with only 43 outputs they collide often, which Section 9 uses to show what a collision would buy.

The genesis state is 29. Stamp A lands in epoch 0, so the new anchor is Hst​(29, 0, 7, 2)=22. Absorbing B after A gives 3, while absorbing A after B gives 18, so the anchor records the order of stamps even though each stamp's own polynomial does not. Claiming epoch 1 for stamp A would give 27 instead of 22, and that value does not appear anywhere in the chain below.

The order_matters test in the code asserts the same dependence on order. The chain records a sequence of stamps, and each stamp's polynomial records an unordered set.

The deep dive also explains why the chain ticks per stamp rather than per block. A stamp already carries its commitment, so one hash is enough. A per-block fold would need a block-wide commitment, and validators would have to rebuild it from every tachygram in the block with an expensive multi-scalar multiplication.

7. Crossing epoch boundaries

Stamp folds alone do not mark where an epoch ends, and an epoch with no stamps would leave no trace at all. Section 9 needs a point in the chain that a proof can recognize as a boundary, so the chain gets a second kind of fold. When the chain enters epoch e+1, it applies a boundary digest

anchor′=PoseidonTachyon-AnchorEp​(anchor, e+1),

which we write as Hep​. It absorbs no tachygrams, and it absorbs the epoch being entered rather than the epoch being left. Since the two folds hash under different tags, an output of one matches an output of the other only through a Poseidon collision, so a proof can check that an anchor is a boundary digest by exhibiting its preimage.

Two positions around each boundary have names:

  • The entry anchor of epoch e is the first anchor of e. It equals Hep​(final(e−1), e) for e≥1, and Hep​(0, 0) for epoch zero, which the code sets as the default anchor.
  • The final anchor of epoch e is the last anchor inside e. It equals the entry anchor when the epoch published no stamp.

The single fold from final(e) to entry(e+1) is the crossing. The deep dive calls the entry anchor the epoch sentinel, and we use the anchor page's names, which the code follows.

The anchor chain across two boundaries. Boundary digests join final(e-1) to entry(e) and final(e) to entry(e+1), and stamp folds run from entry(e) through a1 to final(e). Brackets mark the three epochs.

Here is a complete run. The code's epochs have 4096 blocks, and we use 4 so that three epochs fit on the page. Blocks not listed carry no stamp. Epoch 0 has three stamps, with two of them in block 2. Epoch 1 is empty, and epoch 2 has two stamps.

BlockEpochFoldInputNew anchor
(start)0Hep​(0, 0)29
00Hst​, stamp A(29, 0, 7, 2)22
20Hst​, stamp B(22, 0, 1, 1)3
20Hst​, stamp C(3, 0, 4, 6)26
41Hep​(26, 1)40
82Hep​(40, 2)18
82Hst​, stamp D(18, 2, 4, 7)17
112Hst​, stamp E(17, 2, 0, 5)7
123Hep​(7, 3)38
123Hst​, stamp F(38, 3, 6, 5)41
EpochStampsEntry anchorFinal anchor
0A, B, C2926
1none4040
2D, E187

The empty epoch shows what the boundary digest provides. Epoch 1 has no stamps, yet it has its own value in the chain, and epoch 2's entry anchor differs from it. As the deep dive puts it, even an empty epoch has two distinct boundaries. So each state in the chain belongs to one epoch, determined by the most recent boundary digest. The deep dive writes this map as Epoch(anchor).

8. End-of-block anchors

A block may contain several stamps, so the chain passes through several states within one block. The anchor page says that consensus acknowledges only the state at the end of a block. It gives no reason, and a plausible one is that the inner states depend on how a miner orders the block's stamps, so a wallet cannot predict them. The same page uses the word "anchor" in both a narrow and a broad sense. We say "anchor" for every state of the chain, as the code does, and "end-of-block anchor" for the narrow sense. A block with no stamp leaves the state unchanged unless the block starts a new epoch.

Proofs still handle inner states. A note's creation proof computes the state immediately after its creation stamp, which is probably not the end of a block, so it has to be lifted to an end-of-block state before consensus sees it. Section 10 explains lifting.

The sources disagree on which states consensus accepts, and the question is open

The anchor page says consensus may acknowledge only end-of-block states, with a comment in the source asking "should? must?". A code comment agrees that validators check anchors against a "published per-block anchor sequence." The deep dive asks only that the target anchor occur in canonical chain history, and it allows the target to be a sentinel or an ordinary anchor. The test simulator indexes every state, including inner ones. This site follows the anchor page and the code comment.

The sources also place the crossing differently. The deep dive has the last block of the old epoch commit to the new entry anchor. The anchor page and the simulator apply it at the start of the first block of the new epoch, before that block's stamps, and this site follows them.

The difference in placement changes which states appear at the end of a block, as the run from Section 7 shows.

BlockEpochEnd-of-block state, crossing at the start of the new epoch (code, anchor.md)End-of-block state, crossing at the end of the old epoch (deep dive)
002222
102222
202626
302640
414040
714018
821717
921717
1021717
112738
1234141

Both columns come from the same sequence of states, and only the choice of which states count as end-of-block differs. With the code's placement, the final anchor of epoch 2, 7, is acknowledged at block 11, and the entry anchor of epoch 2, 18, is never an end-of-block value because block 8 carries a stamp. With the deep dive's placement, the reverse holds for this example. In both columns, the state 3 just after stamp B never appears, because stamp C follows it in the same block.

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

With the code's placement, an entry anchor is an end-of-block value only if the first block of its epoch carries no stamp. A lineage that rests on an entry anchor would therefore need one more lift to a later state in the same epoch before consensus accepts it, whenever that first block does carry a stamp. We did not find this stated in the book. It follows from combining the simulator's placement with the end-of-block rule, and it is one reason to treat this rule as unsettled.

9. Covering a whole epoch

An exclusion proof has to show that a nullifier is absent from all of an epoch's stamps, and this creates a difficulty that the boundary digest exists to solve. Suppose someone builds evidence for a run of stamps and stops early. The stamps it left out may contain the nullifier it claims is absent. Every state along the way is a genuine chain state, so checking the end of the run against consensus would reveal nothing wrong, as long as that state is an end-of-block value.

Consensus can only confirm that an anchor exists, and it cannot confirm that a run ending at that anchor was complete. So the evidence has to end on something that only a complete run can reach. The step QrBucketSeal, which chapter 21 discusses in full, does this by performing the crossing itself. It takes the last state of the run, computes the boundary digest into the next epoch, and uses the result as the end of its range.

Suppose someone builds evidence for epoch 0 that stops after stamp A, at anchor 22, and omits B and C. That anchor is a real end-of-block value from block 0, so a consensus check on it would pass. Now perform the crossing from it. The result is Hep​(22, 1)=14, while the true entry anchor of epoch 1 is 40 = Hep​(26, 1). The value 14 appears nowhere in the chain, so nothing built on it can reach a published anchor.

The code contains a test of exactly this situation. It seals a short run, gets a value the pool never published, and asserts that value's absence from the published chain.

The argument depends on the fact that the chain has only one boundary digest into each epoch. A different input to the crossing gives a different output unless the hash collides. Our stand-in shows what the collision would have to look like.

Our stand-in hash has only 43 outputs, so we can count the inputs that would rescue the short evidence. Of the 43 possible states x, the crossing Hep​(x, 1) lands on 40 for 3 of them: the true final anchor 26 and the colliding values 8, 37. A collision like this is what a cheater would need, and on the real field finding one means breaking Poseidon.

This is why both ends of an evidence range are entry anchors and the range excludes its upper end. An entry anchor belongs to the epoch it opens, so the range [entry(e), entry(e+1)) covers exactly epoch e.

The anchor page also warns about a related gap. The value Hep​(x, e) is a boundary digest for any x whatsoever, so a proof that checks an anchor is a boundary digest has not shown that it is the entry anchor of e. The seal step has the same limitation at its lower end, where its input anchor_final_prev is an unchecked witness. The next section explains why this is acceptable.

10. Moving a proof along the chain

Several proof steps take their starting anchor as a free witness, which at first looks like a hole, since a prover can put anything there. It works because the proof's obligations pass along the chain until they reach consensus.

Suppose a proof shows that a run of folds leads from state a to state b, and consensus later confirms that b is in the chain. By collision resistance, the preimage of b can only be the true predecessor, and the same argument applies to that predecessor in turn. So one consensus check at the far end pins every state, epoch, and set commitment along the way. A wrong witness does not break the proof. It only leads to an anchor that consensus does not recognize.

For instance, a wallet could witness the predecessor 23 in place of 22 for stamp B. The fold then gives 6, which is not in the chain, so the proof runs without complaint and the error surfaces only when consensus looks up the anchor.

The same logic covers the epoch witnessed in AnchorSpanSeed, where a span built with the wrong epoch holds anchors that are not on the published chain. It also covers the lower end of the seal from Section 9, because the whole lineage must eventually pass through the note's spend anchor, and consensus checks that.

This structure lets a stamp's anchor move forward. A wallet may move its stamp closer to the chain's latest anchor before publishing, and aggregators need all their inputs at the same anchor. Moving a stamp requires a proof that the old anchor lies on the path to the new one, and an anchor span provides it. The span's header holds a start, an end, and a commitment to a members polynomial M(X) whose roots are the anchors produced inside the span.

The steps reuse the tools from earlier in this chapter. AnchorSpanSeed folds one stamp and commits to X−anchor′. AnchorSpanFuse requires the first span's end to equal the second span's start, and it proves that the combined members polynomial is the product of the two. The lift then opens M at the stamp's anchor a and checks

M(a)⋅(a−anchor_start)=0.

The first factor vanishes for a member and the second for the start, so the product vanishes exactly for anchors in the span. The polynomial does not record the order of its members, so the lift always outputs the span's end rather than a chosen member.

Take epoch 2 from the run above. A span starts at the entry anchor 18 and folds stamps D and E, producing the members 17 and 7. Its members polynomial is

M(X)=(X−17)(X−7)=X2+19X+33.

Stamp anchor aPositionM(a)$a - $ startProductResult
18span start1100lifts to 7
17member0420lifts to 7
7member, span end0320stays at 7
26final anchor of epoch 042835rejected

Among all 43 field elements, exactly 3 pass this check.

A span cannot cross an epoch boundary, and this is deliberate. AnchorSpanSeed folds only with Hst​, so no span contains a crossing.

In our run, a span over epoch 2 ends at 7, and a span over the stamp in epoch 3 starts at 38. The fuse requires those two values to match, and they do not, because the crossing sits between them.

The code tests this failure with the error "spans do not share a vertex." A stamp needs this restriction because each spend beneath it derived its nullifier pair at the anchor's epoch. If a lift could cross a boundary, the published pair would no longer match the epoch the anchor implies. To move a note into a later epoch, a wallet instead carries its spendable through the steps of chapter 22, which prove the crossed nullifier absent, and it builds a new stamp at the end.

11. The consensus window

We can now state what a validator does with a stamp and why it needs a window. The deep dive lists most of the steps. We rephrase them and add the duplicate check and the absorption. No source fixes the order across all six, and no validator code exists to check it against.

  1. Look up the stamp's anchor in the chain and obtain its epoch e.
  2. Require e to be the epoch of the current block or the one before it.
  3. Check the set commitment against the published tachygrams, as in Section 5, and check the action digests in the same way.
  4. Verify the proof against the action commitment, the set commitment, and the anchor.
  5. Check each tachygram against the retained window, inserting it as it passes.
  6. Absorb the set commitment into the chain under the current block's epoch.

Step 4 establishes the note's history, but only up to a limit that needs care. For an older note, the proof shows that the note's nullifiers were absent in every complete epoch before e. It makes no claim about epoch e itself, so the anchor does not mark the end of an exclusion range, even when it sits late in the epoch. The rest of the protection has to come from steps 2 and 5.

The window needs to span two epochs, and the tachygrams page gives two reasons. The first is that a stamp may have been lifted, so it could have been built at any earlier point in its anchor's epoch. Consensus must therefore compare against the whole of that epoch, and not only the blocks after the anchor. The second concerns the pair of nullifiers from chapter 1. A transaction built for epoch e may still be in the mempool when epoch e+1 begins, so a stamp anchored in e stays valid through e+1. The value nfe+1​ published in epoch e is the same value a later spend must publish when it targets e+1, and a validator catches that collision only if it still remembers the preceding epoch.

Three rows for a stamp anchored in epoch e. Landing in e, the window covers e-1 and e, and the stamp is accepted. Landing in e+1, the window covers e and e+1, and the stamp is accepted. Landing in e+2, the stamp is rejected.

The window moves with the block being validated, so old values leave it at each boundary. Here is what the run from Section 7 retains.

Current epochWindowStamps retainedTachygrams retained
0{0}A, B, C9
1{0, 1}A, B, C9
2{1, 2}D, E6
3{2, 3}D, E, F9

The value 19 from stamp C is therefore rejected if a stamp republishes it in epoch 1, and it passes this check in epoch 2, because epoch 0 has left the window. This is safe for spends because of the exclusion proof, as the next table shows.

The deep dive's case analysis can be checked by enumeration. Chapter 1 removed one rule at a time and counted the double spends that got through. Here we keep all the rules, and for each pair of spends we name the rule that stops the second one. The first spend targets epoch 5, the note is older than every epoch in the table, and each spend lands in its target epoch or the next.

First accepted inSecond targetSecond accepted inShared nullifiersWhat stops the second spend
545nf5​duplicate window
555nf5​, nf6​duplicate window
556nf5​, nf6​duplicate window
566nf6​duplicate window and exclusion proof (epoch 5)
567nf6​exclusion proof (epoch 5)
577noneexclusion proof (epoch 5)
578noneexclusion proof (epoch 5)
656nf5​, nf6​duplicate window
666nf6​duplicate window
667nf6​duplicate window
677noneexclusion proof (epoch 6)
678noneexclusion proof (epoch 6)

All 12 cases are stopped: 7 by the window and 6 by the exclusion proof, with 1 stopped by both. The exclusion column works because the first spend publishes the nullifier of the epoch that accepts it, whichever of its two acceptance epochs that is, and that value lies inside the range a later target must prove absent.

The rows with no shared nullifier show why the window can be short. Once two spends are far enough apart to share nothing, the later one has to prove the earlier epoch clean, and it cannot. The rows stopped only by the window show why the window cannot be shorter, because the exclusion proof makes no claim about the target epoch.

Validators process candidates in a fixed order, checking and inserting each value before the next. A candidate can therefore conflict with the retained window, an earlier bundle in its block, or an earlier value in its own bundle. This also gives a useful property to the polynomials of Section 4. Although a set polynomial is a multiset encoding in general, no accepted epoch can repeat a tachygram, so the code treats each epoch's product as square-free.

Step 2 gives aggregators a deadline. A stamp anchored in epoch e cannot be published after the end of epoch e+1, and since lifts cannot cross a boundary, an aggregate in progress at that point has to be rebuilt.

12. What validators keep

A validator keeps two things, and they grow differently. The first is the window, whose size chapter 1 already estimated, and which stops growing because it holds only two epochs. The second is the anchor chain, which the deep dive says validators keep unpruned, so that they can look up any anchor a stamp names. Each state is 32 bytes.

The chain's growth depends on how many stamps reach the chain, and this gives aggregation a second benefit.

At the 75-second block target there are 420,480 blocks per year. Take the hypothetical 100 transactions per second from chapter 1, which is 7,500 transactions per block. These inputs are illustrative.

CaseFolds per blockAnchors per year if every state is keptSize at 32 bytes each
Every transaction is an autonome7,5003,153,600,000100.9 GB
One aggregate per block1420,48013.5 MB

The first row's size is an upper limit. If consensus acknowledges only end-of-block states, a validator could store one state per block in both cases, although it still has to compute every fold in the block. The sources we read do not say what validators store in that case, so we treat the storage format as unsettled. Either way, one fold costs one Poseidon hash, and the chain grows with the number of stamps rather than the number of tachygrams.

The pruned tachygrams still have to exist somewhere, or nobody could build exclusion proofs, which is the data availability question that chapter 1 left open.

13. Why cheating fails

The following table collects the attacks this chapter has considered. Each row assumes that the stamp's proof cannot be forged, which rests on the soundness of the proof system from earlier chapters.

A cheater tries toWhat stops itAssumption
Publish a list that differs from the committed polynomialRecomputing the commitment (Section 5)None beyond correct computation
Merge stamps into a false productThe identity test at a late challenge (Section 4)Fiat–Shamir challenge, Schwartz–Zippel, commitment binding
Claim a stamp with an invented historyThe lookup of the final anchor (Section 10)Poseidon collision resistance
Claim the wrong epoch for a foldThe epoch is hashed in, so the state falls off the chain (Section 6)Poseidon collision resistance
Pass a stamp fold off as a crossingDifferent domain tags (Section 7)Poseidon collision resistance across domains
Seal evidence before the end of an epochThe crossing misses the published entry anchor (Section 9)Poseidon collision resistance
Lift a stamp from outside a spanThe members check (Section 10)Commitment binding, anchors distinct from one another
Lift a stamp across an epoch boundarySpans contain no crossing (Section 10)Commitment binding
Publish a stale stampThe anchor's epoch must be current or preceding (Section 11)Correct validators
Spend a note twiceThe window and the exclusion proof together (Section 11)Nullifiers are determined by the note and epoch (chapter 19)

Most of these rows rest on the same mechanism. A proof may witness history freely, but a false history leads to an anchor that differs from the one consensus holds, and reaching the real anchor from a false history would take a Poseidon collision.

14. Where this chapter leaves us

This chapter took the following steps:

  1. A stamp's tachygrams become the roots of tachygram_acc(X), so that one evaluation tests membership or absence.
  2. A deterministic Pedersen commitment shrinks that polynomial to one point. Validators check the point against the published list, and the sources differ on whether they recompute it or test it at a random point.
  3. Union is polynomial multiplication and removal is division, checked at a challenge fixed after the commitments. Multiplying an epoch's stamps gives one polynomial for the whole epoch.
  4. A Poseidon hash chain absorbs each stamp's commitment and its block's epoch under the domain Tachyon-AnchorSt, and a boundary digest under Tachyon-AnchorEp joins each epoch's final anchor to the next epoch's entry anchor.
  5. Consensus acknowledges end-of-block states. The sources disagree on the exact rule and on which block holds the crossing, so we follow the anchor page and the code.
  6. Evidence for a whole epoch ends on the crossing it computes, because only a complete run reaches the published entry anchor.
  7. Proof steps may witness anchors freely, since one consensus check at the far end pins every earlier state. Anchor spans move stamps forward within an epoch and cannot cross a boundary.
  8. Consensus accepts a stamp anchored in the current or preceding epoch and rejects any tachygram already in those two epochs. Together with the exclusion proof, this stops every double spend in our enumeration.

The exclusion proof has been a black box throughout this chapter, and the epoch polynomial is too large to open directly. The next chapter, Oblivious Sync, shows how an untrusted service proves absence over a whole epoch by splitting that polynomial into small buckets, and what the service learns while doing so.

Last Updated: 11/4/26, 10:49 AM
Contributors: Hanh
Prev
Notes, Nullifiers, and Tachygrams
Next
Oblivious Sync