diff --git a/bonsai/src/trinote/bundle/pack.py b/bonsai/src/trinote/bundle/pack.py index 3482f63..4317ca7 100644 --- a/bonsai/src/trinote/bundle/pack.py +++ b/bonsai/src/trinote/bundle/pack.py @@ -25,6 +25,11 @@ from ..receipts.canonical import canonical_bytes, commit from ..receipts.emit import chain_artifact +from .semantos_cell import ( + EVIDENCE_KIND as SEMANTOS_CELL_KIND, + EvidenceError, + validate_evidence, +) BUNDLE_SCHEMA = "trinote.receipt-bundle/v1" @@ -109,8 +114,23 @@ def pack_bundle( kind = "local" else: kind = onchain.get("kind") - if kind not in ("standalone", "stateful"): - raise BundleError(f"onchain.kind must be 'standalone', 'stateful', or omitted (local), got {kind!r}") + if kind not in ("standalone", "stateful", SEMANTOS_CELL_KIND): + raise BundleError( + f"onchain.kind must be 'standalone', 'stateful', '{SEMANTOS_CELL_KIND}', " + f"or omitted (local), got {kind!r}") + if kind == SEMANTOS_CELL_KIND: + # The third entry is a cell semantos published, not a mark this side wrote. + # Validate the evidence at pack time so a malformed record never reaches a + # bundle: a verifier that has to reject the whole bundle later cannot tell + # the operator which field was wrong, and by then the anchor has been paid for. + try: + validate_evidence(onchain) + except EvidenceError as exc: + raise BundleError(f"semantos-cell evidence is malformed ({exc})") from exc + if onchain["receiptHash"] != receipt["receiptHash"]: + raise BundleError( + "semantos-cell evidence references a different receipt " + f"({onchain['receiptHash']} != {receipt['receiptHash']})") if kind == "stateful": if not isinstance(identity, dict): raise BundleError("stateful bundle requires an 'identity' dict (ricardianHash, genesisTxid, pubkeys)") diff --git a/bonsai/src/trinote/bundle/semantos_cell.py b/bonsai/src/trinote/bundle/semantos_cell.py new file mode 100644 index 0000000..c5cf673 --- /dev/null +++ b/bonsai/src/trinote/bundle/semantos_cell.py @@ -0,0 +1,167 @@ +"""semantos_cell.py — the Trinote side of `onchain.kind = "semantos-cell"`. + +## Why a fourth bundle kind + +Both systems can write to chain. Left alone, an integrated request gets anchored twice: +semantos publishes a verified result cell, Trinote publishes a Third Entry, and the two +marks describe the same computation at two costs with two ways to disagree. + +So semantos publishes, and a Trinote bundle records evidence pointing at what it +published. The division of labour is deliberate and is the reason this module is small: + + * **semantos** proves the transaction is in a block — BEEF/SPV inclusion. + * **Trinote** proves the anchored cell describes *this receipt* — content binding. + +Neither check substitutes for the other. An included transaction that commits somebody +else's computation is not evidence about this one, and a perfectly bound cell that was +never mined is not evidence at all. This module does the second and refuses to pretend +about the first. + +## The state everyone conflates + +A dry run is not an anchor. An accepted broadcast is not a confirmed inclusion. In this +record the difference is carried by `inclusionProofRef`: null means submitted, a +reference means inclusion was proven and can be rechecked by someone else. It is a +separate field from `txid` on purpose — holding a transaction id says a broadcast +happened, which is a different claim from the transaction being in a block. + +## Cross-language agreement + +`evidence_commit` must equal `evidenceCommit()` in the semantos cartridge's +`anchor-evidence.ts`, byte for byte, or the two sides commit to different things while +believing they agree. Every field is therefore restricted to the value domain where +Python's `json.dumps(sort_keys=True, separators=(",",":"))` and RFC 8785 JCS provably +coincide: 64-char lowercase hex, bounded printable-ASCII, safe integers, and null. +""" + +from __future__ import annotations + +import re + +from ..receipts.canonical import canonical_bytes, commit + +EVIDENCE_KIND = "semantos-cell" + +FIELDS = ("kind", "txid", "vout", "cellHash", "typeHash", "receiptHash", + "modelBindingHash", "inclusionProofRef") + +HASH_FIELDS = ("txid", "cellHash", "typeHash", "receiptHash", "modelBindingHash") + +#: `inclusionProofRef` is an opaque locator, not a digest — bounded so it stays inside +#: the domain both canonical encoders agree on +MAX_PROOF_REF_CHARS = 256 +SAFE_INT = 2 ** 53 - 1 + +_HEX64 = re.compile(r"\A[0-9a-f]{64}\Z") +_ASCII = re.compile(r"\A[\x20-\x7e]*\Z") + + +class EvidenceError(ValueError): + """Malformed semantos-cell evidence. `code` is bounded and safe to record.""" + + def __init__(self, code: str, detail: str = "") -> None: + super().__init__(f"{code}: {detail}" if detail else code) + self.code = code + self.detail = detail + + +def validate_evidence(evidence) -> None: + """Refuse anything that is not this evidence form, before it is hashed or trusted. + + Validation happens before hashing rather than only on construction: a value outside + the shared domain would still hash, and the two languages' encoders disagree there. + A digest that differs by encoder is worse than a refusal, because both sides believe + they committed the same thing. + """ + # The order and the codes mirror `evidenceCommit()` in the semantos cartridge. Both + # sides must refuse the same values *for the same stated reason*, or a caller that + # branches on the code behaves differently depending on which implementation it + # happened to reach — and a reason code is part of the contract, not a log message. + if not isinstance(evidence, dict): + raise EvidenceError("bad-type", "evidence must be an object") + + for key in evidence: # unknown before missing + if key not in FIELDS: + raise EvidenceError("unknown-field", key) + for key in FIELDS: + if key not in evidence: + raise EvidenceError("missing-field", key) + + if evidence["kind"] != EVIDENCE_KIND: + raise EvidenceError("bad-kind", str(evidence["kind"])) + + for name in HASH_FIELDS: + value = evidence[name] + if not isinstance(value, str): + raise EvidenceError("bad-type", name) + if len(value) != 64: + raise EvidenceError("bad-length", f"{name} is {len(value)} chars, want 64") + if not _HEX64.match(value): + raise EvidenceError("bad-hex", name) + + vout = evidence["vout"] + if isinstance(vout, bool) or not isinstance(vout, int): + raise EvidenceError("bad-number", "vout") + if abs(vout) > SAFE_INT: + raise EvidenceError("bad-number", "vout outside safe-integer range") + if vout < 0: + raise EvidenceError("bad-number", "vout must be non-negative") + + ref = evidence["inclusionProofRef"] + if ref is not None: + if not isinstance(ref, str): + raise EvidenceError("bad-type", "inclusionProofRef") + if not _ASCII.match(ref): + raise EvidenceError("bad-charset", "inclusionProofRef") + if len(ref) == 0: + # an empty reference is not "no proof" — that is what null says + raise EvidenceError("bad-length", "inclusionProofRef must not be empty") + if len(ref) > MAX_PROOF_REF_CHARS: + raise EvidenceError("bad-length", f"inclusionProofRef is {len(ref)} chars") + + +def evidence_commit(evidence) -> str: + """The digest a Trinote bundle records for this evidence. Bare lowercase hex. + + Must equal `evidenceCommit()` in the semantos cartridge for the same object. + """ + validate_evidence(evidence) + return commit(evidence) + + +def is_confirmed(evidence) -> bool: + """Has inclusion actually been proven, as opposed to merely broadcast? + + Written down once here rather than re-derived at each call site, because "we have a + txid" reads like success and is not. + """ + validate_evidence(evidence) + return evidence["inclusionProofRef"] is not None + + +def evidence_binds_receipt(evidence, expected_receipt_hash: str, + expected_model_binding_hash: str | None = None) -> bool: + """Does this evidence describe the receipt that was replayed? + + The anchored cell must reference the same receipt — and, when the caller knows it, + the same model binding. `expected_model_binding_hash` is optional because a Trinote + verifier cannot derive it: it is a semantos record. Optional means "not checked + here", never "checked and passed", so a caller that has the value must pass it. + """ + validate_evidence(evidence) + if not isinstance(expected_receipt_hash, str) or not _HEX64.match(expected_receipt_hash): + raise EvidenceError("bad-hex", "expected_receipt_hash") + if evidence["receiptHash"] != expected_receipt_hash: + return False + if expected_model_binding_hash is not None: + if not _HEX64.match(expected_model_binding_hash or ""): + raise EvidenceError("bad-hex", "expected_model_binding_hash") + if evidence["modelBindingHash"] != expected_model_binding_hash: + return False + return True + + +def evidence_bytes(evidence) -> bytes: + """Canonical bytes of the evidence — exposed for cross-language vector checks.""" + validate_evidence(evidence) + return canonical_bytes(evidence) diff --git a/bonsai/src/trinote/bundle/verify.py b/bonsai/src/trinote/bundle/verify.py index eef5364..7eaf4aa 100644 --- a/bonsai/src/trinote/bundle/verify.py +++ b/bonsai/src/trinote/bundle/verify.py @@ -22,6 +22,13 @@ from ..receipts.emit import chain_artifact from ..hashing.sha import txid_of from .pack import BUNDLE_SCHEMA, BundleError +from .semantos_cell import ( + EVIDENCE_KIND as SEMANTOS_CELL_KIND, + EvidenceError, + evidence_binds_receipt, + is_confirmed, + validate_evidence, +) from .stateful import agent_action_receipt_hash from . import chain_read @@ -178,9 +185,11 @@ def _verify_offline(loaded: dict) -> dict: else: onchain = obj.get("onchain.json") or {} kind = onchain.get("kind") - _check(checks, "onchain.kind", kind in ("standalone", "stateful"), str(kind)) + _check(checks, "onchain.kind", kind in ("standalone", "stateful", SEMANTOS_CELL_KIND), str(kind)) - if kind == "standalone": + if kind == SEMANTOS_CELL_KIND: + _verify_semantos_cell_offline(checks, receipt, onchain, rh) + elif kind == "standalone": _check(checks, "standalone.modelHash", onchain.get("modelHash") == receipt.get("modelHash"), onchain.get("modelHash", "")) _check(checks, "standalone.receiptHash", onchain.get("receiptHash") == rh, onchain.get("receiptHash", "")) @@ -227,6 +236,44 @@ def _verify_offline(loaded: dict) -> dict: return {"ok": ok, "checks": checks} +def _verify_semantos_cell_offline(checks: list, receipt: dict, onchain: dict, rh: str) -> None: + """Bind a semantos-published result cell to this receipt, offline. + + This is the whole of Trinote's half. semantos proves the transaction is in a block; + what it cannot prove on Trinote's behalf is that the cell describes *this* + computation, because that is a statement about the receipt sitting next to it in + the bundle. + + `modelBindingHash` is checked for shape and committed, but not recomputed — it is a + semantos record and this side has nothing to derive it from. Recording it as + verified would be a claim nobody made. + """ + try: + validate_evidence(onchain) + except EvidenceError as exc: + _check(checks, "semantosCell.wellFormed", False, str(exc)) + return + _check(checks, "semantosCell.wellFormed", True, "") + + _check(checks, "semantosCell.receiptHash", + evidence_binds_receipt(onchain, rh), onchain.get("receiptHash", "")) + + # The evidence commits the model binding it was anchored against; a bundle whose + # receipt carries a different modelHash than the cell claims would bind two + # computations together by proximity alone. + _check(checks, "semantosCell.modelHashPresent", + isinstance(receipt.get("modelHash"), str) and bool(receipt.get("modelHash")), + "receipt.modelHash") + + # Not a failure — a distinction. Submitted evidence is honest evidence; it is simply + # not yet inclusion, and a verifier that reported the two identically would be the + # reason someone treats a broadcast as settled. + confirmed = is_confirmed(onchain) + _check(checks, "semantosCell.anchorState", True, + "confirmed (inclusion proof present)" if confirmed + else "submitted (broadcast, inclusion NOT yet proven)") + + def _verify_stateful_offline(checks: list, receipt: dict, onchain: dict, identity: dict) -> None: """Recompute the AgentTea action hash from the bundle's committed fields and bind it to the receipt.""" action = onchain.get("action") or {} @@ -260,6 +307,39 @@ def _verify_stateful_offline(checks: list, receipt: dict, onchain: dict, identit f"{recomputed} vs {onchain.get('receiptHashOnChain')}") +def _verify_semantos_cell_onchain(checks: list, onchain: dict, network: str) -> None: + """What Trinote can honestly check on chain about a cell it did not publish. + + Not inclusion. Inclusion is a merkle proof against a block header, semantos holds + the BEEF, and this side has no business claiming to have checked it — an auditor who + read `ok: True` here and assumed otherwise would be wrong in the one way that + matters. + + What *is* checkable, and worth checking, is that the transaction the evidence names + exists and really has an output at the stated index. That is what fails when a txid + is invented, which is the cheap forgery this closes; the expensive one is semantos's + to catch. + """ + txid = onchain.get("txid") + try: + raw = chain_read.fetch_raw_tx(txid, network) + parsed = chain_read.parse_tx(raw) + except (chain_read.ChainReadError, ValueError, KeyError, IndexError, TypeError) as exc: + _check(checks, "semantosCell.txFound", False, f"{txid}: {exc}") + return + _check(checks, "semantosCell.txFound", True, str(txid)) + + outputs = parsed.get("outputs") or [] + vout = onchain.get("vout") + _check(checks, "semantosCell.voutExists", + isinstance(vout, int) and 0 <= vout < len(outputs), + f"vout {vout} of {len(outputs)} outputs") + + # Say plainly what was not done, so nobody reads this result as more than it is. + _check(checks, "semantosCell.inclusionCheckedBy", True, + "semantos (BEEF/SPV); Trinote verifies content binding only") + + def _verify_onchain(loaded: dict, network: str) -> dict: checks: list[dict] = [] onchain = loaded["obj"].get("onchain.json") or {} @@ -311,6 +391,8 @@ def _verify_onchain(loaded: dict, network: str) -> dict: walk = chain_read.walk_identity_to_genesis(txid, genesis, net) _check(checks, "onchain.chainToGenesis", walk.get("ok"), f"{walk.get('hops')} hops; {walk.get('reason', 'reached genesis')}") + elif kind == SEMANTOS_CELL_KIND: + _verify_semantos_cell_onchain(checks, onchain, net) else: _check(checks, "onchain.kind", False, f"unknown kind {kind!r}") except chain_read.ChainReadError as exc: diff --git a/bonsai/tests/test_semantos_cell_bundle.py b/bonsai/tests/test_semantos_cell_bundle.py new file mode 100644 index 0000000..266658f --- /dev/null +++ b/bonsai/tests/test_semantos_cell_bundle.py @@ -0,0 +1,166 @@ +"""`onchain.kind = "semantos-cell"` — Trinote's half of a semantos-published anchor. + +Two things are being tested, and only one of them is about this file's code. + +The first is ordinary: a bundle can carry evidence of a cell semantos published, the +verifier binds it to the receipt sitting beside it, and a substituted receipt is caught. + +The second is the cross-language contract. `evidence_commit` here and `evidenceCommit()` +in the semantos cartridge must produce identical digests for identical objects and +refuse identical inputs *with the same reason code*. The vectors in +`vectors/semantos-cell.vectors.json` were generated by running the TypeScript, so a +passing run means the two implementations agree — not that this one is self-consistent. +""" +from __future__ import annotations + +import json +import pathlib + +import pytest + +from trinote.bundle.pack import pack_bundle, BundleError +from trinote.bundle.semantos_cell import ( + EVIDENCE_KIND, + EvidenceError, + evidence_binds_receipt, + evidence_commit, + evidence_bytes, + is_confirmed, + validate_evidence, +) +from trinote.bundle.verify import verify_bundle +from trinote.receipts.receipt import build_receipt +from trinote.receipts.signing_ec import ec_keygen + +VECTORS = json.loads( + (pathlib.Path(__file__).parent / "vectors" / "semantos-cell.vectors.json").read_text() +) + +H = lambda b: b * 64 # noqa: E731 + + +def _evidence(receipt_hash: str, **over): + e = {"kind": EVIDENCE_KIND, "txid": H("1"), "vout": 0, "cellHash": H("2"), + "typeHash": H("3"), "receiptHash": receipt_hash, "modelBindingHash": H("5"), + "inclusionProofRef": None} + e.update(over) + return e + + +def _bundle(): + model, cp = ec_keygen(label="m"), ec_keygen(label="c") + return build_receipt(model_hash=H("a"), input_ids=[1, 2], output_ids=[3], + sampler={"mode": "greedy"}, model_key=model, counterparty_key=cp, + schema_version="v3", context_commit=H("c")) + + +# ------------------------------------------------ the contract with the other language + +@pytest.mark.parametrize("case", VECTORS["accept"], ids=lambda c: c["name"]) +def test_accepted_evidence_commits_to_what_typescript_committed(case): + assert evidence_commit(case["evidence"]) == case["evidenceCommit"] + + +@pytest.mark.parametrize("case", VECTORS["accept"], ids=lambda c: c["name"]) +def test_the_canonical_bytes_are_identical_not_merely_the_digest(case): + """A matching digest with different bytes would mean a collision, so this is the + check that actually says the encoders agree.""" + assert evidence_bytes(case["evidence"]).decode() == case["canonical"] + + +@pytest.mark.parametrize("case", VECTORS["reject"], ids=lambda c: c["name"]) +def test_refusals_agree_on_the_reason_not_only_on_refusing(case): + with pytest.raises(EvidenceError) as exc: + evidence_commit(case["evidence"]) + assert exc.value.code == case["reason"] + + +def test_the_vector_file_covers_both_outcomes(): + # a suite that lost its reject half would still pass every test above + assert len(VECTORS["accept"]) >= 5 and len(VECTORS["reject"]) >= 8 + + +# ------------------------------------------------------------- broadcast vs inclusion + +def test_a_broadcast_is_not_an_inclusion(): + submitted = _evidence(H("4")) + confirmed = _evidence(H("4"), inclusionProofRef="beef:0001") + assert is_confirmed(submitted) is False + assert is_confirmed(confirmed) is True + + +def test_an_empty_proof_reference_is_not_a_second_spelling_of_absent(): + with pytest.raises(EvidenceError) as exc: + validate_evidence(_evidence(H("4"), inclusionProofRef="")) + assert exc.value.code == "bad-length" + + +# ------------------------------------------------------------------ binding a receipt + +def test_evidence_binds_the_receipt_it_names(): + e = _evidence(H("4")) + assert evidence_binds_receipt(e, H("4")) is True + assert evidence_binds_receipt(e, H("9")) is False + + +def test_the_model_binding_is_checked_when_the_caller_knows_it(): + e = _evidence(H("4"), modelBindingHash=H("5")) + assert evidence_binds_receipt(e, H("4"), H("5")) is True + assert evidence_binds_receipt(e, H("4"), H("7")) is False + + +# ------------------------------------------------------------------- through a bundle + +def test_a_bundle_carries_the_evidence_and_verifies_offline(tmp_path): + b = _bundle() + rh = b["receipt"]["receiptHash"] + info = pack_bundle(bundle=b, onchain=_evidence(rh), out_dir=tmp_path / "bundle") + assert info["manifest"]["kind"] == EVIDENCE_KIND + + res = verify_bundle(tmp_path / "bundle") + named = {c["check"]: c for c in res["offline"]["checks"]} + assert named["semantosCell.wellFormed"]["ok"] is True + assert named["semantosCell.receiptHash"]["ok"] is True + assert "submitted" in named["semantosCell.anchorState"]["detail"] + + +def test_a_confirmed_bundle_says_so(tmp_path): + b = _bundle() + rh = b["receipt"]["receiptHash"] + pack_bundle(bundle=b, onchain=_evidence(rh, inclusionProofRef="beef:0001"), + out_dir=tmp_path / "bundle") + res = verify_bundle(tmp_path / "bundle") + named = {c["check"]: c for c in res["offline"]["checks"]} + assert "confirmed" in named["semantosCell.anchorState"]["detail"] + + +def test_evidence_for_a_different_receipt_is_refused_at_pack_time(tmp_path): + """Caught here rather than at verify: by the time a verifier rejects the bundle the + anchor has been paid for, and the rejection cannot say which field was wrong.""" + b = _bundle() + with pytest.raises(BundleError, match="different receipt"): + pack_bundle(bundle=b, onchain=_evidence(H("9")), out_dir=tmp_path / "bundle") + + +def test_malformed_evidence_never_reaches_a_bundle(tmp_path): + b = _bundle() + bad = _evidence(b["receipt"]["receiptHash"]) + bad["vout"] = -1 + with pytest.raises(BundleError, match="malformed"): + pack_bundle(bundle=b, onchain=bad, out_dir=tmp_path / "bundle") + + +def test_a_substituted_receipt_breaks_the_binding_at_verify(tmp_path): + """The bundle's own hash checks catch the swap; this asserts the semantos-cell + binding is not somehow satisfied by a receipt it does not name.""" + b = _bundle() + rh = b["receipt"]["receiptHash"] + root = tmp_path / "bundle" + pack_bundle(bundle=b, onchain=_evidence(rh), out_dir=root) + + other = _bundle() + (root / "receipt.json").write_bytes( + json.dumps(other["receipt"], sort_keys=True, separators=(",", ":")).encode()) + + res = verify_bundle(root) + assert res["ok"] is False diff --git a/bonsai/tests/vectors/semantos-cell.vectors.json b/bonsai/tests/vectors/semantos-cell.vectors.json new file mode 100644 index 0000000..c93d56a --- /dev/null +++ b/bonsai/tests/vectors/semantos-cell.vectors.json @@ -0,0 +1,279 @@ +{ + "accept": [ + { + "name": "submitted-no-proof", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 0, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null + }, + "evidenceCommit": "bbbe1bf3c626f4e858c4e50a1f0b63f98c8bd15e85009ecb17b238aa0291f324", + "canonical": "{\"cellHash\":\"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2\",\"inclusionProofRef\":null,\"kind\":\"semantos-cell\",\"modelBindingHash\":\"e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5\",\"receiptHash\":\"d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4\",\"txid\":\"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1\",\"typeHash\":\"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3\",\"vout\":0}" + }, + { + "name": "confirmed-with-proof", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 0, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": "beef:0001" + }, + "evidenceCommit": "76df6631ea0b98f07cac708e68b3727253a28d538e87a826ab25abad289a0ead", + "canonical": "{\"cellHash\":\"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2\",\"inclusionProofRef\":\"beef:0001\",\"kind\":\"semantos-cell\",\"modelBindingHash\":\"e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5\",\"receiptHash\":\"d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4\",\"txid\":\"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1\",\"typeHash\":\"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3\",\"vout\":0}" + }, + { + "name": "vout-nonzero", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 7, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null + }, + "evidenceCommit": "c50bbaef1833ccf60ab3983b62e42508585cc58135aebde27b3a5db463fdac5c", + "canonical": "{\"cellHash\":\"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2\",\"inclusionProofRef\":null,\"kind\":\"semantos-cell\",\"modelBindingHash\":\"e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5\",\"receiptHash\":\"d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4\",\"txid\":\"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1\",\"typeHash\":\"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3\",\"vout\":7}" + }, + { + "name": "vout-large-safe-int", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 9007199254740991, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null + }, + "evidenceCommit": "ef6ddcb2ce26ea23d2c1cebdd03c88e46c50d22ff748c392d886c855cf8ca622", + "canonical": "{\"cellHash\":\"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2\",\"inclusionProofRef\":null,\"kind\":\"semantos-cell\",\"modelBindingHash\":\"e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5\",\"receiptHash\":\"d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4\",\"txid\":\"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1\",\"typeHash\":\"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3\",\"vout\":9007199254740991}" + }, + { + "name": "proof-ref-printable-ascii", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 0, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": "merkle path #3 (~!@#$%^&*)" + }, + "evidenceCommit": "4a0f455e6786b16db6f3f6402c1f52f20499d4624de337e8e2ab5193f686324a", + "canonical": "{\"cellHash\":\"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2\",\"inclusionProofRef\":\"merkle path #3 (~!@#$%^&*)\",\"kind\":\"semantos-cell\",\"modelBindingHash\":\"e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5\",\"receiptHash\":\"d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4\",\"txid\":\"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1\",\"typeHash\":\"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3\",\"vout\":0}" + }, + { + "name": "all-zero-hashes", + "evidence": { + "kind": "semantos-cell", + "txid": "0000000000000000000000000000000000000000000000000000000000000000", + "vout": 0, + "cellHash": "0000000000000000000000000000000000000000000000000000000000000000", + "typeHash": "0000000000000000000000000000000000000000000000000000000000000000", + "receiptHash": "0000000000000000000000000000000000000000000000000000000000000000", + "modelBindingHash": "0000000000000000000000000000000000000000000000000000000000000000", + "inclusionProofRef": null + }, + "evidenceCommit": "fad4249b50b0c4e92b390d1e12deca061646cc7c977c0bf90224db0fe9db2e8e", + "canonical": "{\"cellHash\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"inclusionProofRef\":null,\"kind\":\"semantos-cell\",\"modelBindingHash\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"receiptHash\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"txid\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"typeHash\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"vout\":0}" + }, + { + "name": "all-f-hashes", + "evidence": { + "kind": "semantos-cell", + "txid": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "vout": 0, + "cellHash": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "typeHash": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "receiptHash": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "modelBindingHash": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "inclusionProofRef": null + }, + "evidenceCommit": "d3de827792387902e2d80795ae17347fd78b019d0450824277c419d66a8eb8bb", + "canonical": "{\"cellHash\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"inclusionProofRef\":null,\"kind\":\"semantos-cell\",\"modelBindingHash\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"receiptHash\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"txid\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"typeHash\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"vout\":0}" + } + ], + "reject": [ + { + "name": "missing-field", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 0, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null + }, + "reason": "missing-field" + }, + { + "name": "extra-field", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 0, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null, + "extra": "9999999999999999999999999999999999999999999999999999999999999999" + }, + "reason": "unknown-field" + }, + { + "name": "wrong-kind", + "evidence": { + "kind": "stateful", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 0, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null + }, + "reason": "bad-kind" + }, + { + "name": "uppercase-hex", + "evidence": { + "kind": "semantos-cell", + "txid": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "vout": 0, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null + }, + "reason": "bad-hex" + }, + { + "name": "short-hex", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 0, + "cellHash": "ab", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null + }, + "reason": "bad-length" + }, + { + "name": "negative-vout", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": -1, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null + }, + "reason": "bad-number" + }, + { + "name": "float-vout", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 1.5, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null + }, + "reason": "bad-number" + }, + { + "name": "non-ascii-proof-ref", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 0, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": "proofé" + }, + "reason": "bad-charset" + }, + { + "name": "empty-proof-ref", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 0, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": "" + }, + "reason": "bad-length" + }, + { + "name": "overlong-proof-ref", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 0, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + "reason": "bad-length" + }, + { + "name": "vout-beyond-safe-int", + "evidence": { + "kind": "semantos-cell", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "vout": 9007199254740992, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null + }, + "reason": "bad-number" + }, + { + "name": "null-hash", + "evidence": { + "kind": "semantos-cell", + "txid": null, + "vout": 0, + "cellHash": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "typeHash": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", + "receiptHash": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", + "modelBindingHash": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", + "inclusionProofRef": null + }, + "reason": "bad-type" + } + ] +}