ガイド

ガイド · 全6部中 第5部

Merkle でまとめて記録する

ファイルが1つではなく、1000個ある場面もあります。文書を集めたフォルダ、イベントのストリーム、1日分の監査ログの行などです。これらを1件ずつ別々のトランザクションで記録するのは無駄が多い方法です。そうではなく、すべてのアイテムをハッシュ化してリーフにし、リーフを1つの Merkle ルートにまとめ上げ、そのルートだけを発行します。順序付けされたリーフはオフチェーンに残しておき、あとから、バッチサイズの対数に比例した大きさにしかならない証明で、どのアイテムも集合に含まれていたことを証明できます。

肝心なのは、ツリーの構築と証明の検証は、完全にオフラインで行えるという点です。ゲートウェイもアカウントもネットワークも要りません。ゲートウェイが必要になるのはルートの発行だけで、それ以外はすべて、どこでもいつまでも実行できる純粋な計算です。

CLI から

merkle build に、記録したいファイルを渡します。各ファイルをハッシュ化してリーフにし、RFC 9162 のルートを構築して、正規化したリーフ一覧を出力します。

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

事前計算済みのリーフ(1行につき64桁の16進 SHA-256 ダイジェスト1つ)を、標準入力または --in leaves.txt で渡すこともできます。root(これを発行します)とリーフ一覧(後で包含を証明する必要がある全員が必要とします)は保存しておいてください。

あるアイテムが発行済みのルートに属することを証明するには、監査パスを記した小さな証明ファイルを用意し、信頼するルートに対して、完全にオフラインで検証します。

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

proof.json の形は { tree_alg, tree_size, index, leaf, proof[] } です。--leaf <hex> を渡せばファイルのリーフを上書きできます。終了コード 0 はリーフがツリーに含まれること、1 は含まれないことを示すので、そのまま CI に組み込めます。

TypeScript SDK から

アイテムをハッシュ化してリーフにし、ルートと証明をローカルで構築してから、ルートだけをゲートウェイ経由で発行します。

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) は純粋なブール値の述語です。ネットワークにアクセスすることはなく、不正な証明に対しても例外を投げず、ただ false を返します。リーフ一覧さえ持っていれば、だれでもルートを再計算し、どの証明も導き直せます。発行者がそこに関わることは一切ありません。

ルートの発行だけが、ゲートウェイを必要とするステップです。見積もりを取ってからリーフを発行します。SDK がローカルでルートを計算し、ゲートウェイがリーフ一覧を保存して、そのコミットメントをチェーン上に記録します。

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);

Python SDK から

バイト単位で一致する双子が、同じツリーと同じ証明を構築します。

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

Rust SDK から

cardanowall クレートも、同じツリーと証明をオフラインで構築し、ルートだけをゲートウェイクライアント経由で発行します。

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,
        chunk_bytes: None,
    })?;

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

1つのルート、多数のアイテム、信頼ゼロ

ルートは、すべてのリーフと、その位置とにコミットします。数か月後でも、リーフ一覧とチェーン上のルートさえあれば、どのゲートウェイにもサーバーにも、そして発行した本人の協力にも頼らず、だれでも証明を導き直して、あるアイテムがバッチに含まれていたことを確認できます。リーフのハッシュ化方法についてはコンテンツとハッシュ化を、単一アイテムのフローについては最初の存在証明を発行するを参照してください。