← Back to Consensus I: Node, Consensus, and Ledger Fundamentals
This chapter traces through the complete consensus lifecycle, from the initiation of a new round to the acceptance of a validated ledger. Understanding this flow is essential for debugging consensus issues, implementing protocol changes, and reasoning about network behavior.
We follow the execution path through the actual codebase, showing how each component interacts to achieve distributed agreement.
High-Level Flow
Copy ┌──────────────────────────────────────────────────────────────────────┐
│ CONSENSUS LIFECYCLE │
│ │
│ Application.cpp │
│ │ │
│ ▼ │
│ NetworkOPs::beginConsensus() │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CONSENSUS ROUND │ │
│ │ │ │
│ │ preStartRound() ──► startRound() ──► startRoundInternal() │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────┐ ┌────────────┐ ┌──────────┐ │ │
│ │ │ OPEN │───►│ ESTABLISH │───►│ ACCEPTED │ │ │
│ │ └──────────┘ └────────────┘ └──────────┘ │ │
│ │ │ │ │ │ │
│ │ timerEntry() timerEntry() onAccept() │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ NetworkOPs::endConsensus() │
│ │ │
│ ▼ │
│ Loop back to beginConsensus() │
└──────────────────────────────────────────────────────────────────────┘ Stage 1: Initiating Consensus
Entry Point: Application.cpp
Consensus begins at application startup:
Key Actions:
Passes the hash of the last closed ledger
NetworkOPs coordinates the consensus initiation
Failure to start consensus is fatal
Stage 2: Pre-Start Preparation
Function: RCLConsensus::Adaptor::preStartRound
Before a round begins, the system prepares:
Checks Performed:
Is the node synchronized with the network?
Is the previous ledger valid and complete?
Are all required data structures ready?
Stage 3: Starting the Round
Function: Consensus::startRound
This function establishes the round parameters:
Parameters Established:
Previous ledger hash and object
Initial mode (proposing or observing)
Stage 4: Open Phase
Function: Consensus::startRoundInternal → ConsensusPhase::open
Key Timing Parameters:
Max time ledger stays open with no transactions
Minimum time ledger must stay open
Must stay open ≥ half of previous consensus time
How long peer proposals remain valid
How often we must refresh our proposal
The ledger opens to accept transactions:
Key Activities:
playbackProposals():
Replays peer proposals received for this ledger that arrived early
Recovers proposals from up to 10 recent proposals per peer
Filters by prevLedgerID to find relevant proposals
Allows fast catch-up when switching ledgers
Example:
timerEntry() in open phase:
Called periodically (~1 second via ledgerGRANULARITY)
Evaluates close conditions via checkLedger()
Calls shouldCloseLedger() to determine if ledger should close
Example:
Close conditions checked:
Minimum time elapsed (2s)
Has transactions OR idle timeout
Not opening too fast (≥ prevRoundTime/2)
Stage 5: Close Decision
Function: shouldCloseLedger
Determines if the ledger should close:
Close Decision Process:
Step 1: Safety Checks
If timing is unusual (prevRoundTime or timeSincePrevClose out of range) → CLOSE immediately
Step 2: Network Coordination
If (proposersClosed + proposersValidated) > prevProposers/2 → CLOSE (follow majority)
Step 3: Transaction-Based Decision
Path A: No Transactions
Close only if: timeSincePrevClose ≥ ledgerIDLE_INTERVAL (15s)
Purpose: Maintain regular ledger cadence even when idle
Path B: Has Transactions
Must satisfy: openTime ≥ ledgerMIN_CLOSE (2s)
Must satisfy: openTime ≥ prevRoundTime/2
Key Parameters:
Minimum time ledger must stay open
Maximum idle time before forcing close
Throttle: ledger must stay open ≥ half of previous round time
Stage 6: Closing the Ledger
Function: Consensus::closeLedger
Transitions from open to establish phase:
Actions:
Finalizes transaction set for the ledger
Creates initial consensus proposal
Broadcasts closure to peers
Transitions to establish phase
Stage 7: Establish Phase
Function: Consensus::phaseEstablish
Key Timing Parameters:
Minimum time before consensus can be declared
Maximum time to wait for lagging validators
Absolute maximum round time
ledgerABANDON_CONSENSUS_FACTOR
Dynamic abandonment: min(prevTime × 10, 120s)
Minimum time used for avalanche calculations
Avalanche Threshold Progression:
The core of consensus—exchanging proposals and resolving disputes:
Key Functions:
updateOurPositions():
Adjusts votes on disputed transactions
Updates local candidate transaction set
shouldPause():
Checks if consensus should pause
Handles lagging or non-participating peers
Prevents premature advancement
haveConsensus() → checkConsensus() → checkConsensusReached():
Evaluates if agreement threshold met
Considers timing constraints
Returns consensus state (Yes/No/MovedOn/Expired)
Stage 8: Consensus Checking
Consensus Evaluation Logic:
Agreement Threshold:
Required: 80% (fixed - minCONSENSUS_PCT)
This checks if 80% of validators have the SAME complete transaction set (same hash).
Two checks performed:
Do 80% agree with OUR position? → Yes
Did 80% move to next ledger? → MovedOn
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:
80%+ moved to next ledger
Stage 9: Acceptance
Function: RCLConsensus::Adaptor::onAccept
When consensus is reached:
Sub-Functions:
buildLCL():
Constructs Last Closed Ledger from agreed transactions
Applies transactions in canonical order
Produces validated ledger object
LedgerMaster::consensusBuilt():
Updates system's view of current ledger
Triggers downstream processing
Apply Disputed Transactions:
Attempts to apply transactions that weren't included
Queues for next round if applicable
Stage 10: Final Acceptance
Function: RCLConsensus::Adaptor::doAccept
Finalizes the acceptance process:
Key Actions:
BuildLedger::buildLedger:
Final ledger construction
app_.getTxQ().processClosedLedger:
Removes included transactions
Manages deferred transactions
app_.openLedger().accept:
Applies local transactions not in closed ledger
Prepares open ledger for next round
Stage 11: End Consensus
Function: NetworkOPsImp::endConsensus
Completes the round and prepares for the next:
Stage 12: Begin Next Round
Function: NetworkOPsImp::beginConsensus
The cycle continues:
Complete Lifecycle Diagram
Lifecycle Stages:
Preparation and validation
Key Takeaways:
Timer-driven : Periodic events drive state transitions
Iterative : Establish phase loops until consensus
Convergent : Avalanche mechanism's rising thresholds force consensus
Resilient : Handles network issues and recovery
Continuous : Rounds follow each other seamlessly
In the next chapter, we'll explore the ledger architecture that stores the results of consensus.