Consensus
Covered in this page
Section titled “Covered in this page”This page covers the protocol that turns proposed blocks into irreversible state:
- The consensus model — chained HotStuff BFT — and the roles of proposer and voter.
- The message flow: proposal → vote → quorum certificate.
- The three-chain commit rule and the block-lock that makes it safe.
- Leader selection by round-robin.
- The pacemaker: view timeouts,
TimeoutCertificates, and the new-view message. - The quorum mathematics behind the fault tolerance.
- How a transaction that touches several shards is sequenced.
The model
Section titled “The model”Ootle’s validators are not one global committee. The state space is split into shard groups, and each shard group is administered by its own committee (see Architecture). Consensus therefore runs many times in parallel — once per shard group — over disjoint slices of state.
Each committee runs the same protocol, and the protocol has a single job: agree on an ordered sequence of blocks, where a block lists the transactions sequenced in one step and commits to the resulting state. Agreement is what gives the network its safety property — no two honest validators ever commit conflicting histories.
A committee of n validators with vote power tolerates up to f Byzantine members, where
and finalizes a block when a quorum of members agree. When , this is the familiar
threshold: you can lose f members and still have enough votes to outvote the f that may
be silent and the f that may lie. The implementation uses directly, including when the
committee size is not exactly .
Put numbers on it. A devnet committee is DEFAULT_DEVNET_COMMITTEE_SIZE = 7, giving
and a quorum of 7 − 2 = 5. A live-network committee of 40 gives
and a quorum of 27. In both cases the same counting argument holds: two quorums must
intersect in more members than can be faulty, so at least one shared member is honest — and an
honest validator votes at most once per view. Enforcing that single rule is the whole job of the
machinery that follows.
Blocks, views, and certificates
Section titled “Blocks, views, and certificates”Consensus proceeds in a sequence of views. In chained HotStuff, the view number is the block height: each new block is a fresh view, and its proposer extends the highest block it knows.
Every block carries a justify field: the certificate for its parent. The protocol distinguishes
two certificates (defined in the tari_consensus_types crate):
| Certificate | Short name | Proves |
|---|---|---|
ProposalCertificate | PC | A quorum voted for a specific block — this is what makes the block justified. |
TimeoutCertificate | TC | A quorum timed out in a view, so the next leader may propose a fresh view without extending the old one. |
A block whose justify is a PC sits directly on top of the block that PC certifies. That parent link is what “chained” means: a single chain of justified blocks, each one also serving as the vote for its parent.
The round trip
Section titled “The round trip”For one view, with a chosen leader:
- Propose. The leader broadcasts a
Blockextending the highest justified block it has seen, with its justify set to that block’s certificate. - Vote. Each validator validates the proposal and, if it is valid and safe, signs a vote. A validator votes at most once per view.
- Certify. When the leader (or the next leader) collects votes from a quorum, it assembles a
ProposalCertificate. The block is now justified, and the next leader can extend it.
The vote is the only per-validator action; everything else is a function of collected votes. Because votes are aggregated, communication is linear in the committee size rather than quadratic.
The three-chain commit rule
Section titled “The three-chain commit rule”Safety rests on a single rule: a block is committed once it is the oldest block in a consecutive three-block chain.
Write the chain as b ← b' ← b'':
bis the block to commit.b'is justified by a PC overb— i.e. a quorum voted forb.b''is justified by a PC overb'.
When a validator receives b'' and sees the two nested links, it commits b. The rule is checked
literally in the block store:
let commit_node = new_locked.justify().calculate_block_id();// b''.parent() == b'.id() && b'.parent() == b.id()if justified_node.parent() == new_locked.id() && *new_locked.parent() == commit_node { // ✅ Block forms a 3-chain b'' = ..., b' = ..., b = ... // Commit prepare_node (b)}Locking
Section titled “Locking”Alongside committing, a validator maintains a locked block — the block it is forbidden to
contradict. When it sees a justified child b' of b, it advances its lock to b' if b' is higher
than the current lock (LockedBlock in crates/consensus/src/traits/block_store.rs). A validator
will not vote for a block that conflicts with its lock, even if the new block is in a higher view.
This is exactly what stops two conflicting chains from both reaching three links.
Justified, locked, committed
Section titled “Justified, locked, committed”These three words describe three different strengths of “this block is not going away”, and they are easy to conflate. Each is set by a different event:
| State | Set by | What it means |
|---|---|---|
| Justified | A ProposalCertificate over the block | A quorum has voted for it. It is a candidate the next leader may extend — but nothing prevents a conflicting block from being justified in a higher view. |
| Locked | Advancing the lock when a justified child arrives | This validator must not vote for a block conflicting with it. The lock is what carries the vote across views. |
| Committed | The three-chain rule | Irreversible. b is committed once b'' exists, and no honest validator will ever vote to replace it. |
The progression is one-directional: every committed block was once locked, and every locked block was once justified — but the converse does not hold, and a block can sit justified forever if the chain stops extending. Committed is the only one that is safe to report to a user as done.
Leader selection
Section titled “Leader selection”The leader of a view is chosen by round-robin over the committee:
fn calculate_leader(&self, committee: &Committee<TAddr>, height: NodeHeight) -> u32 { (height.as_u64() % committee.len() as u64) as u32}Every member takes the proposer role in turn. There is no verifiable random function and no sortition in leader selection — leadership is a deterministic function of the height and the committee, so any validator can compute who is allowed to propose at any height without being told. Rotating the role limits how much a single faulty leader can stall progress before the pacemaker replaces it.
The pacemaker: timeouts and new views
Section titled “The pacemaker: timeouts and new views”A leader can be silent — crashed, slow, or censoring. A BFT protocol cannot wait forever, so each
view is bounded by a timer. This timer is the pacemaker (pacemaker_handle.rs,
pacemaker.rs), and its interval is a per-network constant:
pub pacemaker_block_time: Duration, // 10s on every network in this repoWhen the pacemaker fires for a view and no block has been justified, validators emit a timeout
vote. Once a quorum of timeout votes is collected, they form a TimeoutCertificate for that view.
A new-view phase then follows:
- Validators broadcast a
NewViewMessagecarrying theirhigh_pc(the highest proposal certificate seen), theirtimeoutvote for the view, and theirlast_voteif they cast one — seecrates/consensus/src/messages/new_view.rs. - The next leader aggregates these into a fresh view: it proposes a block justified by the highest certificate it observed, so no locked block is skipped.
Quorum mathematics
Section titled “Quorum mathematics”The quorum thresholds and the failure tolerance are computed from vote power, not head-count, so the protocol keeps working if power is ever weighted (today it is one per validator):
pub fn max_failures(&self) -> VotePower { let power = self.total_power(); if power.is_zero() { return VotePower::of(0); } (power - VotePower::of(1)) / 3}
/// Returns $n - f$, where n is the total committee vote power.pub fn quorum_threshold(&self) -> VotePower { self.total_power() - self.max_failures()}For a committee of n equal-power members this is f = (n - 1) / 3 integer-divided, and a quorum of
n - f. Three consequences follow directly (with vote power and ):
- Two quorums intersect in a fault-free member. Any two sets of size
n - foverlap in at leastn - 2f ≥ f + 1members, at least one of whom is honest. Two conflicting QCs would require an honest validator to vote twice in one view, which the protocol forbids. - No quorum is reachable with fewer than
n - f. A proposer cannot certify without the threshold computed from the committee’s total vote power, so a minority cannot force a block. - Progress needs
n - flive vote power. If the live power drops below that threshold, views time out and the chain stalls rather than forking — a liveness failure, not a safety one.
Cross-shard transactions
Section titled “Cross-shard transactions”A transaction that touches substates in more than one shard group cannot be sequenced by a single committee. Ootle handles this with a pledge protocol layered over the per-shard consensus.
The transaction is broken into per-shard work. Each involved shard group sequences its own part, and
the parts are bound together by pledged inputs — the substates the other shards promise not to
touch until this transaction is resolved. The commitment is carried in a PledgedTransaction:
pub struct PledgedTransaction { pub transaction: TransactionRecord, pub foreign_pledges: SubstatePledges, // inputs pledged by other shard groups pub local_pledges: SubstatePledges, // inputs pledged by this shard group}A transaction moves through stages, each carried as a consensus Command on the local block:
| Stage | Meaning |
|---|---|
LocalOnly | The transaction touches only this shard group; sequence and commit it here. |
LocalPrepare | This group sequences its part and pledges its inputs, awaiting the foreign parts. |
LocalAccept | All foreign pledges are in; execute the transaction with the same COMMIT/ABORT decision the other groups reached. |
AllAccept | Every involved shard group reported ACCEPT; the group records that shared decision. |
SomeAccept | At least one involved shard group did not report ACCEPT, so the transaction is finalised as aborted. |
LocalAccept is where a group executes; AllAccept / SomeAccept are the finalisation stages where
it learns whether every group accepted and records the shared decision that makes the outcome
final in every group.
If a foreign shard group has already consumed an input this transaction expected, the local execution
is rejected with RejectReason::ForeignPledgeInputConflict (crates/engine_types/src/commit_result.rs)
rather than committing an inconsistent result. A cross-shard transaction is therefore atomic: it
either commits in every involved shard group or in none.
Safety and liveness in one paragraph
Section titled “Safety and liveness in one paragraph”Ootle is safe because a block is only final after a three-chain, and a lock prevents honest
validators from voting to contradict a locked block; two conflicting finalized blocks would need two
intersecting quorums and a double-voting honest member. Ootle is live as long as at most f
members of a committee are faulty, because a quorum can always vote and the pacemaker un-sticks any
view whose leader fails. The base layer contributes the membership clock — the epoch oracle decides
who is in a committee — but does not participate in, or verify, any Ootle vote.
Summary
Section titled “Summary”- Consensus is HotStuff, run per shard group by that group’s committee.
- A view = a block height; the leader proposes, validators vote, votes aggregate into a
ProposalCertificate. - A block is committed by the three-chain rule; a lock keeps honest nodes from contradicting it.
- Leaders are round-robin,
height % committee.len()— no VRF, no sortition. - The pacemaker times out stalled views; a quorum of timeout votes forms a
TimeoutCertificate, and aNewViewMessagestarts the next view. - Quorum tolerance is
f = (power - 1) / 3; the quorum and liveness threshold ispower - f. - Cross-shard transactions are pledged input-by-input and driven to a shared decision through the
LocalOnly/LocalPrepare/LocalAcceptstages, finalised byAllAccept/SomeAccept.
Next: how the committed blocks change state — State and Execution.