fix(mining): prefer the first received block on equal work - #1084
czarcas7ic wants to merge 75 commits into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
This comment was marked as resolved.
This comment was marked as resolved.
|
Fixed the three issues introduced here, each in its own commit:
|
This comment was marked as resolved.
This comment was marked as resolved.
evan-forbes
left a comment
There was a problem hiding this comment.
Makes sense, this change has grown on me. I still don't like the inevitable increase in one block reorgs, but as mentioned in the body / by ValarDragon its worth the extra selfish mining protection.
I have two requests.
1. Make the header engine and full state agree on equal work. Right now header_best breaks ties by raw hash and verified_best by receipt order. A node can mine on A while it downloads and serves B's branch. Supporting that split takes the verified_tip override, InvalidVerifiedPreference, the header-branch serving path in block_range.rs, and the two-comparator spec text. It also defeats the policy when both headers arrive before either body: native sync downloads only the higher-hash body, so A's body may never reach the verifier (#1131).
Headers are different because a node can hold a header without its body. So the rule should be that the first body received wins, and having only a header never wins a tie. On equal work, the header engine would:
- prefer a tip whose body full state has verified over a header-only tip, and
- between two verified tips, take full state's receipt-order choice.
The raw hash would then break ties only between header-only tips. Selection stays a deterministic function of the graph plus body-verification evidence, and the engine already consumes that evidence. header_best and verified_best then agree on every tie where a body exists. Native sync downloads and serves the mining branch, so the serving change should no longer be needed.
2. Drop the retry receipt cache (RetryReceipts, allow_retry, retains_error, and the body digest). Without it, a block that fails before it reaches the state queue gets a fresh receipt when someone redelivers it.
- Most of the audit churn comes from the cache. Of the 27 V12 findings across runs 8107–8207, 14 concern retry-receipt retention: F-283675, F-283679, F-283740, F-283741, F-283742, F-283743, F-283789, F-283794, F-283795, F-284871, F-284872, F-284875, F-284876, and F-284877. Each fix added another classification rule or bound: the error allowlist, the Merkle-before-retry reorder, the per-header variant cap, the TTL, and the pending-duplicate exception. Run 8207 still lists F-284875 (a resubmission renews the TTL forever) and F-284877 (the global lock hashes 2 MB bodies) against the current design.
- The cache decides #1127 early. It exists mainly so that a block which fails because its parent is missing keeps its priority. That is the orphan-priority question the PR defers to #1127. The design doc also notes that it lets a miner reserve priority for a tip while withholding an ancestor. zcashd assigns priority only once the block and its ancestors are available, so a fresh receipt on redelivery is closer to zcashd, not further from it.
- Without the cache, the common paths still work. Overlapping deliveries still share one receipt through the active registry. A block waiting for its parent in the state queue still keeps its receipt on the prepared block. Transient local failures (
StateService,Io,QueueFull) are rare, and losing a tie there costs little.
Removing the cache deletes most of receipt.rs, the retry tests in block/tests.rs, and the three params.md rows. It also closes every open retry finding at once. The queue behavior from p0mvn's thread belongs in #1130, as you said.
|
Will drop the retry cache here; for (1), serving would still need to change because mismatched bodies are penalized ( zakura/crates/zakura-network/src/zakura/block_sync/peer_routine.rs Lines 1592 to 1602 in 1b8b57c |
Addresses review feedback on first-received mining. The retry cache kept a block's receipt order after transient failures and cancellation, which required an error allowlist, a Merkle-before-parent check reorder, per-header variant caps, a TTL, and a pending-duplicate exception. Each addition was a response to another audit finding. Remove it. Overlapping deliveries still share one receipt through the active registry, and a body waiting for its parent in the state queue still keeps its receipt on the prepared block. A block redelivered after every earlier attempt has finished now gets a new receipt. That is closer to zcashd and leaves the orphan-priority policy to the existing follow-up issue. Restore main's mined-parent and cached Merkle check order, delete the retry tests and parameter rows, and update the design doc. Header selection agreement is tracked in a new follow-up issue.
evan-forbes
left a comment
There was a problem hiding this comment.
LGTM, thanks for dropping the retry cache, and you're right about serving on (1).
|
trying this out atm on the 80 node cluster |
Motivation
Equal-work forks currently select the greatest raw tip hash. If A arrives before B but B finishes verification first, mining should switch to A once it passes validation. A chain with more work still wins.
Solution
Record receipt order before asynchronous verification and preserve it through duplicates, forks, and reconsideration. Native sync serves retained bodies on the header-selected branch, so a node mining A can still serve retained side-fork B to a peer requesting that branch. A block redelivered after its earlier attempts finish gets a new receipt, while a body waiting in the state queue keeps its own. The orphan-priority policy remains deferred to #1127, making header selection agree on equal work is tracked in #1137, and the pre-existing same-height body-download gap is tracked separately in #1131. Fork eviction recovery and receipt metadata for secondary nodes are already on main. Restored forks use the hash fallback after restart. Upgrade secondaries alongside the primary when enabling the new policy. The version metadata also covers the breaking stream API inherited from main, with dependent patch bumps so published packages use matching network and state types.
Testing
All 690 state library tests pass, with four ignored. The serving regression fails on the previous implementation and covers different mining/download tips, missing selected bodies, greater-work switches, and an in-flight range retaining its original branch. All-target state Clippy, formatting, Markdown lint, and changelog checks pass. Earlier validation covered the verifier, router, header-chain, and conformance suites. Native E2E validation passes block-sync fuzz, cluster integration, and no-checkpoint-long. Its three header-lifecycle/reopen assertion failures also occur on the earlier main run. The trace confirms all 400 headers were admitted before body requests, but that assertion incorrectly requires a single response instead of the actual 250/150 batches. Used Codex for implementation and tests. The version correction passes locked dependency resolution, the crates.io publish-graph dry run, formatting, Markdown lint, and changelog validation. Fresh CI at
9ec0d6db4passes every semver check, crate builds, all unit-test shards, configuration compatibility, lint, and integration tests. The optional Docker image check is still building.