Guides · Part 6 of 6
Inclusion certificates
You have published a Merkle root under label 309, and the off-chain leaves-list that backs it. That root is a perfectly good commitment — but it is not something you can hand to a colleague, attach to a contract, or drop into a court filing. An inclusion certificate is that handoff: a small, self-contained file that pins one or more leaves to the published root, embeds every sibling needed to re-derive that root, and names the Cardano transaction whose block time witnesses the whole thing. Anyone can verify it offline, against any explorer, with no account and no trust in whoever produced it.
If you know OpenTimestamps, this is the same idea — a portable proof-of-existence
receipt — with two deliberate differences. The timestamp authority is the
Cardano blockchain's block time, not a calendar server. And the proof is
generated and verified entirely client-side: no gateway, no issuer server, no
trust in us at any step. It is the Proof-of-Existence analogue of an .ots file,
anchored on Cardano and standalone-verifiable.
What a certificate proves — and what it does not
A certificate makes exactly two cryptographic claims, each independently checkable by anyone:
- Inclusion. A given
leafsits at positionindexof an RFC 9162 SHA-256 Merkle tree of sizetree_sizewhose root isroot. This is proven by recomputing the root from the leaf, its index, the tree size, and the embedded sibling path. It is self-contained — the certificate file alone is enough. - Anchoring. That
rootappears verbatim in themerkle[].rootfield of the label 309 record carried by transactiontx_hash. This is proven by reading that transaction on any public Cardano explorer and comparing the bytes. It needs the certificate file plus an explorer — nothing else.
Together they prove the leaf's content existed on or before the block time of
tx_hash.
What a certificate does NOT prove
The time is asserted by the public blockchain, not cryptographically bound into the proof — exactly as with OpenTimestamps and Chainpoint, you are trusting the chain's block time, never the certificate's producer. A certificate is not an eIDAS "qualified" electronic timestamp (RFC 3161 / a qualified TSA); it is a blockchain-anchored timestamp — strong corroborating evidence of a temporal claim, in the same category as other blockchain timestamps, and the wording in the file says exactly that. It says nothing about who authored the content (that is an optional record signature — see Signatures), and it does not prove the content was not known earlier. It proves existence by a deadline, not authorship and not novelty.
Why the proof is unsigned
A strict IETF COSE Receipt is a COSE_Sign1 whose payload is the Merkle root,
signed by some authority. Here the authority is the blockchain, not a key we
hold — so signing the root with our key would reintroduce server trust and break
the standalone-verifiable property the whole standard rests on. Instead, the
certificate emits the IETF inclusion-proof CBOR structure exactly as specified,
and carries the blockchain anchor in place of the signature. The proof math
is byte-identical to the IETF encoding; it is deliberately unsigned and
blockchain-anchored.
The JSON certificate
The primary artifact is label-309-inclusion-certificate-v1: a human- and
machine-readable JSON file. One file covers one OR many leaves, and every item
embeds its full sibling path, so the file re-verifies forever — no Arweave fetch,
no gateway, no original publisher required.
{
"format": "label-309-inclusion-certificate-v1",
"generated_at": "2026-06-16T12:00:00.000Z", // informational only, never trusted
"anchor": {
"chain": "cardano",
"network": "mainnet",
"tx_hash": "…64hex…",
"metadata_label": 309,
"block_time": 1781611200, // POSIX seconds — explorer-asserted
"block_time_iso": "2026-06-16T12:00:00.000Z",
"block_height": 12345678, // optional; explorer-asserted
"explorer_urls": [
"https://cardanoscan.io/transaction/…",
"https://adastat.net/transactions/…"
]
},
"merkle": {
"tree_alg": "rfc9162-sha256",
"root": "…64hex…",
"tree_size": 1024, // === the on-chain leaf_count
"leaves_list_uri": "ar://<txid>" // optional source reference
},
"items": [
{
"leaf": "…64hex…", // the content hash committed as a leaf
"leaf_alg": "sha2-256", // how to hash a file to reproduce `leaf`
"index": 42,
"proof": ["…64hex…", "…64hex…"], // siblings, leaf→root; [] for a single-leaf tree
"verified": true, // proof recomputes to merkle.root at build time
"label": "contract.pdf" // optional note/filename
}
],
"claim": "Each listed hash was included in a Merkle tree whose root was published on the Cardano blockchain in the referenced transaction under metadata label 309; therefore each hash provably existed on or before the stated block time.",
"verification": {
"method": "RFC 9162 (Certificate Transparency) SHA-256 inclusion proof. For each item, recompute the Merkle root from leaf+index+tree_size+proof and compare to merkle.root; then confirm merkle.root equals the merkle[].root in the label 309 record of anchor.tx_hash on any public Cardano explorer.",
"requires_trust_in_cardanowall": false,
"time_asserted_by": "Cardano blockchain (block time), via public explorers"
}
}A few details worth knowing:
root,leaf, and every entry ofproof[]are raw 32-byte values rendered as hex. Producers emit lowercase; a verifier accepts either case and rejects any non-hex character or odd-length string.- The stored
"verified": trueis the producer's result at build time. A verifier never trusts it — it recomputes the proof itself and reports its own verdict. A leaf that was not found in the tree is recorded with"verified": falseand an"error"field, never silently dropped, so the file is honest about misses. block_timeis the explorer-asserted POSIX timestamp of the including block.block_time_isois its UTC rendering — convenience only.
The CBOR inclusion proof (COSE / RFC 9162 aligned)
Alongside the JSON, each item can be exported as a compact .cbor artifact whose
proof structure is byte-identical to
draft-ietf-cose-merkle-tree-proofs.
This means any RFC 9162 / COSE verifiable-data-structure verifier can read the
proof math directly — the interop kernel is standard, not bespoke. It carries
no absolute block time and no legal prose (that lives in the JSON); it is the
portable proof core only, and it is blockchain-anchored and unsigned for the
reason above.
The bare IETF inclusion proof — bstr .cbor [tree_size, leaf_index, inclusion_path], the verifiable-data-structure (vds) value 1 for
RFC 9162 SHA-256 — is extractable on its own for a pure COSE verifier; the
Cardano anchor is carried beside it as a small map in place of the Sign1
signature.
Build and verify with the tooling
The certificate format is part of the public, gateway-agnostic tooling: the
@cardanowall/sdk-ts SDK (with Python and Rust byte-parity twins), the
cardanowall CLI, and the verifier surfaces in the apps. The build and verify
math is pure and offline — only resolving fresh chain facts touches the network.
With the CLI
certificate build takes the leaves-list, the targets (raw leaf hex, or files to
hash), and the transaction whose anchor facts it resolves. certificate verify
re-runs the inclusion proof per item and prints the anchor you still need to
confirm on chain:
# Build a certificate for two files against a published root.
cardanowall certificate build \
--leaves-list leaves.cbor \
--tx <tx-hash> \
--file contract.pdf --file exhibit-a.png \
--out contract.cert.json
# Re-verify the proofs offline — no network, no trust in the producer.
cardanowall certificate verify contract.cert.jsonExit code 0 means every item's proof recomputes to the root; a non-zero code
flags an inclusion failure, bad input, or an IO error — so it drops straight into
CI. Each single item can also be extracted into the canonical
{ tree_alg, tree_size, index, leaf, proof[] } shape and checked with
cardanowall merkle verify.
With the TypeScript SDK
The certificate API is pure — you fetch the leaves-list bytes with the
platform's own fetch and hand them in; the crypto path never reaches the
network:
import { certificate, merkle } from '@cardanowall/sdk-ts';
// `leaves` comes from decodeLeavesList(...) over the fetched leaves-list bytes.
const cert = certificate.buildInclusionCertificate({
anchor, // chain facts resolved from the tx
merkle: { treeAlg: 'rfc9162-sha256', root, treeSize: leaves.length },
leaves,
targets: [{ leaf, leafAlg: 'sha2-256', label: 'contract.pdf' }],
});
// Pure re-verification from the certificate alone — no Arweave, no chain.
const result = certificate.verifyInclusionCertificate(cert);
console.log(result.ok); // true when every item's proof recomputes to root
console.log(result.anchorClaim); // the anchor you confirm on a public explorer
// Each item also re-verifies through the plain Merkle predicate.
const item = cert.items[0];
const ok = merkle.merkleSha2256VerifyInclusion(
leafBytes, item.index, cert.merkle.tree_size, proofBytes, rootBytes,
);verifyInclusionCertificate reports the proof verdict and echoes the claimed
anchor; confirming that anchor on chain is your separate, explicit step — the
result object says so. The same module is mirrored byte-for-byte in the Python
(certificate) and Rust (certificate) SDKs.
In the browser, on a transaction page
A label 309 record carrying a merkle[] commitment shows an inclusion panel on
its transaction page. Paste one or more hex hashes, or drop the original files to
hash them client-side; the page fetches the leaves-list straight from
content-addressed storage in your browser, recomputes each proof, shows a
green/red verdict per item, and offers the JSON, CBOR, and a printable PDF for
download. The PDF embeds the full JSON as a file attachment, so it is itself the
machine-verifiable artifact — not just a picture of one. None of this touches a
private server.
The verification algorithm, end to end
To verify a certificate independently — in your own code, with no tooling from us — do exactly this:
- Reject malformed fields. Every
root/leaf/proof[]entry must be even- length hex decoding to 32 bytes;tree_sizeand eachindexmust be safe integers withindex < tree_sizeand1 ≤ tree_size ≤ 2³² − 1. - Recompute each item's root. Using RFC 9162 §2.1.3.2, fold the leaf
(
leaf = SHA-256(0x00 ‖ leaf_digest)) with its sibling path (node = SHA-256(0x01 ‖ L ‖ R)), splitting at the largest power of two strictly below the running subtree size, and compare the result tomerkle.rootbyte-for-byte. A single-leaf tree has an empty proof. - Confirm the anchor on chain. Fetch
anchor.tx_hashon any public Cardano explorer, read its label 309 metadata, and confirmmerkle.rootequals the record'smerkle[].root. The block time you read there is the timestamp the certificate asserts.
Steps 1–2 are the self-contained part — they never leave your machine. Step 3 is the one network read, and it goes to an explorer you choose, never to the certificate's producer.
One file, verifiable forever
Because every sibling on the path is embedded, a certificate keeps verifying long after the leaves-list, the original gateway, or the producer are gone. The two checks — recompute the root from the file, confirm the root on chain — are all anyone ever needs. See Batch with Merkle for how the root and leaves-list are built in the first place, and Verification for the full verifier model the chain-side check fits into.