A trimmed (~1.7 KB) copy of the cross-language parity vectors now ships inside
each package (Python package-data JSON, Go go:embed, TS generated module). On
the first hashing operation per process each SDK recomputes the commitment
hash, the receipt domain-hash, and an inclusion fold against the vendored
vectors and fails closed (AttestoSelfTestError / ErrSelfTest) on any mismatch
— a corrupted install or diverging runtime can never silently produce wrong
evidence. Result is cached (including failure); cost <5 ms once. Corrupting a
vendored vector is test-asserted to fail closed in all three languages. The
frozen canonical primitives are untouched; the gate lives in the commitment/
verify entry points built on top of them.
attesto doctor: Go CLI subcommand and Python attesto.doctor(), producing a
deterministic {"ok", "checks"} report — vendored self-test, head-store
writability, number-policy dry-run on a sample payload, Ed25519 availability
(Python), and with credentials: reachability, protocol-header acceptance, and
clock skew vs the server Date header (warn >30 s; webhooks break at 300 s).
package_artifact_policy allows exactly attesto/_selftest_vectors.json in the
wheel (verified: built wheel contains it, policy green). READMEs updated.
This completes the last Phase-1 build item.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All three SDKs now send X-Attesto-Sdk (attesto-<lang>/<version>) and
X-Attesto-Protocol (ATTESTO-PROOFSTREAM-001/0.1-alpha) on every request. A new
backend ProtocolVersionMiddleware logs both headers (operators can see the
SDK/protocol mix in traffic) and, when the protocol header is present on a /v2
request and names a different protocol identifier or major version, answers
426 Upgrade Required with a structured body (error/supported/received/hint).
Absent or unparseable headers change nothing — old clients and curl stay fully
compatible (test-asserted, including /v1 never being handshake-gated).
SDKs surface the 426 as a typed error: Python AttestoProtocolMismatch,
TypeScript AttestoProtocolMismatch, Go IsProtocolMismatch(err) over *APIError
(Go-idiomatic). Tests cover the mismatch rules, the 426 mapping, and that the
handshake headers are actually sent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each limit/offset list method gains an iterator twin that walks pages
transparently and stops on the first short page — same endpoints, no new API
surface: Python generators (iter_tenant_streams / _stream_events / _windows /
_checkpoints / iter_fork_evidence / iter_tenant_ivc_epochs), TypeScript async
iterators (for await ... of client.iterTenantStreamEvents(...)), and a Go
Iterator with Next(ctx) returning (nil, nil) at exhaustion, plus Iter* twins
on the client. Tests drain a 3-page mocked response set in order and confirm
a short first page ends iteration after exactly one request. READMEs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
verify_anchor_onchain / verifyAnchorOnchain / VerifyAnchorOnchain check an
anchor epoch against the chain itself in all three SDKs: one raw JSON-RPC
eth_call to the anchoring contract's getCommitment(batchId) comparing the
on-chain merkle root with the anchor's merkle_root, plus one
eth_getTransactionReceipt confirming status == 0x1 in the expected block.
The customer chooses the RPC endpoint — nothing asks Attesto to confirm
Attesto, and no web3/ethers dependency is added anywhere.
The getCommitment(string) selector (keccak256 first 4 bytes = a7b09e2a) is
pinned as a constant with the dynamic-string ABI encoding done manually;
a worked calldata example (computed once against web3 keccak) is asserted in
all three test suites, and APSProvenance.abi.json is copied into each SDK's
testdata with a test that flags the pinned selector for review if the ABI's
getCommitment signature ever changes. The contract address is read from the
anchor epoch's hashed payload (payload.contract_address).
Mocked-RPC tests cover match / root-mismatch / failed-tx / wrong-block /
missing-fields in each language with identical problem strings; a live test
against the production contract runs only when ATTESTO_LIVE_RPC_URL is set.
Go CLI gains `attesto anchors verify <id> --rpc-url <url>` (API fetch +
on-chain check in one step; existing get/remote-verify behavior unchanged).
READMEs updated per SDK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the verification chain (P1.2 -> P1.1 -> P1.3 -> P1.6). The client
remembers the last accepted (seq_no, event_hash) per stream and checks every
new receipt links forward; if the server rewinds a sequence number or presents
a divergent lineage, log_event / log_events raise AttestoForkDetected (Go:
*ForkDetectedError) and the stored head is NOT advanced. The customer's own
machine becomes the fork detector — no trust in any Attesto-side check.
- Python: HeadStore protocol + FileHeadStore (~/.attesto/heads.json, atomic,
0600, default) + MemoryHeadStore; wired into sync and async v2 clients;
head_store=None disables.
- TypeScript: HeadStore + MemoryHeadStore (default, edge-safe); Node-only
FileHeadStore kept in a separate module (@attesto/sdk/heads-file) so the core
bundle imports no node:fs; headStore: null disables.
- Go: HeadStore interface + MemoryHeadStore (default) + NewFileHeadStore;
WithHeadStore option; WithHeadStore(nil) disables.
Same forward/rewind/divergence/gap semantics across all three (unit-tested:
in-order advance, forged-rewind fork, divergent-next fork, forward-gap accept,
file-store restart persistence). Existing v2 client tests pin head_store=None
(they replay overlapping seq). READMEs gain a "Your SDK is a witness" section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the offline verification stack (P1.2 -> P1.1 -> P1.3) in all three
SDKs, each a faithful port of the backend windows.py / checkpoints.py math on
top of the frozen canonical/domain-hash primitives:
- verify_inclusion_proof: fold a window inclusion proof to the window root
(domain attesto.v2.window; left sibling -> node(sibling,current), right ->
node(current,sibling)).
- verify_checkpoint_root: recompute a checkpoint root from window hashes
(domain attesto.v2.checkpoint), with an odd node at any level **promoted
unchanged** rather than duplicated/hashed with itself (the place a naive
Merkle port silently diverges).
- verify_checkpoint_extension: current.from_seq_no == previous.to_seq_no + 1
and current.previous_checkpoint_hash == previous.checkpoint_hash.
- verify_completeness: proves no events were omitted in a range -- gap-free
seq_no coverage plus prev_event_hash chaining to the previous event_hash.
New corpus golden-vectors/sdk-parity/inclusion.json (5-leaf window exercising
the promoted odd node, 3-window checkpoint root, extension + completeness
negatives), exported from the backend functions. Proven: Python = TypeScript =
Go = backend agree on every case. READMEs updated per SDK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the trust-model gap where Python/TS could only verify receipts by
calling the server (asking the party being distrusted). Both now verify
entirely client-side, mirroring the Go SDK's VerifyReceiptOffline one-to-one
with identical problem strings so reports are comparable cross-language.
- Python: new attesto.verify.verify_receipt + frozen VerifyReport dataclass,
using cryptography>=42 (new dependency; not PyNaCl) for Ed25519.
- TypeScript: verifyReceipt via WebCrypto subtle.verify({name:"Ed25519"}),
throwing a clear AttestoError on runtimes without Ed25519 (Node < 20) rather
than silently falling back to the server.
Both recompute domain_hash("attesto.v2.receipt", payload) and verify the
signature over domain + 0x00 + canonical_json_bytes(payload), reusing the
frozen canonical functions.
New corpus golden-vectors/sdk-parity/receipts.json (valid + payload/hash/
signature/wrong-key negatives). Proven: all five cases agree across Go,
Python, and TypeScript. READMEs document the offline function and note the
existing client.verify_receipt as the server-assisted variant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds payload_commitment / metadata_commitment / verify_payload_commitment
and assert_commitment_safe_numbers to the Python, TypeScript, and Go SDKs,
each building on the frozen canonical_json/domain_hash primitives (no change
to their byte output). The number preflight is a byte-for-byte port of the
backend assert_commitment_safe_numbers (floats rejected, |int| > 2^53-1
rejected, bool exempt) and is wired into the v2 log_event / log_events send
path, raising a typed AttestoUnsafeNumberError with the JSON path so the rule
fails at dev time rather than as a production 422; preflight=False /
SkipPreflight defers to the server.
New shared corpus golden-vectors/sdk-parity/canonical-numbers.json (15 accept
+ 8 reject), accept-hashes generated from the backend _commitment. Proven:
Python = TypeScript = Go = backend produce byte-identical commitment hashes
for every accept vector and identical reject paths (the Go float64-vs-Python-
int serialization parity holds). READMEs updated per SDK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FIX 11 — reject cross-language-divergent numbers in committed payloads.
append_stream_event now rejects non-integer numbers and integers beyond
±(2^53−1) in payload/metadata at ingestion (HTTP 422), so a customer
recomputing a commitment in a Go/TS verifier can never read it as
tampering. Documented in the protocol spec + all three SDK READMEs; 9
tests.
FIX 12 — Nova IVC chain-continuity invariant. record_ivc_epoch now takes
a per-stream transaction-scoped advisory lock and fails closed (409) if
an epoch does not extend the latest verified epoch's next_state_root or
would skip an unproven checkpoint — so two concurrent provers cannot fork
Attesto's own lane and a failed proof cannot be chained over. The worker
stops the pass on a failed proof (break, not continue) and always
backfills the oldest unproven checkpoint first so holes heal. Tests cover
both 409 paths plus in-order record + replay.
FIX 13 — pin the e2e v1 checkpoint shape. Config now requires
PROOFSTREAM_WINDOW_MAX_EVENTS=1 (alongside CHECKPOINT_MAX_WINDOWS=4) when
Nova is enabled, and the worker refuses to close an aged checkpoint below
the 4-window shape (which would be unprovable). Documented as the
deliberate fail-closed v1 behavior; config + worker tests.
VERIFY-1 — investigation: the standard production ingestion path
(SDK → append_stream_event → persist_stream_event_receipt) does NOT write
the nova_e2e boundary metadata the prover requires; no writer exists in
backend/app, and _e2e_vector_for_checkpoint requires (not derives) it. So
Nova proofs currently cover golden-vector/harness inputs, not live
customer receipts — the open Route A/B item. Recorded in
CRYPTO_REVIEW_CHECKLIST as a coverage-scope note; no Route A/B
implementation in this release.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>