advanced 60 min

SHAMap synchronization & proofs

How nodes compare and sync SHAMaps by hash, and how compact Merkle proofs let clients verify state without the full ledger.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Sync state by comparing subtree hashes instead of whole datasets.
  • Generate and verify Merkle inclusion proofs.
  • Explain verifiable state reconstruction.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

≈60 min · Advanced · builds on SHAMap architecture & hashing

A tree of hashes isn't just tidy, it's what lets nodes agree cheaply and clients verify without trusting anyone. In this module you'll see how two nodes sync by comparing subtree hashes instead of whole datasets, and how a compact Merkle proof lets a light client confirm a single entry really is in the ledger. It's the SHAMap's superpower, put to work.

Traversal Algorithms

In brief: how to walk two trees in parallel to find exactly where they differ.

SHAMap provides multiple traversal strategies depending on the use case:

Depth-First Traversal: visitNodes

The visitNodes method provides complete tree traversal:

Use Cases:

  • Tree validation (verify all hashes)
  • Bulk operations on all nodes
  • Custom tree analysis

Leaf-Only Traversal: visitLeaves

void SHAMap::visitLeaves(
    std::function<void(SHAMapItem const&)> callback)
{
    visitNodes([this, &callback](SHAMapTreeNode* node) {
        if (auto leaf = dynamic_cast<SHAMapLeafNode*>(node)) {
            callback(leaf->getItem());
        }
    });
}

Iterator-Based Traversal

for (auto it = shamap.begin(); it != shamap.end(); ++it) {
    // Access SHAMapItem via *it
    // Iteration order matches key ordering
}

Parallel Traversal: walkMapParallel

For performance-critical operations:

void SHAMap::walkMapParallel(
    std::function<void(SHAMapTreeNode*)> callback,
    int numThreads)
{
    // Divide tree into subtrees
    // Process subtrees concurrently
    // Aggregate results
}

Use Cases:

  • Missing node detection at scale
  • High-throughput synchronization

Missing Node Detection

In brief: how a node discovers which subtrees it still needs from a peer.

The core synchronization primitive identifies nodes needed for complete tree reconstruction:

Algorithm: getMissingNodes

Output:

Returns vector of (NodeID, Hash) pairs representing missing nodes, prioritized for network retrieval.

Full Below Optimization

An optimization preventing redundant traversal:

class SHAMapInnerNode {
    // Generation counter: when this subtree was marked "complete"
    std::uint32_t mFullBelow = 0;
};

if (node->mFullBelow == currentGeneration) {
    // Entire subtree known complete
    // Skip traversal
    continue;
}

When a subtree is verified complete (all descendants present), skip traversing it again until a new sync starts.

Node Addition and Canonicalization

Adding the Root Node: addRootNode

Initializes or verifies the root node:

Adding Known Nodes: addKnownNode

Adds interior or leaf nodes during synchronization:

Canonicalization

Purpose:

Ensure nodes are unique in memory (one NodeObject per hash):

Benefits:

  1. Memory Efficiency: Identical nodes stored once
  2. Thread Safety: Cache handles concurrent insertion atomically
  3. Fast Equality: Compare pointers instead of content
  4. Shared Trees: Multiple SHAMaps can share nodes

Synchronization Scenario

In brief: a worked example of syncing state by comparing hashes, not whole datasets.

The Complete Flow:

Synchronizing a ledger in six steps: receive the header (roots), getMissingNodes from the roots, fetch missing nodes from peers in parallel, repeat until the missing list is empty, verify the recomputed roots match the header, then persist to the NodeStore and mark the SHAMap immutable

Performance Metrics:

Small ledger (100k accounts):
  Nodes in tree: ~20,000
  Network requests: ~20,000 (batch fetch reduces count)
  Time to sync: seconds to minutes

Large ledger (10M accounts):
  Nodes in tree: ~2,000,000
  Network requests: ~100,000 (batching and parallel)
  Time to sync: hours to days

State Reconstruction Guarantee

In brief: why the pieces you fetch can be trusted to rebuild the exact state.

The transaction tree, persisted in NodeStore, ensures unique history:

Problem:

Without transaction history, many sequences could produce same state:

Two possible histories, one end state: State A can reach State B via tx1 or tx2; the state tree alone cannot tell which happened, the transaction tree records the actual path

Solution:

The transaction tree proves the exact sequence:

Ledger header contains:
  - Account state tree root (current balances)
  - Transaction tree root (complete history)

Given state tree root + transaction tree root:
  Can verify exact sequence that produced this state
  No ambiguity about what happened

NodeStore's Role:

Both tree nodes are persisted:

  • Query state tree to find current values
  • Query transaction tree to find history
  • Together: complete, verifiable record

Key idea. A Merkle proof lets a client verify that one entry belongs to a state root without downloading the whole ledger, which is what makes light verification possible.

Cryptographic Proofs and State Reconstruction


Introduction

The combination of SHAMap and NodeStore provides more than just efficient storage, they enable cryptographic proofs that transactions were executed correctly and state was computed honestly.

This chapter explores:

  • Merkle proof generation and verification
  • State reconstruction from transaction history
  • Cross-chain or light-client verification
  • The guarantee of verifiable ledger history

Merkle Proof Generation

A Merkle proof allows someone to verify that data is in a tree without reconstructing the entire tree.

Algorithm: getProofPath

Example Proof:

For an account with key 0x3A7F2E1B...:

Proof Size Properties:

Tree size: 1 million accounts
Tree depth: log_16(1M) ≈ 5 levels

Proof includes:
  5 inner nodes + 1 leaf: ~100 bytes per node
  Total: ~600 bytes

Without proof:
  Send all 1M accounts: gigabytes

Proof is logarithmic in tree size
Verification requires one hash per level: O(log N)

Proof Verification

Verifying a proof requires only the root hash and the proof:

Algorithm: verifyProofPath

Verification Process:

Verifying a Merkle proof: hash the leaf, then at each inner level check the child hash matches and recompute upward (leaf 0x8765, inner_3 0x7890, inner_2 0xEF12, inner_1 0xABCD) until the computed root 0x1234 equals the expected root: proof valid, the account is in the ledger

Proof Use Cases

Four proof use cases: light clients (a mobile wallet verifies a balance with a ~600-byte proof instead of gigabytes), cross-chain bridges (verify XRPL state from a header plus proof), auditing and compliance (prove a transaction executed with tx proof, state proof and the ledger chain), and rollups (batch verification with transaction proofs and a state root)

State Reconstruction Guarantee

The transaction tree provides a critical guarantee: verifiable unique history.

The Problem Without Transaction Tree:

Three different transaction paths lead from the initial state S0 to the identical final state S3, showing that a state snapshot alone cannot prove which history actually happened.

The Solution: Transaction Tree

The XRP Ledger maintains both trees in every ledger:

struct LedgerHeader {
    uint256 accountTreeHash;      // Root of account state
    uint256 transactionTreeHash;  // Root of transaction history

    // Both are cryptographically committed
    // Both are signed by validators
    // Both must match
};

State Reconstruction:

Verifiable state reconstruction: from the LedgerHeader's two hashes, fetch the account tree (current state) and the transaction tree (history), replay S0 plus all transactions to S computed, and verify it matches the account tree root: the state is the proven result of the history

Why This Matters:

Without verification: Anyone could claim different history
With verification: Only one possible history is correct

Example:
  Alice claims she sent 100 XRP to Bob
  Bob claims he received only 50 XRP

  Ledger history is immutable and verified
  Proof shows exactly what happened
  No ambiguity

Ledger Verifiability

Every aspect of XRPL state is cryptographically verifiable:

Account Proof

Prove: Account has balance 1000 XRP

Components:
  1. Ledger header with account state root
  2. Merkle proof from root to account leaf
  3. Account data (balance, etc.)

Verification:
  Verify proof leads from root to account
  Account balance is in proof
  Root hash signed by supermajority of validators

Transaction Proof

Prove: Transaction T was executed in ledger L

Components:
  1. Ledger L header with transaction tree root
  2. Merkle proof from root to transaction leaf
  3. Transaction data and execution results

Verification:
  Verify proof leads from root to transaction
  Matches all known transaction identifiers
  Root hash signed by validators

Full Ledger Proof

Practical Verification Workflow

Step 1: Trust the Ledger Header

// Ledger headers are small (~100 bytes)
// Signed by supermajority (>80%) of validators
// Distributed via gossip protocol

LedgerHeader verified_header = getLedgerHeader(ledgerSeq);
// Root hashes are in verified_header

Step 2: Request Proof

Conceptual illustration, not literal rippled APIs: the SHAMap does not expose a MerkleProof type or a requestProof call by these names. The snippet shows the idea, not copy-paste code.

// Client requests proof of account existence (conceptual)
MerkleProof proof = peer.requestProof(
    accountID,
    verified_header.accountStateRoot);

// Proof is small (~500 bytes)

Step 3: Verify Proof

// Client verifies locally (no network needed)
if (verifyProofPath(accountID,
                    verified_header.accountStateRoot,
                    proof)) {
    // Account exists and is proven
    // Can trust all data in proof
    auto account = parseAccountFromProof(proof);
}

Step 4: Use Verified Data

// Application can now use the verified account data
// with absolute confidence it's correct
double balance = account.balance;  // Proven correct

Summary

This module put the SHAMap to work. You saw how two nodes synchronise by comparing subtree hashes and fetching only what differs, instead of exchanging whole datasets, and how a compact Merkle proof lets a light client verify that a single entry belongs to a state root without holding the full ledger. Matching root hashes prove identical state; that is the SHAMap's superpower.

To remember:

  • Sync = compare subtree hashes top-down and fetch only what differs
  • getMissingNodes walks the tree to list exactly what to request from peers
  • Nodes are canonicalized: one in-memory instance per hash
  • The full-below optimization skips subtrees already known complete
  • A Merkle proof = the leaf plus sibling hashes up to the root; verify by recomputing the root
  • Proofs let a light client verify one entry without holding the ledger
  • Same root hash = nothing to sync at all
  • Watch out: a proof only proves inclusion against THAT root; always check the root belongs to a validated ledger

Next up. Every node you synced from stored those tree nodes somewhere. Where, exactly? Next: the NodeStore, the key-value heart of rippled's persistence.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026