fix(state): prefer the first-received chain on equal-work ties - #11245
Open
aphelionz wants to merge 1 commit into
Open
fix(state): prefer the first-received chain on equal-work ties#11245aphelionz wants to merge 1 commit into
aphelionz wants to merge 1 commit into
Conversation
Stamp each block with a receipt sequence when it is committed to the non-finalized state (like zcashd's nSequenceId), break equal-work ties in Chain::cmp by preferring the lower tip sequence (tip hash stays as the final tie-breaker for unstamped test chains and Ord totality), keep the incumbent chain when a duplicate tip is inserted, and dequeue blocks that waited for a missing parent in arrival order so commit order matches receipt order. Closes ZcashFoundation#11240.
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.
Motivation
Closes #11240.
The protocol spec, quoted in
Chain::cmp's own docs: "To break ties between leaf blocks, a node will prefer the block that it received first." Zebra instead compares raw tip-hash bytes, andbest_chain()is re-evaluated on every commit, so an already-validated, already-announced tip is displaced whenever an equal-work sibling with a higher-sorting hash arrives, no matter how much later. On Zcash, sibling blocks always tie on work (nBits is fully determined by ancestors) and equal-length fork subtrees stay tied, so this fires on every sibling race. #11240 documents mainnet blocks orphaned this way despite winning propagation by ~0.4s; the hash rule also removes the race penalty for block withholding, since a sibling released only after seeing a competitor's block still wins about half of ties, deterministically across all zebra nodes.On the documented rationale for the current behavior (parallel downloads make receipt timestamps non-unique, and Zebra doesn't track download times): this change uses no timestamps. It stamps a monotonic sequence at the moment a block is committed to the non-finalized state, which is unique by construction, and makes commit order match arrival order in the one place they could diverge (children queued behind a missing parent). The "consistent across restarts and between nodes" side-effect of hash ordering is deliberately given up: that property is exactly what makes every zebra node prefer the same later-arriving sibling.
Solution
The zcashd
nSequenceIdanalogue, adapted to zebra-state:ContextuallyVerifiedBlockgainsreceipt_sequence: u64, stamped once inNonFinalizedState::validate_and_commitfrom a per-instance counter (the analogue ofReceivedBlockTransactions). Node-local, in-memory, never persisted; backup-restored blocks re-stamp in replay order, matching zcashd's disk-loaded blocks all sharing id 0. The stamp rides through fork/push/pop/invalidate, andreconsider_blockreplays stored blocks, so receipt order survives invalidate/reconsider.Chain::cmporders by cumulative work (unchanged), then the tip's receipt sequence (lower preferred), then the existing tip-hash comparison last. The hash fallback keeps the order total for unstamped (test-built, sequence-0) chains and keeps same-tip chains comparingEqual, preserving the Handleinvalidateblock/reconsiderblockedge cases without panicking聽#10586 invariants.NonFinalizedState::insert_withskips inserting a chain whose tip is already tracked: keeping the incumbent is first-received, and it preserves the duplicate-tip no-op that same-tipEqualused to provide on paths without a duplicate filter (thezebra-rpcsync mirror).QueuedBlocks::by_parentbecomes anIndexSet(withshift_removeon the prune path), so siblings queued while their parent was missing are dequeued, committed, and stamped in arrival order.eq_internal_stateexcludes the counter, since a failed commit advances it without changing any chain.Chain::cmp/PartialEq,find_chain,finalize, backup restore, the Handleinvalidateblock/reconsiderblockedge cases without panicking聽#10586 test comments, and the state RFC'sOrdsection.Strictly more cumulative work always still wins; the sequence only decides exact work ties, which is local policy (both blocks are valid), now aligned with the spec and zcashd.
Tests
equal_work_ties_prefer_first_seen: two equal-work siblings committed in both arrival orders keep the first-received tip best in both (one order fails under hash tie-breaking by construction); a strictly higher-work extension still overrides receipt order.reconsidered_block_keeps_original_receipt_order: invalidate/reconsider preserves the original tie win.dequeue_children_returns_siblings_in_arrival_order: queued siblings dequeue in insertion order.invalidateblock/reconsiderblockedge cases without panicking聽#10586 regression tests and the fork/finalize property tests pass unchanged.cargo fmt --all -- --check,cargo clippy -p zebra-state -p zebra-rpc --all-targets -- -D warnings, andcargo nextest run -p zebra-state -p zebra-rpcpass on this branch.AI Disclosure
This change was developed with Claude (Claude Code): code, tests, and this description, under human direction and review. It ships in Shielded Labs' Zero v26 (a zebra downstream). The contributor is the responsible author.