intermediate 90 min

Ledger architecture & data structures

The `Ledger` object and its data structures — LedgerHeader and the state and transaction trees.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Describe the LedgerHeader fields and the two SHAMaps.
  • Explain how a ledger is built, closed and hashed.
  • Relate ledgers to consensus.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

≈90 min · Intermediate · builds on Advanced RPC features

The consensus phase starts with the object everyone is trying to agree on: the ledger. In this module you'll learn the Ledger and its LedgerHeader, the two SHAMaps it carries (state and transactions), and how a ledger is built, closed, hashed and chained to its parent. It's the noun that the consensus verb acts on.

The Ledger Chain Concept

In brief: each ledger links to its parent by hash, forming the chain.

Sequential Progression:

The ledger chain: Genesis, Ledger 1, Ledger 2 and so on to the current ledger, each cryptographically linked to its parent via parentHash

Properties:

Property Description
Cryptographic linking Each ledger references its predecessor via hash
Monotonic sequence Ledger numbers always increase
Temporal ordering Represents time progression in the network
State evolution Each ledger is a state transition from the previous

Why Sequential Design?

  • Ensures deterministic ordering of events
  • Prevents double-spending and race conditions
  • Enables efficient synchronization between nodes
  • Provides clear audit trail for all network activity

Hierarchical Data Organization

Three-Tier Architecture:

The three tiers of a ledger: the header (sequence, parent hash, both root hashes, close time, total XRP) commits to the transaction set (canonical order, metadata) and the state tree (balances, trust lines, offers, escrows)

Merkle Tree Architecture

In brief: a ledger holds two SHAMaps, one for account state and one for transactions.

The state and transaction data are organized as Merkle trees, enabling efficient verification:

The Merkle shape: a root hash over branch hashes over leaf hashes, with the account data at the leaves

Why Merkle Trees?

Benefit Description
Compact verification Verify specific data without downloading entire ledger
Tamper detection Any change in data changes the root hash
Parallel processing Different branches can be processed independently
Bandwidth optimization Only transmit changed portions

Practical Application:

A node can verify that a specific account balance is correct by checking only the path from that account to the root hash, rather than verifying the entire ledger.

Key idea. A ledger is two Merkle trees plus a header. The header's hashes commit to both trees, so a single ledger hash pins down the entire state and transaction set.

Data Integrity Mechanisms

In brief: hashing, signatures, consensus, and chaining stack up to make a ledger trustworthy.

Multiple Layers of Protection:

The four integrity layers, from cryptographic hashing at the base through digital signatures and consensus validation up to chain validation

Layer 1: Cryptographic Hashing

Uses SHA-512Half to create unique fingerprints. Each ledger contains two Merkle trees:

  • Transaction Map (txMap_): All transactions
  • State Map (stateMap_): All account states and objects

Hash calculation proceeds bottom-up: leaf nodes hash their data, inner nodes hash their children, producing a root hash.

The ledger header stores both root hashes and calculates the final ledger hash:

header_.txHash = txMap_.getHash();
header_.accountHash = stateMap_.getHash();
header_.hash = calculateLedgerHash(header_);

Key verification points: When building a new ledger, hashes are computed FROM data (Ledger.cpp). When loading from database, the system verifies expected roots exist via fetchRoot() (Ledger.cpp). Network-acquired ledgers compare received vs expected hash (InboundLedger.cpp).

Failures detected: Missing nodes → network fetch. Hash mismatch → reject ledger. Internal corruption → abort via invariants() check.


Layer 2: Digital Signatures

Proves authorization using ed25519/secp256k1 signatures.

Transaction signatures: Every transaction must be signed by the account holder's private key. Invalid signatures result in tefBAD_AUTH rejection.

Validator signatures: Validators sign proposals during consensus (RCLCxPeerPos) and validations after building a ledger (STValidation). Each validation includes ledger hash, sequence, consensus hash, close time, and signature timestamp to prevent replay attacks.

Key management: Validators use a two-tier system, a master key (offline, long-term identity) and ephemeral keys (online, rotated regularly). The master key signs tokens authorizing ephemeral keys; each new token invalidates previous ones (see Exercise 1: Validator Keys Setup).

Failures: Invalid signatures → transaction/validation rejected. Revoked keys → all validations ignored. Unknown validators → not counted toward quorum.


Layer 3: Consensus Validation

Provides Byzantine fault tolerance through validator quorum.

A ledger requires validations from floor(trusted_validators × 0.8) + 1 validators, tolerating up to 20% Byzantine failures.

Process: Collect validations for the ledger hash, filter out validators on the Negative UNL (unreliable nodes), check if quorum is met, and verify all validators agree on the same hash (LedgerMaster.cpp).

Byzantine detection: If a node builds a different ledger than the network majority, it enters WrongLedger mode (Consensus.h), stops proposing, acquires the majority ledger from the network, and resumes consensus.

Failures: Insufficient validations → ledger remains unvalidated. Hash disagreement → enter WrongLedger mode. Network partition → wait for reconnection. The 80% quorum ensures agreement even with 20% compromised validators.


Layer 4: Chain Validation

Ensures temporal consistency through parent-child relationships.

Every ledger header references its predecessor (parentHash) and increments the sequence number (seq = parent.seq + 1). The system verifies: (1) parent exists, (2) parent hash matches, (3) sequence is continuous (LedgerMaster::checkAccept() line 922).

Skip list: Maintains references at exponential distances (parent, grandparent, great-grandparent) for efficient historical lookups via Ledger::updateSkipList().

Failures: Parent hash mismatch → reject ledger. Missing parent → fetch from network. Sequence gap → fetch missing ledgers. Chain fork → fixMismatch() repairs (LedgerMaster.cpp).

Protection: Guarantees unbroken sequence from genesis; altering past ledgers breaks the chain and is immediately detectable.


How Layers Work Together

All four layers work sequentially during validation: Layer 1 computes cryptographic hashes, Layer 2 verifies signatures, Layer 3 ensures validator agreement, and Layer 4 confirms chain continuity. If ANY layer fails, the ledger is rejected. This defense-in-depth design means even if one layer is compromised (e.g., a stolen validator key), the other layers protect the system.


The Ledger Lifecycle

In brief: how a ledger is opened, filled, closed, and validated.

Four Phases of Ledger Creation:

A ledger's four stages: collection of pending transactions, assembly onto the previous state, consensus on the canonical version, and validation sealing it to the network

Phase Details:

  1. Transaction Collection
  • Gather pending transactions from network
  • Apply transaction queue ordering rules
  • Filter invalid or expired transactions
  1. Ledger Assembly
  • Apply transactions to previous ledger state
  • Calculate new account balances and states
  • Generate transaction results and metadata
  1. Consensus Process
  • Validators propose their assembled ledger
  • Network reaches agreement on canonical version
  • Disputed elements resolved through voting
  1. Validation and Finalization
  • Final ledger is cryptographically sealed
  • Distributed to all network participants
  • Becomes immutable part of the chain

State Transition Model

How State Changes Work:

State transition: Ledger N-1 (Alice 100 XRP, Bob 50) plus the transaction set (Alice pays Bob 10) equals Ledger N (Alice 90, Bob 60), while Ledger N-1 remains unchanged

ACID Properties:

Property Description
Atomicity All changes in a ledger succeed or fail together
Consistency State transitions follow strict rules
Isolation Concurrent operations don't interfere
Durability Committed changes are permanent

Transaction Ordering and Determinism

Why Deterministic Ordering Matters:

All nodes must process transactions in identical order to reach the same final state.

Ordering Principles:

The four ordering rules: canonical sorting by salted key, fee priority, per-account sequence order, and temporal constraints

Multi-Tier Storage Architecture

Storage Hierarchy:

The three storage tiers: active memory (current state, recent history), fast SSD storage (recent ledgers, indices, snapshots), and archive storage (complete history, audit trails)

Data Lifecycle Management

Retention Policies:

Category Retention Access Speed Contents
Hot Data Current + ~256 ledgers Immediate Current state, recent txns
Warm Data ~32,570 ledgers (~1 day) Fast Recent history
Cold Data Configurable Slower Full historical data

Handling State Conflicts

Conflict Scenarios:

Conflict resolution: insufficient funds (later transactions may fail, reserves enforced), sequence gaps (in-order processing prevents replays), and concurrent modifications (deterministic ordering, first wins)

Ledger vs. Traditional Database

Comparison:

Aspect Traditional Database XRP Ledger
State Model Modify in place Immutable snapshots
History May be lost Always preserved
Verification Trust the database Cryptographic proof
Recovery Restore from backup Rebuild from chain
Consistency Single authority Distributed consensus

Immutability and Its Implications

Once a ledger is validated:

The five immutability guarantees: unalterable data, unique hash fingerprints, verifiable chain integrity, consistent historical queries, and a complete tamper-evident audit trail

Code Enforcement:

class Ledger {
    bool mImmutable;

    void setImmutable() {
        mImmutable = true;
        stateMap_.setImmutable();
        txMap_.setImmutable();
    }
};

Only immutable ledgers can be stored in a LedgerHolder and published to the network.

Ledger Data Structures


Introduction

The XRP Ledger's reliability and performance depend on carefully designed data structures. This chapter explores the core classes that represent ledgers, manage their lifecycle, and provide efficient access to historical data.

The XRPL ledger is the authoritative record of the network's state at a given point in time. It contains all account balances, offers, escrows, and other objects, as well as a record of all transactions included in that ledger.

Every server always has an open ledger. All received new transactions are applied to the open ledger. The open ledger can't close until consensus is reached on the previous ledger and either there is at least one transaction or the ledger's close time has been reached.

Understanding these structures is essential for:

  • Working with the codebase effectively
  • Debugging ledger-related issues
  • Implementing new features that interact with ledger state
  • Understanding how consensus and validation work

The Ledger Class

The Ledger class is the primary representation of a single ledger instance. It manages both the state (account balances, offers, escrows, etc.) and transaction data for a specific ledger.

Location: LedgerHeader.h, Ledger.cpp

Key Points:

  • Immutability: Once mImmutable is set to true via setImmutable(), the ledger cannot be modified. Only immutable ledgers can be stored in a LedgerHolder or published to the network.
  • SHAMaps: Both stateMap_ and txMap_ are Merkle trees. The root hash of each tree is stored in header_.accountHash and header_.txHash respectively.
  • Rules: The rules_ object determines which amendments are active, affecting how transactions are processed.

Core Components:

Inside the Ledger class: the LedgerHeader (seq, hashes, closeTime, drops), the stateMap_ SHAMap of all ledger objects, the txMap_ SHAMap of transactions with metadata, and the Rules and Fees objects

Construction Methods

Ledgers can be created in several ways, depending on the source of data:

1. Genesis Ledger:

Used to create the very first ledger in a new network.

2. From Previous Ledger:

// Ledger.cpp - Constructor signature
Ledger::Ledger(
    Ledger const& prevLedger,
    NetClock::time_point closeTime)
{
    // Copy and evolve:
    // - Increment sequence
    // - Set parent hash to previous ledger's hash
    // - Apply new close time
    // - Inherit state (copy-on-write)
}

This is the most common constructor used during consensus to build the next ledger.

3. From Serialized Data:

// Ledger.cpp - Constructor signature
Ledger::Ledger(
    LedgerHeader const& info,
    Config const& config,
    Family& family)
{
    // Reconstruct from:
    // - Persisted header info
    // - Load SHAMaps from NodeStore
}

Used when loading historical ledgers from the database or acquiring them from peers.

LedgerHeader Structure

The LedgerHeader struct contains all the metadata that uniquely identifies a ledger and its state. This is the data that hashes to the ledger's hash.

Location: LedgerHeader.h

Key Header Fields:

Field Purpose
seq Ledger sequence number, incrementing from genesis
parentHash Cryptographic link to previous ledger
accountHash Root hash of state tree (from stateMap_.getHash())
txHash Root hash of transaction tree (from txMap_.getHash())
drops Total XRP in existence (decreases as fees are burned)
closeTime When this ledger closed (consensus-agreed time)
parentCloseTime Parent ledger's close time
closeTimeResolution Granularity of close time (2-120 seconds)
closeFlags Indicates if close time had consensus
validated Set to true once ledger receives quorum validations

Header Hashing:

The ledger hash uniquely identifies the ledger and is computed by serializing and hashing the header fields:

This creates a unique, tamper-evident fingerprint for the entire ledger state. Any change to the state tree, transaction tree, or metadata produces a different hash.

Note: The "ledger base" refers to a query/response that includes the ledger header and may also contain the root node of the state tree. This is used during ledger acquisition from peers.

LedgerHolder

A thread-safe container that holds an immutable ledger. Only immutable ledgers can be held - this is enforced at runtime.

Location: LedgerHolder.h

Why immutability matters:

  • Multiple threads can safely read from an immutable ledger without locks
  • Once set, the ledger's state never changes, preventing race conditions
  • LedgerMaster uses LedgerHolders to track mValidLedger, mClosedLedger, etc.

Usage Pattern:

LedgerHolder: multiple threads read concurrently through a mutex-guarded holder that only ever hands out const, immutable ledgers

LedgerHistory

Manages the cache and retrieval of historical ledgers.

Location: LedgerHistory.h, LedgerHistory.cpp

Cache Organization:

LedgerHistory's two lookup paths: m_ledgers_by_hash (a TaggedCache from hash to ledger object) and mLedgersByIndex (a map from sequence to hash), so lookups go seq to hash to ledger, or hash to ledger directly

Key Operations:

The insert() method (from LedgerHistory.cpp) adds ledgers to both caches and ensures the index-to-hash mapping is correct. The fixIndex() method is used to correct any inconsistencies in the index mapping that may occur during acquisition.

Immutability Enforcement

Once a ledger is finalized, it can never be changed. This is enforced both at compile time (via const correctness) and runtime (via the mImmutable flag).

Immutability Lifecycle:

The mutability lifecycle: constructed mutable, still mutable through build and consensus, locked by setImmutable() for safe concurrent reads, and finally validated once a quorum of trusted validations arrives

Immutability Rules:

Operation Mutable Ledger Immutable Ledger
Modify state (rawInsert, rawReplace) Allowed Forbidden
Add transactions Allowed Forbidden
Store in LedgerHolder Runtime error Required
Publish to network Forbidden Required
Cache in LedgerHistory Not typical Allowed
Read operations (read, peek) Allowed Allowed

SHAMap Integration

Ledgers use SHAMaps for state and transaction storage. The SHAMap is a Merkle-Patricia trie covered in detail in later modules. Here we focus on how the Ledger class interacts with it:

The two SHAMaps inside a ledger: stateMap_ with account nodes and txMap_ with transaction nodes, whose root hashes are exactly the header's accountHash and txHash

When setImmutable(true) is called, the final root hashes are retrieved from the SHAMaps and stored in the LedgerHeader.

The Rules Class

The Rules class encapsulates which amendments (protocol upgrades) are enabled for a specific ledger. All nodes must agree on which amendments are active for deterministic transaction processing.

Location: Rules.h

How Rules Are Used:

Throughout transaction processing, the code checks if specific amendments are enabled to determine which logic path to use:

// Example from payment processing
if (view.rules().enabled(featureFlowCross))
{
    // Use new payment engine introduced by FlowCross amendment
    return flowCross(view, deliver, account, src, dst, ...);
}
else
{
    // Use legacy payment engine
    return legacyPaymentEngine(view, deliver, ...);
}

Amendment Activation:

Amendments ledger by ledger: validators vote in validations, a majority over 80% for two weeks is recorded at a flag ledger, makeRulesGivenLedger builds a Rules object per ledger, and processing stays deterministic across eras

Why This Matters:

  • Consensus requires determinism: All validators must process transactions identically
  • Amendments change behavior: Different amendment sets → different results
  • Rules track the truth: The Rules object for ledger N reflects exactly which amendments were active when ledger N was built

See the Amendments, overview & architecture module for full details on the amendment system.

Key Operations

The Ledger class provides methods to read and modify state objects. All operations work through the stateMap_ SHAMap.

Reading State:

The read() method uses a Keylet for type-safe lookups. A Keylet combines a key with a type checker, ensuring you retrieve the correct object type (e.g., keylet::account(accountID) for account roots).

Modifying State (Mutable Ledgers Only):

// Insert new object
void rawInsert(std::shared_ptr<SLE> const& sle);

// Update existing object
void rawReplace(std::shared_ptr<SLE> const& sle);

// Delete object
void rawErase(std::shared_ptr<SLE> const& sle);

These methods:

  • Only work on mutable ledgers (before setImmutable() is called)
  • Serialize the SLE to binary format
  • Add/update/remove items in stateMap_
  • Throw LogicError if the operation fails (duplicate key, missing key, etc.)

Database Persistence

Ledgers are stored using a two-tier system:

Persistence: the LedgerHeader goes to the SQL database for fast seq/hash lookups, while stateMap_ and txMap_ nodes go to the NodeStore, stored by hash, deduplicated across ledgers and lazily loaded

Why Two Storage Systems?

  1. SQL Database: Stores ledger headers for fast sequential access and indexing
  2. NodeStore: Stores SHAMap tree nodes for efficient content-addressable storage

When loading a ledger:

  1. Retrieve header from SQL database by sequence or hash
  2. Construct empty SHAMaps with root hashes from header
  3. Load tree nodes from NodeStore on-demand as they're accessed

Integrity Verification:

Before persisting, the system verifies hash consistency:

assert(header_.accountHash == stateMap_.getHash().as_uint256());
assert(header_.txHash == txMap_.getHash().as_uint256());

This ensures the header accurately represents the tree contents. Mismatches indicate data corruption, Byzantine attacks, or implementation bugs.

Implementation: See Node.cpp and SQLiteDatabase.cpp for details.


Summary

This module described the ledger itself, the object consensus is trying to agree on. You learned the LedgerHeader and the two SHAMaps a ledger carries (account state and transactions), how a ledger is built, closed, and hashed, and how each ledger is chained to its parent by hash. A single ledger hash pins down the entire state and transaction set.

To remember:

  • A ledger = header + state SHAMap + transaction SHAMap
  • Header fields to know: ledger_index, parent_hash, account_hash, transaction_hash, close_time
  • ledger_hash is the hash of the header, which commits both trees: one hash pins everything
  • Chaining: each header carries parent_hash; tamper with one ledger and every later hash breaks
  • Close time is agreed during consensus and rounded
  • Lifecycle: open (collecting), closed (agreed set), validated (quorum signed)
  • Code: src/xrpld/app/ledger and include/xrpl/ledger
  • Watch out: only validated data is safe to act on; the open ledger is a moving draft

Next up. You know what a ledger is made of. Now the famous question: how do hundreds of nodes agree on the next one, every few seconds, with no leader? Consensus fundamentals.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026