Skip to content

Commit the round reference time in the SMT leaf value #176

Description

@ristik

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.goRound needs a pinned ReferenceTime, set by StartNewRound / StartNewRoundWithSnapshot from the UC that starts the round.
  • internal/bft/client.gobuildCertificationInputRecord 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.goLeafValue() takes τ and returns SHA-256(CBOR([txhash, τ])).
  • internal/round/leaf_add.gocommitmentLeafInput 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/apiInclusionProofV2 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

  • A round's leaf values and its IR.Timestamp provably come from one pinned τ.
  • Inclusion proofs carry τ, and local proof verification reconstructs the root from H(txhash, τ).
  • Recovery and restart reproduce identical roots for already-certified rounds.
  • Unit tests pin the leaf-value encoding against a fixed vector shared with the other implementations.

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

Repository Reference time in the leaf value Request timeout
aggregator-go #176 #177
rugregator ristik/rugregator#3 ristik/rugregator#4
bft-core (branch l1) unicitynetwork/bft-core#23 (RSMT), unicitynetwork/bft-core#24 (ZK) out of scope
state-transition-sdk-js unicitynetwork/state-transition-sdk-js#144 unicitynetwork/state-transition-sdk-js#145
state-transition-sdk-java unicitynetwork/state-transition-sdk-java#81 unicitynetwork/state-transition-sdk-java#82
state-transition-sdk-rust unicitynetwork/state-transition-sdk-rust#16 unicitynetwork/state-transition-sdk-rust#17

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions