Skip to content

Fork Choice — Picking the Heaviest Fork

The previous page built the tower: a stack of votes where each new vote doubles the lockout on the ones beneath it, so a validator that has voted deep on a block is committed to it and cannot cheaply take it back. That page answered how a single validator commits to a chain over time. This page answers the question that commitment presupposes: when there is more than one chain to commit to, which one does the validator pick?

That situation is not exotic. It is the normal, expected state of a chain producing a block every ~400 ms across a globe-spanning validator set. The network forks — routinely — and every validator must, in the same instant, look at the competing branches and choose one to extend and vote on. Get this wrong at scale and the network splits into camps that never reconcile. This page is about the rule that keeps that from happening: build on the fork with the greatest accumulated stake-weighted votes — the heaviest fork — subject to whatever your own tower still has you locked on.

A fork is just two valid blocks that both claim the same parent, or two branches descending from the same slot. On Solana the leader schedule is known in advance, so most of the time there is exactly one leader per slot and no ambiguity about who should produce the next block. Forks arise when reality diverges from that clean schedule:

  • The scheduled leader is offline. Its slot passes with no block. The next leader must decide whether to build on the last block it saw or skip the gap — and different validators may have seen different “last blocks.”
  • The leader produces late. Its block exists but reaches part of the network after the next slot’s leader has already committed to building without it. Now two branches exist: one that includes the late block, one that skipped it.
  • The network partitions. A transient split in connectivity leaves two groups of validators each seeing a different tip, each extending it, for as long as the split lasts.
slot: 100 101 102 103
fork A: [ L100 ]──[ L101 ]──[ L102a ]──[ L103a ] ← one branch
fork B: └────[ L102b ]──[ L103b ] ← competing branch from slot 102
both L102a and L102b descend from L101 — same parent, two children.

None of these require a malicious actor. They are the ordinary consequence of independent machines, finite bandwidth, and light that travels at a finite speed. A consensus design that assumed forks never happen would fall apart the first time a leader’s packet was late. So Solana treats forking as the default and gives every validator a deterministic rule for resolving it.

The rule is a weighing. Every fork carries a running total: the sum of the stake behind every validator that has voted for a block on that fork. Call it the fork’s weight. A validator’s job, each time it must choose, is to identify the fork with the greatest accumulated stake-weighted vote and build there.

Weight is measured in stake, not in vote count, because voting is stake-weighted: a vote from a validator holding 5% of the total stake counts for 5%, and a vote from one holding 0.01% counts for 0.01%. Ten tiny validators do not outweigh one large one. This is what makes the choice Sybil-resistant — you cannot conjure fork weight by spinning up more machines, only by controlling more stake, which is scarce and must be bought.

fork A weight = Σ stake of validators whose latest vote is on fork A
fork B weight = Σ stake of validators whose latest vote is on fork B
choose argmax( weight ) → extend and vote on the heavier one

A useful illustration of the comparison — not code from the companion crate, but the shape of the decision a validator makes:

struct Fork {
id: u64,
/// Total stake, in lamports, of validators whose latest vote lands on this fork.
stake_weight: u128,
}
/// Given the forks a validator can see, pick the heaviest one to build on.
/// (Ties are broken deterministically — e.g. by slot then hash — so every
/// honest validator computing this from the same data lands on the same fork.)
fn heaviest_fork(forks: &[Fork]) -> Option<&Fork> {
forks.iter().max_by_key(|f| f.stake_weight)
}

If you have read the Bitcoin book, this rhymes with the longest / heaviest-work chain rule: Bitcoin nodes follow the chain with the most accumulated proof-of-work. Both are “heaviest wins” rules, and both are what let independent nodes converge on one history without a central coordinator. But the unit of weight is completely different, and that difference is the whole point:

Bitcoin heaviest chain = most accumulated PROOF-OF-WORK (burned energy)
Solana heaviest fork = most accumulated STAKE-WEIGHTED VOTES (bonded capital)

Bitcoin measures weight in work already spent — hashes that are gone the moment they are computed. Solana measures it in stake currently bonded — capital that is committed and, crucially, slashable. That last property is why Solana can layer the tower on top: a validator’s vote is not a free signal it can flip at will, it is a lockout backed by stake it can lose. Bitcoin has no equivalent binding; a miner can point its hashpower at either chain with no penalty. So while both rules pick “the heaviest,” Solana’s weight is a standing commitment, not a historical expenditure — which is what couples fork choice to the tower.

Here is where the previous page comes back. Fork choice does not get to freely pick the heaviest fork every slot as if the validator had no history. A validator carries its tower — the stack of votes it has already cast, each with a lockout that grows exponentially with depth. Those lockouts are promises. And a promise on one fork forbids a conflicting vote on another.

The constraint is: a validator may only switch to a heavier fork if that fork does not conflict with any block it is still locked on. If your tower has you locked on block L102a for, say, the next 16 slots, you cannot vote for L102b during those slots even if fork B becomes heavier — because L102b conflicts with L102a (they are siblings; committing to one abandons the other), and your lockout on L102a has not expired.

your tower (locked votes): fork weights right now:
vote on L102a (lockout: 8) fork A (contains L102a): 38% stake
vote on L101 (lockout: 16) fork B (contains L102b): 55% stake ← heavier!
vote on L100 (lockout: 32)
Fork B is heavier — but you are still locked on L102a, which conflicts with
fork B. You must keep voting fork A until the L102a lockout expires. Only then
are you free to switch.

This is deliberate friction. Without it, a validator could chase whichever fork looked heaviest this millisecond, flip-flopping with every packet that arrived, and the network would never settle — the “heaviest” fork would be a moving target that everyone kept jumping onto and off. The tower forces each validator to stay put for a bounded time after committing, so weight can actually accumulate and stabilize on one side. Fork choice says “prefer the heaviest”; the tower says “but honor your outstanding commitments first.” The two together are what converge.

Put both halves in motion. Take a validator set with total stake normalized to 100%, split into a few groups, and watch a fork resolve.

Setup. At slot 102 the network forks. Leader L102a and a competing block L102b both descend from L101. Validators are scattered across the two branches by which block reached them first.

slot 102 — the split, as votes first land:
fork A (L102a): validators holding 42% vote here first
fork B (L102b): validators holding 38% vote here first
not yet voted: the remaining 20% (still catching up)

Round 1 — the undecided break the tie. The 20% that had not yet voted finish syncing, run fork choice, and see fork A is currently heavier (42% vs 38%). Those not locked elsewhere pile onto the heavier side. Say 15% of that 20% lands on A and 5% on B:

after round 1:
fork A: 42% + 15% = 57% ← now the clear heaviest
fork B: 38% + 5% = 43%

Round 2 — momentum compounds. Fork A is now heavier by a wide margin, so every validator newly free to choose — anyone whose relevant lockout just expired, plus every fresh vote — prefers A. Fork A keeps gaining; fork B stops growing. This is a positive feedback loop: heavier attracts more weight, which makes it heavier still.

Round 3 — the minority unlocks and switches. What about the 43% stuck on fork B? They are not stuck forever. Their lockouts on B’s blocks are finite. As those lockouts expire, those validators are freed from their promise to B, they run fork choice, they see A is overwhelmingly heavier, and — now permitted, because their conflicting lockout is gone — they switch their votes to A.

fork B validators, as their lockouts expire:
locked on L102b (lockout N slots) ──expires──► free to re-vote
└─► run fork choice → A is heaviest → vote A (allowed now)
result: fork B's weight bleeds away to fork A slot by slot,
until A carries a supermajority and B is abandoned.

The minority fork does not have to be forcibly killed. It dies because the validators on it are, one by one, released by their own expiring lockouts and rationally choose the heavier fork. Weight flows downhill toward the fork that got ahead early, and the lockouts merely ration how fast any one validator is allowed to move — preventing thrash, not preventing convergence.

The reason this settles in a bounded number of slots — rather than dragging on the way timestamp-based agreement can — comes straight from the book’s throughline. Validators are not negotiating. They are reading weight off a stream whose order they already agree on.

Two facts do the heavy lifting:

  • The PoH clock fixes order without a conversation. Every validator sees the same sequence of blocks and votes in the same order, because that order is established by the Proof-of-History hash chain, not by anyone’s local wall clock. There is no round of “what time did you see this vote?” — the ordering is a property of the data. So fork choice is a pure, deterministic function of an agreed-upon stream: everyone computing “which fork is heaviest” from the same inputs gets the same answer.
  • The known leader schedule removes surprise. Because everyone knows in advance who produces each slot, a missing or late block is recognized as such immediately — it is not a mystery to be resolved by messaging, it is a gap at a known position. Fork choice can react to it the instant the slot passes.
handshake-based agreement Solana fork choice
───────────────────────── ──────────────────
nodes exchange timestamps nodes read one PoH-ordered stream
argue about who saw what when compute argmax(stake weight) locally
converge at RTT speed (slow) converge at read-the-weight speed (fast)

Because there is no negotiation, convergence is limited only by how fast weight accumulates on the leading fork and how fast losing-fork lockouts expire — both measured in slots of ~400 ms, not in rounds of cross-planet messaging. That is the same “don’t pay for a handshake” discipline that hardware speed is built on, applied to fork resolution.

Under the hood — fork choice reads latest votes, not all history

Section titled “Under the hood — fork choice reads latest votes, not all history”

A subtlety worth pinning down: a fork’s weight counts each validator’s latest vote, not the whole history of votes ever cast on that branch. When a validator switches from the losing fork to the winning one, its stake moves — it stops counting toward the fork it left and starts counting toward the fork it joined. This is why the minority fork’s weight actively drains rather than merely stopping its growth: every switch is a subtraction from one side and an addition to the other.

validator V (holds 8% stake), latest vote was on fork B:
before switch: fork A += 0 fork B += 8%
V's lockout on B expires, V votes A:
after switch: fork A += 8% fork B += 0 (net swing: 16%)

Counting only the latest vote is also what keeps the tally bounded and cheap to compute — a validator maintains a current weight per fork and updates it as new votes arrive, rather than replaying every vote in history each slot. Cheap, incremental, and deterministic: exactly the properties you want in a function every validator must run continuously at slot speed.

  • Why does it exist? Because a globe-spanning chain producing a block every ~400 ms will fork — from offline leaders, late blocks, and partitions — and every validator needs one deterministic rule to pick a branch, or the network splits permanently.
  • What problem does it solve? It lets independent validators, with no coordinator and no negotiation, converge on a single history — by all preferring the fork carrying the most stake-weighted votes, a quantity each computes locally from the same PoH-ordered stream.
  • What are the trade-offs? Preferring the heaviest fork means a minority fork’s transactions are abandoned, and the tower’s lockouts intentionally slow how fast a validator may switch — trading responsiveness for stability so weight can actually settle instead of thrashing.
  • When should I avoid it? The stake-weighted “heaviest” rule is inappropriate where you have no bonded, slashable capital to measure weight with — a proof-of-work chain must use accumulated work, and a small trusted-quorum system may prefer explicit BFT rounds over a weight race.
  • What breaks if I remove it? Drop fork choice and a forked network never reconciles — validators extend whichever branch they happen to see, permanently splitting state; drop the lockout constraint on it and validators flip-flop onto whatever looks heaviest each millisecond, and weight never converges at all.
  1. Name the three ordinary (non-malicious) situations that cause the chain to fork, and explain why a consensus design cannot simply assume forks never happen.
  2. State the heaviest-fork rule precisely. What is the unit of “weight,” and why does using that unit rather than a raw vote count make fork choice Sybil-resistant?
  3. Solana’s heaviest-fork rule is analogous to Bitcoin’s heaviest-chain rule but measures weight differently. What is each unit of weight, and why does Solana’s choice let it couple fork choice to the tower?
  4. A fork B has just become heavier than fork A, but a given validator keeps voting for A anyway. Give the exact condition under which that is not only allowed but required, and say when the validator finally becomes free to switch.
  5. Why does fork choice converge quickly compared to timestamp-negotiation schemes? Point to the two specific mechanisms that let a validator compute the answer without any back-and-forth messaging.
Show answers
  1. (a) The scheduled leader is offline, so its slot passes empty and different validators may have different “last blocks”; (b) the leader produces late, so its block reaches some validators after the next slot already committed without it, creating two branches; (c) the network partitions, leaving two groups each extending a different tip. A design that assumed forks never happen would break the first time a packet was late or a leader missed a slot — forks are the ordinary result of independent machines and finite-speed networks, not an anomaly.
  2. Build on and vote for the fork with the greatest accumulated stake-weighted votes — the sum of the stake behind every validator whose latest vote is on that fork. Weight is measured in stake, not vote count, so you cannot inflate a fork’s weight by spinning up many machines (a Sybil attack); you can only add weight by controlling more stake, which is scarce and must be bought.
  3. Bitcoin’s weight is accumulated proof-of-work (energy already burned); Solana’s is accumulated stake-weighted votes (capital currently bonded). Because Solana’s weight is standing, slashable commitment rather than a spent historical expenditure, each vote can carry a lockout backed by losable stake — which is exactly what the tower needs, so fork choice and the tower are coupled.
  4. It is required whenever the validator is still locked (via its tower) on a block that conflicts with fork B — e.g. locked on a sibling of one of B’s blocks. During that lockout it cannot vote for the conflicting heavier fork; it must keep voting the fork consistent with its outstanding commitment. It becomes free to switch only once that conflicting lockout expires, after which it runs fork choice and moves to the (still heavier) fork B.
  5. Because validators are reading weight off an agreed-order stream, not negotiating. Two mechanisms make that possible: the PoH clock fixes the order of blocks and votes for everyone without exchanging timestamps, so “which fork is heaviest” is a deterministic function of shared inputs; and the known leader schedule means a missing or late block is recognized immediately as a gap at a known slot rather than a mystery to resolve by messaging. Convergence is then bounded only by how fast weight accumulates and lockouts expire (slots of ~400 ms), not by cross-planet round trips.