Skip to content

Consensus

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.

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

f=⌊n−13⌋f = \left\lfloor \frac{n - 1}{3} \right\rfloor

and finalizes a block when a quorum of n−fn - f members agree. When n=3f+1n = 3f + 1, this is the familiar 2f+12f + 1 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 n−fn - f directly, including when the committee size is not exactly 3f+13f + 1.

Put numbers on it. A devnet committee is DEFAULT_DEVNET_COMMITTEE_SIZE = 7, giving f=⌊6/3⌋=2f = \lfloor 6/3 \rfloor = 2 and a quorum of 7 − 2 = 5. A live-network committee of 40 gives f=13f = 13 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.


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):

CertificateShort nameProves
ProposalCertificatePCA quorum voted for a specific block — this is what makes the block justified.
TimeoutCertificateTCA 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.

For one view, with a chosen leader:

  1. Propose. The leader broadcasts a Block extending the highest justified block it has seen, with its justify set to that block’s certificate.
  2. Vote. Each validator validates the proposal and, if it is valid and safe, signs a vote. A validator votes at most once per view.
  3. 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.

One view, in four stepsA view is a block height. The protocol runs this loop for every view, forever.1. ProposeThe leader broadcastsa Block extending thehighest justified block.justify = parent’s cert2. VoteEach validator checksthe block, then signs —at most once per view.one signature each3. CertifyA quorum of votes isaggregated into oneProposalCertificate.n − f votes4. ExtendThe block is nowjustified; the nextleader builds on it.the chain grows a linkthe loop repeats for every viewIf the leader is silent, the pacemaker times the view out; a TimeoutCertificate lets the next leader start a fresh view.

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'':

  • b is the block to commit.
  • b' is justified by a PC over b — i.e. a quorum voted for b.
  • b'' is justified by a PC over b'.

When a validator receives b'' and sees the two nested links, it commits b. The rule is checked literally in the block store:

crates/consensus/src/traits/block_store.rs
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)
}
The three-chain commit ruleEach block carries a certificate for its parent. Three consecutive blocks make the oldest one final.bthe block to commitb′justified by a PC over bb″justified by a PC over b′PC over bPC over b′final once b″ existsOne link proves a quorum voted; two links set the lock; three links give honest validators time to observe the lock before committing.

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.

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:

StateSet byWhat it means
JustifiedA ProposalCertificate over the blockA 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.
LockedAdvancing the lock when a justified child arrivesThis validator must not vote for a block conflicting with it. The lock is what carries the vote across views.
CommittedThe three-chain ruleIrreversible. 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.


The leader of a view is chosen by round-robin over the committee:

applications/tari_validator_node/src/consensus/leader_selection.rs
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.


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:

crates/consensus/src/consensus_constants.rs
pub pacemaker_block_time: Duration, // 10s on every network in this repo

When 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:

  1. Validators broadcast a NewViewMessage carrying their high_pc (the highest proposal certificate seen), their timeout vote for the view, and their last_vote if they cast one — see crates/consensus/src/messages/new_view.rs.
  2. 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.

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):

crates/common_types/src/committee.rs
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 PP and f=⌊(P−1)/3⌋f = \lfloor (P - 1) / 3 \rfloor):

  • Two quorums intersect in a fault-free member. Any two sets of size n - f overlap in at least n - 2f ≥ f + 1 members, 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 - f live 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.

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:

crates/consensus/src/hotstuff/transaction_manager/pledged.rs
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:

StageMeaning
LocalOnlyThe transaction touches only this shard group; sequence and commit it here.
LocalPrepareThis group sequences its part and pledges its inputs, awaiting the foreign parts.
LocalAcceptAll foreign pledges are in; execute the transaction with the same COMMIT/ABORT decision the other groups reached.
AllAcceptEvery involved shard group reported ACCEPT; the group records that shared decision.
SomeAcceptAt 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.


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.


  • 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 a NewViewMessage starts the next view.
  • Quorum tolerance is f = (power - 1) / 3; the quorum and liveness threshold is power - f.
  • Cross-shard transactions are pledged input-by-input and driven to a shared decision through the LocalOnly / LocalPrepare / LocalAccept stages, finalised by AllAccept / SomeAccept.

Next: how the committed blocks change state — State and Execution.