State commitment addendum

The daily anchored state commitment: a sparse Merkle tree over the registry's per-domain state, root signed and anchored every midnight UTC. Publicly verifiable proofs of membership and absence for any specific domain — without the registry ever publishing its contents.

Document status: Draft v0.2, 2026-07-10. Addendum to the technical specification. Implemented in the registry service as of v0.2: the SMT construction, the daily commitment job and proof generation, the API surface (GET /v1/public/state-roots/*, GET /v1/state/proof), and the per-lab attestation watermarking. The generator/verifier round-trip is covered by the registry's test suite (populations 0–32, tamper rejections, dry-run anchoring, entitlement gating).

Audience: Technical evaluator at a lab; the engineer implementing the registry's anchoring pipeline; anyone auditing the registry's publication policy.

Scope: Specifies the registry's daily anchored state commitment — a sparse Merkle tree over the registry's per-domain opt-out state, with its root signed and Arweave-anchored once per day — and the membership / non-membership proofs served against it. Replaces the previously proposed daily snapshot manifest. Upgrades the ephemeral no_optouts_attestation with an anchored non-inclusion proof.


1. Why this document exists

The lab integration runbook previously answered "Can I bulk-export the entire registry?" with a promise: a daily snapshot manifest, an Arweave-anchored list of all anchored batches as of midnight UTC, published free at v1 launch. That promise had a flaw the registry's own economics can't survive: an offline artifact that enumerates the registry's contents is the registry's dataset, handed to any competitor nightly. The audit function it was meant to serve — "prove to me what the registry's state was at time T" — does not require enumeration. It requires a commitment.

This addendum replaces the snapshot manifest with a daily anchored state commitment: a sparse Merkle tree (SMT) over the registry's entire per-domain opt-out state, keyed by SHA-256(domain), whose root is signed by the registry and anchored to Arweave once per day. Against that anchored root, the registry can serve two kinds of proof for any specific domain:

  • Membership proof — this domain had this exact opt-out state as of the epoch, provable against the anchored root.
  • Non-membership proof — this domain had no opt-out state as of the epoch, provable against the same anchored root.

This is strictly stronger than the snapshot manifest for auditing (the proof is per-domain, cryptographically bound to a public timestamp, and does not depend on trusting the registry at proof time), and it publishes nothing enumerable. It also repairs a weakness the runbook has been honest about since its first draft: the no_optouts_attestation (runbook §3.3) is an ephemeral registry signature that a colluding registry could re-sign after the fact. An anchored non-inclusion proof cannot be re-signed into the past — the root it verifies against was on Arweave before the question was asked.

One property does all the work here, and it is the same property the registry is built on: nobody can anchor in the past. A commitment anchored on day N is evidence about day N forever. That is what makes the proofs in this document audit-grade, and it is also why the registry's accumulated history — every verification performed, every root anchored — is not reproducible by any later-arriving party, including a party that somehow obtained the full domain list.


2. Publication policy: commitments are public, contents are not

This addendum makes the registry's publication boundary explicit. It was implicit — and in one place violated — in the earlier drafts.

Public, forever:

  • Batch Merkle roots and batch metadata (already anchored per spec §5.6).
  • The daily state commitment root and its signed canonical payload (§4).
  • The registry's signing-key catalog (already anchored per spec §9.2).
  • Any individual record a holder chooses to publish. A record bundle in a lab's audit log, a court exhibit, or a publisher's own archive verifies against Arweave with no Akaeon involvement — that property is untouched.

Private, disclosed selectively:

  • The domain list, in any enumerable form. There is no bulk export, no manifest, no leaf-list dump.
  • The enrichment attached to each record: multi-resolver DNS transcripts, DNSSEC chain captures, verification metadata, content fingerprints, the publisher→policy→time graph. The canonical record already commits to evidence by hash (dns_challenge_record_sha256); the evidence itself stays in the registry's vault and is revealed selectively — in a dispute, to a court, to a credentialed lab — where it verifies against the hash that was anchored long before the disclosure. Selective disclosure carries the same trust properties as publication, without being publication.

The honest caveat. The state tree is keyed by SHA-256(domain), and domains are guessable: anyone holding the anchored root and a candidate domain can hash it and ask for a proof. Hashing does not hide the membership of a specific, guessed domain — and this document does not claim it does. What the construction prevents is enumeration: learning the set without already knowing its elements. Testing a guess costs one credentialed, rate-limited, watermarked API call — economically equivalent to a lookup, which is already the product. Reconstructing the registry via guessing is a dictionary attack against the rate limits, the acceptable-use contract, and the per-lab watermark (§7) simultaneously.


3. The sparse Merkle tree construction

3.1 Key derivation

The tree is keyed by domain. The key is:

domain_key = SHA-256(UTF-8(normalized_domain))

where normalized_domain follows the same normalization as the lookup endpoint (runbook §3.1): lowercase, punycode-encoded for IDNs, no trailing dot, no scheme, no port. The key is 256 bits; each key addresses exactly one leaf position in a depth-256 binary tree. There is no leaf-splitting or prefix-truncation scheme — the full 256-bit path is always used, which keeps non-membership proofs trivially well-defined (the leaf slot for any key either holds a state commitment or is empty).

3.2 Leaf value — the domain state record

A non-empty leaf commits to the domain's complete opt-out state as of the epoch. The committed document is a canonical record in the same serialization discipline as spec §5.4 (UTF-8, lexicographic key sort, no insignificant whitespace, no extension fields):

{
  "version": 1,
  "type": "domain_state",
  "domain": "example-publisher.com",
  "epoch": "2026-07-10",
  "active_leaf_hashes": [
    "a3f5d8b9c2e1...",
    "b7e2c4d1f8a3..."
  ],
  "app": "akaeon-registry",
  "network": "arweave"
}
  • epoch — the UTC date the state is as of (§4.1).
  • active_leaf_hashes — the batch-tree leaf hashes (spec §5.5) of every anchored, non-withdrawn opt-out for the domain as of the epoch boundary, sorted lexicographically. Each entry is independently verifiable: it is a leaf in some already-anchored batch, so a disclosed domain state chains down to the same Arweave transactions the per-record proofs use.
  • A domain with only withdrawn opt-outs has an empty active_leaf_hashes array — it is still a member (its history exists); the state record says its active set is empty. A domain the registry has never anchored anything for has no leaf at all (non-membership).

The leaf hash is:

smt_leaf_hash = SHA-256(0x00 || canonical_domain_state_bytes)

using the same 0x00 leaf-domain-separation prefix as the batch trees.

3.3 Empty subtrees

The empty leaf is defined as 32 zero bytes:

EMPTY[256] = 0x0000000000000000000000000000000000000000000000000000000000000000

Empty subtree roots at each height are precomputed by folding upward:

EMPTY[d] = SHA-256(0x01 || EMPTY[d+1] || EMPTY[d+1])     for d = 255 … 0

An implementation computes the 257-entry EMPTY[] table once. These constants will ship in the published test vectors (§10).

3.4 Internal nodes and path convention

Internal nodes use the same 0x01 prefix as the batch trees:

node = SHA-256(0x01 || left_child || right_child)

Path convention: the bits of domain_key, most-significant bit first, select the branch at each depth — bit i (0-indexed from the MSB of the key) chooses the child at depth i, with 0 = left, 1 = right. Depth 0 is the root; depth 256 is the leaf layer.

The tree is never materialized in full (2²⁵⁶ slots); the registry stores only the populated paths and derives everything else from the EMPTY[] table. This is the standard sparse-Merkle-tree optimization; the on-disk representation is an implementation detail and carries no protocol weight.


4. The daily epoch and the anchored payload

4.1 Epoch

One commitment per UTC day. The epoch label is the UTC date (e.g. 2026-07-10), and the committed state is the registry's anchored state as of 00:00:00Z of that date: an opt-out is included iff its batch's Arweave anchor precedes the epoch boundary; a withdrawal is applied iff its anchor precedes the boundary. Ties (anchor exactly at the boundary) resolve into the next epoch. The commitment job runs after the boundary; its output is deterministic given the anchored history, so a delayed or re-run job produces byte-identical roots.

4.2 Canonical message and on-chain payload

The registry signs the root with the canonical message:

akaeon-registry:state-root:v1|<epoch>|<smt_root_hex>|<domain_count>

domain_count is the number of non-empty leaves. It is included to make the commitment's scope explicit; aggregate registry size is already derivable from the public per-batch leaf_count fields, so this leaks nothing new.

The anchored payload (one Arweave transaction per day, built through the same @akaeon/core-arweave canonical-payload path and subject to the same safety controls as batch anchoring):

{
  "version": 1,
  "type": "state_commitment",
  "epoch": "2026-07-10",
  "smt_root_sha256_hex": "e7c4a1d9f2b8...",
  "domain_count": 18342,
  "tree_construction": "smt-sha256-v1",
  "prev_state_arweave_tx_id": "GHI789_previous_day_tx",
  "registry_signature": {
    "canonical_message": "akaeon-registry:state-root:v1|2026-07-10|e7c4a1d9f2b8...|18342",
    "signature": "<base64>",
    "public_key": "<base64-32-byte-raw>",
    "signature_scheme": "ed25519",
    "version": "v1"
  },
  "app": "akaeon-registry",
  "network": "arweave"
}

prev_state_arweave_tx_id chains each day's commitment to the previous day's, producing a walkable, tamper-evident daily history on Arweave (genesis epoch uses null). The chain is the artifact that compounds: each link is timestamped by a network the registry does not control, so the sequence — unlike the dataset — cannot be recreated by anyone at any later date.

4.3 Cost envelope

~700 bytes per day, one transaction per day. Within the substrate's defaults (ANCHORING_MAX_PAYLOAD_BYTES=8192 for the registry, ANCHORING_MAX_PER_PROOF_USD=$0.50, $10/day budget) with no overrides. The state-commitment job goes through the same preflight pipeline (kill switches, budget caps, dry-run mode) as batch anchoring.


5. Proofs of membership and non-membership

5.1 Proof object

A proof against an anchored root, as served by the API:

{
  "epoch": "2026-07-10",
  "domain": "example-publisher.com",
  "domain_key": "9f86d081884c7d65...",
  "proof_type": "membership",
  "domain_state": { /* canonical domain_state record, §3.2 — present iff membership */ },
  "smt_proof": {
    "siblings_bitmap": "e000000000000000000000000000000000000000000000000000000000000000",
    "siblings": [
      "b4f1e2d8c7a9...",
      "c5a2f3e9d8b1...",
      "d6b3a4f1e2c8..."
    ]
  },
  "state_commitment": {
    "smt_root_sha256_hex": "e7c4a1d9f2b8...",
    "arweave_tx_id": "JKL012_state_tx",
    "arweave_url": "https://arweave.net/JKL012_state_tx",
    "arweave_block_timestamp": "2026-07-10T00:41:03Z"
  }
}
  • siblings_bitmap — 256 bits, hex-encoded, bit i (MSB-first, matching the path convention) set iff the sibling at depth i+1 along the path is not the empty subtree for that height. In a sparse tree almost all siblings are empty; only the non-empty ones are transmitted.
  • siblings — the non-empty sibling hashes, deepest first (leaf-adjacent first), one per set bit consumed from deepest to shallowest.
  • proof_type"membership" or "non_membership". For non-membership, domain_state is absent and the leaf value is EMPTY[256].

5.2 Verification algorithm

// Verify an SMT membership / non-membership proof against an anchored root.
// EMPTY[] is the precomputed empty-subtree table (§3.3).

function verifySmtProof(domain, proof, expectedRootHex) {
  const key = sha256(Buffer.from(domain, 'utf8'))            // domain_key
  let h = proof.proof_type === 'membership'
    ? sha256(Buffer.concat([Buffer.from([0x00]),
        canonicalSerialize(proof.domain_state)]))            // recompute leaf
    : EMPTY[256]

  const bitmap = Buffer.from(proof.smt_proof.siblings_bitmap, 'hex')
  let next = 0                                               // siblings cursor

  for (let depth = 256; depth >= 1; depth--) {
    const i = depth - 1                                      // bit index, MSB-first
    const bitmapSet = (bitmap[i >> 3] >> (7 - (i & 7))) & 1
    const sibling = bitmapSet
      ? Buffer.from(proof.smt_proof.siblings[next++], 'hex')
      : EMPTY[depth]
    const keyBit = (key[i >> 3] >> (7 - (i & 7))) & 1
    h = keyBit === 1
      ? sha256(Buffer.concat([Buffer.from([0x01]), sibling, h]))
      : sha256(Buffer.concat([Buffer.from([0x01]), h, sibling]))
  }

  return next === proof.smt_proof.siblings.length
    && h.toString('hex') === expectedRootHex
}

The verifier additionally checks, exactly as in the runbook §5 flow:

  1. domain_key in the proof equals the recomputed SHA-256(domain) (and, for membership, that proof.domain_state.domain equals the queried domain and proof.domain_state.epoch equals the claimed epoch).
  2. expectedRootHex comes from the anchored payload — fetch state_commitment.arweave_url, verify the payload's registry_signature against a trusted registry key (trust anchor per runbook §5 step 0), and take smt_root_sha256_hex from the anchored body, not from the proof envelope.
  3. For membership: optionally chain down — each entry in active_leaf_hashes can be cross-checked against its batch's anchored root via GET /v1/public/batches/:batch_id and the per-record inclusion proofs.

If all checks pass, the holder has a statement — this domain had exactly this state (or no state) as of epoch E — that verifies against the public Arweave network with no Akaeon service in the loop.


6. API surface and product tiers

Two new endpoint families, implemented in the registry service.

6.1 Public: the anchored roots

GET /v1/public/state-roots/latest
GET /v1/public/state-roots/:epoch        (epoch = YYYY-MM-DD)

Unauthenticated, rate-limited per IP like the rest of /v1/public/*. Returns the signed state-commitment payload (§4.2) plus the Arweave transaction reference. The roots are public because verification must be: anyone holding a proof can check it forever, for free. Publishing the root reveals nothing about the tree's contents.

6.2 Credentialed: the proofs

GET /v1/state/proof?domain=<domain>&epoch=<epoch|latest>

Authenticated with a lab bearer token; paid-tier entitlement. Returns the proof object of §5.1, wrapped in the watermarked envelope of §7. Rate-limited (initial proposal: 10/s, 100/min, 10,000/day per token — same accounting family as bulk lookup).

The split is deliberate and is the productization of the publication policy: checking a proof you hold is free forever; obtaining a proof about a domain is a credentialed, metered act. The proof endpoint is what the snapshot manifest never should have been — the offline-audit product, priced, with the audit properties strengthened rather than traded away.

6.3 Composition with the lookup attestation

For labs on a tier with state-proof entitlement, GET /v1/lookup responses for a domain with no opt-outs include, alongside the ephemeral no_optouts_attestation, an anchored_non_inclusion block: the non-membership proof against the most recent anchored epoch. The two compose:

  • The anchored non-inclusion proof covers up to the last epoch boundary with anchored-grade trust (the registry cannot rewrite it — the root predates the query).
  • The ephemeral attestation covers the gap from the epoch boundary to lookup_at with signed-statement-grade trust.

A challenger disputing "the domain had no opt-out at training time" must now dispute an Arweave-anchored commitment for everything up to midnight UTC, and a registry signature only for the final hours.


7. Traceability: per-lab watermarking of served proofs

Every credential is issued with a lab key ID (lab_key_id, format akr_kid_<8 hex>): a public, non-secret identifier stored alongside the token's Argon2id hash and returned at issuance. It is embedded in the canonical message of every per-lab attestation the registry signs:

akaeon-registry:no-optouts:v1|<domain>|<lookup_at>|<lab_key_id>
akaeon-registry:bulk-attestation:v1|<sha256-of-sorted-empty-domain-list>|<lookup_at>|<lab_key_id>
akaeon-registry:state-proof:v1|<epoch>|<domain_key_hex>|<proof_type>|<lab_key_id>

The state-proof envelope wraps the §5.1 proof object with a registry signature over the state-proof:v1 message. The SMT proof inside verifies against the anchored root regardless of the envelope — third-party verifiability is not weakened — but the served artifact is signed to a specific credential.

What this buys: a leaked, resold, or scraped-and-redistributed corpus of registry responses carries, in every signed object, the key ID of the token that obtained it. Because the key ID is inside the signed bytes, it cannot be stripped without destroying the signature — and an unsigned corpus is worthless as evidence, which is the only thing the corpus is for. Redistribution is already barred by the acceptable-use agreement signed at credentialing (runbook §2.1); the watermark converts that contract term from "hard to prove" to "self-proving." Rate limits make bulk reconstruction slow; the contract makes it actionable; the watermark makes it attributable.

Anchored publisher records themselves (canonical records, batch payloads) are lab-independent and carry no watermark — they are the same public facts for everyone, which is what makes them verifiable by courts and successors. The watermark applies only to per-lab attestations and served proof envelopes.


8. Security considerations

Nobody can anchor in the past. The core property, restated as the threat model's floor: a root anchored at epoch E is immutable evidence of the registry's claimed state at E. No later compromise — of the registry's key, database, or operator — can alter what was committed. This is also the structural answer to "what if a competitor copies the dataset": a copied domain list reproduces none of the anchored history. Every verification timestamp, every daily root, every chained commitment is temporally irreproducible. Secrecy (§2) protects the edges of the registry's value; the anchored history is the part that cannot be cloned even in principle.

Equivocation is publicly provable. Suppose the registry anchors an opt-out for domain D in a batch at time T, then serves a non-membership proof for D at a later epoch (suppressing D from the tree). The two artifacts — the anchored batch record with its inclusion proof, and the signed non-membership proof against the anchored state root — are mutually contradictory and both publicly verifiable. Any holder of both can demonstrate registry misbehavior to a third party without cooperation from anyone. The daily commitment therefore doesn't just let the registry prove honesty; it makes dishonesty a permanently-attributable artifact. (The same argument covers a fabricated membership: a domain-state record whose active_leaf_hashes don't verify against any anchored batch is self-evidently forged.)

No consistency proofs between epochs. Unlike an append-only CT-style log, an SMT keyed by hash does not admit cheap consistency proofs between successive roots — day N+1's root is not provably a superset of day N's. This is acceptable because append-only-ness of the underlying records is already carried by the batch chain (spec §5.6): state roots commit to sets of batch-tree leaves, and batches are anchored monotonically. A state root that "forgets" an anchored record is an equivocation, caught as above.

Dictionary testability. Stated honestly in §2 and repeated here as a security property, not a privacy claim: SHA-256(domain) keys are guessable for specific domains. The construction's confidentiality goal is enumeration resistance, not membership hiding for guessed inputs. Salted or keyed hashing (e.g. HMAC under a registry secret) would hide membership from guessers but would put the registry back in the trust path for every verification — the salt would be needed to verify, so verification would no longer survive the registry. The trade is deliberate: verifiability wins.

Proof-endpoint scraping. The proof endpoint answers one domain per call, credentialed and metered (§6.2), with every response watermarked (§7). Enumerating the registry through it is the same dictionary attack as against the lookup endpoint, at the same cost, with the same contractual exposure — the state commitment adds no new enumeration surface beyond what the lookup product already prices.


9. Relationship to existing mechanisms

MechanismStatus after this addendum
Daily snapshot manifest (old runbook §11 FAQ answer)Withdrawn. Replaced by the daily state commitment. No enumerable artifact is published, free or paid.
no_optouts_attestation (runbook §3.3)Retained for the epoch-boundary-to-lookup_at gap and for tiers without state-proof entitlement; now carries lab_key_id in its canonical message. Its known weakness (re-signable by a colluding registry) is mitigated for the anchored window by anchored_non_inclusion.
bulk_attestation (runbook §4.2)Retained, now carries lab_key_id. Per-domain anchored non-inclusion for bulk results is available by following up on GET /v1/state/proof for the domains that matter.
Public verification surface (runbook §7)Extended with GET /v1/public/state-roots/*. The cross-check described in §3.3 of the runbook ("verify no opt-out was anchored before your lookup_at") remains valid and is now constructive: the non-inclusion proof is that cross-check, packaged.
Full-leaf-list anchoring (spec §10.1)Largely superseded. The motivation for anchoring full leaf lists was removing the registry as sole mediator of inclusion proofs. The daily state commitment addresses the audit-side need without publishing the leaf population; §10.1's option remains open for labs whose assurance requirements demand mediator-free positive proofs after registry wind-down.
Wind-down protocol (spec §9.1)Extended (proposal): the final wind-down declaration includes the last state commitment and, escrowed with the historical-records archive, the populated-path data needed for a successor to serve proofs against all historical roots.

10. Open questions

10.1 Epoch cadence. Daily is the initial proposal (cost: one tx/day; staleness: up to 24h covered by the ephemeral attestation). Labs with tighter assurance windows may want 6-hour epochs; the construction is cadence-agnostic and the canonical message already carries the epoch label. Decide from lab demand, not speculatively.

10.2 Proof pricing. The state-proof entitlement is a paid-tier feature; specific pricing is an operational policy decision out of scope here, same as spec §10.7. What is in scope: the free tier of this feature is verification (public roots, public verifier code), never proof issuance.

10.3 Bulk proof requests. A POST /v1/state/proof/bulk (up to 1,000 domains, mirroring bulk lookup) is plausible for audit-time workflows. Deferred until a lab asks; the single-domain endpoint is sufficient for the dispute/deposition use case that motivates the product.

10.4 Test vectors. The v1-launch test-vectors/ directory gains an smt/ section: the EMPTY[] table, a small populated tree (3 domains) with its root, one membership proof, one non-membership proof, and the signed state-commitment payload. A verifier passes if it agrees on all of them. (The registry's own generator/verifier round-trip is already exercised in its test suite — populations 0–32, tamper rejections, and a hand-computed single-leaf root; the published vectors are the lab-facing extraction of the same cases.)

10.5 Escrowed proof service after wind-down. §9's wind-down extension (escrow populated paths so a successor can serve historical proofs) needs an escrow design: where, under what release conditions, and whether the escrowed artifact itself leaks enumerable state (it does — it's the populated paths — so the escrow must be access-controlled, not public). Deferred to the wind-down protocol's own revision.


Document conventions: identical to the runbook's: what is implemented is marked in the status block at the top; what remains open lives in §10; initial proposals are marked as such.