Option C. A verifier bundle over a provenance stream carries provenance_root
(Merkle over one leaf per event: seq_no, capsule_root, installation, key,
assurance, occurred_at, under attesto.provenance.v1.bundle_tree), the event
count and vault_key_lifecycle, all conditional so legacy bundle hashes are
unchanged. Rust is normative (edge/src/bundle_tree.rs, nine golden vectors);
Python, Go and TypeScript verify an inclusion and apply the frozen revocation
rule against the receipt time offline. The inclusion endpoint in router.py
lands with the next commit, which carries the shared router edits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Twelve of twenty-five provenance vectors were consumed by no SDK. From outside
the repository that looked exactly like full coverage, which is the problem: a
corpus proves nothing about an implementation that never loads it.
Vectors now declare what they require. `sha256` vectors use SHA-256 and
canonical JSON, which all three SDKs have, so an unconsumed one is a gap and
fails a contract. `ristretto255` vectors need curve scalar arithmetic no SDK
carries; those are a declared boundary with a stated reason rather than a silent
skip, so the exemption cannot spread by habit.
Coverage went from 13/17 reachable in each SDK to 17/17. Closing the four gaps
surfaced a real verifier weakness: `capsule_root` receives digests, so by then a
role is no longer visible, and a tree carrying `evidence_root` twice with
`vault_identity_commitment` missing folds to a root all three SDKs accepted.
Each gains `ordered_top_leaf_digests`, which requires each of the six roles
exactly once, and the safe path is now the easy one.
Two findings of my own drift:
* The Go corpus-typing test accepted only `valid` and `invalid`, so it had been
failing since the Sprint 1 recovery added vectors carrying `differs` and
`rejected`. I updated Python's typing test then and not Go's, and no gate
caught it because the SDK parity suites are not in the sprint gates. Fixed,
and both Go and TypeScript now also require the capability declaration.
* TypeScript's strict indexing caught that a missing randomizer would have
reached the hash as the string "undefined". Both halves are now checked.
Python 88, Go ok, TypeScript 107, Local Vault 375, edge 116, backend 1404.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sprint 1 recovery, first item. §5.2 of REVIEW-02 adds three domains the registry
did not carry: attesto.disclosure.v2, attesto.zk.range.v1.statement and
attesto.zk.range.v1.transcript.
They are not in the attesto.provenance.v1. namespace, and that is deliberate:
disclosure v2 and the ZK range protocol are separate protocols with their own
versions, so a preimage space is named after the protocol that owns it rather
than the one it happens to travel with.
That namespace difference meant the parity contract could not see them at all —
its pattern matched attesto.provenance.v1.* only, so three new domains would have
been silently unguarded. The pattern now names each protocol explicitly rather
than loosening to a prefix wildcard: a looser first attempt also matched prose
that mentions a namespace without a terminal segment and reported it as an
unknown domain.
All four registry locations updated together with the golden vector, and all
three SDK parity suites plus the contract are green. The Go failure message was
also corrected: it printed "rust=21 go=21" while failing on a third hardcoded
expectation it never named.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Establishes the single normative cryptographic authority for the provenance
lane, and freezes the boundary and Merkle semantics before any ingestion path
exists to depend on them.
New crate edge/ (attesto-edge)
- domains.rs — the closed 18-domain v1 registry. Unknown domains are errors,
never a fallback: a generic attesto.provenance.v1.commitment would let two
unrelated objects share a preimage space, which is what domain separation
exists to prevent.
- canonical.rs — conforming ATTESTO-CANONICAL-JSON-001, not a second
serializer. Floats and integers past 2^53-1 are refused with their JSON path.
- commitment.rs — randomized, domain-separated commitments. Legacy Proofstream
commitments stay deterministic; provenance values are low-entropy, so
claim_type = "c2pa_manifest_valid" hashed deterministically is a dictionary
lookup and a deterministic asset digest links a file across events. Debug for
Randomizer prints <redacted>: it is C1 and Debug output reaches logs.
- merkle.rs — the two-level capsule forest. A claim leaf cannot verify against
evidence_root on two independent grounds: subtrees fold under different node
domains, and the top leaf binds leaf_role. Odd nodes are promoted, never
duplicated, matching the rule inclusion.json already pins for Proofstream.
- boundary.rs — derives nothing. It shapes a request for attesto-nova, reusing
the existing event-payload 16 KiB size class so Nova's closed
boundary_max_len() allowlist needs no new entry. On-wire artifact is N10.R
redacted.
- main.rs — NDJSON surface (handshake, canonicalize, commit, capsule-root,
boundary-derive, self-test), the transport the backend already speaks.
Poseidon is deliberately absent. It stays in proofs/nova, reached through that
crate's public boundary API, so there remains exactly one Poseidon authority.
The only Nova change is making CIRCUIT_ID and PROVER_VERSION pub so the edge
handshake can report the prover it wraps; its 40 tests are unchanged.
Test-only randomizers are gated behind the `test-vectors` cargo feature and
compiled out of release builds. A caller who can choose the randomizer can make
production commitments deterministic — that is not a debug convenience, it is
the vulnerability. A release build refuses one and reports
accepts_caller_randomizers: false in its handshake.
Conformance
- golden-vectors/provenance-v0.1-dev/ — 14 Rust-authored vectors, 9 valid and 5
invalid. CI regenerates them and requires git diff --exit-code, so the
committed corpus cannot drift from what the normative core produces.
- Python (sdk/python/src/attesto/provenance.py, 19 tests) and Go
(sdk/go/provenance.go, 14 tests) reproduce every valid vector and refuse every
invalid one. Both reuse their existing canonical-JSON primitives rather than
forking a second implementation.
- provenance_domain_registry_contract.py pins Rust = spec = Python = Go =
vector, and that no registry declares the forbidden fallback. It reads each
declaration block rather than whole files, so the negative test cases that
must name the fallback do not trip it.
TypeScript parity is still owed and Sprint 1's DoD is not fully closed: the
sdk/typescript build break recorded in the Sprint 0 baseline makes its whole
suite unrunnable.
Also fixes a Sprint 0 guard found by the guard itself: the naming lint scanned
only tracked files, so new work read green until it was committed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>