コンテンツにスキップ

ブロックへのトランザクション包含の証明⚓︎

中級

Symbolの各 ブロック は、その トランザクション を マークルツリー (Merkle tree)に記録します。そのルート(根)である transactionsHash は、ブロックヘッダーに保存されます。ブロックの全トランザクションをダウンロードすることなく、このルートに対してトランザクションを検証することで、そのトランザクションがブロックに含まれていることを証明できます。

このチュートリアルでは、APIからマークル証明(Merkle proof)を取得し、特定のトランザクションがブロックの一部であることを検証する方法を説明します。

前提条件⚓︎

開始する前に:

このチュートリアルではネットワークからのデータの読み取りのみを行います。 アカウント や XYM の残高は必要ありません。

完全なコード⚓︎

このチュートリアルの完全なコード一覧を以下に示します。 詳細な手順ごとの説明は次のセクションで行います。

import json
import os
import urllib.request

from symbolchain.CryptoTypes import Hash256
from symbolchain.symbol.Merkle import MerklePart, prove_merkle

NODE_URL = os.getenv('NODE_URL', 'https://reference.symboltest.net:3001')
print(f'Using node {NODE_URL}')

TX_HASH = os.getenv('TRANSACTION_HASH',
    '99011A8DBC086E0C359E9D8A38FEC6714C33726FCD0C1B5C0F772A82400D808B')
print(f'Transaction hash: {TX_HASH}')

try:
    # Fetch the confirmed transaction to get its block height
    tx_path = f'/transactions/confirmed/{TX_HASH}'
    print(f'Fetching transaction from {tx_path}')
    with urllib.request.urlopen(f'{NODE_URL}{tx_path}') as response:
        tx_data = json.loads(response.read().decode())

    print(json.dumps(tx_data['meta'], indent=2))
    block_height = tx_data['meta']['height']
    merkle_component_hash = Hash256(
        tx_data['meta']['merkleComponentHash'])

    # Fetch the block header to get the transactions hash
    block_path = f'/blocks/{block_height}'
    print(f'Fetching block from {block_path}')
    with urllib.request.urlopen(f'{NODE_URL}{block_path}') as response:
        block_data = json.loads(response.read().decode())

    print(json.dumps({
        'height': block_data['block']['height'],
        'transactionsHash': block_data['block']['transactionsHash'],
    }, indent=2))
    transactions_hash = Hash256(
        block_data['block']['transactionsHash'])

    # Fetch the merkle proof path for the transaction
    merkle_path = (f'/blocks/{block_height}'
        f'/transactions/{TX_HASH}/merkle')
    print('Fetching merkle proof:')
    print(f'  {merkle_path}')
    with urllib.request.urlopen(f'{NODE_URL}{merkle_path}') as response:
        merkle_data = json.loads(response.read().decode())

    print(json.dumps(merkle_data, indent=2))

    # Convert the API response to the format expected by the SDK
    merkle_proof_path = [
        MerklePart(Hash256(part['hash']), part['position'] == 'left')
        for part in merkle_data['merklePath']
    ]
    print(f'  Merkle path length: {len(merkle_proof_path)}')

    # Verify that the transaction is included in the block
    is_proven = prove_merkle(
        merkle_component_hash, merkle_proof_path,
        transactions_hash)

    if is_proven:
        print(
            f'Transaction {TX_HASH[:16]}...'
            f' proven in block {block_height}')
    else:
        raise RuntimeError(
            f'Transaction {TX_HASH[:16]}...'
            f' NOT proven in block {block_height}')

except Exception as e:
    print(e)

Download source

import { Hash256 } from 'symbol-sdk';
import { proveMerkle } from 'symbol-sdk/symbol';

const NODE_URL = process.env.NODE_URL ||
    'https://reference.symboltest.net:3001';
console.log('Using node', NODE_URL);

const TX_HASH = process.env.TRANSACTION_HASH ||
    '99011A8DBC086E0C359E9D8A38FEC6714C33726FCD0C1B5C0F772A82400D808B';
console.log('Transaction hash:', TX_HASH);

try {
    // Fetch the confirmed transaction to get its block height
    const txPath = `/transactions/confirmed/${TX_HASH}`;
    console.log('Fetching transaction from', txPath);
    const txResponse = await fetch(`${NODE_URL}${txPath}`);
    const txData = await txResponse.json();

    console.log(JSON.stringify(txData.meta, undefined, 2));
    const blockHeight = txData.meta.height;
    const merkleComponentHash = new Hash256(
        txData.meta.merkleComponentHash);

    // Fetch the block header to get the transactions hash
    const blockPath = `/blocks/${blockHeight}`;
    console.log('Fetching block from', blockPath);
    const blockResponse = await fetch(`${NODE_URL}${blockPath}`);
    const blockData = await blockResponse.json();

    console.log(JSON.stringify({
        height: blockData.block.height,
        transactionsHash: blockData.block.transactionsHash
    }, undefined, 2));
    const transactionsHash = new Hash256(
        blockData.block.transactionsHash);

    // Fetch the merkle proof path for the transaction
    const merklePath = `/blocks/${blockHeight}` +
        `/transactions/${TX_HASH}/merkle`;
    console.log('Fetching merkle proof:');
    console.log(`  ${merklePath}`);
    const merkleResponse = await fetch(`${NODE_URL}${merklePath}`);
    const merkleData = await merkleResponse.json();

    console.log(JSON.stringify(merkleData, undefined, 2));

    // Convert the API response to the format expected by the SDK
    const merkleProofPath = merkleData.merklePath.map(
        part => ({
            hash: new Hash256(part.hash),
            isLeft: 'left' === part.position
        }));
    console.log('  Merkle path length:', merkleProofPath.length);

    // Verify that the transaction is included in the block
    const isProven = proveMerkle(
        merkleComponentHash, merkleProofPath, transactionsHash);

    if (isProven) {
        console.log(
            `Transaction ${TX_HASH.slice(0, 16)}...` +
            ` proven in block ${blockHeight}`);
    } else {
        throw new Error(
            `Transaction ${TX_HASH.slice(0, 16)}...` +
            ` NOT proven in block ${blockHeight}`);
    }
} catch (e) {
    console.error(e.message);
}

Download source

//JAVA 21+
//DEPS org.symbol:symbol-sdk:3.3.1

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.symbol.sdk.CryptoTypes;
import org.symbol.sdk.symbol.Merkle;
import org.symbol.sdk.symbol.Merkle.MerklePart;

public final class ProveTransaction {
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

    private static final HttpClient HTTP_CLIENT =
        HttpClient.newHttpClient();

    private final String nodeUrl = System.getenv().getOrDefault(
        "NODE_URL", "https://reference.symboltest.net:3001");

    private final String txHash = System.getenv().getOrDefault(
        "TRANSACTION_HASH",
        "99011A8DBC086E0C359E9D8A38FEC6714C33726FCD0C1B5C0F772A8240"
            + "0D808B");

    public static void main(final String[] args) {
        try {
            new ProveTransaction().run();
        } catch (final Exception ex) {
            System.out.println(null == ex.getMessage()
                ? ex.toString()
                : ex.getMessage());
        }
    }

    private void run() throws IOException, InterruptedException {
        System.out.printf("Using node %s%n", nodeUrl);
        System.out.printf("Transaction hash: %s%n", txHash);


        // Fetch the confirmed transaction to get its block height
        final String txPath = "/transactions/confirmed/" + txHash;
        System.out.printf("Fetching transaction from %s%n", txPath);
        final JsonNode txData = getJson(txPath);

        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(txData.get("meta")));
        final long blockHeight = txData.get("meta").get("height")
            .asLong();
        final CryptoTypes.Hash256 merkleComponentHash =
            new CryptoTypes.Hash256(txData.get("meta")
                .get("merkleComponentHash").asText());


        // Fetch the block header to get the transactions hash
        final String blockPath = "/blocks/" + blockHeight;
        System.out.printf("Fetching block from %s%n", blockPath);
        final JsonNode blockData = getJson(blockPath);

        final JsonNode blockSummary = JSON_MAPPER.createObjectNode()
            .put("height", blockData.get("block")
                .get("height").asText())
            .put("transactionsHash", blockData.get("block")
                .get("transactionsHash").asText());
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(blockSummary));
        final CryptoTypes.Hash256 transactionsHash =
            new CryptoTypes.Hash256(blockData.get("block")
                .get("transactionsHash").asText());

        // Fetch the merkle proof path for the transaction
        final String merklePath = "/blocks/" + blockHeight
            + "/transactions/" + txHash + "/merkle";
        System.out.println("Fetching merkle proof:");
        System.out.printf("  %s%n", merklePath);
        final JsonNode merkleData = getJson(merklePath);

        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(merkleData));

        // Convert the API response to the format expected by the SDK
        final List<MerklePart> merkleProofPath = new ArrayList<>();
        for (final JsonNode part : merkleData.get("merklePath"))
            merkleProofPath.add(new MerklePart(
                new CryptoTypes.Hash256(part.get("hash").asText()),
                "left".equals(part.get("position").asText())));
        System.out.printf("  Merkle path length: %d%n",
            merkleProofPath.size());


        // Verify that the transaction is included in the block
        final boolean isProven = Merkle.proveMerkle(
            merkleComponentHash, merkleProofPath, transactionsHash);

        if (isProven) {
            System.out.printf(
                "Transaction %s... proven in block %d%n",
                txHash.substring(0, 16), blockHeight);
        } else {
            throw new IllegalStateException(String.format(
                "Transaction %s... NOT proven in block %d",
                txHash.substring(0, 16), blockHeight));
        }
    }

    private JsonNode getJson(
        final String path
    ) throws IOException, InterruptedException {
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + path)).GET().build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        return JSON_MAPPER.readTree(response.body());
    }
}

Download source

このスニペットは、証明するトランザクションの ハッシュ を TRANSACTION_HASH 環境変数から読み取ります。設定されていない場合は、Symbolテストネットのブロック 55 にある既知のトランザクションがデフォルトとして使用されます。

コード解説⚓︎

承認済みトランザクションの取得⚓︎

    # Fetch the confirmed transaction to get its block height
    tx_path = f'/transactions/confirmed/{TX_HASH}'
    print(f'Fetching transaction from {tx_path}')
    with urllib.request.urlopen(f'{NODE_URL}{tx_path}') as response:
        tx_data = json.loads(response.read().decode())

    print(json.dumps(tx_data['meta'], indent=2))
    block_height = tx_data['meta']['height']
    merkle_component_hash = Hash256(
        tx_data['meta']['merkleComponentHash'])
    // Fetch the confirmed transaction to get its block height
    const txPath = `/transactions/confirmed/${TX_HASH}`;
    console.log('Fetching transaction from', txPath);
    const txResponse = await fetch(`${NODE_URL}${txPath}`);
    const txData = await txResponse.json();

    console.log(JSON.stringify(txData.meta, undefined, 2));
    const blockHeight = txData.meta.height;
    const merkleComponentHash = new Hash256(
        txData.meta.merkleComponentHash);
        // Fetch the confirmed transaction to get its block height
        final String txPath = "/transactions/confirmed/" + txHash;
        System.out.printf("Fetching transaction from %s%n", txPath);
        final JsonNode txData = getJson(txPath);

        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(txData.get("meta")));
        final long blockHeight = txData.get("meta").get("height")
            .asLong();
        final CryptoTypes.Hash256 merkleComponentHash =
            new CryptoTypes.Hash256(txData.get("meta")
                .get("merkleComponentHash").asText());

コードは、 /transactions/confirmed/{transactionId} GET エンドポイントから承認済みのトランザクションを取得します。

meta.height フィールドはトランザクションが承認されたブロックの高さであり、次のステップでブロックヘッダーを取得するために必要です。

レスポンスには merkleComponentHash も含まれています。これはブロックのマークルツリーで使用されるリーフ(葉)ハッシュです。通常のトランザクションでは、この値はトランザクションハッシュと等しくなります。 アグリゲートトランザクション の場合、トランザクションハッシュと連署者の公開鍵を連結したものの SHA3-256 ハッシュとして計算されます。

ブロックヘッダーの取得⚓︎

    # Fetch the block header to get the transactions hash
    block_path = f'/blocks/{block_height}'
    print(f'Fetching block from {block_path}')
    with urllib.request.urlopen(f'{NODE_URL}{block_path}') as response:
        block_data = json.loads(response.read().decode())

    print(json.dumps({
        'height': block_data['block']['height'],
        'transactionsHash': block_data['block']['transactionsHash'],
    }, indent=2))
    transactions_hash = Hash256(
        block_data['block']['transactionsHash'])
    // Fetch the block header to get the transactions hash
    const blockPath = `/blocks/${blockHeight}`;
    console.log('Fetching block from', blockPath);
    const blockResponse = await fetch(`${NODE_URL}${blockPath}`);
    const blockData = await blockResponse.json();

    console.log(JSON.stringify({
        height: blockData.block.height,
        transactionsHash: blockData.block.transactionsHash
    }, undefined, 2));
    const transactionsHash = new Hash256(
        blockData.block.transactionsHash);
        // Fetch the block header to get the transactions hash
        final String blockPath = "/blocks/" + blockHeight;
        System.out.printf("Fetching block from %s%n", blockPath);
        final JsonNode blockData = getJson(blockPath);

        final JsonNode blockSummary = JSON_MAPPER.createObjectNode()
            .put("height", blockData.get("block")
                .get("height").asText())
            .put("transactionsHash", blockData.get("block")
                .get("transactionsHash").asText());
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(blockSummary));
        final CryptoTypes.Hash256 transactionsHash =
            new CryptoTypes.Hash256(blockData.get("block")
                .get("transactionsHash").asText());

/blocks/{height} GET エンドポイントは、 transactionsHash フィールドを含むブロックのメタデータを返します。このハッシュは、ブロック内の各トランザクションの merkleComponentHash から構築されたマークルツリーのルートです。

コードは16進文字列を Hash256 オブジェクトにラップします。これは 関数が期待する形式です。

マークル証明パスの取得⚓︎

    # Fetch the merkle proof path for the transaction
    merkle_path = (f'/blocks/{block_height}'
        f'/transactions/{TX_HASH}/merkle')
    print('Fetching merkle proof:')
    print(f'  {merkle_path}')
    with urllib.request.urlopen(f'{NODE_URL}{merkle_path}') as response:
        merkle_data = json.loads(response.read().decode())

    print(json.dumps(merkle_data, indent=2))

    # Convert the API response to the format expected by the SDK
    merkle_proof_path = [
        MerklePart(Hash256(part['hash']), part['position'] == 'left')
        for part in merkle_data['merklePath']
    ]
    print(f'  Merkle path length: {len(merkle_proof_path)}')
    // Fetch the merkle proof path for the transaction
    const merklePath = `/blocks/${blockHeight}` +
        `/transactions/${TX_HASH}/merkle`;
    console.log('Fetching merkle proof:');
    console.log(`  ${merklePath}`);
    const merkleResponse = await fetch(`${NODE_URL}${merklePath}`);
    const merkleData = await merkleResponse.json();

    console.log(JSON.stringify(merkleData, undefined, 2));

    // Convert the API response to the format expected by the SDK
    const merkleProofPath = merkleData.merklePath.map(
        part => ({
            hash: new Hash256(part.hash),
            isLeft: 'left' === part.position
        }));
    console.log('  Merkle path length:', merkleProofPath.length);
        // Fetch the merkle proof path for the transaction
        final String merklePath = "/blocks/" + blockHeight
            + "/transactions/" + txHash + "/merkle";
        System.out.println("Fetching merkle proof:");
        System.out.printf("  %s%n", merklePath);
        final JsonNode merkleData = getJson(merklePath);

        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(merkleData));

        // Convert the API response to the format expected by the SDK
        final List<MerklePart> merkleProofPath = new ArrayList<>();
        for (final JsonNode part : merkleData.get("merklePath"))
            merkleProofPath.add(new MerklePart(
                new CryptoTypes.Hash256(part.get("hash").asText()),
                "left".equals(part.get("position").asText())));
        System.out.printf("  Merkle path length: %d%n",
            merkleProofPath.size());

/blocks/{height}/transactions/{hash}/merkle GET エンドポイントは、マークル証明パス(Merkle proof path)を返します。これは、 merkleComponentHash から始めて transactionsHash を再計算するために必要な最小限の中間ハッシュのセットです(マークルツリーの各レベルに1つずつ)。

パスの各項目には以下が含まれます:

  • hash: ツリーの次のレベルを再計算するために必要な中間ハッシュ。
  • position: 前の結果と結合する際に、このハッシュが「左(left)」か「右(right)」のどちらに位置するか。

コードは各項目をハッシュとブール値(ハッシュが左側の場合は true )のペアに変換し、 関数が期待する形式に合わせます。

証明の検証⚓︎

    # Verify that the transaction is included in the block
    is_proven = prove_merkle(
        merkle_component_hash, merkle_proof_path,
        transactions_hash)

    if is_proven:
        print(
            f'Transaction {TX_HASH[:16]}...'
            f' proven in block {block_height}')
    else:
        raise RuntimeError(
            f'Transaction {TX_HASH[:16]}...'
            f' NOT proven in block {block_height}')
    // Verify that the transaction is included in the block
    const isProven = proveMerkle(
        merkleComponentHash, merkleProofPath, transactionsHash);

    if (isProven) {
        console.log(
            `Transaction ${TX_HASH.slice(0, 16)}...` +
            ` proven in block ${blockHeight}`);
    } else {
        throw new Error(
            `Transaction ${TX_HASH.slice(0, 16)}...` +
            ` NOT proven in block ${blockHeight}`);
    }
        // Verify that the transaction is included in the block
        final boolean isProven = Merkle.proveMerkle(
            merkleComponentHash, merkleProofPath, transactionsHash);

        if (isProven) {
            System.out.printf(
                "Transaction %s... proven in block %d%n",
                txHash.substring(0, 16), blockHeight);
        } else {
            throw new IllegalStateException(String.format(
                "Transaction %s... NOT proven in block %d",
                txHash.substring(0, 16), blockHeight));
        }

は、指定された位置順序に従って merkleComponentHash を証明パス内の各中間ハッシュと反復的に結合することで、マークルルートを再計算します。計算されたルートがブロックの transactionsHash と一致すれば、そのトランザクションがブロックの一部であることが証明されます。

出力⚓︎

以下に示す出力は、プログラムの典型的な実行結果に対応しています:

Using node https://reference.symboltest.net:3001
Transaction hash: 99011A8DBC086E0C359E9D8A38FEC6714C33726FCD0C1B5C0F772A82400D808B
Fetching transaction from /transactions/confirmed/99011A8DBC086E0C359E9D8A38FEC6714C33726FCD0C1B5C0F772A82400D808B
{
  "height": "55",
  "hash": "99011A8DBC086E0C359E9D8A38FEC6714C33726FCD0C1B5C0F772A82400D808B",
  "merkleComponentHash": "99011A8DBC086E0C359E9D8A38FEC6714C33726FCD0C1B5C0F772A82400D808B",
  "index": 1,
  "timestamp": "34025525",
  "feeMultiplier": 100
}
Fetching block from /blocks/55
{
  "height": "55",
  "transactionsHash": "058BED8B39E4D0335DA9EE4B29344F8A594A995B151E5CF705FBB9D106B6D52B"
}
Fetching merkle proof:
  /blocks/55/transactions/99011A8DBC086E0C359E9D8A38FEC6714C33726FCD0C1B5C0F772A82400D808B/merkle
{
  "merklePath": [
    {
      "hash": "A40B90776E7BFD66BF65125087327342589B01DFD01D5B00E599342C135AE6AF",
      "position": "left"
    },
    {
      "hash": "A54D55B20DD2CDC183C1F8687701BFCA973F8B8ACA69736EE824A1D960E3E2D6",
      "position": "right"
    },
    {
      "hash": "C779EA8E5EA495E3A24F8460C3222E779B8C80848F7EE513F570C0F1D27A6657",
      "position": "right"
    },
    {
      "hash": "1BEB7949C5A4B58536BF1046E1B0493A53C10A991449FF67D151C39BE10E1437",
      "position": "right"
    }
  ]
}
  Merkle path length: 4
Transaction 99011A8DBC086E0C... proven in block 55

出力の主なポイント:

  • トランザクションメタデータ(5行目と7行目): /transactions/confirmed/{transactionId} GET からのJSONレスポンスには、証明に必要なブロックの高さ( height )と merkleComponentHash が含まれています。

  • ブロックトランザクションハッシュ(15行目): /blocks/{height} GET からのJSONレスポンスには、そのブロックで承認された全トランザクションのマークルルートである transactionsHash が含まれています。

  • マークルパスの長さ(39行目): /blocks/{height}/transactions/{hash}/merkle GET からのJSONレスポンスには 4 つのエントリが含まれています。これはツリーが4レベルあり、ブロックに最大 \(2^4 = 16\) 個のトランザクションが含まれていることを意味します。

  • 証明結果(40行目): 計算されたルートが transactionsHash と一致し、トランザクションが正真正銘ブロック 55 の一部であることが確認されました。

エクスプローラーでトランザクションやそのブロックを調査するには、 Symbol Testnet Explorer にアクセスし、トランザクションハッシュまたはブロック高を入力してください。

結論⚓︎

このチュートリアルでは、以下の方法を説明しました:

ステップ 関連ドキュメント
承認済みトランザクションの取得 /transactions/confirmed/{transactionId} GET
ブロックヘッダーの取得 /blocks/{height} GET
マークル証明パスの取得 /blocks/{height}/transactions/{hash}/merkle GET
証明の検証

次のステップ⚓︎

同じ手順は、 /blocks/{height}/statements/{hash}/merkle GET エンドポイントを使用し、 ブロックの receiptsHash に対して検証することで、レシートの証明にも使用できます。