fix(consensus): authenticate authorizing data before proof verification - #1038
evan-forbes wants to merge 11 commits into
Conversation
…' into security/early-body-commitment # Conflicts: # docs/changelog/params.md
…' into security/early-body-commitment
Analyzed 10 files, diff |
The audit of #1038 reported five ways the missing-parent retry path outlives or escapes its own bounds. An expired wait can never schedule another retry, but it stayed in the table. Once 4,096 of them accumulated, every later missing-context hash returned early before it was recorded or scheduled, and the round-exhaustion predicate ignores those waits, so the required hash disappeared from the round. Expired waits are now pruned on each miss. A failure arriving just before the eight-minute boundary scheduled its retry after it, and the retry dispatch does not constrain the new attempt to the original window. The backoff is now clamped to the retention deadline. A retry wave dispatched every due hash at once, ignoring the configured lookahead. It is now bounded by the remaining lookahead, and the hashes it defers are re-armed rather than left with a past deadline. The exhausted-tip refresh was gated on no retries being pending at all. A registry miss means no peer has the block, so the round must let it drain, but a missing parent is found by extending tips, so a child-only tip trapped discovery for the whole window. The gate now distinguishes the two. The gossip retention loop checked its deadline only after a verifier request returned, so an attempt begun at the deadline held the body, the queue slot and the permit for another full verification timeout. It now re-checks first. The retention semaphore also took the minimum supported full-verification concurrency of one to zero permits, which disabled retention entirely.
…tment branch Main's #940 added `forged_expiry_with_authentic_height_is_a_payload_mismatch`, which caught a real regression in this branch: requesting the parent's context before the body's Merkle root meant an unavailable parent reported `MissingParentContext` and masked a malleated body. That classifies a permanently invalid body retryable and leaves its supplier unscored against a header the node keeps re-requesting. Every Merkle-root outcome is decided from the header's commitment and the delivered list, so the check now runs before the pending-parent error is reported. `is_poisoned_body` from #940 keeps its shape but widens to every body-commitment kind, because the early commitment check proves an authorizing-data mismatch that an alternate body for the same header can repair. Two test mocks answer the requests the other side of the merge introduced.
…attribution branch `is_context_independent_body_error` is gone. #1038 now propagates every merkle-root attribution outcome through the pending-parent path, because all of them are decided from the header's commitment and the delivered transaction list, so a predicate that selected a subset can only lose evidence. Its test is retargeted at that broader rule and covers a plain malleated body too. `merkle_root_validity` keeps both sides' comments, and the misbehavior score list keeps both sides' variants.
…nt branch #1036 now authenticates coinbase heights without chain state. Main added NSM and ZIP 234 verifier tests whose state mocks did not answer this branch's parent context request; they now get a bound stand-in context. The checkpoint handoff router test uses a V4 coinbase, so its height-gap child still reaches state instead of failing at the parent context check.
…tion branch Main's newer floor-preference tests call floor_has_preferred_unsaturated_server without this branch's body-retry filter; they now pass one that admits every peer. Manifests take main's published versions.
Motivation
A V5+ body can retain its header hash and transaction IDs while changing authorizing data. Dispatching proofs before checking the header commitment wastes verification work. Requiring parent context also needs recovery when an honest child arrives before its parent commits.
Solution
Based on #1036 for shared height admission and supplier feedback. Review only the commits above that base.
V4-only bodies keep their existing path. Prepared-candidate caching retains its exact-body checks.
Testing
The implementation passes 100 sync tests, 16 inbound downloader tests, and 33 semantic block tests. New gossip regressions cover reuse of the same body, source-gate release, successful retry, expiry, and retention-capacity exhaustion without peer scoring.
The semantic regression sends a poisoned body and then its honest counterpart through the same verifier. It checks zero transaction dispatch for the poisoned body and transaction dispatch for the honest body. A complete honest-body state commit remains an integration gate.
Strict clippy passed for the state, consensus, network, and node libraries and tests. Formatting and changelog checks passed.
Changelog
Adds a changelog fragment and parameter-ledger entries.
Release gates
Keep this PR in draft. Validate full alternate-body recovery, competing branches, parent finalization, and checkpoint/native handoff with actual state commits. Measure throughput, retained-body memory, repeated context checks, and sync re-download bandwidth under out-of-order traffic.
Provenance
Ported unchanged from the private security review. It no longer needs to stay private.
CI coverage
Workflows filter
branches: [main, "feat/**", "release/**"], so a PR based onsecurity/early-body-commitmentruns only the docs and security jobs. Retarget this PR tomainonce #1036 merges to get the full suite. Verified locally on Rust 1.98.0 against this branch: strict Clippy over the workspace,cargo fmt --check, changelog validation, and 2,300 library tests acrosszakura,zakura-network, andzakura-state.