Skip to content

Prove a Mosaic's Definition⚓︎

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.

Prerequisites⚓︎

Before you start, set up your development environment. This tutorial only reads data from the network. No account or XYM balance is required.

Additionally, review how block hashes work, in particular the state hash section.

Full Code⚓︎

import hashlib
import json
import os
import urllib.request
from binascii import unhexlify

from symbolchain.BufferWriter import BufferWriter
from symbolchain.CryptoTypes import Hash256
from symbolchain.symbol.Merkle import (
    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 ID
    url = f'{NODE_URL}/network/properties'
    with urllib.request.urlopen(url) as response:
        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 properties
    mosaic_path = f'/mosaics/{mosaic_id_hex}'
    print(f'Fetching mosaic from {mosaic_path}')
    url = f'{NODE_URL}{mosaic_path}'
    with urllib.request.urlopen(url) as response:
        mosaic_data = json.loads(response.read().decode())
    mosaic = mosaic_data['mosaic']
    print(json.dumps(mosaic, indent=2))

    # Serialize and hash the mosaic properties
    writer = 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 key
    writer = 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 height
    url = f'{NODE_URL}/chain/info'
    with urllib.request.urlopen(url) as response:
        chain_info = json.loads(response.read().decode())
    height = int(chain_info['height'])
    print(f'Current height: {height}')

    # Fetch the block's state hash and roots
    block_path = f'/blocks/{height}'
    print(f'Fetching block from {block_path}')
    url = f'{NODE_URL}{block_path}'
    with urllib.request.urlopen(url) as response:
        block_data = json.loads(response.read().decode())
    state_hash = Hash256(block_data['block']['stateHash'])
    sub_cache_key = 'stateHashSubCacheMerkleRoots'
    roots = [Hash256(r) for r in block_data['meta'][sub_cache_key]]
    print(f'State hash: {state_hash}')

    # Fetch the patricia tree path
    tree_url = f'/mosaics/{mosaic_id_hex}/merkle'
    print(f'Fetching tree path from {tree_url}')
    url = f'{NODE_URL}{tree_url}'
    with urllib.request.urlopen(url) as response:
        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 = 0
    for i, node in enumerate(merkle_path):
        key_pos += node.path.size
        path_str = (f'  path: {node.hex_path}' if node.path.size else '')
        if hasattr(node, 'value'):
            print(f'  [{i}] leaf{path_str}  value: {node.value}')
        else:
            nibble = key_hex[key_pos]
            key_pos += 1
            active = [
                f'{j:X}' for j, link in enumerate(node.links) if link
            ]
            print(f'  [{i}] branch{path_str}'
                f'  links: [{",".join(active)}]'
                f'  -> follow {nibble}')

    # Verify the mosaic state
    result = prove_patricia_merkle(
        encoded_key, hashed_value, merkle_path, state_hash, roots)

    if result.name == 'VALID_POSITIVE':
        print(f'Mosaic {mosaic_id_hex} state verified at height {height}')
    else:
        raise RuntimeError(
            f'Mosaic {mosaic_id_hex} proof failed: {result.name}')

except Exception as e:
    print(e)

Download source

import crypto from 'crypto';
import { Hash256, utils } from 'symbol-sdk';
import {
    deserializePatriciaTreeNodes,
    provePatriciaMerkle
} from 'symbol-sdk/symbol';

const { hexToUint8, intToBytes } = utils;

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

const sha3_256 = (data) =>
    crypto.createHash('sha3-256').update(data).digest();

const concat = (...arrays) => {
    const total = arrays.reduce((n, a) => n + a.length, 0);
    const buf = new Uint8Array(total);
    let off = 0;
    for (const a of arrays) {
        buf.set(a, off);
        off += a.length;
    }
    return buf;
};

try {
    // Fetch the network currency mosaic ID
    const propsRes = await fetch(`${NODE_URL}/network/properties`);
    const props = await propsRes.json();
    const rawId = props.chain.currencyMosaicId;
    const mosaicId = BigInt(rawId.replaceAll("'", ''));
    const mosaicIdHex = mosaicId.toString(16)
        .toUpperCase().padStart(16, '0');
    console.log('Currency mosaic ID:', mosaicIdHex);

    // Fetch the mosaic properties
    const mosaicPath = `/mosaics/${mosaicIdHex}`;
    console.log('Fetching mosaic from', mosaicPath);
    const mosaicRes = await fetch(`${NODE_URL}${mosaicPath}`);
    const mosaicData = await mosaicRes.json();
    const mosaic = mosaicData.mosaic;
    console.log(JSON.stringify(mosaic, undefined, 2));

    // Serialize and hash the mosaic properties
    const serialized = 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));
    const hashedValue = new Hash256(sha3_256(serialized));
    console.log('Hashed value:', hashedValue.toString());

    // Hash the mosaic ID to get the encoded key
    const keyBytes = intToBytes(BigInt(`0x${mosaic.id}`), 8);
    const encodedKey = new Hash256(sha3_256(keyBytes));
    console.log('Encoded key:', encodedKey.toString());

    // Fetch the current network height
    const chainRes = await fetch(`${NODE_URL}/chain/info`);
    const chainInfo = await chainRes.json();
    const height = parseInt(chainInfo.height, 10);
    console.log('Current height:', height);

    // Fetch the block's state hash and roots
    const blockPath = `/blocks/${height}`;
    console.log('Fetching block from', blockPath);
    const blockRes = await fetch(`${NODE_URL}${blockPath}`);
    const blockData = await blockRes.json();
    const stateHash = new Hash256(blockData.block.stateHash);
    const roots = blockData.meta.stateHashSubCacheMerkleRoots
        .map(r => new Hash256(r));
    console.log('State hash:', stateHash.toString());

    // Fetch the patricia tree path
    const treeUrl = `/mosaics/${mosaicIdHex}/merkle`;
    console.log('Fetching tree path from', treeUrl);
    const treeRes = await fetch(
        `${NODE_URL}${treeUrl}`);
    const treeData = await treeRes.json();
    const merklePath =
        deserializePatriciaTreeNodes(hexToUint8(treeData.raw));
    console.log('Tree path:', merklePath.length, 'nodes');
    const keyHex = encodedKey.toString();
    let keyPos = 0;
    merklePath.forEach((node, i) => {
        keyPos += node.path.size;
        const pathStr = node.path.size
            ? `  path: ${node.hexPath}` : '';
        if ('value' in node) {
            console.log(`  [${i}] leaf${pathStr}  value: ${node.value}`);
        } else {
            const nibble = keyHex[keyPos];
            keyPos += 1;
            const active = 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 state
    const result = provePatriciaMerkle(
        encodedKey, hashedValue, merklePath, stateHash, roots);

    if (result === 0x0001) { // VALID_POSITIVE
        console.log(
            `Mosaic ${mosaicIdHex} state verified at height ${height}`);
    } else {
        throw new Error(
            `Mosaic ${mosaicIdHex} proof failed: ${result}`);
    }
} catch (e) {
    console.error(e.message);
}

Download source

This example verifies the network's currency mosaic (XYM on mainnet), whose ID is discovered automatically from /network/properties GET. 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.

Code Explanation⚓︎

Fetching the Mosaic Definition⚓︎

    # Fetch the network currency mosaic ID
    url = f'{NODE_URL}/network/properties'
    with urllib.request.urlopen(url) as response:
        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 properties
    mosaic_path = f'/mosaics/{mosaic_id_hex}'
    print(f'Fetching mosaic from {mosaic_path}')
    url = f'{NODE_URL}{mosaic_path}'
    with urllib.request.urlopen(url) as response:
        mosaic_data = json.loads(response.read().decode())
    mosaic = mosaic_data['mosaic']
    print(json.dumps(mosaic, indent=2))
    // Fetch the network currency mosaic ID
    const propsRes = await fetch(`${NODE_URL}/network/properties`);
    const props = await propsRes.json();
    const rawId = props.chain.currencyMosaicId;
    const mosaicId = BigInt(rawId.replaceAll("'", ''));
    const mosaicIdHex = mosaicId.toString(16)
        .toUpperCase().padStart(16, '0');
    console.log('Currency mosaic ID:', mosaicIdHex);

    // Fetch the mosaic properties
    const mosaicPath = `/mosaics/${mosaicIdHex}`;
    console.log('Fetching mosaic from', mosaicPath);
    const mosaicRes = await fetch(`${NODE_URL}${mosaicPath}`);
    const mosaicData = await mosaicRes.json();
    const mosaic = mosaicData.mosaic;
    console.log(JSON.stringify(mosaic, undefined, 2));

The code starts by fetching the network currency mosaic ID from /network/properties GET. 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.

Computing the Key and Value Hashes⚓︎

    # Serialize and hash the mosaic properties
    writer = 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 key
    writer = 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 properties
    const serialized = 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));
    const hashedValue = new Hash256(sha3_256(serialized));
    console.log('Hashed value:', hashedValue.toString());

    // Hash the mosaic ID to get the encoded key
    const keyBytes = intToBytes(BigInt(`0x${mosaic.id}`), 8);
    const encodedKey = new Hash256(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.

Fetching the Block State Hash⚓︎

    # Fetch the current network height
    url = f'{NODE_URL}/chain/info'
    with urllib.request.urlopen(url) as response:
        chain_info = json.loads(response.read().decode())
    height = int(chain_info['height'])
    print(f'Current height: {height}')

    # Fetch the block's state hash and roots
    block_path = f'/blocks/{height}'
    print(f'Fetching block from {block_path}')
    url = f'{NODE_URL}{block_path}'
    with urllib.request.urlopen(url) as response:
        block_data = json.loads(response.read().decode())
    state_hash = Hash256(block_data['block']['stateHash'])
    sub_cache_key = 'stateHashSubCacheMerkleRoots'
    roots = [Hash256(r) for r in block_data['meta'][sub_cache_key]]
    print(f'State hash: {state_hash}')
    // Fetch the current network height
    const chainRes = await fetch(`${NODE_URL}/chain/info`);
    const chainInfo = await chainRes.json();
    const height = parseInt(chainInfo.height, 10);
    console.log('Current height:', height);

    // Fetch the block's state hash and roots
    const blockPath = `/blocks/${height}`;
    console.log('Fetching block from', blockPath);
    const blockRes = await fetch(`${NODE_URL}${blockPath}`);
    const blockData = await blockRes.json();
    const stateHash = new Hash256(blockData.block.stateHash);
    const roots = blockData.meta.stateHashSubCacheMerkleRoots
        .map(r => new Hash256(r));
    console.log('State hash:', stateHash.toString());

The code fetches the current chain height from /chain/info GET, 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).

Fetching the Tree Path⚓︎

    # Fetch the patricia tree path
    tree_url = f'/mosaics/{mosaic_id_hex}/merkle'
    print(f'Fetching tree path from {tree_url}')
    url = f'{NODE_URL}{tree_url}'
    with urllib.request.urlopen(url) as response:
        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 = 0
    for i, node in enumerate(merkle_path):
        key_pos += node.path.size
        path_str = (f'  path: {node.hex_path}' if node.path.size else '')
        if hasattr(node, 'value'):
            print(f'  [{i}] leaf{path_str}  value: {node.value}')
        else:
            nibble = key_hex[key_pos]
            key_pos += 1
            active = [
                f'{j:X}' for j, link in enumerate(node.links) if link
            ]
            print(f'  [{i}] branch{path_str}'
                f'  links: [{",".join(active)}]'
                f'  -> follow {nibble}')
    // Fetch the patricia tree path
    const treeUrl = `/mosaics/${mosaicIdHex}/merkle`;
    console.log('Fetching tree path from', treeUrl);
    const treeRes = await fetch(
        `${NODE_URL}${treeUrl}`);
    const treeData = await treeRes.json();
    const merklePath =
        deserializePatriciaTreeNodes(hexToUint8(treeData.raw));
    console.log('Tree path:', merklePath.length, 'nodes');
    const keyHex = encodedKey.toString();
    let keyPos = 0;
    merklePath.forEach((node, i) => {
        keyPos += node.path.size;
        const pathStr = node.path.size
            ? `  path: ${node.hexPath}` : '';
        if ('value' in node) {
            console.log(`  [${i}] leaf${pathStr}  value: ${node.value}`);
        } else {
            const nibble = keyHex[keyPos];
            keyPos += 1;
            const active = 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}/merkle GET 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.

Verifying the Proof⚓︎

    # Verify the mosaic state
    result = prove_patricia_merkle(
        encoded_key, hashed_value, merkle_path, state_hash, roots)

    if result.name == 'VALID_POSITIVE':
        print(f'Mosaic {mosaic_id_hex} state verified at height {height}')
    else:
        raise RuntimeError(
            f'Mosaic {mosaic_id_hex} proof failed: {result.name}')
    // Verify the mosaic state
    const result = provePatriciaMerkle(
        encodedKey, hashedValue, merklePath, stateHash, roots);

    if (result === 0x0001) { // VALID_POSITIVE
        console.log(
            `Mosaic ${mosaicIdHex} state verified at height ${height}`);
    } else {
        throw new Error(
            `Mosaic ${mosaicIdHex} proof failed: ${result}`);
    }

takes five arguments, each computed in an earlier step:

Parameter Source Role
encoded_key SHA3-256 of the mosaic ID Identifies which leaf to look up in the tree
hashed_value SHA3-256 of the serialized definition The expected value stored in the leaf
merkle_path /mosaics/{mosaicId}/merkle GET The chain of branch and leaf nodes from root to leaf
state_hash stateHash from /blocks/{height} GET The block header's hash of all chain state
roots stateHashSubCacheMerkleRoots from /blocks/{height} GET The individual root hash of each sub-cache

The function then verifies the proof in three stages:

  1. 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).
  2. 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).
  3. 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.

See PatriciaMerkleProofResult for the full set of possible result codes.

Height consistency

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.

Output⚓︎

The following output shows a typical run of the program:

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.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Fetch mosaic definition /mosaics/{mosaicId} GET
Compute the key and value hashes MosaicEntry
Fetch the block state hash /chain/info GET, /blocks/{height} GET
Fetch the tree path /mosaics/{mosaicId}/merkle GET, deserializePatriciaTreeNodes
Verify the proof

Next Steps⚓︎

The same technique applies to any sub-cache in the state hash, not just mosaics.

Each sub-cache has its own catbuffer schema that defines the binary layout (e.g. AccountState, RootNamespaceHistory, MetadataEntry) and a corresponding /merkle endpoint for fetching the tree path.

To verify that a specific transaction is part of a block instead, see the Prove Transaction Inclusion tutorial.