Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions bonsai/src/trinote/receipts/receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@
"""
from __future__ import annotations

import re
from .canonical import canonical_bytes, commit, token_commit
from .signing import LocalKey, sign
from ..hashing.sha import sha256_hex
from ..infer_int.sampler import RECEIPT_SAFE_MODES, inv_temp_fp, top_p_fp, min_p_fp

_HEX64 = re.compile(r"\A[0-9a-f]{64}\Z")

SCHEMA = "trinote.receipt/v2"
PREIMAGE_SCHEMA = "trinote.receipt-preimage/v2"
SCHEMA_V3 = "trinote.receipt/v3"
PREIMAGE_SCHEMA_V3 = "trinote.receipt-preimage/v3"
SCHEMA_V1 = "trinote.receipt/v1"
PREIMAGE_SCHEMA_V1 = "trinote.receipt-preimage/v1"

Expand Down Expand Up @@ -120,19 +125,34 @@ def build_receipt(*, model_hash: str, input_ids, output_ids, sampler,
model_key: LocalKey, counterparty_key: LocalKey,
trace: dict | None = None, model_label: str = "",
artifact_digest: str | None = None,
fp_frac_bits: int = 16, schema_version: str = "v2") -> dict:
fp_frac_bits: int = 16, schema_version: str = "v2",
context_commit: str | None = None) -> dict:
"""Build a receipt BUNDLE for one inference turn. Pure: no I/O, no chain. Greedy → receiptBound.

`schema_version="v2"` (default) commits a fully-integer sampler block (float-free, language-neutral);
`fp_frac_bits` is the fixed-point scale (must match the engine's `model.cfg["frac"]`). Pass
`schema_version="v1"` ONLY to reproduce a historical float-block receiptHash byte-for-byte."""
if schema_version not in ("v1", "v2"): # fail loud, never silently fall through to v2
raise ValueError(f"unknown receipt schema_version {schema_version!r} (expected 'v1' or 'v2')")
`schema_version="v1"` ONLY to reproduce a historical float-block receiptHash byte-for-byte.

`schema_version="v3"` additionally binds the receipt to the request that asked for it: the caller
supplies `context_commit` (semantos.trinote.context/v1) and it is committed inside BOTH signed
messages and the receipt body. v2 receipts are byte-unchanged — a v2 receiptHash computed before v3
existed still recomputes identically, which matters because those digests are anchored on-chain."""
if schema_version not in ("v1", "v2", "v3"): # fail loud, never silently fall through to v2
raise ValueError(f"unknown receipt schema_version {schema_version!r} (expected 'v1', 'v2' or 'v3')")
if schema_version == "v3":
if not isinstance(context_commit, str) or not _HEX64.match(context_commit):
# a v3 receipt without a valid binding would claim freshness it does not have
raise ValueError("schema_version='v3' requires context_commit as 64 lowercase hex chars")
elif context_commit is not None:
raise ValueError(f"context_commit is only meaningful for v3, not {schema_version!r}")
input_commit = token_commit(input_ids)
output_commit = token_commit(output_ids)
if schema_version == "v1":
sampler_block = _sampler_to_block_v1(sampler)
schema, preimage_schema = SCHEMA_V1, PREIMAGE_SCHEMA_V1
elif schema_version == "v3":
sampler_block = sampler_to_block(sampler, fp_frac_bits)
schema, preimage_schema = SCHEMA_V3, PREIMAGE_SCHEMA_V3
else:
sampler_block = sampler_to_block(sampler, fp_frac_bits)
schema, preimage_schema = SCHEMA, PREIMAGE_SCHEMA
Expand All @@ -148,7 +168,7 @@ def build_receipt(*, model_hash: str, input_ids, output_ids, sampler,
"sampler": sampler_block,
"miStatus": trace.get("miStatus", "pending"), # honest: MI attribution not wired
}
if schema_version == "v2":
if schema_version in ("v2", "v3"):
# The v2 float-free / language-neutral guarantee covers the WHOLE committed preimage, not just the
# sampler block. The MI fields are empty today (P5/pending), but enforce the invariant now so that
# when MI lands it must commit fixed-point ints (e.g. actFp=round(act·2^f)) rather than IEEE floats
Expand All @@ -160,13 +180,20 @@ def build_receipt(*, model_hash: str, input_ids, output_ids, sampler,

# 1st entry — model signs the full claim including the trace commitment. `key.sign` is polymorphic:
# a LocalKey emits a symmetric HMAC vouch; an ECKey emits a third-party-verifiable secp256k1 signature.
model_msg = canonical_bytes({"modelHash": model_hash, "inputCommit": input_commit,
"outputCommit": output_commit,
"traceCommit": trace_block["traceCommit"]})
model_entry = {"modelHash": model_hash, "inputCommit": input_commit,
"outputCommit": output_commit, "traceCommit": trace_block["traceCommit"]}
if context_commit is not None:
model_entry["contextCommit"] = context_commit
model_msg = canonical_bytes(model_entry)
sig_model = model_key.sign(model_msg)
# 2nd entry — counterparty co-signs only the input/output it observed (not the internal trace).
cp_msg = canonical_bytes({"modelHash": model_hash, "inputCommit": input_commit,
"outputCommit": output_commit})
# the counterparty co-signs only what it observed — plus the context, or its
# signature would be transferable between requests
cp_entry = {"modelHash": model_hash, "inputCommit": input_commit,
"outputCommit": output_commit}
if context_commit is not None:
cp_entry["contextCommit"] = context_commit
cp_msg = canonical_bytes(cp_entry)
sig_counterparty = counterparty_key.sign(cp_msg)

body = {
Expand All @@ -189,6 +216,8 @@ def build_receipt(*, model_hash: str, input_ids, output_ids, sampler,
cp_pub = getattr(counterparty_key, "public_hex", None)
if cp_pub:
body["sigCounterpartyPubKey"] = cp_pub
if context_commit is not None:
body["contextCommit"] = context_commit
receipt = dict(body)
receipt["receiptHash"] = sha256_hex(canonical_bytes(body)) # commits the signed pair (3rd-entry payload)

Expand Down
119 changes: 119 additions & 0 deletions bonsai/tests/fixtures/receipt-v3.vectors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{
"schema": "trinote.receipt/v3",
"note": "Signed-message and receipt-hash vectors. v2 rows pin what must not change. Synthetic inputs only.",
"messages": [
{
"name": "v2-model",
"entry": "model",
"note": "unchanged from today \u2014 these bytes must never move",
"inputs": {
"model_hash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
"input_commit": "0101010101010101010101010101010101010101010101010101010101010101",
"output_commit": "0202020202020202020202020202020202020202020202020202020202020202",
"trace_commit": "0303030303030303030303030303030303030303030303030303030303030303"
},
"canonicalText": "{\"inputCommit\":\"0101010101010101010101010101010101010101010101010101010101010101\",\"modelHash\":\"e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5\",\"outputCommit\":\"0202020202020202020202020202020202020202020202020202020202020202\",\"traceCommit\":\"0303030303030303030303030303030303030303030303030303030303030303\"}",
"signingDigest": "b9e44da58d175a5511b90b83e1e52db7f5fd7ba31ec6ef713e418c04d328da79"
},
{
"name": "v2-counterparty",
"entry": "counterparty",
"note": "the counterparty co-signs only what it observed",
"inputs": {
"model_hash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
"input_commit": "0101010101010101010101010101010101010101010101010101010101010101",
"output_commit": "0202020202020202020202020202020202020202020202020202020202020202"
},
"canonicalText": "{\"inputCommit\":\"0101010101010101010101010101010101010101010101010101010101010101\",\"modelHash\":\"e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5\",\"outputCommit\":\"0202020202020202020202020202020202020202020202020202020202020202\"}",
"signingDigest": "154553d06d1928ce2d9f3fd7ae7edde4569c0317f7c4fa647bd45e0bf8c62439"
},
{
"name": "v3-model",
"entry": "model",
"note": "exactly one key added; contextCommit sorts first",
"inputs": {
"model_hash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
"input_commit": "0101010101010101010101010101010101010101010101010101010101010101",
"output_commit": "0202020202020202020202020202020202020202020202020202020202020202",
"trace_commit": "0303030303030303030303030303030303030303030303030303030303030303",
"context_commit": "c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0"
},
"canonicalText": "{\"contextCommit\":\"c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0\",\"inputCommit\":\"0101010101010101010101010101010101010101010101010101010101010101\",\"modelHash\":\"e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5\",\"outputCommit\":\"0202020202020202020202020202020202020202020202020202020202020202\",\"traceCommit\":\"0303030303030303030303030303030303030303030303030303030303030303\"}",
"signingDigest": "951962e9fc34ab0acbbbccc4894c12154a1ba2eb72acc82b5caaf090ba88fa4a"
},
{
"name": "v3-counterparty",
"entry": "counterparty",
"note": "the counterparty signs the context too, or its signature would be transferable between requests",
"inputs": {
"model_hash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
"input_commit": "0101010101010101010101010101010101010101010101010101010101010101",
"output_commit": "0202020202020202020202020202020202020202020202020202020202020202",
"context_commit": "c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0"
},
"canonicalText": "{\"contextCommit\":\"c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0\",\"inputCommit\":\"0101010101010101010101010101010101010101010101010101010101010101\",\"modelHash\":\"e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5\",\"outputCommit\":\"0202020202020202020202020202020202020202020202020202020202020202\"}",
"signingDigest": "226af39bc6415e940ec740c38da09c4ecd8fd94d1575aaa50ce62f4291cd07f9"
},
{
"name": "v3-model-different-context",
"entry": "model",
"note": "same execution, different request context \u2014 the signed bytes must differ",
"inputs": {
"model_hash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
"input_commit": "0101010101010101010101010101010101010101010101010101010101010101",
"output_commit": "0202020202020202020202020202020202020202020202020202020202020202",
"trace_commit": "0303030303030303030303030303030303030303030303030303030303030303",
"context_commit": "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1"
},
"canonicalText": "{\"contextCommit\":\"c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1\",\"inputCommit\":\"0101010101010101010101010101010101010101010101010101010101010101\",\"modelHash\":\"e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5\",\"outputCommit\":\"0202020202020202020202020202020202020202020202020202020202020202\",\"traceCommit\":\"0303030303030303030303030303030303030303030303030303030303030303\"}",
"signingDigest": "7014b249fc3b02fe4dc6b848df7d5d4f4721858fd20803da91c0bc96321953d0"
}
],
"receipts": [
{
"name": "v2-body",
"note": "abridged body; the point is that the hash covers every field",
"body": {
"schema": "trinote.receipt/v2",
"modelHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
"inputCommit": "0101010101010101010101010101010101010101010101010101010101010101",
"outputCommit": "0202020202020202020202020202020202020202020202020202020202020202",
"receiptBound": true,
"sigModelKeyId": "key:model-a",
"sigCounterpartyKeyId": "key:cp-b"
},
"receiptHash": "034c423b293b1d2fae6a81984c1010721db006aba1ac1c6d4b4ea5b243da3202"
},
{
"name": "v3-body",
"note": "same execution plus contextCommit \u2014 a different receiptHash",
"body": {
"schema": "trinote.receipt/v3",
"modelHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
"inputCommit": "0101010101010101010101010101010101010101010101010101010101010101",
"outputCommit": "0202020202020202020202020202020202020202020202020202020202020202",
"receiptBound": true,
"sigModelKeyId": "key:model-a",
"sigCounterpartyKeyId": "key:cp-b",
"contextCommit": "c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0"
},
"receiptHash": "70c3eb418571bd898327b433141e0251b03975dae47d0ba80975808a900589f6"
},
{
"name": "v3-body-hash-excludes-itself",
"note": "receiptHash recomputes identically when the field is already present",
"body": {
"schema": "trinote.receipt/v3",
"modelHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
"inputCommit": "0101010101010101010101010101010101010101010101010101010101010101",
"outputCommit": "0202020202020202020202020202020202020202020202020202020202020202",
"receiptBound": true,
"sigModelKeyId": "key:model-a",
"sigCounterpartyKeyId": "key:cp-b",
"contextCommit": "c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0",
"receiptHash": "70c3eb418571bd898327b433141e0251b03975dae47d0ba80975808a900589f6"
},
"receiptHash": "70c3eb418571bd898327b433141e0251b03975dae47d0ba80975808a900589f6"
}
]
}
Loading
Loading