A consensus round in detail — modes, phases (open / establish / accepted) and timing.
What you'll learn
≈90 min · Advanced · builds on Consensus fundamentals
Now let's watch a single consensus round unfold in detail. In this module you'll walk it through its phases (open, establish, accepted) and learn the ConsensusMode states (proposing, observing, wrong-ledger…) and the timing that governs when a ledger closes. It's consensus in motion, step by step.
In brief: one consensus round from start to a validated ledger, at a glance.
Key idea. A round moves through open, establish, and accepted. The
ConsensusMode(proposing, observing, wrong-ledger) tells you what part your node is playing at any moment.
Entry Point: Application.cpp
Consensus begins at application startup:
// Application.cpp line 1385
// Start first consensus round
if (!m_networkOPs->beginConsensus(
m_ledgerMaster->getClosedLedger()->header().hash, {}))
{
JLOG(m_journal.fatal()) << "Unable to start consensus";
return false;
}
Key Actions:
Function: RCLConsensus::Adaptor::preStartRound
Before a round begins, the system prepares:
Checks Performed:
Function: Consensus::startRound
This function establishes the round parameters:
void startRound(
NetClock::time_point const& now,
typename Ledger_t::ID const& prevLedgerID,
Ledger_t prevLedger,
hash_set<NodeID_t> const& nowUntrusted,
hash_set<NodeID_t> const& nowTrusted)
{
// Adaptor determines if we should propose
bool proposing = adaptor_.preStartRound(prevLedger, nowTrusted);
// Set initial mode
ConsensusMode mode = proposing ?
ConsensusMode::Proposing : ConsensusMode::Observing;
// Enter internal initialization
startRoundInternal(now, prevLedgerID, prevLedger, mode, clog);
}
Parameters Established:
In brief: transactions accumulate in the open ledger before it closes.
Function: Consensus::startRoundInternal → ConsensusPhase::Open
Key Timing Parameters:
| Parameter | Value | Purpose |
|---|---|---|
| ledgerIdleInterval | 15s | Max time ledger stays open with no transactions |
| ledgerMinClose | 2s | Minimum time ledger must stay open |
| prevRoundTime/2 | Dynamic | Must stay open ≥ half of previous consensus time |
| proposeFRESHNESS | 20s | How long peer proposals remain valid |
| proposeINTERVAL | 12s | How often we must refresh our proposal |
The ledger opens to accept transactions:
Key Activities:
playbackProposals():
Example:
timerEntry() in open phase:
checkLedger()shouldCloseLedger() to determine if ledger should closeExample:
t=0s: Open phase starts
t=1s: timerEntry() → shouldCloseLedger() → NO (too early)
t=2s: timerEntry() → shouldCloseLedger() → YES (conditions met)
└─ closeLedger() → phase = ESTABLISH
Close conditions checked:
Function: shouldCloseLedger
Determines if the ledger should close:
// Key logic from shouldCloseLedger() in Consensus.cpp
// If majority of peers already closed, follow them
if ((proposersClosed + proposersValidated) > (prevProposers / 2)) {
JLOG(j.trace()) << "Others have closed";
return true; // Close with the network
}
// No transactions? Only close after idle interval
if (!anyTransactions) {
return timeSincePrevClose >= idleInterval; // Default 15s
}
// Enforce minimum ledger open time
if (openTime < parms.ledgerMinClose) { // 2 seconds
JLOG(j.debug()) << "Must wait minimum time before closing";
return false;
}
// Don't close faster than half the previous consensus time
// (allows slower validators to keep up)
if (openTime < (prevRoundTime / 2)) {
JLOG(j.debug()) << "Ledger has not been open long enough";
return false;
}
return true; // All conditions met
Close Decision Process:
Step 1: Safety Checks
Step 2: Network Coordination
Step 3: Transaction-Based Decision
Path A: No Transactions
Path B: Has Transactions
Key Parameters:
| Parameter | Value | Purpose |
|---|---|---|
| ledgerMinClose | 2s | Minimum time ledger must stay open |
| ledgerIdleInterval | 15s | Maximum idle time before forcing close |
| prevRoundTime/2 | Dynamic | Throttle: ledger must stay open ≥ half of previous round time |
Function: Consensus::closeLedger
Transitions from open to establish phase:
Actions:
In brief: nodes converge their transaction sets until they agree.
Function: Consensus::phaseEstablish
Key Timing Parameters:
| Parameter | Value | Purpose |
|---|---|---|
| ledgerMinConsensus | 1.95s | Minimum time before consensus can be declared |
| ledgerMaxConsensus | 15s | Maximum time to wait for lagging validators |
| ledgerABANDON_CONSENSUS | 120s | Absolute maximum round time |
| ledgerABANDON_CONSENSUS_FACTOR | 10× | Dynamic abandonment: min(prevTime × 10, 120s) |
| avMIN_CONSENSUS_TIME | 5s | Minimum time used for avalanche calculations |
Avalanche Threshold Progression:
convergePercent = (currentRoundTime × 100) / max(prevRoundTime, avMIN_CONSENSUS_TIME)
Thresholds by state:
init (0% time): 50% agreement needed
mid (50% time): 65% agreement needed
late (85% time): 70% agreement needed
stuck (200% time): 95% agreement needed
Example: If previous round took 4s, use avMIN_CONSENSUS_TIME (5s)
At 2.5s: convergePercent = (2500 × 100) / 5000 = 50%
→ Transition from 'init' to 'mid'
→ Threshold increases from 50% to 65%
The core of consensus, exchanging proposals and resolving disputes:
Key Functions:
updateOurPositions():
shouldPause():
haveConsensus() → checkConsensus() → checkConsensusReached():
Consensus Evaluation Logic:
ConsensusState checkConsensus(
std::size_t prevProposers,
std::size_t currentProposers,
std::size_t currentAgree,
std::size_t currentFinished,
std::chrono::milliseconds previousAgreeTime,
std::chrono::milliseconds currentAgreeTime,
bool stalled,
ConsensusParms const& parms,
bool proposing)
{
// Check minimum time elapsed
if (currentAgreeTime <= parms.ledgerMinConsensus)
return ConsensusState::No;
// Check if we have reduced participation
if (currentProposers < prevProposers * 3/4) {
if (currentAgreeTime < previousAgreeTime + parms.ledgerMinConsensus)
return ConsensusState::No;
}
// Check if we reached 80% agreement on transaction set
if (checkConsensusReached(
currentAgree,
currentProposers,
proposing,
parms.minCONSENSUS_PCT, // Always 80%
currentAgreeTime > parms.ledgerMaxConsensus,
stalled))
return ConsensusState::Yes;
// Check if 80% of peers moved on without us
if (checkConsensusReached(
currentFinished,
currentProposers,
false,
parms.minCONSENSUS_PCT, // Always 80%
currentAgreeTime > parms.ledgerMaxConsensus,
false))
return ConsensusState::MovedOn;
// Check if consensus has taken too long
// Timeout is bounded: min(max(prevTime × 10, 15s), 120s)
std::chrono::milliseconds const maxAgreeTime =
previousAgreeTime * parms.ledgerABANDON_CONSENSUS_FACTOR; // 10
if (currentAgreeTime > std::clamp(
maxAgreeTime,
parms.ledgerMaxConsensus, // 15s minimum
parms.ledgerABANDON_CONSENSUS)) // 120s maximum
return ConsensusState::Expired;
return ConsensusState::No;
}
Agreement Threshold:
checkConsensusReached():
agreement_pct = (agreeing / total) * 100
Required: 80% (fixed - minCONSENSUS_PCT)
This checks if 80% of validators have the SAME complete transaction set (same hash).
Two checks performed:
Important Distinction:
The 80% threshold here is for final consensus (comparing complete transaction set hashes).
This is separate from avalanche voting (50%→65%→70%→95%) which determines which individual transactions to include during updateOurPositions().
Consensus States:
| State | Meaning | Action |
|---|---|---|
| No | Not enough agreement yet | Continue voting |
| Yes | 80%+ agree with us | Accept consensus! |
| MovedOn | 80%+ moved to next ledger | We're behind, catch up |
| Expired | Took too long | Bow out of consensus |
In brief: the agreed ledger is closed and accepted.
Function: RCLConsensus::Adaptor::onAccept
When consensus is reached:
Sub-Functions:
buildLCL():
LedgerMaster::consensusBuilt():
Apply Disputed Transactions:
Function: RCLConsensus::Adaptor::doAccept
Finalizes the acceptance process:
Key Actions:
BuildLedger::buildLedger:
app_.getTxQ().processClosedLedger:
app_.openLedger().accept:
Function: NetworkOPsImp::endConsensus
Completes the round and prepares for the next:
Function: NetworkOPsImp::beginConsensus
The cycle continues:
// Begin next consensus round
reportConsensusStateChange(); // Log state change
RCLConsensus::startRound(); // Initialize next round
// Loop back to Stage 2
The twelve stages above chain into one continuous cycle: begin consensus, run the open, establish, and accepted phases on timer ticks, end consensus, and immediately begin again. The lifecycle diagram at the top of this module shows the full loop in one picture.
The consensus process operates as a state machine with two orthogonal dimensions: the mode (the node's relationship with the network) and the phase (the current stage of the consensus round). Understanding these states is crucial for debugging, monitoring, and developing consensus-related functionality.
This chapter provides a detailed breakdown of each mode and phase, their transitions, and how they affect node behavior.
The ConsensusMode enum defines how a node participates in consensus:
enum class ConsensusMode {
Proposing, // Actively proposing positions
Observing, // Watching but not proposing
WrongLedger, // Out of sync with network
SwitchedLedger // Recently switched to different ledger
};
Definition: The node is actively participating in consensus by broadcasting proposals.
Requirements for Proposing:
Behavior:
Definition: The node monitors consensus but doesn't submit proposals.
When Observing:
Behavior:
Definition: The node is operating on a different ledger than the network consensus.
Causes:
Recovery Actions:
Definition: The node recently switched to a different ledger chain.
Behavior:
The ConsensusPhase enum defines the current stage within a consensus round:
enum class ConsensusPhase {
Open, // Collecting transactions
Establish, // Exchanging proposals, resolving disputes
Accepted // Consensus reached, ledger closed
};
Definition: The ledger is open and accepting new transactions.
Close Conditions:
The open phase continues until specific conditions signal it's time to close the ledger and begin consensus. Multiple conditions are evaluated:
1. Minimum Time Requirement (ledgerMinClose = 2s)
2. Transaction Activity or Idle Timeout
3. Speed Limiting (openTime ≥ prevRoundTime/2)
4. Network Coordination (>50% validators closed)
Key Functions:
playbackProposals()
shouldCloseLedger()
rippled/src/xrpld/consensus/Consensus.cpptrue when conditions are met, triggering the transition to establish phaseTiming Characteristics:
The open phase duration varies based on network conditions:
Typical Duration: 2-15 seconds
Minimum Duration: 2 seconds (ledgerMinClose)
Maximum Idle Duration: 15 seconds (ledgerIdleInterval)
Definition: Proposals are being exchanged and disputes are being resolved.
Activities:
Timing:
Avalanche Thresholds:
As time progresses through the establish phase, the threshold for including disputed transactions increases:
| State | Time Threshold | Agreement Required | Purpose |
|---|---|---|---|
| init | 0% of prevRoundTime | 50% | Initial voting - easy to add transactions |
| mid | 50% of prevRoundTime | 65% | Mid-consensus - slightly harder |
| late | 85% of prevRoundTime | 70% | Late consensus - harder still |
| stuck | 200% of prevRoundTime | 95% | Stuck - very hard to change |
The time percentage is calculated as:
convergePercent = (currentRoundTime × 100) / max(prevRoundTime, avMIN_CONSENSUS_TIME)
currentRoundTime
prevRoundTime
avMIN_CONSENSUS_TIME
max(prevRoundTime, avMIN_CONSENSUS_TIME)
This rising threshold forces the network to converge on a stable transaction set.
Key Functions:
updateOurPositions(): Adjusts local votes based on peer inputhaveConsensus(): Checks if agreement threshold reachedcreateDisputes(): Identifies transactions with disagreementDefinition: Consensus has been reached and the ledger is being finalized.
Actions:
Key Functions:
onAccept(): Handles consensus acceptancebuildLCL(): Constructs the Last Closed LedgerdoAccept(): Finalizes acceptance and prepares next roundThe consensus process is driven by periodic timer events through the timerEntry() function.
Location: rippled/src/xrpld/consensus/Consensus.h:840-869
Actual Implementation:
void Consensus<Adaptor>::timerEntry(
NetClock::time_point const& now,
std::unique_ptr<std::stringstream> const& clog)
{
// Nothing to do if we are currently working on a ledger
if (phase_ == ConsensusPhase::Accepted)
return;
now_ = now; // Update network-adjusted time
// Check we are on the proper ledger (this may change phase_)
auto const phaseOrig = phase_;
checkLedger(clog);
// If checkLedger changed our phase, return
if (phaseOrig != phase_)
return;
// Execute phase-specific logic
if (phase_ == ConsensusPhase::Open)
phaseOpen(clog);
else if (phase_ == ConsensusPhase::Establish)
phaseEstablish(clog);
}
Key Steps:
now_ to the current network-adjusted timecheckLedger())WrongLedger modephaseOpen() to check if ledger should closephaseEstablish() to process consensus roundTimer Responsibilities:
ledgerGRANULARITY = 1 second)The ConsensusState enum captures the result of consensus attempts:
enum class ConsensusState {
No, // No consensus reached
MovedOn, // Network moved on without this node
Expired, // Consensus timed out
Yes // Consensus successfully reached
};
State Implications:
| State | Meaning | Action |
|---|---|---|
| No | Insufficient agreement | Continue voting |
| MovedOn | Network ahead of this node | Resync required |
| Expired | Timeout reached | Build best-effort ledger |
| Yes | Agreement achieved | Finalize and close |
JSON Serialization:
Consensus state can be serialized for monitoring:
Json::Value getJson() const {
Json::Value ret;
ret["phase"] = to_string(phase_);
ret["mode"] = to_string(mode_);
ret["proposers"] = proposerCount_;
ret["agreements"] = agreementCount_;
// ... additional state
return ret;
}
RPC Access:
The consensus_info RPC provides real-time consensus status:
{
"result": {
"info": {
"phase": "establish",
"mode": "proposing",
"proposers": 35,
"current_ms": 2100
}
}
}
This module walked a single consensus round in detail, through its open, establish, and accepted phases, from collecting transactions in the open ledger to converging on a set and accepting the closed ledger. You also learned the ConsensusMode states (proposing, observing, wrong-ledger) that tell you what part your node is playing at any moment.
To remember:
ConsensusMode: proposing, observing, wrongLedger, switchedLedgerconsensus_info (admin) shows the live phase and proposer countConsensusParmssrc/xrpld/consensus/Consensus.hwrongLedger means you are building on a different parent than the network; check connectivity and UNL before blaming consensusNext up. Rounds assume every node has the ledgers; reality disagrees. Next: ledger acquisition, or how a node that fell behind catches up.
Resources
Assignments
0 of 2 complete