The record
The Label 309 wire format — where the record lives under metadata label 309, its map shape, canonical-CBOR rules, transport chunking, and the CDDL schema.
A Label 309 record is a single CBOR map carried in Cardano transaction metadata under label 309. The map commits one or more content hashes to the chain; the block time of the transaction is the witness that those bytes existed no later than that moment. Everything else the record can carry — storage URIs, an encryption envelope, authorship signatures, a supersedence pointer — is optional metadata about that core claim.
This page defines the on-wire shape: where the record sits, how it is encoded, how oversized values are transported, and the closed schema a structural validator checks against. The cryptographic constructions referenced here (hash algorithms, the sealed envelope, signatures) have their own pages; this one is the wire format.
Where the record lives
A PoE record MUST be placed under transaction metadata label 309, which is
reserved as "Proof of Existence record" in the
CIP-10 metadata-label registry.
Transaction metadata is a map from integer label to value, so a transaction
MUST NOT carry more than one PoE record — exactly one record per transaction.
A transaction MAY carry additional metadata under other labels (for example a
CIP-20
674 message). A verifier processing PoE MUST ignore every label other than
309.
On the Conway-era ledger, transaction metadata is the metadata field inside the
transaction's auxiliary_data. The values admitted under any label are
constrained to the ledger's recursive metadatum type — integers, byte strings,
text strings, arrays, and maps, with both byte strings and text strings capped
at 64 bytes each:
metadatum =
{ * metadatum => metadatum }
/ [ * metadatum ]
/ int
/ bstr .size (0..64)
/ tstr .size (0..64)A transaction carrying any single bstr or tstr longer than 64 bytes is
rejected by Cardano nodes at submission, before any verifier sees it. That cap is
the reason Label 309 defines a transport-chunking discipline (below); every field
the record carries — base or extension — must reduce to a metadatum.
Transport: chunking and reassembly
Because no metadata byte- or text-string may exceed 64 bytes, any logical value whose content can be longer is carried as an array of ≤ 64-byte strings and reassembled by concatenation before use. There are two parallel shapes:
- Byte-string chunks — a non-empty CBOR array of byte strings (
bstr .size (1..64)), used for signature blobs and for the record body itself. - Text-string chunks — a non-empty CBOR array of text strings (
tstr .size (1..64)), used for storage URIs.
The whole record body is serialised once to canonical CBOR, then that single byte string is split into a byte-string chunk array and stored under label 309. A verifier MUST byte-concatenate the array elements in order to reassemble the record body before structural validation. Chunk boundaries carry no semantic meaning: a producer may split a value mid-codepoint, and consumers validate only the reassembled value.
The chunk-array wrapper is always required, even for values that fit in a single 64-byte chunk: such a value is encoded as a length-1 array, never as a bare string. Producers SHOULD NOT over-chunk — splitting an 8-byte URI into three fragments wastes bytes with no benefit — but MUST split anything longer than 64 bytes.
A structural validator enforces the per-chunk range [1, 64] bytes (a zero-length
chunk is rejected on the same footing as an over-long one), the non-empty-array
invariant, and the major-type discriminator (byte-string arrays carry major
type 2; URI arrays carry major type 3). For URI chunks, a multi-byte UTF-8
codepoint MUST NOT be split across a chunk boundary, so the reconstructed
concatenation is always valid UTF-8.
The schema describes the reassembled body
Everything below — the record map, the CDDL, the field rules — describes the record body after chunk reassembly. The chunk-array transport wrapper is not part of the schema; it is undone first, then the body is validated.
The record map
The reassembled record body is a CBOR map. Integer-valued fields are CBOR major type 0/1; text fields are major type 3 and MUST be valid UTF-8; byte fields are major type 2; arrays are major type 4; nested maps are major type 5. An optional field that is present MUST NOT carry an empty value.
The top-level shape is:
| Key | Type | Status | Meaning |
|---|---|---|---|
v | uint | REQUIRED | Schema version; this document defines v = 1. |
items | array of item maps | OPTIONAL | Per-content commitments — see Content and hashing. |
merkle | array of commitments | OPTIONAL | List commitments binding off-chain leaf lists to one root. |
supersedes | bytes (32) | OPTIONAL | Transaction hash of a prior record this one replaces. |
sigs | array of signature maps | OPTIONAL | Record-level authorship signatures — see Signatures. |
crit | array of text strings | OPTIONAL | Extension keys that are mandatory to understand. |
A conformant record MUST commit to at least one of items (with ≥ 1 entry) or
merkle (with ≥ 1 entry). A record carrying neither — or carrying one of them as
an empty array — is rejected as an empty record. Apart from this rule, items and
merkle are orthogonal: a record may carry either alone or both together.
Label 309 imposes no numeric cap on entry counts. The only ceiling is the live Cardano maximum transaction size, and producers pay per-byte fees that naturally bound record size. A validator MUST NOT reject a record purely because it carries many entries, as long as it fits under the ledger's size limit.
The version field
v is a CBOR unsigned integer, not a semantic-version string. This document
defines exactly v = 1. A validator MUST reject a record whose v is outside
its supported set with a typed error; it MUST NOT panic, abort, or silently
treat the record as a different metadata schema. The v integer bumps only when a
change would cause a v1 parser to misinterpret the record — additive,
namespaced extensions do not bump it.
Items
Each entry in items is a CBOR map with one required field and two optional ones:
hashes— REQUIRED, a non-empty map from hash-algorithm identifier to raw 32-byte digest. At least one entry; duplicate algorithms are impossible because CBOR map keys are unique. See Content and hashing.uris— OPTIONAL, a plural list of discovery URIs (rules below).enc— OPTIONAL, the encryption envelope for a sealed item. See Sealed PoE.
There is no per-item signature slot. Authorship is expressed only at the record
level, by a sigs[] entry that covers every item uniformly.
Merkle commitments
Each entry in merkle binds the record to an ordered list of 32-byte leaves via a
canonical hash-tree construction, so one 32-byte root on the chain can stand in for
an arbitrarily large off-chain leaf list. A commitment is a closed map:
| Field | Type | Status | Meaning |
|---|---|---|---|
alg | tstr | REQUIRED | Registered list-commitment algorithm identifier. |
root | bytes (32) | REQUIRED | Canonical root over the producer's ordered leaf list. |
leaf_count | uint | REQUIRED | Number of committed leaves; binds the root to the list size. |
uris | URI list | OPTIONAL | Content-addressed URI(s) for the off-chain leaves-list file. |
A Merkle root commits to a leaf-list structure, whereas a hashes entry commits to
plaintext bytes; the two are verified differently (inclusion proof versus plaintext
recomputation), which is why list commitments live at the top level rather than
inside an item. The list-commitment registry is disjoint from the content-hash
registry — see Algorithm registries.
Supersedes
supersedes is an optional 32-byte Cardano transaction hash pointing at one earlier
Label 309 record. It is a service-independent, append-only link: a later record can
point at a prior record with no off-chain database or vendor record id.
Supersedence does not remove, revoke, or invalidate the prior record — the chain is append-only, and verifiers MUST continue to treat the earlier record as existent and independently verifiable. The pointer carries no reason or free-text field; any human meaning (correction, replacement, withdrawal) belongs in the new content, not in label 309. A verifier resolving the pointer MUST look it up on the same Cardano network as the containing transaction; the field carries no network discriminator because a transaction hash is unique only within its own network.
Signatures
sigs is an optional array of record-level signature entries. Each entry carries a
detached COSE_Sign1 structure over the
record body — that is, the full record map with sigs removed — and optionally the
signer's public key for the wallet-signing path. A single signature attests to the
entire body: every item, every URI, every envelope, the supersedence pointer if
present, and any extension keys. Signatures are always optional, and an
unrecognised signature algorithm never invalidates the content claim. The signed
payload, the domain-separation prefix, signer-key resolution, and strict
verification rules are specified on Signatures.
URI rules
When present, uris is a non-empty list; each entry is itself a chunked text array
that reconstructs to a single URI. Reconstructed URIs MUST be absolute, MUST
include a scheme and hierarchical part, and MUST NOT contain a fragment
identifier — a PoE is a claim about content bytes, not about a sub-component of a
document.
The v1 scheme set is closed and content-addressed:
| Scheme | Notes |
|---|---|
ar:// | Arweave transaction id (43-character base64url). Form ar://<txid>. |
ipfs:// | IPFS CID, CIDv1 preferred. Form ipfs://<cid> or ipfs://<cid>/<path>. |
Producers MUST NOT emit any other scheme — https://, http://, file://,
data:, and the rest are all rejected. The restriction is deliberate, not
temporary: a content-addressed URI binds the fetched bytes to the URI itself
through the storage layer's integrity model (an IPFS CID is a multihash of the
content; an Arweave transaction id commits to the data under Arweave consensus), so
a verifier can confirm "the bytes I fetched are the bytes the producer committed
to" without trusting DNS, TLS, gateways, or certificate authorities. An out-of-set
scheme makes a record structurally invalid; it never validates as valid.
uris is optional throughout. A hash-only record with uris omitted is a complete
claim — content existence is asserted without committing to a retrieval channel.
The exact CID profile (accepted multibase prefixes, codecs, and multihashes) is part
of the verification rules; see Verification.
Canonical CBOR
Every Label 309 record MUST be encoded as canonical CBOR per RFC 8949 §4.2.1 (Core Deterministic Encoding). Concretely:
- Preferred (shortest-form) serialisation for every integer.
- Definite-length encoding for all byte strings, text strings, arrays, and maps.
- No semantic tags (this document requires none — a bignum tag 2/3 MUST NOT appear).
- Map keys sorted in bytewise lexicographic order of their CBOR encoding.
- UTF-8 text strings with no byte-order mark.
- No duplicate keys in any map.
- No floating-point or non-trivial simple values — a record carries only
integers, byte strings, text strings, arrays, maps, and (where a schema admits
it)
true/false/null. Major-type-7 floats (including an integral1.0), negative zero, andundefinedMUST be rejected, not coerced.
Determinism is what makes the format interoperable: two producers expressing the same logical record emit byte-identical bytes, so a signature computed over the body by one implementation verifies under another. A validator MUST reject a non-canonical encoding. Explorers and wallets may surface metadata through a JSON projection, but a conformant verifier MUST validate the original transaction CBOR, never a lossy JSON re-encoding of it.
Forward compatibility
Label 309 v1 reserves a closed set of base keys: v, items, merkle,
supersedes, sigs, crit. A record MAY additionally carry extension keys
whose names match one of two reserved namespaces:
^x-.+— the vendor / experimental namespace.^[a-z]+-.+— the companion-specification namespace, where the prefix names the registering specification.
A validator MUST decode and preserve extension keys, MUST NOT reject a record
solely because they are present, and MUST surface them informationally without
claiming to have verified their contents. Extension keys are part of the signed
body, so a record-level signature covers them — a relay cannot inject an extension
key after the signature was produced. Any unknown top-level key that matches
neither pattern (a typo such as supersedess, or a case variant such as Sigs)
is rejected as an unknown field. The pattern-based tolerance preserves typo
detection on the base set while keeping a stable pool open for future additions.
A producer that requires a verifier to understand a non-base field MUST list that
field's name in the top-level crit array. A v1 verifier that encounters a crit
entry it does not implement MUST NOT report the record as valid. Each crit
entry MUST match the extension-key pattern (base keys are forbidden in crit),
MUST name a field actually present in the record, and MUST be unique — so a
critical mark is always traceable to a concrete field whose semantics the verifier
is obliged to understand. These rules follow the must-understand / must-ignore
precedents in RFC 9052 §3.1
(COSE crit) and RFC 7515 §4.1.11
(JWS crit).
Byte budget
The only hard ceiling on record size is the live Cardano maxTxSize protocol
parameter — 16 384 bytes at protocol version 9. Label 309 imposes no schema-level cap
below that. Records exceeding the limit are rejected by Cardano nodes at submission,
so no verifier ever sees one; a validator MUST NOT invent a Label 309-specific
ceiling beneath maxTxSize.
In practice a transaction's non-metadata structure (inputs, outputs, witnesses, fee and validity fields) consumes roughly 245 bytes, leaving on the order of 16 KB for the label-309 record. Producers SHOULD target a few hundred bytes below the limit to absorb fee variance and SHOULD compute the candidate record's size before submission, failing fast if it would not fit. The realistic shapes that fit are generous: well over a hundred single-hash items, dozens of record-level signatures, or many classical recipient slots all sit comfortably within one transaction — and a single Merkle root commits to an unbounded off-chain leaf list at a fixed on-chain cost of 32 bytes.
CDDL schema
The following CDDL is the structural schema for the reassembled record body — the canonical-CBOR bytes obtained after concatenating the ≤ 64-byte chunk array stored under label 309. The chunk-array transport wrapper is not modelled here.
The block describes the permissive superset of well-formed shapes; cross-field
invariants (the items-or-merkle rule, the slots ⊕ passphrase exclusivity of
the encryption envelope, registry membership of algorithm identifiers) are enforced
by a typed validation pass over the decoded structure, not by the CDDL itself.
; Every field a Label 309 record carries — base or extension — MUST be a
; `metadatum`: the record sits inside transaction metadata under label 309,
; and the ledger admits only the recursive metadatum type (no floats, no
; tags, bstr/tstr <= 64 bytes).
metadatum =
{ * metadatum => metadatum }
/ [ * metadatum ]
/ int
/ bstr .size (0..64)
/ tstr .size (0..64)
; A conformant record MUST carry at least one of `items` (>= 1 entry) or
; `merkle` (>= 1 entry); a record with both absent (or both empty) is rejected
; as an empty record by the typed pass, not at the CDDL layer.
poe-record = {
poe-common,
? "items": [ 1* item-entry ],
? "crit": [ 1* tstr ],
* extension-key => metadatum
}
poe-common = (
"v": 1,
? "merkle": [ 1* merkle-commit ],
? "supersedes": bytes32,
? "sigs": [ 1* sig-entry ],
)
extension-key = tstr .regexp "^x-.+"
/ tstr .regexp "^[a-z]+-.+"
item-entry = {
"hashes": hash-map,
? "uris": [ 1* uri-chunk-array ],
? "enc": enc,
}
; A non-empty CBOR map keyed by a content-hash algorithm identifier with the
; 32-byte digest as value. Map-key uniqueness makes duplicate algorithms
; structurally impossible.
hash-map = { + content-hash-alg => bytes32 }
; A list commitment binds the record to an ordered leaf list. `leaf_count`
; binds the on-chain commitment to the off-chain list size.
merkle-commit = {
"alg": merkle-commit-alg,
"root": bytes32,
"leaf_count": uint,
? "uris": [ 1* uri-chunk-array ],
}
; `enc` is modelled as a permissive superset admitting both key-delivery
; paths; the typed pass enforces the slots/passphrase exclusivity and the
; per-KEM slot-shape rules.
enc = {
enc-common,
? "kem": kem-alg,
? "slots": [ 1* slot ],
? "slots_mac": bytes32,
? "passphrase": passphrase-block,
}
enc-common = (
"scheme": 1,
"aead": aead-alg,
"nonce": bstr,
)
slot = classical-slot / hybrid-slot
classical-slot = {
"epk": bytes32,
"wrap": bytes48,
}
hybrid-slot = {
"kem_ct": [ 1* bstr .size (1..64) ],
"wrap": bytes48,
}
passphrase-block = {
"alg": kdf-alg,
"salt": bstr .size (16..64),
"params": { "m": uint, "t": uint, "p": uint },
}
; A signature entry is a closed map. `cose_sign1` is REQUIRED and carries the
; chunked CBOR-encoded COSE_Sign1; `cose_key` is OPTIONAL and carries the
; chunked CBOR-encoded COSE_Key for the wallet-signing path.
sig-entry = {
"cose_sign1": bytes-chunk-array,
? "cose_key": bytes-chunk-array,
}
bytes-chunk-array = [ 1* bytes-chunk ]
bytes-chunk = bstr .size (1..64)
; A uri-chunk-array reconstructs to one absolute URI string; producers MUST
; NOT split a multi-byte UTF-8 codepoint across chunks.
uri-chunk-array = [ 1* uri-chunk ]
uri-chunk = tstr .size (1..64)
bytes32 = bstr .size 32
bytes48 = bstr .size 48
; Algorithm-identifier strings are open `tstr`: the registries are
; authoritative for accepted values, and the typed pass emits the precise
; unsupported-algorithm code for any unrecognised identifier.
content-hash-alg = tstr ; e.g. "sha2-256", "blake2b-256"
merkle-commit-alg = tstr ; e.g. "rfc9162-sha256"
aead-alg = tstr
kem-alg = tstr
kdf-alg = tstrRelated pages
- Content and hashing — the
hashesmap, what a digest commits to, and exact-bytes semantics. - Algorithm registries — the named identifiers for hashes, list commitments, AEADs, KEMs, and KDFs.
- Signatures — the record-level
sigsconstruction and verification. - Sealed PoE — the
encenvelope and recipient key slots. - Verification — the validation pipeline, the CID profile, and the error catalogue.