Why blockchain state needs a cryptographic commitment, and the tree / Merkle / Patricia-trie foundations behind XRPL's SHAMap.
What you'll learn
≈60 min · Intermediate · builds on Protocols & wire messages
Before you meet XRPL's clever state structures, it's worth understanding the problem they solve. In this module you'll see why a naive database can't work for a trustless network, and build up the ideas (cryptographic hashing, Merkle trees, radix-16 Patricia tries) that make verifiable, efficiently-syncable state possible. Get these foundations and the SHAMap, coming next, will feel inevitable rather than magical.
In brief: a plain key-value store cannot prove its state is correct to an untrusting network.
Let's consider what happens if you store blockchain state like a simple key-value database:
State Storage:
accounts = {
"rN7n7otQDd6FczFgLdlqtyMVrn3LNU8B4C": { balance: 100 XRP, ... },
"rLHzPsX6oXkzU2qL12kHCH8G8cnZv1rBJh": { balance: 50 XRP, ... },
"r3kmLJN5D28dHuH8vZvVrDjiV5sNSiUQXD": { balance: 75 XRP, ... },
...
}
Transaction Processing:
The Problem: No Verification
Without a cryptographic commitment to the state, any node can claim any state is correct:
Alice broadcasts: "My balance is 1,000,000 XRP"
Bob broadcasts: "My balance is 1,000,000 XRP"
Charlie broadcasts: "Everyone's balance is 0 XRP"
Which is correct? Without a central authority, there's no way to know.
The Problem: Expensive Synchronization
A new node joining the network needs to learn the current state. With naive storage:
In brief: you need one hash that commits to the entire state, authentically and completely.
Blockchain state needs a cryptographic commitment: a single value that guarantees:
This is what cryptographic hashes provide:
uint256 stateHash = hashFunction(serializeAllState());
Now a validator can broadcast: "The current state root is 0xAB12EF..."
Other nodes can verify this is correct by computing the same hash. If someone tries to cheat with different state, the hash will be different.
The Trade-off Problem:
But hashing all state from scratch has a terrible cost:
In brief: a tree of hashes makes single-entry changes cheap and lets nodes compare state by root hash.
XRPL solves the cryptographic commitment problem with a Merkle tree:
Instead of hashing all accounts together, organize them in a tree structure:
Key Insight: Each node's hash depends only on its descendants, not the entire tree:
Synchronization Benefit:
When syncing with a peer:
A tree of 1 million accounts becomes synchronizable in a few thousand comparisons instead of millions.
Key idea. Change one leaf and only the hashes on the path to the root change. That single property is what makes both tamper-evidence and cheap syncing possible.
In brief: keying the tree by the entry's own hash keeps it balanced and navigable, radix-16.
A simple binary tree has a problem: unbalanced growth. If accounts are added sequentially, the tree becomes a linked list, losing logarithmic properties.
Patricia tries (Radix N tries) solve this:
XRPL's Choice: Patricia trie with radix 16 (hex digits):
Level 0 (root): Evaluate first hex digit of account hash (0-F)
→ Child 0, 1, 2, ... or F
Level 1: Evaluate second hex digit
→ One of 16 children
... and so on
This gives a balanced tree with:
Now we have SHAMap: an elegant in-memory data structure for cryptographically-committed state.
But there's a problem: When the validator crashes, all in-memory state vanishes.
The next startup:
NodeStore solves this by making SHAMap persistent:
The Storage Challenge:
But persistence introduces new challenges:
NodeStore addresses each:
SHAMap's Role:
NodeStore's Role:
Together:
They solve the complete blockchain state management problem:
A validator can:
In brief: these ideas are exactly what the SHAMap, coming next, is built from.
Understanding SHAMap and NodeStore is essential because:
These aren't just implementation details, they're fundamental to what XRPL is and how it works.
In the next chapter, we'll explore the mathematical foundations: Merkle trees, Patricia tries, and cryptographic hashing. Then we'll dive deep into the SHAMap implementation, followed by the NodeStore persistence layer.
By the end of this module, you'll understand not just what SHAMap and NodeStore do, but why they're architected the way they are, and how to reason about their correctness, performance, and limitations.
Before diving into the implementation of SHAMap and NodeStore, we need to understand the mathematical foundations they're built on. This chapter covers:
Don't skip this chapter thinking it's just theory. Every design decision in SHAMap flows directly from the properties of these data structures. Understanding the foundations will make the implementation clear.
A cryptographic hash function H has three critical properties:
1. Determinism
H(x) always returns the same result
H("hello") = 0x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
H("hello") = 0x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
Same input, always same output. This enables verification: others can compute the same hash and confirm correctness.
2. Collision Resistance
Finding two different inputs with the same hash output is computationally infeasible:
H(x) = H(y) where x ≠ y → "collision"
For SHA-256: Finding a collision requires ~2^128 operations
Current computers: ~10^18 operations/second
Time required: 10^20 seconds ≈ 3 billion years
Cryptographic security: "practical impossibility"
3. Avalanche Effect
Tiny changes produce completely different outputs:
H("hello") = 0x2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
H("hallo") = 0xd3751713b7ac3d1ab39b1f7b1859d6e7baeac08b8cdb1c8fefd1f96a3f3c17f8
Change one character → hash completely changes
Implication for XRPL:
Hash output acts as a "fingerprint" of data:
XRPL uses SHA-512Half: First 256 bits of SHA-512. This gives:
Trees are recursive data structures: a root node with zero or more children, each of which is a tree.
Key Tree Property: Logarithmic Depth
For a balanced tree with branching factor B and N items:
Why Depth Matters:
Higher branching factor = shallower tree = faster operations.
The Balance Problem:
But trees can become unbalanced:
For blockchain state, accounts are inserted in unpredictable order. Without careful tree structure, you get unbalanced trees and logarithmic operations degrade.
Patricia tries solve the balance problem by using the key itself as navigation.
Basic Idea:
For a 256-bit key (like an account address), use the bits to guide traversal:
Example: Radix-16 Patricia Trie
Balance Guarantee:
Since navigation is determined by the key:
Space Trade-off:
Inner nodes have up to 16 pointers (for radix-16), but most sparse:
XRPL's Choice: Radix-16
// From rippled source: 16 possible children per level
static const int NUM_BRANCHES = 16;
// Each level represents 4 bits (one hex digit) of the key
for (int i = 0; i < keyLengthInBits; i += 4) {
int branch = (key >> (keyLengthInBits - i - 4)) & 0x0F;
// Navigate to child at position 'branch'
}
Why radix-16?
A Merkle tree combines tree structure with cryptographic hashing:
Definition:
Key Property: Hash Propagation
When a leaf changes, its hash changes. This affects its parent:
Benefit: O(1) Subtree Comparison
Compare two trees:
Without Merkle trees:
Compare all N leaves directly: O(N) time
With Merkle trees:
Compare root hashes: O(1) time
If different, compare child hashes to find divergence: O(log N) comparisons
Benefit: Logarithmic Proofs
Prove that a specific item is in the tree:
Merkle Proof for "Alice: 90 XRP":
Show: H("Alice: 90 XRP") = 0x1234...
H(Sibling1) = 0x5678...
H(Sibling2) = 0x9ABC...
Verifier computes:
Combine with siblings working up to root
Verify final hash matches claimed root
Only need to show O(log N) nodes, not entire tree
XRPL's SHAMap combines both approaches:
Patricia Trie Structure:
Merkle Properties:
The Result:
Properties:
XRPL uses SHA-512Half for all hashing:
// From rippled source
uint256 hashFunc(Blob const& data) {
using hasher = SHA512Half; // SHA-512, keep first 256 bits
return hasher()(data);
}
Why SHA-512Half instead of SHA-256?
Hash Computation in SHAMap:
Inner nodes hash their children's hashes:
uint256 computeHash(SHAMapInnerNode& node) {
Blob data;
for (int i = 0; i < 16; ++i) {
if (node.hasChild(i)) {
uint256 childHash = node.getChildHash(i);
data.append(childHash); // Concatenate child hashes
}
}
return SHA512Half(data);
}
Leaf nodes hash their content with a type prefix:
uint256 computeHash(SHAMapLeafNode& leaf, Type type) {
Blob data;
data.push_back(type); // Type prefix prevents hash collision
data.append(leaf.getKey());
data.append(leaf.getData());
return SHA512Half(data);
}
Why Type Prefixes?
Prevent collisions between different types of data:
Account with data: 0xAA || 0xBBBB
Leaf type 1, data 0xAA, item 0xBBBB
Transaction with data: 0xAABB || 0xBB
Leaf type 2, data 0xAABB, item 0xBB
These have same content but different meaning!
With type prefix:
H(1 || 0xAA || 0xBBBB) ≠ H(2 || 0xAABB || 0xBB)
Once a Merkle tree root hash is committed to (published in a ledger), that entire tree must be immutable:
Solution: Snapshots
For each ledger version, create a snapshot:
Copy-on-Write:
When a mutable SHAMap modifies a shared node:
Both ledgers are verified by their root hashes, but they share most of the tree (unchanged nodes).
This module built up the ideas behind XRPL's state storage. It showed why a naive key-value store fails for a trustless network, then introduced cryptographic hashing, Merkle trees, and radix-16 Patricia tries, the pieces that make state verifiable and cheap to sync. The key insight is that a tree of hashes lets a single change touch only the path to the root, and lets nodes compare state by comparing one root hash. These foundations are exactly what the SHAMap is built from.
To remember:
Next up. You know why the state tree exists. Now go inside it: how the SHAMap actually stores a million accounts and fingerprints them all in one 32-byte hash.
Resources
Assignments
0 of 2 complete