Background
Yellowpaper commit 407b07f ("commit reference time in the SMT leaf value") changes the Unicity Service SMT leaf value from the transaction hash alone to H(txhash, τ), where τ is the reference time of the round the request was validated in (UC.IR.t).
Predicate evaluation already takes τ as an argument, but τ was recoverable only from the inclusion proof, as UC.IR.t. The SMT is append-only, so a leaf can be certified afresh against any later root, and a later proof carries a later round's IR.t. Reference time was therefore a property of the proof rather than of the leaf, and re-presenting a leaf changed the predicate evaluation outcome. Binding τ into the leaf value fixes the value the transition was validated under, for any proof of that leaf.
τ cannot go into txhash: it is chosen by the Aggregator after the sender has signed. The leaf value is the first field written once τ is known, and it is the one field re-presentation preserves verbatim.
Affected yellowpaper sections: platform.tex (Unicity Service Request, request validation, SMT leaf structure, inclusion proof), execution-layer.tex (mint/transfer transaction structure and verification, sec:time-extraction, Unicity Service processing), appendix-hashtrees.tex (ZK-compressed consistency proof).
Normative encodings
These must be byte-identical across aggregator-go, rugregator, bft-core, and the three SDKs.
| Symbol |
Meaning |
Type |
τ |
Round reference time: UC.InputRecord.Timestamp of the round the leaf was certified in. BFT Core requires it to equal the previous round's Unicity Seal timestamp. |
uint64, Unix seconds |
τ_Q |
Exclusive request timeout, chosen by the sender |
uint64, Unix seconds |
Transaction hash. τ_Q is the last element of the transaction's own deterministic CBOR array, so
txhash = SHA-256( <transaction CBOR, ending in τ_Q> )
is unchanged in form and is exactly appendix-token.tex's SHA-256(CBOR(…, τ_Q)). Keeping τ_Q inside the transaction rather than beside it (as the appendix's four-element certified transaction does) means the request, the certified transaction and the hash preimage cannot disagree about it, and the unlock script signs it along with the rest of the transaction.
SMT leaf value.
v = SHA-256( CBOR([ txhash, τ ]) ) // 32 raw bytes
txhash enters as a CBOR byte string of the raw 32-byte digest (no algorithm-id prefix), τ as a CBOR unsigned integer. This is the same construction already used for StateID and for the signature preimage.
Shared test vector:
txhash = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
τ = 1755000000
preimage = 825820000102…1e1f 1a689b2cc0
v = 0235bd52cfa10c9785dfa01942bc396f201fe715dbc3896ee117a97e895e1e36
Certification request. CertificationData carries τ_Q between the transaction hash and the witness, matching Q = (ρ, sthash, txhash, τ_Q, u). The aggregator never sees the transaction, so it needs the value explicitly to enforce expiry.
Inclusion proof. Carries τ. A verifier cannot recover it from the certificate chain, because an aggregator serves proofs against the current certified root rather than the one the leaf was created under.
Certified transaction. [ transaction, τ, inclusionProof ]. τ_Q is already inside element 0.
Admission rule. A request may be inserted only in a round whose reference time satisfies τ < τ_Q.
Scope in aggregator-go
The SMT leaf value becomes H(txhash, τ), where τ is the reference time of the round the leaf is inserted in. τ is the value this node already puts into IR.Timestamp (the previous UC's Unicity Seal timestamp, internal/bft/client.go:buildCertificationInputRecord), so no new source of truth is introduced. The leaf value and IR.Timestamp of a round must be derived from the same pinned τ, or BFT Core will not reproduce the root.
Round must pin τ
τ is known when a round starts (it is the seal timestamp of the UC that triggered the start) but leaves are currently inserted before it is recorded anywhere:
internal/round/round_manager.go — Round needs a pinned ReferenceTime, set by StartNewRound / StartNewRoundWithSnapshot from the UC that starts the round.
internal/bft/client.go — buildCertificationInputRecord must use the round's pinned τ rather than re-reading the latest UC at proposal time, so a UC arriving mid-round cannot make IR.Timestamp disagree with the leaves already inserted. When the pinned τ and the latest UC disagree, the round has to be discarded and re-collected rather than proposed.
internal/round/precollector.go — the precollector inserts the next round's leaves into a forked snapshot while the current round is still awaiting its UC, i.e. before that round's τ exists. Leaf materialisation has to move to the point where τ is known; precollection of the requests themselves can stay.
Leaf value derivation
internal/models/certification_request.go — LeafValue() takes τ and returns SHA-256(CBOR([txhash, τ])).
internal/round/leaf_add.go — commitmentLeafInput threads the round's τ.
internal/round/recovery.go:506 — recovery recomputes leaves for a round; it must use that round's τ, read back from the stored block's UC.
Serving τ to clients
A verifier cannot recover τ from the proof: this node serves inclusion proofs against the current certified root, so UC.IR.t of a served proof is the latest round's reference time, not the one the leaf was created under. That divergence is exactly what the yellowpaper change fixes, and it means τ has to be returned explicitly.
- Store the round's
τ on models.CertificationRequest and models.AggregatorRecord (BSON included).
pkg/api — InclusionProofV2 carries τ and GetInclusionProofV2 fills it from the record.
pkg/api/types.go:InclusionProofV2.Verify and internal/proofverify/local.go derive the leaf value as SHA-256(CBOR([txhash, τ])) instead of using the raw transaction hash.
Notes
- Non-backward-compatible and state-resetting. There is no mainnet; no migration path is required, but a stored SMT from before the change cannot be reused.
- Cross-implementation agreement is what makes this safe:
rugregator, bft-core (branch l1) and the three SDKs land the same encoding, and the change is switched on across the deployment in one coordinated step.
Acceptance criteria
Cross-repository change. The same encoding lands in aggregator-go, rugregator, bft-core (branch l1, consistency proof only) and the three state transition SDKs, and is switched on across the deployment in one coordinated step. Cross-implementation test vectors are part of the work.
Companion issues
Background
Yellowpaper commit
407b07f("commit reference time in the SMT leaf value") changes the Unicity Service SMT leaf value from the transaction hash alone toH(txhash, τ), whereτis the reference time of the round the request was validated in (UC.IR.t).Predicate evaluation already takes
τas an argument, butτwas recoverable only from the inclusion proof, asUC.IR.t. The SMT is append-only, so a leaf can be certified afresh against any later root, and a later proof carries a later round'sIR.t. Reference time was therefore a property of the proof rather than of the leaf, and re-presenting a leaf changed the predicate evaluation outcome. Bindingτinto the leaf value fixes the value the transition was validated under, for any proof of that leaf.τcannot go intotxhash: it is chosen by the Aggregator after the sender has signed. The leaf value is the first field written onceτis known, and it is the one field re-presentation preserves verbatim.Affected yellowpaper sections:
platform.tex(Unicity Service Request, request validation, SMT leaf structure, inclusion proof),execution-layer.tex(mint/transfer transaction structure and verification,sec:time-extraction, Unicity Service processing),appendix-hashtrees.tex(ZK-compressed consistency proof).Normative encodings
These must be byte-identical across
aggregator-go,rugregator,bft-core, and the three SDKs.τUC.InputRecord.Timestampof the round the leaf was certified in. BFT Core requires it to equal the previous round's Unicity Seal timestamp.τ_QTransaction hash.
τ_Qis the last element of the transaction's own deterministic CBOR array, sois unchanged in form and is exactly
appendix-token.tex'sSHA-256(CBOR(…, τ_Q)). Keepingτ_Qinside the transaction rather than beside it (as the appendix's four-element certified transaction does) means the request, the certified transaction and the hash preimage cannot disagree about it, and the unlock script signs it along with the rest of the transaction.SMT leaf value.
txhashenters as a CBOR byte string of the raw 32-byte digest (no algorithm-id prefix),τas a CBOR unsigned integer. This is the same construction already used forStateIDand for the signature preimage.Shared test vector:
Certification request.
CertificationDatacarriesτ_Qbetween the transaction hash and the witness, matchingQ = (ρ, sthash, txhash, τ_Q, u). The aggregator never sees the transaction, so it needs the value explicitly to enforce expiry.Inclusion proof. Carries
τ. A verifier cannot recover it from the certificate chain, because an aggregator serves proofs against the current certified root rather than the one the leaf was created under.Certified transaction.
[ transaction, τ, inclusionProof ].τ_Qis already inside element 0.Admission rule. A request may be inserted only in a round whose reference time satisfies
τ < τ_Q.Scope in
aggregator-goThe SMT leaf value becomes
H(txhash, τ), whereτis the reference time of the round the leaf is inserted in.τis the value this node already puts intoIR.Timestamp(the previous UC's Unicity Seal timestamp,internal/bft/client.go:buildCertificationInputRecord), so no new source of truth is introduced. The leaf value andIR.Timestampof a round must be derived from the same pinnedτ, or BFT Core will not reproduce the root.Round must pin
ττis known when a round starts (it is the seal timestamp of the UC that triggered the start) but leaves are currently inserted before it is recorded anywhere:internal/round/round_manager.go—Roundneeds a pinnedReferenceTime, set byStartNewRound/StartNewRoundWithSnapshotfrom the UC that starts the round.internal/bft/client.go—buildCertificationInputRecordmust use the round's pinnedτrather than re-reading the latest UC at proposal time, so a UC arriving mid-round cannot makeIR.Timestampdisagree with the leaves already inserted. When the pinnedτand the latest UC disagree, the round has to be discarded and re-collected rather than proposed.internal/round/precollector.go— the precollector inserts the next round's leaves into a forked snapshot while the current round is still awaiting its UC, i.e. before that round'sτexists. Leaf materialisation has to move to the point whereτis known; precollection of the requests themselves can stay.Leaf value derivation
internal/models/certification_request.go—LeafValue()takesτand returnsSHA-256(CBOR([txhash, τ])).internal/round/leaf_add.go—commitmentLeafInputthreads the round'sτ.internal/round/recovery.go:506— recovery recomputes leaves for a round; it must use that round'sτ, read back from the stored block's UC.Serving
τto clientsA verifier cannot recover
τfrom the proof: this node serves inclusion proofs against the current certified root, soUC.IR.tof a served proof is the latest round's reference time, not the one the leaf was created under. That divergence is exactly what the yellowpaper change fixes, and it meansτhas to be returned explicitly.τonmodels.CertificationRequestandmodels.AggregatorRecord(BSON included).pkg/api—InclusionProofV2carriesτandGetInclusionProofV2fills it from the record.pkg/api/types.go:InclusionProofV2.Verifyandinternal/proofverify/local.goderive the leaf value asSHA-256(CBOR([txhash, τ]))instead of using the raw transaction hash.Notes
rugregator,bft-core(branchl1) and the three SDKs land the same encoding, and the change is switched on across the deployment in one coordinated step.Acceptance criteria
IR.Timestampprovably come from one pinnedτ.τ, and local proof verification reconstructs the root fromH(txhash, τ).Cross-repository change. The same encoding lands in
aggregator-go,rugregator,bft-core(branchl1, consistency proof only) and the three state transition SDKs, and is switched on across the deployment in one coordinated step. Cross-implementation test vectors are part of the work.Companion issues
aggregator-gorugregatorbft-core(branchl1)state-transition-sdk-jsstate-transition-sdk-javastate-transition-sdk-rust