Skip to content

fix(state): own zakura header-store mutations with a single suffix-replacement primitive#495

Draft
p0mvn wants to merge 1 commit into
ironwood-mainfrom
zakura-canonical-suffix
Draft

fix(state): own zakura header-store mutations with a single suffix-replacement primitive#495
p0mvn wants to merge 1 commit into
ironwood-mainfrom
zakura-canonical-suffix

Conversation

@p0mvn

@p0mvn p0mvn commented Jul 7, 2026

Copy link
Copy Markdown

Motivation

Pillar 1 of REORG_PLAN.md (§2), stacked on #494. The §0 diagnosis of the reorg-corruption bug class is a multi-writer mutable chain view with no owner and no enforced invariants: four independent writers each ran their own bounded delete loop over the five zakura header column families, and every proven corruption shape came from one writer preserving the invariants only under assumptions about the others. #491 patched the three proven holes narrowly; this PR restructures the write path so the invariants hold by construction. It also lands the deferred half of Pillar 3: the after-every-reorg-batch audit hook.

Solution

New canonical_suffix module: DiskWriteBatch::set_canonical_suffix(fork_point, new_rows) is now the only way any path changes which headers the zakura store considers canonical above a height. In one atomic batch it deletes every row strictly above the fork in all five column families and writes the new suffix. Contract (all hard errors, nothing staged on failure):

  • Deletion is total-above-fork by construction. Each column family's actual on-disk rows are iterated (RocksDB range scans, never a cached or computed tip), so rows stranded above gaps are deleted with the linked suffix — the §0 "bounded delete loop made empty by an under-reporting tip" mechanism is structurally impossible. The hash-keyed reverse index is scanned in full (it is frontier-sized) and every entry pointing above the fork is deleted, displaced and already-orphaned entries alike (a contract test caught that deriving reverse deletions from the hash rows misses pre-existing orphans).
  • Linkage is structural: new_rows[0] must link to the fork row (which must be the row on disk), and every row to its predecessor; row hashes are computed inside the primitive, so a caller cannot stage a hash that disagrees with its header (bug-1 class closed at the mechanism level, not as a caller obligation).
  • Committed heights are immutable for deletes and inserts alike: the fork must be at or above the finalized tip (valid on pruned stores — bodies absent, heights committed), closing the bug-2 insert shape structurally; a committed-body row above the fork aborts the replacement loudly (the Pillar-1a safety interlock, unreachable unless orchestration is buggy — pinned by a test that hand-plants a body marker).

All three writers route through it (per the §2 contract additions):

  • prepare_header_range_batch_with_roots: validation/policy unchanged (contextual difficulty, checkpoints, most-work gate, reorg depth); the mutation tail is now fork decision + one primitive call. The fork is the first conflicting height − 1, or the stored tip for pure extensions — matching the Phase-1 oracle's apply_header_range (total suffix replacement above the first conflict) exactly, including the property that a non-conflicting redelivery must not truncate stored rows above the range end. Advisory refreshes of matching re-delivered rows (body-size hint merge, roots re-staging) never change chain membership and stay outside the primitive.
  • Seed path (bug 3's option-(b) end state): a linked seed is now a suffix replacement at height − 1, matching the oracle's apply_seed (truncate-from-height on any non-identical seed). The identical-row early return and the fix(state): validate linkage in zakura header-store writers #491 unlinked-seed refusal are unchanged.
  • Release path (body commit): conflicting provisional descendants are truncated through the primitive with an empty replacement (new_rows = [] skips the fork-row read because the full-block row at the fork is staged in the same batch); the at-height frontier trim is unchanged.

After-every-reorg-batch audit hook (Pillar 3's deferred half): DiskWriteBatch counts the rows its suffix replacements delete; the three write sites (range commit, seed, body commit) run audit_and_repair_zakura_header_store() after committing any batch with a nonzero count. A pure append deletes nothing, so the steady-state extension path never pays the audit. The audit's log wording drops "startup" since it now runs at both points (the t4 watcher grep gets updated when this deploys).

Scope: Pillar 1 only. Pillar 1a's explicit switch orchestration for body-backed switches (candidate assembly → work decision → body invalidation → header rewrite → frontier notification) is the next change; its step-4 safety interlock and crash-recovery backstop (startup audit) are already in place via this stack.

Tests

  • New primitive.rs contract suite: unlinked rows / stale fork row / fork below finalized tip / body-above-fork interlock all reject with the right variant and stage nothing (zakura_suffix_replaced_rows() == 0, dump-identical store); truncation is total-above-fork including hand-planted strays and rows stranded above a gap; an empty replacement truncates to the fork; a pure append reports zero replaced rows.
  • The full header_store_coherence harness (oracle-checked scenarios + reads + startup audit) passes unchanged against the rerouted writers: 42/42 — the oracle is the replacement-semantics spec, so this is the primary evidence the refactor preserves specified behavior.
  • prop_random_sequences_uphold_invariants sustained at PROPTEST_CASES=1024 (release), clean.
  • cargo test -p zebra-state — 343 passed, 0 failed. cargo fmt --all -- --check, cargo clippy -p zebra-state --all-targets -- -D warnings, cargo check -p zebrad — clean.

Specifications & References

Follow-up Work

  • Pillar 1a switch orchestration for body-backed switches, feeding walk-back candidates through an explicit evaluate-then-mutate sequence (REORG_PLAN §2 Pillar 1a).
  • Update the t4 soak watcher's grep (passed its startup coherence auditpassed its coherence audit) when a binary with this change deploys.

AI Disclosure

  • AI tools were used: Claude Code wrote the primitive, writer refactors, audit hook, tests, changelog, and this PR description, directed and reviewed by the author.

PR Checklist

  • The PR title follows conventional commits format
  • This change was discussed with the team beforehand (Pillar 1 of the agreed REORG_PLAN.md)
  • The solution is tested.
  • The documentation and changelogs are up to date.

@p0mvn p0mvn added the C-bug Bug fix label Jul 7, 2026
@v12-auditor

v12-auditor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Note

Complete: Audit complete. No review-worthy issues remain after automatic triage. One finding was auto-invalidated.

Open the full results here.

Analyzed five files, diff 6dffe6d...44f0130.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant