Skip to main content

Consensus Protocol

Part of: System Overview

The consensus protocol's core job is agreement: given competing chains, determine which one to adopt. This requires validating that every block in a chain was produced by a legitimate leader and comparing chains to pick the best one.

Rather than hardcoding this logic for a specific protocol, the system defines an abstraction — the ConsensusProtocol class — that captures what any Ouroboros-family protocol must provide. This is what allows the same consensus infrastructure to run PBFT (Byron), TPraos (Shelley through Babbage), and Praos (Conway onwards) without changes to the core logic.

Chain selection

The problem

The Ouroboros protocol papers describe chain selection as a comparison of two entire chains. In practice, this is not feasible: the Cardano chain contains millions of blocks, and scanning all of them each time a new block arrives is out of the question.

This practical constraint drives the design of chain selection in the consensus layer. Instead of comparing full chains, the node:

  1. Downloads and validates only headers first — headers are small and can be checked cheaply. This is why blocks are split into a header and a body: the split exists to make chain selection efficient.
  2. Builds candidate chain fragments — connected sequences of validated headers, one per upstream peer, anchored at the intersection with our chain.
  3. Compares candidate header fragments by looking at their tips to decide which blocks are worth downloading — for current Ouroboros protocols, the block number and a protocol-specific tiebreaker at the tip carry enough information to decide.1
  4. Downloads full block bodies (via BlockFetch) for the most promising candidates, and then performs the actual chain selection — validating blocks via the ledger and adopting the best chain.

Header validation

Before candidates can be compared, their headers must be validated. As headers arrive via ChainSync, the node performs three kinds of checks:

  • Envelope checks (validateEnvelope) — block numbers are consecutive, slots are monotonically increasing, and each header's previous hash matches its predecessor. These structural checks are independent of the consensus protocol.
  • Protocol-specific checks (updateChainDepState) — the block's producer was a legitimate leader for its slot. For Praos, this means verifying KES and VRF signatures. Validating a peer's header and checking local leadership use the same cryptographic mechanism — the difference is the direction: one asks "was this node a legitimate leader?", the other asks "am I a legitimate leader?" (before forging a block).
  • Time-based checks (realHeaderInFutureCheck) — headers with timestamps too far in the future are rejected. Near-future headers (within the maximum permissible clock skew) are accepted and stored for later processing.

Only headers that pass all three checks enter a candidate fragment. This means that by the time we compare tips, every header in the fragment has already been verified.

Note that block validity — applying full blocks via the ledger — is a separate step that happens later during the selection process.

See The ConsensusProtocol class for how these checks are encoded in the abstraction.

Comparing candidates

What information from a tip is needed for comparison? The answer depends on the protocol — for BFT, just the block number suffices; for Praos, a tiebreaker based on the VRF output is also needed. The consensus layer abstracts this as SelectView, which is projected from the header at the tip of each fragment and combines:

  1. The block number — longer chains are always preferred.
  2. A protocol-specific tiebreaker — for Praos, this is based on the VRF output, giving a deterministic way to break ties between equal-length chains.

Both candidates fork from block #99.

  • Blue — our chain, tip at #100.
  • Green — candidate 1, tip at #102. Longer than ours, so it is preferred.
  • Red — candidate 2, tip at #100. Same length as ours — the node sticks with its current chain, since when a candidate is equally preferable, the conservative choice is to not switch. This is a property of all Ouroboros protocols.

Candidates that fork deeper than k blocks from the current tip are rejected outright, without comparison.

The selection process

Chain selection is triggered when a new block arrives at ChainDB (downloaded via BlockFetch). It is implemented as a single-threaded process inside ChainDB — not in a separate protocol component — because ChainDB knows what candidates exist and when new blocks arrive.

The core of the process is finding the preferred candidate among the chain fragments that extend or fork from the current selection at or after the immutable tip. As part of adopting a candidate, its blocks are validated by applying them via the ledger. If a block fails validation, it is recorded as invalid and the candidate is truncated to the last valid block. All remaining candidates containing the invalid block are also truncated, and chain selection restarts with the updated candidate list. Valid prefixes are preserved because blocks from different peers are mixed in the VolatileDB — the valid prefix before an invalid block from one peer might be part of a valid chain from another. The peer that sent the block triggering chain selection is punished if the invalid block is at or before that block in the chain.

ChainDB assumes that no blocks come from the far future — this is enforced earlier by ChainSync, which rejects headers with timestamps too far ahead of the node's wall clock. One caveat: during initialization, blocks already in the VolatileDB are not checked for future timestamps.

See Blocks from the future for details.

Outcomes

When chain selection succeeds:

  • A volatile fragment is selected as the current chain.
  • Blocks at the beginning of the selected fragment may become immutable, moving past the k boundary.
  • An extended ledger state is produced for each block in the selected volatile fragment.
  • The new selection is made available for other peers to download via ChainSync.

Security parameter k

The security parameter k is the maximum number of blocks the node will ever roll back. On Cardano, k = 2160. With an active slot coefficient of 1/20 and a slot length of one second, a block is expected every 20 seconds on average, so k blocks are produced in roughly 12 hours. Even under adversarial conditions, the chain growth property guarantees at least k blocks within ~36 hours (3k/f slots). This is not just a practical limit — it is required by the Ouroboros analysis for consensus to be reached. The consensus layer assumes k always applies, even for protocols like BFT and Genesis where the formal requirement is slightly different.

Architectural consequences

The guarantee that rollbacks are bounded by k is exploited throughout the system:

  • Storage splitChainDB divides the chain into an ImmutableDB (blocks beyond k) and a VolatileDB (the last k blocks). Since the vast majority of the chain is immutable, it can be stored in a simple append-only structure where blocks are efficiently looked up by index rather than searched for.
  • Chain selection rejects candidates that fork deeper than k from the current tip, as described above.
  • Historical ledger states — when switching to a new fork, blocks must be validated against the ledger state at the rollback point. With k, the node knows it only needs to maintain k+1 historical ledger states, enabling efficient rollbacks without having to reconstruct the state by replaying from the beginning of the chain.
  • Peer tracking — the ChainSync client only tracks candidate fragments that fork within the last k blocks. Peers whose chains fork deeper are disconnected.
  • Forecast rangeheader validation needs ledger views for future slots; the range over which these can be forecast is bounded by k.

Limitations

Severe network partitions lasting in the order of days can cause chains to diverge beyond k. When this happens, recovery requires manual intervention — this is true for all Ouroboros protocols.

The ConsensusProtocol class

The previous sections described what a consensus protocol must do: compare chains, validate headers (including verifying leadership), and respect the security parameter k. The ConsensusProtocol class encodes all of this in a single abstraction.

Why a single class

These responsibilities share the same protocol state and configuration, so bundling them in one class is natural. The class is parameterized by a type-level tag p that identifies the protocol — not by a block type. This keeps the protocol definition independent from any particular ledger or block format.

Each protocol carries static configuration via ConsensusConfig p, a data family. The rest of the consensus layer treats this configuration as opaque — it just passes it through to where it's needed. The only thing the system extracts from it directly is k (via protocolSecurityParam).

Associated types

Each protocol defines seven associated types. The contrast between BFT and Praos illustrates what the abstraction buys — BFT needs almost nothing, while Praos carries real cryptographic content:

TypePurposeBFTPraos
ChainDepStateProtocol state, updated per header, subject to rollback()PraosState (nonces, OCert counters, last slot)
IsLeaderProof of leadership()VRF certificate
CanBeLeaderCredentials needed to participateCoreNodeIdVRF + KES signing keys
LedgerViewWhat the protocol needs from the ledger()Stake distribution
TiebreakerViewView for equal-length chain comparisonNoTiebreakerVRF output hash
ValidateViewView on header for validationDSIGN fieldsKES + VRF fields
ValidationErrWhat can go wrongBad signatureKES/VRF/OCert errors

SelectView — introduced in Comparing candidates — is built from the block number and the TiebreakerView. Its default is simply BlockNo, which suffices for protocols where only chain length matters.

Methods

The class has five methods, each corresponding to a concept already introduced:

  • checkIsLeader — determines whether the node may produce a block in a given slot. Returns a proof of leadership (IsLeader) if so, Nothing otherwise. Uses the same cryptographic rules as header validation — applied in the opposite direction.
  • tickChainDepState — advances the protocol state to a given slot (see Ticked state below).
  • updateChainDepState — validates a header and updates the protocol state, or returns a ValidationErr. This is the protocol-specific check described in header validation.
  • reupdateChainDepState — re-applies a previously validated header, skipping cryptographic checks. Used during chain selection when replaying blocks on a different fork, and during node initialization when replaying blocks from the ImmutableDB.
  • protocolSecurityParam — extracts k from the protocol configuration.

Ticked state

Several methods require the protocol state to be ticked to a specific slot before use. tickChainDepState takes a ChainDepState p and produces a Ticked (ChainDepState p) — a type-level distinction that makes it impossible to accidentally use an unticked state where a ticked one is expected.

Both checkIsLeader and updateChainDepState require a ticked state as input. Ticking also requires a LedgerView (see below), because some time-dependent state transitions need ledger information — for example, Praos rotates its nonces at epoch boundaries using the ledger's epoch structure.

For BFT, ticking is a no-op (TickedTrivial) since there is no protocol state.

LedgerView and forecasting

The LedgerView is a projection of the ledger state that provides what the protocol needs — for Praos, this is the stake distribution used for leader election.

Why does the ledger determine which stake distribution to use, rather than the consensus layer? Because the ledger's reward calculations must use the same distribution. Placing the sampling decision in the ledger means the consensus algorithm works unchanged even if the sampling rule changes.

The consensus layer accesses the LedgerView through LedgerSupportsProtocol, which provides:

  • protocolLedgerView — extract the view from a ticked ledger state.
  • ledgerViewForecastAt — obtain a LedgerView for a future slot from a ledger state at a prior slot, without having seen the intervening blocks. This is needed to validate headers on candidate chains that extend beyond the current tip.

Forecasting is distinct from ticking: ticking advances a full ledger state to a slot (expensive, no blocks allowed in between), while forecasting returns only the LedgerView (fast, blocks may exist in between). Cross-era forecasting — forecasting across a hard fork boundary — is more complex and is handled by the Hard Fork Combinator.

Connecting blocks to protocols

The class above is parameterized by a protocol p, not a block type. Several additional type families and classes provide the glue between blocks and protocols:

The Cardano chain combines all eras (and their protocols) through the Hard Fork Combinator, which provides composite instances for ConsensusProtocol and the classes above.

Further reading

Footnotes

  1. Peras, an extension of Praos under development, assigns weight to blocks via certificates. Its chain selection compares the suffixes of fragments after their intersection point, not just the tips.