Each Symbol block records its transactions in a
Merkle tree whose root, the transactionsHash,
is stored in the block header.
A transaction can be verified against this root to prove it was included in a block without having to download all the
block's transactions.
This tutorial shows how to fetch a Merkle proof from the API and verify that a specific transaction is part of a block.
importjsonimportosimporturllib.requestfromsymbolchain.CryptoTypesimportHash256fromsymbolchain.symbol.MerkleimportMerklePart,prove_merkleNODE_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 heighttx_path=f'/transactions/confirmed/{TX_HASH}'print(f'Fetching transaction from {tx_path}')withurllib.request.urlopen(f'{NODE_URL}{tx_path}')asresponse: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 hashblock_path=f'/blocks/{block_height}'print(f'Fetching block from {block_path}')withurllib.request.urlopen(f'{NODE_URL}{block_path}')asresponse: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 transactionmerkle_path=(f'/blocks/{block_height}'f'/transactions/{TX_HASH}/merkle')print('Fetching merkle proof:')print(f' {merkle_path}')withurllib.request.urlopen(f'{NODE_URL}{merkle_path}')asresponse:merkle_data=json.loads(response.read().decode())print(json.dumps(merkle_data,indent=2))# Convert the API response to the format expected by the SDKmerkle_proof_path=[MerklePart(Hash256(part['hash']),part['position']=='left')forpartinmerkle_data['merklePath']]print(f' Merkle path length: {len(merkle_proof_path)}')# Verify that the transaction is included in the blockis_proven=prove_merkle(merkle_component_hash,merkle_proof_path,transactions_hash)ifis_proven:print(f'Transaction {TX_HASH[:16]}...'f' proven in block {block_height}')else:raiseRuntimeError(f'Transaction {TX_HASH[:16]}...'f' NOT proven in block {block_height}')exceptExceptionase:print(e)
import{Hash256}from'symbol-sdk';import{proveMerkle}from'symbol-sdk/symbol';constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';console.log('Using node',NODE_URL);constTX_HASH=process.env.TRANSACTION_HASH||'99011A8DBC086E0C359E9D8A38FEC6714C33726FCD0C1B5C0F772A82400D808B';console.log('Transaction hash:',TX_HASH);try{// Fetch the confirmed transaction to get its block heightconsttxPath=`/transactions/confirmed/${TX_HASH}`;console.log('Fetching transaction from',txPath);consttxResponse=awaitfetch(`${NODE_URL}${txPath}`);consttxData=awaittxResponse.json();console.log(JSON.stringify(txData.meta,undefined,2));constblockHeight=txData.meta.height;constmerkleComponentHash=newHash256(txData.meta.merkleComponentHash);// Fetch the block header to get the transactions hashconstblockPath=`/blocks/${blockHeight}`;console.log('Fetching block from',blockPath);constblockResponse=awaitfetch(`${NODE_URL}${blockPath}`);constblockData=awaitblockResponse.json();console.log(JSON.stringify({height:blockData.block.height,transactionsHash:blockData.block.transactionsHash,},undefined,2));consttransactionsHash=newHash256(blockData.block.transactionsHash);// Fetch the merkle proof path for the transactionconstmerklePath=`/blocks/${blockHeight}`+`/transactions/${TX_HASH}/merkle`;console.log('Fetching merkle proof:');console.log(` ${merklePath}`);constmerkleResponse=awaitfetch(`${NODE_URL}${merklePath}`);constmerkleData=awaitmerkleResponse.json();console.log(JSON.stringify(merkleData,undefined,2));// Convert the API response to the format expected by the SDKconstmerkleProofPath=merkleData.merklePath.map(part=>({hash:newHash256(part.hash),isLeft:part.position==='left',}));console.log(' Merkle path length:',merkleProofPath.length);// Verify that the transaction is included in the blockconstisProven=proveMerkle(merkleComponentHash,merkleProofPath,transactionsHash);if(isProven){console.log(`Transaction ${TX_HASH.slice(0,16)}...`+` proven in block ${blockHeight}`);}else{thrownewError(`Transaction ${TX_HASH.slice(0,16)}...`+` NOT proven in block ${blockHeight}`);}}catch(e){console.error(e.message);}
The snippet reads the hash of the transaction to prove from the TRANSACTION_HASH environment variable.
If not set, a known transaction from block 55 of the Symbol testnet is used as the default.
# Fetch the confirmed transaction to get its block heighttx_path=f'/transactions/confirmed/{TX_HASH}'print(f'Fetching transaction from {tx_path}')withurllib.request.urlopen(f'{NODE_URL}{tx_path}')asresponse: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 heightconsttxPath=`/transactions/confirmed/${TX_HASH}`;console.log('Fetching transaction from',txPath);consttxResponse=awaitfetch(`${NODE_URL}${txPath}`);consttxData=awaittxResponse.json();console.log(JSON.stringify(txData.meta,undefined,2));constblockHeight=txData.meta.height;constmerkleComponentHash=newHash256(txData.meta.merkleComponentHash);
The meta.height field is the block height where the transaction was confirmed, needed in the following step to
retrieve the block header.
The response also contains the merkleComponentHash, which is the leaf hash used in the block's Merkle tree.
For regular transactions, this value equals the transaction hash.
For aggregate transactions, it is computed as the SHA3-256 hash of the transaction hash concatenated with the
public keys of the cosignatories.
# Fetch the block header to get the transactions hashblock_path=f'/blocks/{block_height}'print(f'Fetching block from {block_path}')withurllib.request.urlopen(f'{NODE_URL}{block_path}')asresponse: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 hashconstblockPath=`/blocks/${blockHeight}`;console.log('Fetching block from',blockPath);constblockResponse=awaitfetch(`${NODE_URL}${blockPath}`);constblockData=awaitblockResponse.json();console.log(JSON.stringify({height:blockData.block.height,transactionsHash:blockData.block.transactionsHash,},undefined,2));consttransactionsHash=newHash256(blockData.block.transactionsHash);
The /blocks/{height}GET endpoint returns block metadata, including the transactionsHash field.
This hash is the root of the Merkle tree built from the
merkleComponentHash of each transaction in the block.
The code wraps the hex string in a Hash256 object, which is the format expected by the function.
# Fetch the merkle proof path for the transactionmerkle_path=(f'/blocks/{block_height}'f'/transactions/{TX_HASH}/merkle')print('Fetching merkle proof:')print(f' {merkle_path}')withurllib.request.urlopen(f'{NODE_URL}{merkle_path}')asresponse:merkle_data=json.loads(response.read().decode())print(json.dumps(merkle_data,indent=2))# Convert the API response to the format expected by the SDKmerkle_proof_path=[MerklePart(Hash256(part['hash']),part['position']=='left')forpartinmerkle_data['merklePath']]print(f' Merkle path length: {len(merkle_proof_path)}')
// Fetch the merkle proof path for the transactionconstmerklePath=`/blocks/${blockHeight}`+`/transactions/${TX_HASH}/merkle`;console.log('Fetching merkle proof:');console.log(` ${merklePath}`);constmerkleResponse=awaitfetch(`${NODE_URL}${merklePath}`);constmerkleData=awaitmerkleResponse.json();console.log(JSON.stringify(merkleData,undefined,2));// Convert the API response to the format expected by the SDKconstmerkleProofPath=merkleData.merklePath.map(part=>({hash:newHash256(part.hash),isLeft:part.position==='left',}));console.log(' Merkle path length:',merkleProofPath.length);
The /blocks/{height}/transactions/{hash}/merkleGET endpoint returns a Merkle proof path:
the minimum set of intermediate hashes needed to recompute the transactionsHash starting from the
merkleComponentHash (one per level of the Merkle tree).
Each item in the path contains:
hash: An intermediate hash needed to recompute the next level of the tree.
position: Whether this hash sits to the left or right when combined with the previous result.
The code converts each item into a pair of hash and boolean (true if the hash is on the left), to match the format
expected by the function.
# Verify that the transaction is included in the blockis_proven=prove_merkle(merkle_component_hash,merkle_proof_path,transactions_hash)ifis_proven:print(f'Transaction {TX_HASH[:16]}...'f' proven in block {block_height}')else:raiseRuntimeError(f'Transaction {TX_HASH[:16]}...'f' NOT proven in block {block_height}')
// Verify that the transaction is included in the blockconstisProven=proveMerkle(merkleComponentHash,merkleProofPath,transactionsHash);if(isProven){console.log(`Transaction ${TX_HASH.slice(0,16)}...`+` proven in block ${blockHeight}`);}else{thrownewError(`Transaction ${TX_HASH.slice(0,16)}...`+` NOT proven in block ${blockHeight}`);}
recomputes the Merkle root by iteratively combining the merkleComponentHash with each intermediate
hash in the proof path, following the specified position order.
If the computed root matches the block's transactionsHash, the transaction is proven to be part of the block.
Transaction metadata (lines 5-6): The JSON response from /transactions/confirmed/{transactionId}GET includes
the block height and merkleComponentHash needed for the proof.
Block transactions hash (lines 15): The JSON response from /blocks/{height}GET includes the transactionsHash,
which is the Merkle root for all transactions confirmed in that block.
Merkle path length (lines 39): The JSON response from /blocks/{height}/transactions/{hash}/merkleGET
contains 4 entries, meaning the tree has 4 levels and the block contains up to 2⁴ = 16 transactions.
Proof result (line 40): The computed root matched the transactionsHash, confirming the transaction is genuinely
part of block 55.
To inspect the transaction or its block in the explorer, visit the
Symbol Testnet Explorer and enter the transaction hash or block height.