Verification
The three Label 309 verifier roles, the verdict states, finality depth, and the typed error catalogue — how anyone reaches the same answer from public infrastructure alone.
Label 309 is verified, never asserted. A publisher anchors a content hash on Cardano under label 309; from that point on the claim stands on its own bytes, and anyone holding the transaction reference can check it. This page defines how that check runs: the three verifier roles, what each one does and does not touch, the verdict states they emit, the confirmation depth below which a verdict stays provisional, and the typed error catalogue that makes two independent implementations agree on the same failure for the same input.
The defining property is service independence. A conformant verifier reaches its verdict using only the public chain, a verifier-chosen Cardano explorer or gateway, and — for content and sealed claims — content-addressed storage gateways the verifier also chooses. It never contacts the publisher. The standard names no specific provider; the gateway is an input the operator supplies.
Three roles, each a strict extension
Verification is layered. Each role does everything the role above it does, then adds one capability. A lower role is a complete, useful verifier on its own — it simply proves less.
| Role | Adds | Touches |
|---|---|---|
| Structural validator | schema + domain conformance over the record bytes | nothing — a pure function |
| Public verifier | chain resolution, on-chain inclusion, signature checks | a Cardano explorer + content gateways |
| Recipient verifier | trial-decrypt of a sealed payload, plaintext-hash recompute | the verifier's own private key |
Structural validator — a pure function over the bytes
The structural validator is a single function from a byte string to a result. It performs no I/O, no cryptographic signature checks, and no decryption. It never sees a network, a transaction, or a key. Given the same input it returns the same output, every time, anywhere — which is what lets it run pre-submission inside a publisher's tooling, inside a third-party indexer, or inside an archival tool confirming long-term well-formedness, all without a server.
Its pipeline is fixed:
1. resource bounds — a local, non-normative guard on input size; never a
Label 309 conformance error.
2. canonical decode — decode with a canonical-CBOR decoder (RFC 8949 §4.2.1):
definite lengths, sorted map keys, no duplicate keys,
valid UTF-8. Any malformed or non-canonical input →
a single MALFORMED_CBOR.
3. schema parse — type, length, and the chunk/length bounds; a strict
object mode that rejects unknown fields.
4. domain rules — cross-field constraints the schema cannot express:
registry membership, the items-or-merkle rule, COSE
structural shape, URI reconstruction, envelope shape.
5. result — { valid, record } with optional warnings/info, or a
sorted list of typed issues.The validator is profile-agnostic: it parses the full v1 schema regardless of which
subset a downstream verifier intends to act on. Errors fail the record; warnings and
info entries are surfaced but leave it valid. Crucially, it confirms the shape of a
COSE_Sign1 — four-element array, detached (null) payload, a well-formed protected
header — but never verifies the signature, and it never rejects a record merely
because the signature algorithm is one it does not recognise (that is tagged
SIGNATURE_UNSUPPORTED, severity info, and the record stays valid). Verifying the
signature is the public verifier's job. See The record for the
schema this pass enforces.
Public verifier — chain, inclusion, and signatures
The public verifier layers the chain on top of the structural validator. Given a Cardano transaction reference, it:
- Resolves a verifier-chosen explorer. The gateway chain is an input; the verifier tries them in order. If a gateway is merely unreachable it falls through to the next, but a definitive "no metadata under label 309" answer is authoritative and is not retried elsewhere — every gateway sees the same chain.
- Fetches the raw transaction CBOR — never a lossy JSON projection. Explorers commonly expose a metadata-JSON view that collapses CBOR major types into a JSON union and discards map-key order, definite-length framing, and the bytes-vs-text discriminator. A verifier that re-encoded from that projection could not reproduce the exact bytes the signer signed, so every signature on a conforming record would fail. The verifier MUST fetch the raw transaction CBOR and decode label 309 from those bytes.
- Unwraps the Conway-era
auxiliary_data. Post-Alonzo transactions wrapauxiliary_datain CBOR tag 259, with the metadata map at key0; the verifier unwraps the tag to reach it. A bare, untagged map is accepted as the pre-Alonzo fallback; any other tag at that position is rejected asMALFORMED_CBOR. - Reassembles the chunked record body. The label-309 value is the canonical-CBOR record body split into an array of ≤ 64-byte byte strings. The verifier byte-concatenates the elements in order — returning the raw bytes, with no re-encode pass — so the canonical-CBOR check can still catch a non-conformant on-chain encoding.
- Structurally validates the reassembled body (the role above).
- Confirms on-chain inclusion and reads the confirmation depth (below).
- Verifies every record-level signature under strict Ed25519. It does not decrypt. Signature resolution, the domain-separated payload, and the strict verification rules are specified on Signatures.
Why raw CBOR, not JSON
A signature is computed over the byte-exact canonical CBOR of the record body. A JSON projection of metadata is lossy by construction — it cannot round-trip back to those bytes. Re-encoding from JSON breaks every signature on a conforming record. The raw transaction CBOR is the only authoritative input to any cryptographic check; a JSON view is for human display, after verification has already passed.
Recipient verifier — decrypt and recompute
The recipient verifier is a public verifier that additionally holds a private key. For a sealed item addressed to it, it trial-decrypts the on-chain key slots with its key, recovers the content key on success, decrypts the ciphertext, and then recomputes the plaintext hashes against the on-chain commitment — closing the loop between the encrypted bytes and the content-existence claim. Because every sealed item carries at least one content-hash entry, that recomputation always has something concrete to compare against. The sealed envelope, the key slots, and the unwrap construction are specified on Sealed PoE.
The recipient path is where the error catalogue earns its precision: it distinguishes the case where no slot accepted this key (wrong recipient) from the case where a slot accepted the key but the slot set or the ciphertext was tampered. Those are different security claims and carry different codes (below).
Finality: confirmation depth
Cardano settlement is probabilistic. A transaction one block deep can still be
orphaned by a short reorg; a transaction many blocks deep has settled with
overwhelming likelihood. A verifier that called a one-block-deep record valid
would let an attacker re-anchor a contradictory record on a competing fork and
collect a "valid" verdict on both — silently breaking the append-only assumption the
whole proof rests on.
So a verifier reports the record as pending, not failed, while it sits below a
confirmation-depth threshold. The RECOMMENDED general-purpose threshold is ≥ 15
blocks (roughly five minutes). The threshold is verifier policy, not a wire
constant: deployments handling high-value or evidentiary records SHOULD raise it
toward hard finality, and a verifier MUST surface the threshold it used so
consumers can layer a stricter policy on top. A pending record is well-formed and
on-chain; it simply has not settled deeply enough yet, and may resolve to valid
on a later retry.
Verdict states
A verifier emits one of three machine verdicts. They map onto a four-state exit
code — the failed verdict splits into an integrity class and a network class —
so that a caller — a CI gate, a monitor, a script — can tell a record-attributable
failure apart from a transient operational one without parsing the structured report.
| Verdict | Exit | Meaning |
|---|---|---|
| valid | 0 | every check the verifier ran returned ok; no error-severity issue is present. |
| pending | 3 | structurally well-formed and on-chain, but below the confirmation-depth threshold; may settle. |
| failed | 1 | an integrity check failed: the structural validator rejected the bytes, a signature did not verify, a hash mismatched, or a deny-host rule fired. |
| failed | 2 | a network class failure: content could not be fetched, or every explorer was unreachable. |
A valid verdict MUST NOT be reported when any error-severity issue is present;
a record MAY be valid with a non-empty warnings and/or info list. Neither
layer may "soften" an error into a warning to make a record pass. pending is
reserved exclusively for the below-threshold case and is never substituted for
valid or failed.
The typed error catalogue
Every failure mode resolves to a code from a single closed catalogue. Codes are
SCREAMING_SNAKE_CASE, and a conformant implementation MUST emit exactly those
strings — never a parser's internal lowercase code, never a free-form message. Two
implementations in two languages reaching the same input emit the same code; the
full normative list is pinned byte-exact by the conformance test suite, and the
catalogue is locked (codes are added only by amendment).
Severity model
Every issue carries one of three severities, and the distinction is load-bearing:
- error — invalidates the verdict. A
validresult cannot coexist with anyerror. - warning — a non-fatal run-time anomaly (a single gateway failed, a leaves-list
was only partially available) that does not block
valid. - info — a deliberate non-check: an aspect the verifier chose not to evaluate (a field outside its profile, an unrecognised optional algorithm). An info entry is not a softened error and is never used as one.
One code stands apart: INSUFFICIENT_CONFIRMATIONS maps to the pending verdict
rather than to a severity, because the record is well-formed and only awaiting
settlement.
Error families
The catalogue groups into families. A representative — not exhaustive — set of codes:
| Family | Severity | Representative codes |
|---|---|---|
| Malformed / non-canonical CBOR | error | MALFORMED_CBOR, CHUNK_TOO_LARGE |
| Schema | error | SCHEMA_TYPE_MISMATCH, SCHEMA_MISSING_REQUIRED, SCHEMA_UNKNOWN_FIELD, SCHEMA_EMPTY_RECORD |
| Unsupported algorithm | error | UNSUPPORTED_HASH_ALG, UNSUPPORTED_AEAD_ALG, UNSUPPORTED_KEM_ALG, UNSUPPORTED_MERKLE_COMMIT_ALG |
| Signature | error / info | MALFORMED_SIG_COSE_SIGN1, SIGNER_KEY_UNRESOLVED, SIGNATURE_INVALID; SIGNATURE_UNSUPPORTED (info) |
| Encryption / KEM / wrap | error | KEM_EPK_LENGTH_MISMATCH, KEM_CT_LENGTH_MISMATCH, WRAP_LENGTH_MISMATCH, ENC_REQUIRES_CONTENT_HASH |
| Decryption outcome | error | WRONG_RECIPIENT_KEY, TAMPERED_HEADER, TAMPERED_CIPHERTEXT |
| URI | error / warning | INVALID_URI, URI_TARGET_FORBIDDEN, URI_INTEGRITY_MISMATCH; URI_FETCH_FAILED (warning) |
| Merkle list-commitment | error / warning / info | MERKLE_ROOT_MISMATCH, SCHEMA_MERKLE_LEAF_COUNT_MISMATCH; MERKLE_LEAVES_UNAVAILABLE (warning); MERKLE_UNSUPPORTED (info) |
| Confirmations | (pending) | INSUFFICIENT_CONFIRMATIONS |
| Service independence | error | SERVICE_INDEPENDENCE_VIOLATION |
The recipient path is the most diagnostically precise family. A failed decrypt is
never reported as a generic error: WRONG_RECIPIENT_KEY means no slot accepted the
supplied key (no content key was ever recovered); TAMPERED_HEADER means a key was
recovered but the slot-set authentication tag did not match; TAMPERED_CIPHERTEXT
means the slot set was intact but the content authentication tag failed. The three
are structurally distinguishable, and the boundary between them leaks no key
material — a verifier can tell a wrong-recipient from a tampered ciphertext without
learning anything about the key.
Structural rejection is failed (integrity class)
A code emitted by the structural validator means the record bytes do not conform.
The public verifier short-circuits the report with verdict failed in the
integrity class — exit 1 — and the validator's issue list, without running any
further chain or crypto work. A code emitted only after structural validation
passes — a signature that did not verify, a hash that did not match — is also a
failed verdict in the integrity class, while a transient operational failure —
content that could not be fetched, every explorer unreachable — is failed in the
network class (exit 2). The distinction the exit code preserves is
record-attributable integrity (exit 1) versus transient operational failure
(exit 2).
A few codes carry context-dependent severity. A verifier reading a record richer than
its declared profile (for example a hash-only verifier encountering a sealed item)
reports the extra field as info in a display context and still validates the hash
claim, or as error in a strict end-to-end audit context. Likewise an unrecognised
optional signature algorithm is info — the content-existence claim does not
depend on it — so a public hash-only proof remains valid even when an advisory
signature is unverifiable.
Service independence is not optional
Verification never reaches back to the publisher. Every outbound call a conformant
verifier makes is directed at infrastructure the operator chose — a Cardano explorer
for the transaction, and content-addressed storage gateways for any ar:// or
ipfs:// bytes. This is structurally enforced, not a code-comment promise:
- Every network call routes through a single egress wrapper that records
url,method,status, byte count, and purpose for every call — success, failure, and retry alike — into a mandatory audit trail on the report. A verifier that cannot produce that trail cannot prove its independence. - That wrapper accepts a deploy-supplied deny-host list and hard-fails any call to a
matching host with
SERVICE_INDEPENDENCE_VIOLATION. The list is an operator input — a conformance suite populates it with the implementer's own domains — not a wire constant of Label 309. - A conformance harness runs the verifier against frozen fixture transactions in a
network where the operator's own domains resolve to nowhere, and asserts the
verifier still returns
valid. The assertion is made at the OS network layer, not by grepping source — a verifier reaching a forbidden host by hardcoded IP would pass a source scan but fail this test.
The verifier needs a reachable Cardano explorer and nothing implementer-specific. Content gateways, a recipient private key, and an off-chain leaves-list are optional inputs that unlock the content, recipient, and Merkle checks respectively. None of them is a service the standard names, and none of them is the publisher.
Related pages
- The record — the wire format the structural validator checks against: label 309, the map shape, chunk reassembly, and the CDDL schema.
- Signatures — the record-level COSE_Sign1 construction, the domain-separated payload, and the strict Ed25519 verification rules.
- Sealed PoE — the encryption envelope, recipient key slots, and the unwrap the recipient verifier performs.
Sealed PoE
The Label 309 encryption envelope — how a sender seals content to one or more recipient keys while the chain carries only the plaintext hash and the wrapped key slots, never the plaintext and never the recipients.
Security model
What a Label 309 verifier trusts and what it does not — the standalone-verifiability invariant, the privacy guarantees of sealed PoE, the normative crypto rules every implementation must uphold, and the known limits of the wire format.