Guides

Guides · Part 5 of 5

Batch with Merkle

Sometimes you don't have one file — you have a thousand. A folder of documents, a stream of events, a day's worth of audit-log lines. Anchoring each one in its own transaction is wasteful. Instead, hash every item into a leaf, fold the leaves into a single Merkle root, and publish that one root. The ordered leaves stay off chain; later you can prove any single item was in the set with a proof that grows only with the logarithm of the batch size.

The important part: building the tree and verifying proofs is fully offline. No gateway, no account, no network. Only publishing the root touches a gateway — everything else is pure computation you can run anywhere, forever.

With the CLI

Hand merkle build the files you want to anchor. It hashes each into a leaf, builds the RFC 9162 root, and emits the canonical leaves-list:

cardanowall merkle build --file a.pdf --file b.pdf --file c.pdf --json

You can also feed precomputed leaves — one 64-hex SHA-256 digest per line — on stdin or via --in leaves.txt. Keep the root (publish it) and the leaves-list (everyone who needs to prove inclusion later will want it).

To prove one item belongs to a published root, write a small proof file with the audit path and verify it — entirely offline, against any root you trust:

cardanowall merkle verify --root <root-hex> --proof proof.json

The proof.json shape is { tree_alg, tree_size, index, leaf, proof[] }; pass --leaf <hex> to override the file's leaf. Exit code 0 means the leaf is in the tree, 1 means it is not — drop it straight into CI.

With the TypeScript SDK

Hash your items into leaves, build the root and proofs locally, then publish only the root through a gateway:

import { Label309Client, hash, merkle } from '@cardanowall/sdk-ts';

const items = [docA, docB, docC]; // Uint8Array content
const leaves = items.map((bytes) => hash.sha2256(bytes));

const root = merkle.merkleSha2256Root(leaves);

// Prove item 1 is in the set — no network, no gateway.
const proof = merkle.merkleSha2256InclusionProof(leaves, 1);
const ok = merkle.merkleSha2256VerifyInclusion(leaves[1], 1, leaves.length, proof, root);
console.log(ok); // true

merkleSha2256VerifyInclusion(leaf, index, treeSize, proof, root) is a pure boolean predicate — it never reaches the network and never throws on a bad proof, it just returns false. Anyone holding the leaves-list can recompute the root and re-derive any proof; the publisher is never in the loop.

Publishing the root is the one step that needs a gateway. Quote it, then publish the leaves — the SDK computes the root locally, the gateway stores the leaves-list and anchors the commitment on chain:

const client = new Label309Client({
  baseUrl: 'https://your-gateway.example',
  apiKey: process.env.CW_API_KEY,
});

const quote = await client.poe.quote({
  recordBytes: 512,
  recipientCount: 0,
  fileBytesTotal: leaves.length * 32,
});

const published = await client.poe.publishMerkle({
  leaves, // raw 32-byte digests or hex strings
  quoteId: quote.quote_id,
});

console.log(published.root, published.leaf_count, published.tx_hash, published.ar_uri);

With the Python SDK

The byte-for-byte twin builds the same tree and the same proofs:

import cardanowall

items = [doc_a, doc_b, doc_c]  # bytes
leaves = [cardanowall.hash.sha2_256(b) for b in items]

root = cardanowall.merkle.merkle_sha2_256_root(leaves)

proof = cardanowall.merkle.merkle_sha2_256_inclusion_proof(leaves, 1)
ok = cardanowall.merkle.merkle_sha2_256_verify_inclusion(
    leaves[1], 1, len(leaves), proof, root
)
print(ok)  # True

With the Rust SDK

The cardanowall crate builds the same tree and proofs offline, then publishes only the root through the gateway client:

use cardanowall::client::{
    Label309Client, Label309ClientConfig, MerkleLeaf, PublishMerkleInput, QuoteInput,
};
use cardanowall::{hash, merkle};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let items: Vec<Vec<u8>> = vec![doc_a, doc_b, doc_c]; // content bytes
    let leaves: Vec<[u8; 32]> = items.iter().map(|b| hash::sha256(b)).collect();

    // Build the root and prove inclusion — pure computation, no network.
    let root = merkle::merkle_root(&leaves)?;
    let proof = merkle::merkle_inclusion_proof(&leaves, 1)?;
    let ok = merkle::verify_inclusion(&leaves[1], 1, leaves.len(), &proof, &root);
    println!("{ok}"); // true

    // Publishing the root is the one step that needs a gateway.
    let client = Label309Client::new(Label309ClientConfig {
        base_url: Some("https://your-gateway.example".into()),
        api_key: std::env::var("CW_API_KEY").ok(),
    })?;

    let quote = client.poe().quote(&QuoteInput {
        record_bytes: 512,
        recipient_count: 0,
        file_bytes_total: (leaves.len() * 32) as u64,
    })?;

    let published = client.poe().publish_merkle(&PublishMerkleInput {
        leaves: leaves.iter().map(|l| MerkleLeaf::Bytes(l.to_vec())).collect(),
        quote_id: quote.quote_id,
        hash_alg: None,
        signer: None,
        idempotency_key: None,
    })?;

    println!("{} {} {:?}", published.root, published.leaf_count, published.tx_hash);
    Ok(())
}

One root, many items, zero trust

The root commits to every leaf and to its position. Months later — with only the leaves-list and the on-chain root — anyone can re-derive a proof and confirm an item was in the batch, without your gateway, your server, or your cooperation. See Content and hashing for how leaves are hashed, and Publish your first PoE for the single-item flow.