fix(consensus): make leader election converge across nodes (testnet fork fix)#126
Merged
Conversation
Root cause of the testnet's three-way fork (validators diverged from block #1, three independent chains): leader election was not a pure, node-agnostic function of (validator set, epoch seed, slot), so different nodes elected different producers and never agreed. Two defects fixed in qfc-consensus: 1. select_producer (Defect ②): the zero-score fallback returned `validators[0]` — whose identity depends on each node's internal list order, so every node elected ITSELF and forked immediately. Now: filter to active validators, sort canonically by address, and on zero total score do a deterministic round-robin by slot (`sorted[slot % len]`). The weighted path also iterates the address-sorted set, so selection is order-independent even with non-zero scores. 2. maybe_advance_epoch (Defect ③): the epoch seed was copied from the local chain head hash, so once two nodes diverged their seeds diverged and the fork became permanent. Now the seed is a hash chain rooted at the genesis seed (`seed_n = blake3(seed_{n-1} || n)`), walked from the current epoch to the target — identical on every node regardless of chain head. Dropped the now-unused `head_hash` param (callers in producer.rs/miner.rs updated). Tests: producer selection is identical across two nodes holding the set in opposite order (weighted path); zero-score path round-robins deterministically and covers every validator; epoch seed is the deterministic genesis-rooted hash chain. 27 qfc-consensus tests pass. NOT in scope (follow-ups): Defect ④ (qfc-node sync MAX_PENDING_BLOCKS can't bridge a deep fork) and the contribution-score scaling on the deployed image. A testnet reset is still required — existing chains diverge at block #1 and cannot reconcile; they need a fresh genesis on this fixed binary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lai3d
added a commit
that referenced
this pull request
Jun 14, 2026
… rustc bump (#127) for testnet image
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the root cause of the testnet's three-way consensus fork — validators diverged from block #1 into three independent chains and could never reconcile. (Full investigation in the session; the running
staging-sha-8cf3cb0image exhibits it live.)Root cause
Leader election was not a pure, node-agnostic function of (validator set, epoch seed, slot), so different nodes elected different producers and never agreed. Two defects in
qfc-consensus:②
select_producer— zero-score fallback was order-dependentvalidators[0]'s identity depends on each node's internal list order, so every node elected itself and forked at block #1. Now:sorted[slot % len],③
maybe_advance_epoch— seed came from the local chain headOnce two nodes diverged, their seeds diverged and the fork became permanent. Now the seed is a hash chain rooted at the genesis seed (
seed_n = blake3(seed_{n-1} || n)), walked from the current epoch to the target — identical on every node regardless of chain head. Dropped the now-unusedhead_hashparam (updatedproducer.rs/miner.rscallers). Deterministic ⇒ predictable beacon, an acceptable trade for convergence on this chain.Tests (27 qfc-consensus pass; 20 qfc-node)
test_producer_selection_is_order_independent— two nodes holding the set in opposite order elect the same producer for 200 slots (weighted path).test_zero_score_round_robin— zero-score path round-robins deterministically and covers every validator, order-independent.test_epoch_seed_is_deterministic_hash_chain— epoch seed is the genesis-rooted hash chain, independent of head.Scope / follow-ups
qfc-nodesyncMAX_PENDING_BLOCKS=1000can't bridge a deep fork) and contribution-score scaling on the old deployed image — separate PRs.🤖 Generated with Claude Code