feat(receipts): trinote.receipt/v3 binds a receipt to the request that asked for it - #1
Merged
Merged
Conversation
…t asked for it
v2 commits a model, its input tokens, its output tokens and its sampling trace. It
commits nothing about the request, so a valid old receipt can be presented against
a fresh one and nothing in the receipt itself contradicts that.
v3 adds one field, contextCommit, committed in BOTH signed messages and the body:
v3 model {contextCommit, inputCommit, modelHash, outputCommit, traceCommit}
v3 counterparty {contextCommit, inputCommit, modelHash, outputCommit}
The counterparty signs it too. It co-signs only what it observed and never the
internal trace, but it is the party attesting that this result answers this
request — a counterparty signature without the context would be transferable
between requests, which is the failure v3 exists to prevent.
v2 is untouched. Its bytes, its receiptHash and its signatures are byte-identical
to what they were before this commit, which matters because those digests are
anchored on-chain and pinned by cross-language vectors. Passing context_commit to
v2 or v1 is an error rather than a silent no-op, and emitting v3 without a valid
64-hex binding raises rather than producing a receipt that claims a freshness it
does not have.
tests/fixtures/receipt-v3.vectors.json holds signed-message bytes produced by an
independent implementation of the same specification; the new tests compare this
engine's encoding against them for v2 and v3 alike. If the two ever disagree about
a byte, a signature made by one cannot be verified by the other.
15 new tests pass; 137 existing receipt, canonical and trace tests still pass.
…lly signs The fixture carried more than the tests read. `signingDigest` was unchecked on all five messages, the whole `receipts` array went unread — including the v2 receiptHash that is anchored on-chain — and `v3-model-different-context` sat in the file but not in the parametrize list. Data that claims a coverage it does not have is worse than no data. The vector check also rebuilt the signed entry inside the test rather than taking it from build_receipt, so it compared an independent implementation against a shape this engine might not use. Renaming contextCommit to ctxCommit inside build_receipt left all 15 tests passing while nothing this engine signed could be verified by the other implementation — precisely the failure the vectors exist to catch. `_signed_entry` is now the single definition of the signed shape. The vector tests check its bytes and digests against the independent implementation; new tests check that build_receipt's own sigModel and sigCounterparty are that shape signed. Both halves must agree for either to pass, which closes the chain from build_receipt to the foreign bytes. v2's anchored receiptHash is asserted against a literal. Recomputing a receipt's hash and comparing it against itself passes just as happily when both sides move together, and an on-chain anchor is the one digest that cannot tolerate that. Message and receipt vectors are parametrized over the fixture itself, so a vector that is added but never asserted cannot pass unnoticed. Also moves _HEX64 below the imports; it was sitting between two of them. 24 tests, up from 15.
itsmygithubacct
force-pushed
the
feat/receipt-v3-context-binding
branch
from
August 4, 2026 13:48
b4cefb8 to
2996f81
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
trinote.receipt/v3, which binds a receipt to the request that asked for it.v2 commits a model, its input tokens, its output tokens and its sampling trace. It commits nothing about the request, so a valid old receipt can be presented against a fresh one and nothing in the receipt itself contradicts that.
v3 adds one field,
contextCommit, committed in both signed messages and the body:The counterparty signs it too. It co-signs only what it observed and never the internal trace, but it is the party attesting that this result answers this request — a counterparty signature without the context would be transferable between requests, which is the failure v3 exists to prevent.
v2 is untouched
Its bytes, its
receiptHashand its signatures are byte-identical to before. Those digests are anchored on-chain and pinned by cross-language vectors, so nothing about them may move. Passingcontext_committo v2 or v1 is an error rather than a silent no-op, and emitting v3 without a valid 64-hex binding raises rather than producing a receipt that claims a freshness it does not have.Cross-implementation vectors
tests/fixtures/receipt-v3.vectors.jsonholds signed-message bytes produced by an independent implementation of the same specification. The new tests compare this engine's encoding against them for v2 and v3 alike — if the two ever disagree about a byte, a signature made by one cannot be verified by the other.Testing
15 new tests pass. 137 existing receipt, canonical and trace tests still pass.