Guide

Guide · Parte 5 di 8

Batch con Merkle

A volte non hai un solo file: ne hai un migliaio. Una cartella di documenti, uno stream di eventi, le righe di log di un'intera giornata. Ancorare ciascuno nella propria transazione è uno spreco. Conviene invece calcolare l'hash di ogni elemento come foglia, ripiegare le foglie in un'unica radice Merkle e pubblicare quella singola radice. Le foglie ordinate restano off-chain; in seguito puoi dimostrare che un qualsiasi elemento faceva parte dell'insieme con una prova che cresce solo con il logaritmo della dimensione del batch.

La parte importante: la costruzione dell'albero e la verifica delle prove avvengono interamente offline. Nessun gateway, nessun account, nessuna rete. Solo la pubblicazione della radice tocca un gateway; tutto il resto è pura computazione, eseguibile ovunque e per sempre.

Con la CLI

Passa a merkle build i file che vuoi ancorare. Ne calcola l'hash di ciascuno come foglia, costruisce la radice secondo RFC 9162 ed emette la lista canonica delle foglie:

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

Puoi anche fornire foglie precalcolate, un digest SHA-256 esadecimale a 64 caratteri per riga, su stdin oppure tramite --in leaves.txt. Conserva la root (è ciò che pubblichi) e la lista delle foglie (servirà a chiunque debba dimostrare l'inclusione in seguito).

Per dimostrare che un elemento appartiene a una radice pubblicata, scrivi un piccolo file di prova con il percorso di audit e verificalo, interamente offline, rispetto a qualsiasi radice di cui ti fidi:

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

La struttura di proof.json è { tree_alg, tree_size, index, leaf, proof[] }; passa --leaf <hex> per sovrascrivere la foglia del file. Il codice di uscita 0 significa che la foglia è nell'albero, 1 che non lo è: si inserisce direttamente in un job di CI.

Pubblicare la radice è l'unico passaggio che tocca la rete. Passa a submit --merkle la lista delle foglie emessa da merkle build (o un semplice file con una foglia esadecimale a 64 caratteri per riga); il gateway ancora la radice e conserva la lista. Una pubblicazione Merkle può anche sostituire direttamente un record precedente: passa --supersedes <tx-hash> per contrassegnare questo batch come successore di una radice anteriore, per esempio una cartella ri-raggruppata o una giornata di log corretta:

cardanowall submit \
  --merkle ./leaves.txt \
  --supersedes <tx-hash> \
  --base-url https://your-gateway.example \
  --api-key "$CW_API_KEY"

Con l'SDK TypeScript

Calcola l'hash dei tuoi elementi come foglie, costruisci la radice e le prove in locale, poi pubblica solo la radice tramite un 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) è un predicato booleano puro: non tocca mai la rete e non solleva eccezioni su una prova errata, restituisce semplicemente false. Chiunque possieda la lista delle foglie può ricalcolare la radice e riderivare qualsiasi prova; chi ha pubblicato non è mai coinvolto.

La pubblicazione della radice è l'unico passaggio che richiede un gateway. publishMerkle stabilisce da sé il prezzo della pubblicazione: misura il record esatto, blocca un prezzo entro il tuo tetto maxUsdMicros, carica la lista delle foglie e ancora l'impegno on-chain. L'SDK calcola la radice in locale; non stimi mai la dimensione del record né blocchi un preventivo a mano:

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

const published = await client.poe.publishMerkle({
  leaves, // raw 32-byte digests or hex strings
  leafAlg: 'sha2-256', // advisory: names how the leaves were computed
  maxUsdMicros: 2_000_000n, // refuse to spend more than $2
});

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

Con l'SDK Python

Il gemello byte per byte costruisce lo stesso albero e le stesse prove:

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

Con l'SDK Rust

Il crate cardanowall costruisce offline lo stesso albero e le stesse prove, poi pubblica solo la radice tramite il client gateway:

use cardanowall::client::{
    Label309Client, Label309ClientConfig, MerkleLeaf, PublishMerkleInput,
};
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. `publish_merkle`
    // quotes the exact record size internally and enforces the price cap.
    let client = Label309Client::new(Label309ClientConfig {
        base_url: Some("https://your-gateway.example".into()),
        api_key: std::env::var("CW_API_KEY").ok(),
    })?;

    let published = client.poe().publish_merkle(
        &PublishMerkleInput::new(leaves.iter().map(|l| MerkleLeaf::Bytes(l.to_vec())).collect())
            .with_leaf_alg("sha2-256") // advisory: names how the leaves were computed
            .with_max_usd_micros(2_000_000), // refuse to spend more than $2
    )?;

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

Una radice, molti elementi, zero fiducia

La radice si impegna su ogni foglia e sulla sua posizione. Mesi dopo, con la sola lista delle foglie e la radice on-chain, chiunque può riderivare una prova e confermare che un elemento faceva parte del batch, senza il tuo gateway, il tuo server o la tua collaborazione. Vedi Contenuto e hashing per il modo in cui vengono calcolati gli hash delle foglie, e Pubblica la tua prima PoE per il flusso a singolo elemento.