Status: APPROVED 2026-08-12. What an
implementation must do to claim it speaks ZDDC custody v1,
what Stage 4 will build, and what v1 deliberately does not do
— each deferral with its hook named. Requirement ids
[M-C*] here are already taken by canonical form
(manifest.md §1), so
this document's own requirements are [M-R*]
(runner).
Two implementations must satisfy this contract, and the spec exists precisely because they are different worlds:
zddc-server(Go) — the inbound door, the mirror walker, the ledger reader;- the
.htmltools (browser, Web Crypto,file://) — the rung-1 producer and the offline verifier.
Per memo constraint 2, both are reference implementations of these documents, never the definition of them. If a third implementation passes the vectors, it interoperates.
1. Levels of conformance
An implementation declares one or more of these. They compose; nothing requires all four.
| Level | Must do |
|---|---|
| V — Verifier | Everything in envelope.md §6, keys.md §5–§6, and the
per-type rules of statements.md; the
tri-state result with its as-of date; the currency axis. |
| P — Producer | Emit only canonical payloads ([M-C1]), correct PAE ([M-E6]), the manifest and Merkle construction ([M-M1]–[M-M12]), and the key-file rules ([M-K1]–[M-K6]). |
| H — Hasher | The chunked Merkle construction and inclusion proofs alone ([M-M1]–[M-M5]). The minimum a mirror or an indexer needs. |
| L — Ledger | Monotonic acceptance at the door ([M-S6]),
_ledger/ placement ([M-S43]), and currency
derivation ([M-S46]–[M-S49]). |
- [M-R1] A verifier (V) MUST NOT be shipped without a hasher (H) — verifying a manifest it cannot recompute is not verification.
- [M-R2] A producer (P) MUST also be a verifier (V) of its own output. The cheapest way to emit a non-canonical payload is to never read one back.
2. The runner
[M-R3] A conformance runner loads
vectors/keys.json and
vectors/contexts.json, walks
vectors/<area>/{valid,invalid}/, and for
each vector dispatches on kind (vectors/README.md §2). It
passes when it accepts every valid/
vector and rejects every invalid/ vector with the
named reason class, and derives the named verdict for
every currency/ vector.
[M-R4] The runner MUST read no clock and
no randomness. Every instant it needs is in the vector's
context (as_of, now); Ed25519 is
deterministic. A runner that consults the wall clock will pass
today and fail after the test keys' certified windows close,
which is the single most common way a conformance suite
rots.
[M-R5] The runner MUST be runnable
offline. This is not convenience: keys.md [M-K27] forbids a
network fetch during verification, and the only honest way to
test that is to run with the network removed. A runner that
needs to resolve anything is testing a non-conforming
implementation. This holds for every input
the suite needs, with no exception — which is why the
third-party Ed25519 corpora [M-R12] requires are vendored at
corpora/ rather than
fetched. Nothing in this suite may reach the network.
[M-R6] Two requirements are
not vector-testable and MUST be pinned by the
implementation's own tests instead, with the reason recorded
(vectors/README.md §5
lists all 35 such ids):
- [M-E22] — no failure oracle on a network surface (a deployment property);
- [M-M13] — the 16 MiB ceiling, which the runner MUST synthesize by padding a valid payload past the limit rather than by loading a committed fixture.
[M-R12] Two axes of [M-E10] are likewise
not expressible as ZDDC envelope vectors — a small-order
A, and a signature satisfying the cofactored
equation but not the cofactorless one — because neither can be
built from an anchorable test key, so a vector presenting one
would be refused at [M-E12] key resolution and would never
reach the verification equation. An implementation MUST
instead demonstrate [M-E10] by running its Ed25519 verifier
over the two corpora vendored at corpora/,
matching the reference result on every case:
corpora/wycheproof-ed25519_test.json— 151 cases, each of which MUST produce the verdict in its ownresultfield. Cases 63–66, 67–70 and 85 are theS ≥ Land high-bit axes; 42–62 and 151 are the non-canonical-Raxis, and reject only under [M-E10] rule 5's encoding comparison.corpora/ed25519-speccheck-cases.json— 12 cases, indexed by array position. The implementation MUST reject cases 4 and 5 (they satisfy the cofactored equation and not the cofactorless one — the only cofactor discriminator in either corpus) and MUST accept cases 0 and 1 (a small-orderAwith the equation otherwise satisfied). Getting either pair backwards is a conformance failure, not a preference.
corpora/README.md
carries the provenance, the pinned SHA-256 of each file, and
the per-case tables in full.
The corpora are vendored, not fetched, and [M-R5]
applies to this run exactly as it does to the
vectors. Two reasons, both structural. First, ZDDC's
premise is that artifacts verify offline from a
file:// page; a conformance suite that needs the
network to run contradicts the contract it is testing, and
would leave [M-E10] as the one requirement unverifiable on an
air-gapped machine — the machine most likely to be checking
custody in the first place. Second, a fixed contract needs
fixed inputs: these corpora gain cases upstream, so a suite
that fetched the latest would let two runs months apart test
different things while both reported "pass". Refreshing the
pinned bytes is a reviewed spec change with its own procedure
(corpora/README.md),
never an automatic update.
A ZDDC implementation that adopts a
different Ed25519 library than the two
reference implementations MUST run these corpora before
claiming conformance — the profile is behaviour, and only the
corpora demonstrate it. If its library disagrees with the
reference results on any case, [M-E10] narrows to the
intersection and D-19 reopens; the implementation does not get
to declare the difference immaterial (decisions.md D-19
records the reference run).
[M-R7] Drift is a test failure, not a
review item. The committed vectors must equal a fresh
gen.js run byte for byte
(tests/custody-vectors.spec.js). A hand-edited
fixture silently redefines the wire format.
3. Reason classes
The eleven-name closed vocabulary is defined once, in vectors/README.md §4. Two
rules govern its use:
- [M-R8] Classes are for logs, local UI, and this suite. A network surface answers one undifferentiated refusal ([M-E22]) — a reason class on the wire is an oracle, and the shipped peer middleware's bare 401 is the posture to match.
- [M-R9] Where two classes could plausibly
apply to one input (
bad_paevsbad_signature), the vector names the more specific and a runner MAY accept the more general. Nothing else may be widened.
4. What Stage 4 builds
The §10.8 v1 scope, expressed as work:
- A shared hashing/canonicalization core,
twice.
zddc-merkle-v1and JCS in Go (zddc/internal/custody/) and inshared/for the tools.shared/hash.jsalready carries the incremental digest the chunked construction needs; the Merkle layer sits on top of it. - The DSSE profile — PAE, envelope parse/serialize, the numbered verification procedure, in both languages.
- The key layer — key files under
.zddc.d/keys/(the shape Stage 1 already ships for the transport key), the party-registry anchor field group (keys.md§3.2), and the length-1/length-2 chain resolver. - The seven statement types — payload
builders, validators, and the
_ledger/reader/writer. - The producer UX — first-produce key generation, the signing gesture that shows what is being signed before confirm (memo §6.16), and the verification report with its tri-state × currency result and the anchor as-of date.
- The inbound door — monotonic acceptance ([M-S6]), fail-closed refusal of unanchored keys, no auto-anchoring (memo §6.18).
- The browser-verifiable
.zipaffordance — open the.html, point it at the zip, get a per-file table with fingerprints, printable (memo §6.17). - The conformance runner itself, in both
implementations, wired into
go test ./...and the Playwright suite.
Not Stage 4, though the memo scopes them in v1 and they
touch nothing here:
audit-access --as <peer> (already
independently useful) and the staleness UI vocabulary (already
shipped as X-ZDDC-Remote).
5. What v1 does NOT do — deferrals and their hooks
Every deferral from memo §10.8 with the named place a later version plugs into. A hook that is not named is a hook that will be retrofitted badly.
| Deferred | Hook |
|---|---|
| Payload encryption (client-side encrypt-before-upload, memo §5b) | kem_key/kem_alg in the party
anchor ([M-K15]) + the manifest entry's enc field
([M-M8]). A v1 verifier already carries enc
through untouched and verifies the ciphertext against
root, which is the "a mirror verifies without
reading" property. Nothing else in v1 mentions
encryption. |
| RFC 3161 timestamping (memo §6.5) | An optional payload field ridden by [M-C7]'s
ignore-unknown rule, and — if it must be mandatory for a
deployment — [M-C8]'s critical. The receipt is
the baseline timestamp and stays so ([M-S17]). |
| The completeness head / transparency log (memo §6.9) | The prev chain ([M-S5]) already gives
middle-completeness for free; only the tip is
unprotected. A head statement would be another payloadType,
which the registry ([M-E4]) is shaped to take — as the
2026-08-16 record-revision addition (its eighth
entry, statements.md §11)
demonstrated in practice. |
| Full LTV machinery — evidence-record renewal, re-timestamping (memo §6.8) | The hooks are the version identifiers:
the algorithm rides the anchor ([M-E8]) and the format version
rides payloadType ([M-E4]), so a future algorithm
change is a version bump old verifiers refuse rather than
mis-verify. The evidentiary export (memo §6.17) is the archive
format; it is a view, so it needs no format work
now. |
| Key-hierarchy ceremony (memo §10.8) | The record format already expresses key succession ([M-S37]–[M-S42]) and operational-key certification ([M-S29]–[M-S31]). What is deferred is ceremony — HSMs, quorums, offline-root procedure — none of which changes a byte on the wire. |
| Multi-recipient envelopes | Deliberately permanent, not deferred
(memo §6.14): to keeps exactly one element
forever ([M-S3]). Fan-out is N per-link statements; the join
is content-addressing. The array shape stays only because it
is already in the grammar. |
| Equivocation detection | Deliberately permanent (memo §10.6, §11.5): bilateral witnessing gives non-repudiation per relationship, never consistency across relationships. The escape hatch is optional milestone head-gossip, strictly additive. |
| X.509 / eIDAS-qualified / RFC 4998 conformance | Deliberately permanent (memo §11.4). The escape hatch is wrapping, never re-architecture: standards-grade signatures travel inside files and survive transmittal untouched, exactly as PE seals do today. |
[M-R10] An implementation MUST NOT present a deferral as unsupported-by- accident. The three "permanent" rows above are architectural limits with documented escape hatches; a proposal that requires breaking one is a redesign of the system and must present itself as such (memo §11).
6. Reading order and authority
manifest.md → envelope.md → keys.md → statements.md → this
file, with decisions.md as the
review agenda and vectors/ as the
executable half.
[M-R11] These specs are the contract. The
design memo they were transcribed from,
docs/design/federation-and-custody.md (cited as
"memo §N"), is non-normative background: where the two appear
to conflict, the spec wins and the memo is
wrong — report it so the memo is corrected, and never
code to the memo. (Inverted 2026-09-22 for publication;
until then the memo won. An unpublished document cannot bind a
third-party implementation.) The three places where the
memo conflicted with itself were routed back rather
than improvised, and all three are now CLOSED (decisions.md §"The
three memo contradictions"): the succession
continuity proof (D-10), the anchor-placement reading (D-12),
and the stale sha256File justification (corrected
on main in 14e1582). Each correction
landed in the memo itself, so a reader of either document
reaches the same answer.