Guides

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:

  1. Inclusion. A given leaf sits at position index of an RFC 9162 SHA-256 Merkle tree of size tree_size whose root is root. 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.
  2. Anchoring. That root appears verbatim in the merkle[].root field of the label 309 record carried by transaction tx_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.

contract.cert.json
{
  "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 of proof[] 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": true is 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": false and an "error" field, never silently dropped, so the file is honest about misses.
  • block_time is the explorer-asserted POSIX timestamp of the including block. block_time_iso is 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:

terminal
# 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.json

Exit 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:

build-and-verify.ts
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:

  1. Reject malformed fields. Every root/leaf/proof[] entry must be even- length hex decoding to 32 bytes; tree_size and each index must be safe integers with index < tree_size and 1 ≤ tree_size ≤ 2³² − 1.
  2. 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 to merkle.root byte-for-byte. A single-leaf tree has an empty proof.
  3. Confirm the anchor on chain. Fetch anchor.tx_hash on any public Cardano explorer, read its label 309 metadata, and confirm merkle.root equals the record's merkle[].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.