The XRP Ledger Consensus Protocol — what it agrees on and the generic consensus engine.
What you'll learn
≈90 min · Intermediate · builds on Ledger architecture & data structures
How do thousands of independent nodes agree on one history, without a central authority, and without proof-of-work? In this module you'll learn what XRPL consensus actually decides, why it relies on trusted validators and a UNL instead of mining, and the roles of proposals and quorum. This is the idea at the very core of the ledger.
In brief: what consensus decides (the transaction set), and why it is not proof-of-work.
Consensus is the process by which a distributed network of nodes agrees on a single shared state. In the context of the XRP Ledger, this means agreeing on:
Without consensus, different nodes might have different views of the ledger, leading to double-spending and inconsistencies.
Traditional blockchains like Bitcoin use proof-of-work (PoW):
XRP Ledger's approach is different:
The XRP Ledger consensus protocol is Byzantine Fault Tolerant (BFT), meaning it can tolerate some validators being:
Key Property: As long as >80% of trusted validators are honest and online, consensus will be reached correctly.
Security Model:
Network can tolerate up to 20% faulty validators
Examples:
- 100 validators → Can handle 20 failures
- 50 validators → Can handle 10 failures
- 35 validators (XRP Ledger mainnet) → Can handle 7 failures
In brief: each node trusts a list of validators (its UNL) to agree with.
Validators are nodes that participate in the XRP Ledger consensus process, validating transactions and agreeing on the state of the ledger. Each validator proposes and votes on ledger updates during consensus rounds.
A Unique Node List (UNL) is a trusted set of validators chosen by a participant. By relying on their UNL, a node can efficiently reach consensus while protecting against faulty or malicious validators. Proper UNL selection is crucial for network security, decentralization, and ledger reliability.
{% embed url="https://www.youtube.com/watch?v=4b58RtqO-oU" %}
A validator is a rippled server configured to participate in consensus by:
Not all rippled servers are validators. Most servers are:
To become a validator, a server needs:
validator-keys tool)xrpld.cfg to enable validationEach validator maintains a Unique Node List (UNL), a list of validators it trusts to be honest and not collude.
Key Concepts:
Personal Choice: Each validator operator chooses their own UNL based on their trust relationships.
Overlap Required: For the network to reach consensus, there must be sufficient overlap between validators' UNLs. The protocol requires >90% overlap to ensure agreement.
Default UNL: Most operators use the default UNL provided by the XRP Ledger Foundation, which is regularly updated and reviewed.
Dynamic Updates: UNLs can be updated over time as validators join or leave the network.
In validators.txt:
# Validator List (maintained by XRP Ledger Foundation)
# Format: validator_public_key [optional_comment]
nHUon2tpyJEHHYGmxqeGu37cvPYHzrMtUNQFVdCgGNvYkr4k
nHBidG3pZK11zQD6kpNDoAhDxH6WLGui6ZxSbUx7LSqLHsgzMPe
nHUcNC5ni7XjVYfCMe38Rm3KQaq27jw7wJpcUYdo4miWwpNePRTw
nHU95JxeaHJoSdpE7R49Mxp4611Yk5yL9SGEc12UDJLr4oEUN
# ... more validators
# Optional: Add custom validators
# nH... My Custom Validator
The validator list can be automatically fetched from trusted sources:
[validators_file]
validators.txt
[validator_list_sites]
https://vl.ripple.com
https://vl.xrplf.org
[validator_list_keys]
ED2677ABFFD1B33AC6FBC3062B71F1E8397C1505E1C42C64D11AD1B28FF73F4734
This allows dynamic updates without manual configuration changes.
Key idea. Consensus is trust-based, not work-based. Your node only counts proposals from validators on its UNL, and roughly 80% of them must agree.
In brief: the repeating round that closes one ledger after another.
Consensus operates in discrete rounds, each typically lasting 3-5 seconds. Each round attempts to agree on the next ledger.
Round Phases:
Duration: Variable (typically 2 to 5 seconds; it can stretch toward ~15 seconds when the network is idle and there is nothing to close)
Purpose: Collect transactions for the next ledger
What Happens:
Key Point: The open ledger is not final, it shows what might be in the next ledger, but consensus hasn't been reached yet.
// Transactions entering open ledger
void NetworkOPs::processTransaction(
std::shared_ptr<Transaction> const& transaction)
{
// Apply to open ledger
auto const result = app_.openLedger().modify(
[&](OpenView& view)
{
return transaction->apply(app_, view);
});
if (result.second) // Transaction applied successfully
{
// Relay to network
app_.overlay().relay(transaction);
}
}
Duration: 2-4 seconds (multiple sub-rounds with 50% increase each time)
Purpose: Validators exchange proposals and converge on a common transaction set
Process:
Initial Proposal
Each validator creates a proposal containing:
// Simplified proposal structure
struct ConsensusProposal
{
uint256 previousLedger; // Hash of previous ledger
uint256 position; // Hash of proposed transaction set
NetClock::time_point closeTime; // Proposed close time
PublicKey publicKey; // Validator's public key
Signature signature; // Proposal signature
};
Proposal Exchange
Validators broadcast proposals to the network using tmPROPOSE_LEDGER messages.
Agreement Threshold
Validators track which transactions appear in proposals from their UNL:
Iterative Refinement
Multiple rounds of proposals:
Round 1 (Initial): Each validator proposes their transaction set
Round 2 (50% threshold): Validators update proposals, including only transactions with >50% support
Round 3+ (Increasing threshold): Threshold increases each round, converging toward agreement
Round 1: 50% threshold, 2 second timer
Round 2: 65% threshold, 3 second timer (50% increase)
Round 3: 80% threshold, 4.5 second timer
Round 4: 95% threshold, 6.75 second timer
...
Avalanche Effect
Once enough validators converge on the same set, others quickly follow (avalanche effect), achieving rapid consensus.
Duration: Instant (threshold is reached)
Purpose: Consensus is reached, transaction set is accepted
Trigger: When a validator sees >80% of its UNL agreeing on the same transaction set
What Happens:
// Simplified consensus acceptance check
bool hasConsensus(ConsensusMode mode, int validations)
{
if (mode == ConsensusMode::Proposing)
{
// Need 80% of UNL to agree
return validations >= (unlSize_ * 4 / 5);
}
return false;
}
Duration: 1-2 seconds
Purpose: Validators sign and broadcast validations
What Happens:
Validation Message (tmVALIDATION):
struct STValidation
{
uint256 ledgerHash; // Hash of validated ledger
uint32 ledgerSequence; // Ledger sequence number
NetClock::time_point signTime; // When validation was signed
PublicKey publicKey; // Validator's public key
Signature signature; // Validation signature
bool full; // Full validation vs partial
};
Validation Collection:
Total time from consensus start to validation: ~3-5 seconds. Total time from transaction submission to confirmation: typically ~4-10 seconds (depending on when in the open phase you submit; Mainnet closes a ledger every 3-5 seconds)
For all validators to reach the same ledger state, they must apply transactions in exactly the same order. Different orders can produce different results:
Example:
The XRP Ledger uses canonical ordering to ensure determinism:
Primary Sort: By account (lexicographic order of account IDs)
Secondary Sort: By transaction sequence number (nonce)
// Canonical transaction ordering
bool txOrderCompare(STTx const& tx1, STTx const& tx2)
{
// First, sort by account
if (tx1.getAccountID(sfAccount) < tx2.getAccountID(sfAccount))
return true;
if (tx1.getAccountID(sfAccount) > tx2.getAccountID(sfAccount))
return false;
// Same account, sort by sequence number
return tx1.getSequence() < tx2.getSequence();
}
This ensures:
The transaction set is represented by a hash:
// Calculate transaction set hash
uint256 calculateTxSetHash(std::vector<STTx> const& transactions)
{
// Sort transactions canonically
auto sortedTxs = transactions;
std::sort(sortedTxs.begin(), sortedTxs.end(), txOrderCompare);
// Hash all transactions together
Serializer s;
for (auto const& tx : sortedTxs)
{
s.addBitString(tx.getHash());
}
return s.getSHA512Half();
}
Simplified. The real canonical order is salted:
CanonicalTXSetXORs each account key with a salt derived from the previous ledger's hash, so the order is deterministic for everyone but unpredictable in advance (nobody can buy a better position). The sort shown here keeps the idea readable; see the Transaction ordering module for the real mechanics.
This hash is what validators include in their proposals, a compact representation of the entire transaction set.
A dispute occurs when validators initially disagree about which transactions should be included in the next ledger. This is normal and expected, validators may have different views due to:
Disputes are resolved through the iterative consensus rounds:
Round 1: Initial Disagreement
Validator A proposes: {TX1, TX2, TX3, TX4, TX5}
Validator B proposes: {TX1, TX2, TX3, TX6, TX7}
Validator C proposes: {TX1, TX2, TX4, TX5, TX8}
Agreement:
- TX1: 100% (all three)
- TX2: 100% (all three)
- TX3: 67% (A, B)
- TX4: 67% (A, C)
- TX5: 67% (A, C)
- TX6: 33% (B only)
- TX7: 33% (B only)
- TX8: 33% (C only)
Round 2: Converge on High-Agreement TXs
Validators drop transactions with <50% support:
Validator A proposes: {TX1, TX2, TX3, TX4, TX5} (drops nothing, all >50%)
Validator B proposes: {TX1, TX2, TX3} (drops TX6, TX7)
Validator C proposes: {TX1, TX2, TX4, TX5} (drops TX8)
Agreement:
- TX1: 100%
- TX2: 100%
- TX3: 67% (A, B)
- TX4: 67% (A, C)
- TX5: 67% (A, C)
Round 3: Further Convergence
As threshold increases to 80%, validators must drop disputed transactions:
All validators propose: {TX1, TX2}
Agreement:
- TX1: 100% ✓ (exceeds 80%)
- TX2: 100% ✓ (exceeds 80%)
Consensus reached on {TX1, TX2}
TX3, TX4, TX5 deferred to next ledger
Transactions that don't reach consensus are not lost:
If a validator behaves maliciously:
A ledger close is triggered when:
Timer-based: Minimum time has elapsed (typically 2-10 seconds)
Transaction threshold: Sufficient transactions have accumulated
Consensus readiness: Validators are ready to reach agreement
// Simplified close trigger logic
bool shouldCloseLedger()
{
auto const elapsed = std::chrono::steady_clock::now() - lastClose_;
// Minimum close interval elapsed?
if (elapsed < minCloseInterval_)
return false;
// Sufficient transactions?
if (openLedger_.size() >= closeThreshold_)
return true;
// Maximum close interval elapsed?
if (elapsed >= maxCloseInterval_)
return true;
return false;
}
Validators must also agree on the close time of the ledger:
Why it matters: Some transactions are time-dependent (escrows, offers with expiration)
Process:
Close Time Resolution:
After consensus is reached:
Step 1: Apply agreed transaction set
// Apply transactions in canonical order
for (auto const& tx : canonicalOrder(agreedTxSet))
{
auto const result = applyTransaction(tx, view);
// Record metadata
}
Step 2: Compute ledger hash
// Hash includes:
// - Parent ledger hash
// - Transaction set hash
// - Account state hash
// - Close time
// - Ledger sequence
auto ledgerHash = computeLedgerHash(
parentHash,
txSetHash,
stateHash,
closeTime,
ledgerSeq);
Step 3: Create and broadcast validation
STValidation validation;
validation.setLedgerHash(ledgerHash);
validation.setLedgerSequence(ledgerSeq);
validation.setSignTime(now);
validation.sign(validatorKey);
overlay().broadcast(validation);
Step 4: Collect validations
// Wait for validations from UNL
while (validationCount < unlSize_ * 4 / 5)
{
// Process incoming validations
auto val = receiveValidation();
if (val.getLedgerHash() == ledgerHash)
validationCount++;
}
// Ledger is now fully validated
ledgerMaster_.setFullyValidated(ledger);
Consensus Core:
src/xrpld/consensus/Consensus.h - Main consensus engine interfacesrc/xrpld/consensus/ConsensusProposal.h - Proposal structuresrc/xrpld/consensus/Validations.h - Validation trackingConsensus Implementation:
src/xrpld/app/consensus/RCLConsensus.h - XRP Ledger-specific consensussrc/xrpld/app/consensus/RCLValidations.cpp - Validation handlingNetwork Messages:
src/xrpld/overlay/detail/ProtocolMessage.h - tmPROPOSE_LEDGER, tmVALIDATIONConfiguration:
validators.txt - UNL configurationxrpld.cfg - Validator key configurationConsensus Class
template <class Adaptor>
class Consensus
{
public:
// Start new consensus round
void startRound(
LedgerHash const& prevLedgerHash,
Ledger const& prevLedger,
NetClock::time_point closeTime);
// Process peer proposal
void peerProposal(
NetClock::time_point now,
ConsensusProposal const& proposal);
// Simulate new round
void timerEntry(NetClock::time_point now);
// Check if consensus reached
bool haveConsensus() const;
private:
// Current round state
ConsensusPhase phase_;
std::map<NodeID, ConsensusProposal> peerProposals_;
std::set<TxID> disputes_;
TxSet ourPosition_;
};
RCLConsensus (Ripple Consensus Ledger)
XRP Ledger-specific consensus implementation:
class RCLConsensus
{
public:
// Handle consensus result
void onAccept(
Result const& result,
RCLCxLedger const& prevLedger,
NetClock::duration closeResolution,
CloseTimes const& rawCloseTimes,
ConsensusMode mode);
// Create initial position
RCLTxSet getInitialPosition(
RCLCxLedger const& prevLedger);
// Check if we should close ledger
void checkClose(NetClock::time_point now);
};
Finding Consensus Start
Search for ledger close triggers:
// In NetworkOPs or LedgerMaster
void beginConsensus(LedgerHash const& prevHash)
{
// Build initial transaction set
auto initialSet = buildTxSet();
// Start consensus round
consensus_.startRound(
prevHash,
prevLedger,
suggestCloseTime());
}
Tracing Proposal Handling
Follow proposal processing:
// Overlay receives tmPROPOSE_LEDGER message
void onProposal(std::shared_ptr<protocol::TMProposeSet> const& proposal)
{
// Validate proposal signature
if (!verifyProposal(proposal))
return;
// Pass to consensus engine
consensus_.peerProposal(
now(),
parseProposal(proposal));
}
Understanding Validation
Follow validation creation and verification:
// Create validation
auto validation = std::make_shared<STValidation>(
ledgerHash,
signTime,
publicKey,
nodeID,
[&](STValidation& v)
{
v.sign(secretKey);
});
// Broadcast to network
overlay().send(validation);
In brief: clearing up what consensus does and does not guarantee.
False: Validators don't perform computational work. They simply vote on which transactions to include.
False: Any organization can run validators, and each validator operator independently chooses their UNL. While many operators use the recommended UNL from the XRP Ledger Foundation, they're free to customize it.
False: Most rippled servers are tracking servers that follow consensus but don't vote. Only configured validators participate.
False: As long as >80% of a validator's UNL is operational and honest, consensus proceeds normally.
False: The XRP Ledger has never had a fork (competing chains). The consensus protocol prevents this by design.
src/xrpld/consensus - Generic consensus frameworksrc/xrpld/app/consensus - XRP Ledger-specific implementationsrc/xrpld/app/ledger/ConsensusTransSetSF.cpp - Transaction set managementThe XRP Ledger consensus mechanism is the heart of how distributed nodes agree on a single, canonical view of the network state. Unlike proof-of-work systems that rely on computational puzzles, or proof-of-stake systems that rely on economic incentives, XRPL uses a unique consensus protocol based on trusted validators reaching agreement through iterative voting.
This chapter introduces the fundamental concepts that underpin the consensus process, providing the foundation for understanding how thousands of independent nodes create identical ledgers without central coordination.
The Challenge:
In a distributed system with no central authority, how do nodes agree on:
Traditional Solutions and Their Trade-offs:
| Approach | Mechanism | Trade-off |
|---|---|---|
| Proof of Work | Computational puzzles | High energy cost, slow finality |
| Proof of Stake | Economic staking | Wealth concentration, nothing-at-stake |
| PBFT | Voting rounds | Limited scalability |
| XRPL Consensus | Trusted validators + iterative agreement | Requires UNL overlap |
The XRPL consensus protocol achieves agreement through a federated model:
Key Principles:
The consensus process is implemented as a template-based state machine in the codebase:
// Core consensus engine (Consensus.h)
template <typename Adaptor>
class Consensus {
// Current phase of consensus
ConsensusPhase phase_;
// Operating mode of this node
ConsensusMode mode_;
// Timing for this round
ConsensusTimer timer_;
// Peer proposals and disputes
std::map<NodeID, ConsensusProposal> currPeerPositions_;
std::map<TxID, DisputedTx> disputes_;
};
Design Properties:
The UNL is the set of validators a node trusts for consensus:
UNL Requirements:
Important Distinction:
| Consensus | Validation |
|---|---|
| Agreement on transaction set | Cryptographic endorsement of ledger |
| Happens during ledger close | Happens after ledger is built |
| Determines what's in the ledger | Confirms ledger is correct |
| Internal process | Published to network |
Flow:
XRPL consensus tolerates Byzantine (malicious or faulty) validators:
Tolerance Threshold:
For UNL of size n:
- Can tolerate up to ⌊n/5⌋ Byzantine validators (less than 20%)
- Requires 80% honest agreement
- Safety guaranteed with <20% Byzantine
Attack Resistance:
Key parameters that control consensus behavior:
struct ConsensusParms {
// Minimum consensus percentage required
static constexpr int minCONSENSUS_PCT = 80;
// Close time consensus threshold (added)
static constexpr int avCT_CONSENSUS_PCT = 75;
// Minimum time before consensus can be reached
std::chrono::milliseconds ledgerMinConsensus{1950};
// Maximum time before consensus times out
std::chrono::seconds ledgerMaxConsensus{15};
// Avalanche state machine thresholds
std::map<AvalancheState, int> avalancheCutoffs;
};
Parameter Impact:
| Parameter | Low Value | High Value |
|---|---|---|
| minCONSENSUS_PCT | Faster but less secure | Slower but more secure |
| ledgerMinConsensus | Faster finality | More time for propagation |
| ledgerMaxConsensus | Potential stalls | Eventual termination |
XRPL's avalanche mechanism uses increasing thresholds to force convergence on disputed transactions:
Threshold Progression:
How It Works:
Key Point: Thresholds rise over time, making it progressively harder to dissent from the majority. This creates an "avalanche effect" - once a majority position emerges, validators are forced to converge to it.
Purpose:
Analogy: Like a real avalanche, once momentum builds (majority emerges), the increasing thresholds make it impossible to stop the convergence toward that position.
Consensus doesn't operate in isolation, it's tightly integrated with ledger management:
Understanding consensus fundamentals is essential because:
This module explained what XRPL consensus is. It decides which transactions enter the next ledger and in what order, and it does so without proof-of-work: each node trusts a list of validators (its UNL), and a ledger is agreed once roughly 80% of them concur, round after round. That trust-based design is what keeps thousands of independent nodes on one shared history.
To remember:
src/xrpld/consensus; XRPL glue: src/xrpld/app/consensus (RCLConsensus)validators.txt / published validator lists (e.g. vl.ripple.com)Next up. You have the theory of agreement; next you watch a round actually run: the consensus lifecycle, phase by phase, timer tick by timer tick.
Resources
Assignments
0 of 2 complete