feat(state): sequence branch switches as evaluate, body rollback, then header rewrite#496
feat(state): sequence branch switches as evaluate, body rollback, then header rewrite#496p0mvn wants to merge 1 commit into
Conversation
Analyzed 13 files, diff |
…n header rewrite (Pillar 1a)
c4345ff to
e7bdbb4
Compare
Motivation
Pillar 1a of
REORG_PLAN.md(§2): sequence every branch switch as evaluate → body rollback → header rewrite, with every crash intermediate coherent. On this branch the walk-back (PR #476) already recovers from forks, but its sequencing is inverted relative to the plan's crash-point table: the header suffix is rewritten on disk first, and the stranded non-finalized body suffix is invalidated afterwards, from the driver, as a separate request — leaving a crash window where the header store names branch B while the body chain still extends branch A past the fork. Two further gaps surfaced while pinning the plan's requirements in tests:InvalidateBlockis a ban, not a rollback (BlockPreviouslyInvalidatedrefuses re-delivery — exactly the stranding hazard §2 warns about if the winning branch evaporates), and invalidating the non-finalized root panics inupdate_latest_chain_channels(expecton an empty chain set) — a latent crash reachable by the existing driver flow for forks right above the finalized tip.Base/stack note: this PR targets the fleet branch because the machinery it orchestrates (walk-back,
HeaderRangeCommitOutcome.reorged_at) exists only here. The head branch also carries cherry-picks of the main-stacked PRs #491/#493/#494/#495 (tests dropped, per canary convention); those are under review in their own PRs. The reviewable delta of this PR is the last three commits: theStoreIncoherent → ContextMismatchrouting (the fleet-branch decision documented in #493), the #495 pick conflict resolution, and the Pillar-1a commit.Solution
The switch orchestration moves inside the state write worker (
commit_header_rangeinwrite.rs), which owns both stores, so the sequencing is a straight-line code path rather than a driver round-trip:set_canonical_suffix(fix(state): own zakura header-store mutations with a single suffix-replacement primitive #495). Rejection leaves every store untouched.NonFinalizedState::rollback_blockand the truncated chain is published. The commit outcome now carriesreorged_to_hash(the new branch's hash at h) so the worker recognizes the stranded suffix without re-reading state.Supporting changes:
NonFinalizedState::rollback_block— same chain surgery asinvalidate_block(extracted into a sharedremove_block_and_descendants), but without recording the blocks in the invalidated list. A reorg rollback is a chain-selection outcome, not a verdict: the rolled-back branch revalidates and recommits normally the moment it wins the work race again.Request::InvalidateBlock(operator-facing) keeps its ban-until-reconsidered semantics unchanged.update_latest_chain_channels'expect. (The tip watch keeps the stale tip until the new branch's bodies commit; block sync follows the header store, so re-download reanchors correctly — same behavior as today, minus the panic.)invalidate_reorged_body_suffixcall is removed from the commit path (replaced by a reorg-commit log +sync.header.reorg_commitscounter). The helper itself stays for the startup/periodic body-suffix reconciliation sweep, which repairs stranding left behind by older binaries — that path is recovery, not switch orchestration.Tests
write.rs(fake-chain fixture, three-block nf chain):stranded_body_suffix_rolls_back_from_the_fork— suffix removed from the reorged height up, shared prefix kept, truncated chain published on the watch channel;stranded_suffix_rollback_is_not_a_ban— caught the ban live: written againstinvalidate_blockfirst, it failed withBlockPreviouslyInvalidated; green withrollback_block. Pins the §2 requirement that a rolled-back block recommits when its branch wins again;whole_suffix_rollback_skips_publish_without_panicking— regression for the empty-chain-set panic;rollback_is_a_noop_when_bodies_match_or_are_absent— matching and absent heights are untouched.cargo test -p zebra-state --lib— 305 passed, 0 failed.cargo test -p zebrad --lib header_sync— 43 passed (includes the fix(zakura): recover header sync from a frontier stranded on an abandoned branch #476 walk-back reactor tests, unchanged, and the classification test re-pinned toContextMismatch).cargo fmt --all -- --check,cargo clippy -p zebra-state -p zebrad --all-targets -- -D warnings— clean.731b7610…) with the full stack (fix(state): validate linkage in zakura header-store writers #491+fix(state): verify zakura header-store invariants in consensus reads #493+fix(state): audit and self-repair the zakura header store at startup #494+fix(state): own zakura header-store mutations with a single suffix-replacement primitive #495+1a); startup audit passed on the real store, node at the fleet tip. The soak watcher now tracksBODY-ROLLBACKevents so natural fork events during the soak exercise the new sequencing.Specifications & References
REORG_PLAN.md§2 Pillar 1a (steps 1–5, the crash-point table, and the "rollback, not ban" requirement), §3 row 3.set_canonical_suffix) ← fix(state): audit and self-repair the zakura header store at startup #494 (startup audit) ← fix(state): verify zakura header-store invariants in consensus reads #493 (verified reads) ← fix(state): validate linkage in zakura header-store writers #491 (write guards); this head branch carries them as cherry-picks because they are main-stacked while the walk-back lives here.Follow-up Work
AI Disclosure
PR Checklist
REORG_PLAN.md)