Every Symbol block header has a stateHash that covers the full chain state, including all mosaic definitions.
By requesting a proof and verifying it locally, you can confirm that the data a node returns matches what is actually
recorded on chain, without having to trust the node or run one yourself.
This tutorial shows how to fetch a mosaic's definition from the API, serialize it into the same binary format used by
the chain, and verify the result against the block's stateHash using a Patricia tree proof.
importhashlibimportjsonimportosimporturllib.requestfrombinasciiimportunhexlifyfromsymbolchain.BufferWriterimportBufferWriterfromsymbolchain.CryptoTypesimportHash256fromsymbolchain.symbol.Merkleimport(deserialize_patricia_tree_nodes,prove_patricia_merkle)NODE_URL=os.getenv('NODE_URL','https://reference.symboltest.net:3001')print(f'Using node {NODE_URL}')try:# Fetch the network currency mosaic IDurl=f'{NODE_URL}/network/properties'withurllib.request.urlopen(url)asresponse:props=json.loads(response.read().decode())raw_id=props['chain']['currencyMosaicId']mosaic_id=int(raw_id.replace("'",""),16)mosaic_id_hex=f'{mosaic_id:016X}'print(f'Currency mosaic ID: {mosaic_id_hex}')# Fetch the mosaic propertiesmosaic_path=f'/mosaics/{mosaic_id_hex}'print(f'Fetching mosaic from {mosaic_path}')url=f'{NODE_URL}{mosaic_path}'withurllib.request.urlopen(url)asresponse:mosaic_data=json.loads(response.read().decode())mosaic=mosaic_data['mosaic']print(json.dumps(mosaic,indent=2))# Serialize and hash the mosaic propertieswriter=BufferWriter()writer.write_int(int(mosaic['version']),2)writer.write_int(int(mosaic['id'],16),8)writer.write_int(int(mosaic['supply']),8)writer.write_int(int(mosaic['startHeight']),8)writer.write_bytes(unhexlify(mosaic['ownerAddress']))writer.write_int(int(mosaic['revision']),4)writer.write_int(int(mosaic['flags']),1)writer.write_int(int(mosaic['divisibility']),1)writer.write_int(int(mosaic['duration']),8)hashed_value=Hash256(hashlib.sha3_256(writer.buffer).digest())print(f'Hashed value: {hashed_value}')# Hash the mosaic ID to get the encoded keywriter=BufferWriter()writer.write_int(int(mosaic['id'],16),8)encoded_key=Hash256(hashlib.sha3_256(writer.buffer).digest())print(f'Encoded key: {encoded_key}')# Fetch the current network heighturl=f'{NODE_URL}/chain/info'withurllib.request.urlopen(url)asresponse:chain_info=json.loads(response.read().decode())height=int(chain_info['height'])print(f'Current height: {height}')# Fetch the block's state hash and rootsblock_path=f'/blocks/{height}'print(f'Fetching block from {block_path}')url=f'{NODE_URL}{block_path}'withurllib.request.urlopen(url)asresponse:block_data=json.loads(response.read().decode())state_hash=Hash256(block_data['block']['stateHash'])sub_cache_key='stateHashSubCacheMerkleRoots'roots=[Hash256(r)forrinblock_data['meta'][sub_cache_key]]print(f'State hash: {state_hash}')# Fetch the patricia tree pathtree_url=f'/mosaics/{mosaic_id_hex}/merkle'print(f'Fetching tree path from {tree_url}')url=f'{NODE_URL}{tree_url}'withurllib.request.urlopen(url)asresponse:tree_data=json.loads(response.read().decode())merkle_path=deserialize_patricia_tree_nodes(unhexlify(tree_data['raw']))print(f'Tree path: {len(merkle_path)} nodes')key_hex=str(encoded_key)key_pos=0fori,nodeinenumerate(merkle_path):key_pos+=node.path.sizepath_str=(f' path: {node.hex_path}'ifnode.path.sizeelse'')ifhasattr(node,'value'):print(f' [{i}] leaf{path_str} value: {node.value}')else:nibble=key_hex[key_pos]key_pos+=1active=[f'{j:X}'forj,linkinenumerate(node.links)iflink]print(f' [{i}] branch{path_str}'f' links: [{",".join(active)}]'f' -> follow {nibble}')# Verify the mosaic stateresult=prove_patricia_merkle(encoded_key,hashed_value,merkle_path,state_hash,roots)ifresult.name=='VALID_POSITIVE':print(f'Mosaic {mosaic_id_hex} state verified at height {height}')else:raiseRuntimeError(f'Mosaic {mosaic_id_hex} proof failed: {result.name}')exceptExceptionase:print(e)
importcryptofrom'crypto';import{Hash256,utils}from'symbol-sdk';import{deserializePatriciaTreeNodes,provePatriciaMerkle}from'symbol-sdk/symbol';const{hexToUint8,intToBytes}=utils;constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';console.log('Using node',NODE_URL);constsha3_256=(data)=>crypto.createHash('sha3-256').update(data).digest();constconcat=(...arrays)=>{consttotal=arrays.reduce((n,a)=>n+a.length,0);constbuf=newUint8Array(total);letoff=0;for(constaofarrays){buf.set(a,off);off+=a.length;}returnbuf;};try{// Fetch the network currency mosaic IDconstpropsRes=awaitfetch(`${NODE_URL}/network/properties`);constprops=awaitpropsRes.json();constrawId=props.chain.currencyMosaicId;constmosaicId=BigInt(rawId.replaceAll("'",''));constmosaicIdHex=mosaicId.toString(16).toUpperCase().padStart(16,'0');console.log('Currency mosaic ID:',mosaicIdHex);// Fetch the mosaic propertiesconstmosaicPath=`/mosaics/${mosaicIdHex}`;console.log('Fetching mosaic from',mosaicPath);constmosaicRes=awaitfetch(`${NODE_URL}${mosaicPath}`);constmosaicData=awaitmosaicRes.json();constmosaic=mosaicData.mosaic;console.log(JSON.stringify(mosaic,undefined,2));// Serialize and hash the mosaic propertiesconstserialized=concat(intToBytes(parseInt(mosaic.version),2),intToBytes(BigInt(`0x${mosaic.id}`),8),intToBytes(BigInt(mosaic.supply),8),intToBytes(BigInt(mosaic.startHeight),8),hexToUint8(mosaic.ownerAddress),intToBytes(parseInt(mosaic.revision),4),intToBytes(parseInt(mosaic.flags),1),intToBytes(parseInt(mosaic.divisibility),1),intToBytes(BigInt(mosaic.duration),8));consthashedValue=newHash256(sha3_256(serialized));console.log('Hashed value:',hashedValue.toString());// Hash the mosaic ID to get the encoded keyconstkeyBytes=intToBytes(BigInt(`0x${mosaic.id}`),8);constencodedKey=newHash256(sha3_256(keyBytes));console.log('Encoded key:',encodedKey.toString());// Fetch the current network heightconstchainRes=awaitfetch(`${NODE_URL}/chain/info`);constchainInfo=awaitchainRes.json();constheight=parseInt(chainInfo.height,10);console.log('Current height:',height);// Fetch the block's state hash and rootsconstblockPath=`/blocks/${height}`;console.log('Fetching block from',blockPath);constblockRes=awaitfetch(`${NODE_URL}${blockPath}`);constblockData=awaitblockRes.json();conststateHash=newHash256(blockData.block.stateHash);constroots=blockData.meta.stateHashSubCacheMerkleRoots.map(r=>newHash256(r));console.log('State hash:',stateHash.toString());// Fetch the patricia tree pathconsttreeUrl=`/mosaics/${mosaicIdHex}/merkle`;console.log('Fetching tree path from',treeUrl);consttreeRes=awaitfetch(`${NODE_URL}${treeUrl}`);consttreeData=awaittreeRes.json();constmerklePath=deserializePatriciaTreeNodes(hexToUint8(treeData.raw));console.log('Tree path:',merklePath.length,'nodes');constkeyHex=encodedKey.toString();letkeyPos=0;merklePath.forEach((node,i)=>{keyPos+=node.path.size;constpathStr=node.path.size?` path: ${node.hexPath}`:'';if('value'innode){console.log(` [${i}] leaf${pathStr} value: ${node.value}`);}else{constnibble=keyHex[keyPos];keyPos+=1;constactive=node.links.map((l,j)=>(l?j.toString(16).toUpperCase():null)).filter(x=>x!==null);console.log(` [${i}] branch${pathStr}`+` links: [${active}]`+` -> follow ${nibble}`);}});// Verify the mosaic stateconstresult=provePatriciaMerkle(encodedKey,hashedValue,merklePath,stateHash,roots);if(result===0x0001){// VALID_POSITIVEconsole.log(`Mosaic ${mosaicIdHex} state verified at height ${height}`);}else{thrownewError(`Mosaic ${mosaicIdHex} proof failed: ${result}`);}}catch(e){console.error(e.message);}
This example verifies the network's currency mosaic (XYM on mainnet), whose ID is discovered automatically from
/network/propertiesGET.
The currency mosaic is a convenient choice because it exists on every Symbol network, but the same process works
for any mosaic by replacing the ID.
// Fetch the network currency mosaic IDconstpropsRes=awaitfetch(`${NODE_URL}/network/properties`);constprops=awaitpropsRes.json();constrawId=props.chain.currencyMosaicId;constmosaicId=BigInt(rawId.replaceAll("'",''));constmosaicIdHex=mosaicId.toString(16).toUpperCase().padStart(16,'0');console.log('Currency mosaic ID:',mosaicIdHex);// Fetch the mosaic propertiesconstmosaicPath=`/mosaics/${mosaicIdHex}`;console.log('Fetching mosaic from',mosaicPath);constmosaicRes=awaitfetch(`${NODE_URL}${mosaicPath}`);constmosaicData=awaitmosaicRes.json();constmosaic=mosaicData.mosaic;console.log(JSON.stringify(mosaic,undefined,2));
The code starts by fetching the network currency mosaic ID from /network/propertiesGET.
The currencyMosaicId field is a hex string with embedded apostrophes (e.g. 0x72C0212E'67A08BCE), so the code strips
them before parsing.
The mosaic's full definition is then fetched from /mosaics/{mosaicId}GET.
The response includes all the fields that make up the mosaic's definition: version, id, supply,
startHeight, ownerAddress, revision, flags, divisibility, and duration.
# Serialize and hash the mosaic propertieswriter=BufferWriter()writer.write_int(int(mosaic['version']),2)writer.write_int(int(mosaic['id'],16),8)writer.write_int(int(mosaic['supply']),8)writer.write_int(int(mosaic['startHeight']),8)writer.write_bytes(unhexlify(mosaic['ownerAddress']))writer.write_int(int(mosaic['revision']),4)writer.write_int(int(mosaic['flags']),1)writer.write_int(int(mosaic['divisibility']),1)writer.write_int(int(mosaic['duration']),8)hashed_value=Hash256(hashlib.sha3_256(writer.buffer).digest())print(f'Hashed value: {hashed_value}')# Hash the mosaic ID to get the encoded keywriter=BufferWriter()writer.write_int(int(mosaic['id'],16),8)encoded_key=Hash256(hashlib.sha3_256(writer.buffer).digest())print(f'Encoded key: {encoded_key}')
// Serialize and hash the mosaic propertiesconstserialized=concat(intToBytes(parseInt(mosaic.version),2),intToBytes(BigInt(`0x${mosaic.id}`),8),intToBytes(BigInt(mosaic.supply),8),intToBytes(BigInt(mosaic.startHeight),8),hexToUint8(mosaic.ownerAddress),intToBytes(parseInt(mosaic.revision),4),intToBytes(parseInt(mosaic.flags),1),intToBytes(parseInt(mosaic.divisibility),1),intToBytes(BigInt(mosaic.duration),8));consthashedValue=newHash256(sha3_256(serialized));console.log('Hashed value:',hashedValue.toString());// Hash the mosaic ID to get the encoded keyconstkeyBytes=intToBytes(BigInt(`0x${mosaic.id}`),8);constencodedKey=newHash256(sha3_256(keyBytes));console.log('Encoded key:',encodedKey.toString());
To verify the mosaic's definition, the code must reproduce the exact hash that the chain stores internally.
This requires serializing all fields into a binary buffer in the exact field order and sizes defined by the
MosaicEntry schema, then computing the SHA3-256 hash of the result.
Nested structures
MosaicEntry contains a MosaicDefinition structure, which in turn contains a MosaicProperties
structure.
The code serializes fields from all three levels, so the full field list is longer than what MosaicEntry alone
shows.
The mosaic sub-cache Patricia tree stores each mosaic as a key-value pair in a
leaf node.
The value is the hashed value (SHA3-256 of the serialized definition).
The key is computed by hashing just the mosaic ID (8 bytes, little-endian) with SHA3-256, and is used to locate
the mosaic's leaf node in the tree.
# Fetch the current network heighturl=f'{NODE_URL}/chain/info'withurllib.request.urlopen(url)asresponse:chain_info=json.loads(response.read().decode())height=int(chain_info['height'])print(f'Current height: {height}')# Fetch the block's state hash and rootsblock_path=f'/blocks/{height}'print(f'Fetching block from {block_path}')url=f'{NODE_URL}{block_path}'withurllib.request.urlopen(url)asresponse:block_data=json.loads(response.read().decode())state_hash=Hash256(block_data['block']['stateHash'])sub_cache_key='stateHashSubCacheMerkleRoots'roots=[Hash256(r)forrinblock_data['meta'][sub_cache_key]]print(f'State hash: {state_hash}')
// Fetch the current network heightconstchainRes=awaitfetch(`${NODE_URL}/chain/info`);constchainInfo=awaitchainRes.json();constheight=parseInt(chainInfo.height,10);console.log('Current height:',height);// Fetch the block's state hash and rootsconstblockPath=`/blocks/${height}`;console.log('Fetching block from',blockPath);constblockRes=awaitfetch(`${NODE_URL}${blockPath}`);constblockData=awaitblockRes.json();conststateHash=newHash256(blockData.block.stateHash);constroots=blockData.meta.stateHashSubCacheMerkleRoots.map(r=>newHash256(r));console.log('State hash:',stateHash.toString());
The code fetches the current chain height from /chain/infoGET, then uses it to retrieve the corresponding block
header from /blocks/{height}GET.
The block's stateHash field is the SHA3-256 hash of all sub-cache Merkle roots concatenated together.
The stateHashSubCacheMerkleRoots array contains the individual root hash for each sub-cache
(accounts, mosaics, namespaces, and so on).
// Fetch the patricia tree pathconsttreeUrl=`/mosaics/${mosaicIdHex}/merkle`;console.log('Fetching tree path from',treeUrl);consttreeRes=awaitfetch(`${NODE_URL}${treeUrl}`);consttreeData=awaittreeRes.json();constmerklePath=deserializePatriciaTreeNodes(hexToUint8(treeData.raw));console.log('Tree path:',merklePath.length,'nodes');constkeyHex=encodedKey.toString();letkeyPos=0;merklePath.forEach((node,i)=>{keyPos+=node.path.size;constpathStr=node.path.size?` path: ${node.hexPath}`:'';if('value'innode){console.log(` [${i}] leaf${pathStr} value: ${node.value}`);}else{constnibble=keyHex[keyPos];keyPos+=1;constactive=node.links.map((l,j)=>(l?j.toString(16).toUpperCase():null)).filter(x=>x!==null);console.log(` [${i}] branch${pathStr}`+` links: [${active}]`+` -> follow ${nibble}`);}});
The /mosaics/{mosaicId}/merkleGET endpoint returns the raw path through the mosaic sub-cache, from the root down to
the leaf that stores the mosaic's hashed value.
deserializePatriciaTreeNodes converts this raw binary into a list of tree nodes.
For educational purposes, the code then walks the deserialized path and prints each node for inspection.
This step is not required for verification, as the SDK handles it internally in the next step.
Branch nodes show their active links and which nibble was followed to reach the
next node.
The leaf node at the end stores the remaining path nibbles and the hashed value.
# Verify the mosaic stateresult=prove_patricia_merkle(encoded_key,hashed_value,merkle_path,state_hash,roots)ifresult.name=='VALID_POSITIVE':print(f'Mosaic {mosaic_id_hex} state verified at height {height}')else:raiseRuntimeError(f'Mosaic {mosaic_id_hex} proof failed: {result.name}')
The function then verifies the proof in three stages:
Link to the block: Checks that SHA3-256(roots) matches state_hash, confirming the sub-cache roots are
genuine.
Then checks that the hash of the first node in merkle_path matches one of those roots (the mosaic sub-cache).
Walk the tree: Follows merkle_path from root to leaf, checking that each node's hash appears among its
parent's 16 links (one per nibble value 0–F).
Match the leaf: Checks that the leaf's value matches hashed_value and that the path through the tree
reconstructs encoded_key.
If all checks pass, the result is 0x0001 (VALID_POSITIVE), confirming that the mosaic definition returned by the API
is exactly what is recorded in the chain at the given height.
The mosaic definition, block header, and tree path must all reflect the same chain state.
If a new block is confirmed between requests, the state hash will have changed and the proof will fail.
When this happens, re-fetch all three pieces of data and try again.
If the proof still fails after retrying, the node may be serving incorrect data.
Using node https://reference.symboltest.net:3001
Currency mosaic ID: 72C0212E67A08BCE
Fetching mosaic from /mosaics/72C0212E67A08BCE
{
"version": 1,
"id": "72C0212E67A08BCE",
"supply": "8323465708553682",
"startHeight": "1",
"ownerAddress": "9889432DE263BB8FE88444A4DA28D3609BD8BB8FAE18AE95",
"revision": 1,
"flags": 2,
"divisibility": 6,
"duration": "0"
}
Hashed value: DABD58337F1BA84742EE4F0EBDC17BC9C40E07AC3A7C7D5646DD558514162EA8
Encoded key: 3A4C540D7E6C7DF3E924E9F1B7D468FF771301AF00B0476E2B7E511379BB559E
Current height: 3220296
Fetching block from /blocks/3220296
State hash: BE6796E2F0E57A9F56910F7AC6D50BB730698184AAA29CED9167CFBD3F3C6A78
Fetching tree path from /mosaics/72C0212E67A08BCE/merkle
Tree path: 5 nodes
[0] branch links: [0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F] -> follow 3
[1] branch links: [0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F] -> follow A
[2] branch links: [0,1,3,4,5,6,7,8,9,A,B,C,D,E,F] -> follow 4
[3] branch links: [4,5,7,C,E] -> follow C
[4] leaf path: 540D7E6C7DF3E924E9F1B7D468FF771301AF00B0476E2B7E511379BB559E value: DABD58337F1BA84742EE4F0EBDC17BC9C40E07AC3A7C7D5646DD558514162EA8
Mosaic 72C0212E67A08BCE state verified at height 3220296
Some highlights from the output:
Mosaic definition (lines 3-14): The full mosaic definition as returned by /mosaics/{mosaicId}GET, showing all
fields that are serialized for the proof.
Hashed value and encoded key (lines 15-16): The SHA3-256 hashes used as the value and encoded key in the Patricia
tree.
State hash (line 19): The block header's hash of all chain state.
The proof checks that the tree path traces back to this hash, which ties the verification to a specific block.
Tree path (lines 22-26): The deserialized path from root to leaf.
Each branch node shows its links and which nibble was followed (-> follow).
Every branch node consumes one nibble (hex digit) of the encoded key, and the leaf stores the remaining nibbles
that were not consumed by any branch.
Concatenating the nibbles 3, A, 4, C with the leaf path 540D7E...559E reconstructs the full encoded key
from line 16.
The leaf's value matches the hashed value from line 15.
See the state hash diagram in the Textbook for a visual representation
of this tree structure.
Proof result (line 27): The proof succeeded, confirming that the mosaic definition served by the node is identical to
what is stored on chain at height 3220296.