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.

RoleAddsTouches
Structural validatorschema + domain conformance over the record bytesnothing — a pure function
Public verifierchain resolution, on-chain inclusion, signature checksa Cardano explorer + content gateways
Recipient verifiertrial-decrypt of a sealed payload, plaintext-hash recomputethe 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:

  1. 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.
  2. 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.
  3. Unwraps the Conway-era auxiliary_data. Post-Alonzo transactions wrap auxiliary_data in CBOR tag 259, with the metadata map at key 0; 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 as MALFORMED_CBOR.
  4. 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.
  5. Structurally validates the reassembled body (the role above).
  6. Confirms on-chain inclusion and reads the confirmation depth (below).
  7. 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.

VerdictExitMeaning
valid0every check the verifier ran returned ok; no error-severity issue is present.
pending3structurally well-formed and on-chain, but below the confirmation-depth threshold; may settle.
failed1an integrity check failed: the structural validator rejected the bytes, a signature did not verify, a hash mismatched, or a deny-host rule fired.
failed2a 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 valid result cannot coexist with any error.
  • 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:

FamilySeverityRepresentative codes
Malformed / non-canonical CBORerrorMALFORMED_CBOR, CHUNK_TOO_LARGE
SchemaerrorSCHEMA_TYPE_MISMATCH, SCHEMA_MISSING_REQUIRED, SCHEMA_UNKNOWN_FIELD, SCHEMA_EMPTY_RECORD
Unsupported algorithmerrorUNSUPPORTED_HASH_ALG, UNSUPPORTED_AEAD_ALG, UNSUPPORTED_KEM_ALG, UNSUPPORTED_MERKLE_COMMIT_ALG
Signatureerror / infoMALFORMED_SIG_COSE_SIGN1, SIGNER_KEY_UNRESOLVED, SIGNATURE_INVALID; SIGNATURE_UNSUPPORTED (info)
Encryption / KEM / wraperrorKEM_EPK_LENGTH_MISMATCH, KEM_CT_LENGTH_MISMATCH, WRAP_LENGTH_MISMATCH, ENC_REQUIRES_CONTENT_HASH
Decryption outcomeerrorWRONG_RECIPIENT_KEY, TAMPERED_HEADER, TAMPERED_CIPHERTEXT
URIerror / warningINVALID_URI, URI_TARGET_FORBIDDEN, URI_INTEGRITY_MISMATCH; URI_FETCH_FAILED (warning)
Merkle list-commitmenterror / warning / infoMERKLE_ROOT_MISMATCH, SCHEMA_MERKLE_LEAF_COUNT_MISMATCH; MERKLE_LEAVES_UNAVAILABLE (warning); MERKLE_UNSUPPORTED (info)
Confirmations(pending)INSUFFICIENT_CONFIRMATIONS
Service independenceerrorSERVICE_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.

  • 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.