Skip to content

fix(hotblocks): enforce finalized fork floor#96

Open
mo4islona wants to merge 1 commit into
masterfrom
fix/hotblocks-finalized-fork-floor
Open

fix(hotblocks): enforce finalized fork floor#96
mo4islona wants to merge 1 commit into
masterfrom
fix/hotblocks-finalized-fork-floor

Conversation

@mo4islona

@mo4islona mo4islona commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • fix GAP-22: an equivocating source could silently replace the finalized prefix, and an honest tip reorg with fin inside the head chunk could wedge the dataset
  • resolve the fork at the stored chunk boundary (which may sit at or below fin) instead of clamping to a mid-chunk fin + 1 that insert_fork cannot satisfy — no chunk-splitting primitive is introduced
  • move the finalized-prefix guard to the write path: a replacement reaching into the finalized region is admitted only when it spans fin and reproduces fin's hash there; otherwise it is refused atomically
  • preserve the finalized hash at a fixed height in composed finality updates
  • name the rollback contract explicitly as resume_from / expected_parent_hash
  • add seven write-controller regressions and three black-box CT-4 scenarios (two equivocation, one honest-reorg recovery)
  • give CT-4 a compaction switch so the straddling two-chunk layout is deterministic
  • update the living conformance matrix and close GAP-22; normative and frozen audit documents stay unchanged

Why not clamp to fin + 1

A fork whose common ancestor lies inside a finality-straddling chunk cannot resume at fin + 1: that is a mid-chunk position, and insert_fork only replaces whole chunks, so the commit wedges the dataset on a 60-second restart loop. Resuming at the chunk boundary below fin is safe because the write path — not the resume position — guards the finalized prefix: the whole-chunk rewrite is admitted only when it reproduces the finalized block's hash. This routes every equivocation shape through one loud, bounded 60-second loop instead of a silent finalized-prefix replacement, and lets honest reorgs above finality recover.

Regression proof

  • unit: rollback_from_straddling_chunk_resumes_at_chunk_boundary — chunks [0,5],[6,9], fin = 7, hint {8, fork-8} resumes from 6 (the boundary), not the mid-chunk 8 the clamp produced and insert_fork rejected
  • honest-reorg recovery (ct4_honest_reorg_into_straddling_chunk_recovers) was verified red against the clamp: fin + 1 landed mid-chunk, insert_fork refused it, and the dataset spun on the deterministic 60-second restart loop until the test timed out; green with the boundary resume — the service converges on the reorged chain and FINALIZED-HEAD is preserved, not rewound
  • equivocation (ct4_finality_equivocation_does_not_replace_finalized_prefix, ct4_straddling_chunk_rollback_respects_finalized_floor): the source rewrites a suffix including fin with a different hash and claims it final; fork resolution resumes at a chunk boundary and the write path refuses the rewrite, so HEAD, FINALIZED-HEAD and the accepted window stay fixed while the SUT keeps re-engaging the faulty source

Known limitation

If re-ingest flushes a chunk ending strictly below fin before reaching it (e.g. a data-availability-mask change between two adjacent finalized blocks), the write path refuses it and the dataset takes the same bounded 60-second loop. This partial-flush edge is orthogonal to GAP-22 and pre-existed this PR (the prior can't fork safely bail wedged identically); tracked separately.

Test plan

  • cargo test -p sqd-hotblocks (7 write-controller unit tests + 3 ct4_finality black-box scenarios)
  • cargo test -p sqd-hotblocks-harness
  • cargo clippy -p sqd-hotblocks-harness --all-targets -- -D warnings
  • cargo +nightly fmt --all -- --check
  • git diff --check

@mo4islona mo4islona force-pushed the fix/hotblocks-finalized-fork-floor branch 3 times, most recently from 0737e03 to 45bf5ea Compare July 16, 2026 11:33
A fork whose common ancestor lies inside a finality-straddling chunk cannot
resume at fin+1: that is a mid-chunk position, and insert_fork only replaces
whole chunks, so the commit wedges the dataset on a 60s restart loop. Resuming
below fin instead — the fallback's behaviour — silently rewrites the finalized
prefix (GAP-22). An honest tip reorg triggers this whenever fin sits inside the
head chunk, which is the common case, not just equivocation.

Resolve the fork at the stored chunk boundary (which may sit at or below fin)
and move the finalized-prefix guard to the write path: a replacement reaching
into the finalized region is admitted only when it reproduces the finalized
block's hash there, and must span fin so the whole-chunk rewrite never
transiently drops it; otherwise it is refused atomically. This keeps the
existing whole-chunk replace — no chunk-splitting primitive — and routes every
equivocation shape through one loud, bounded 60s loop instead of the clamp's
silent retry spin. Honest reorgs above finality now recover.

Pinned by seven write-controller unit tests and three ct4_finality black-box
scenarios (equivocation refused at both the window floor and a straddling
chunk; honest reorg recovers) — the recovery scenario was verified red against
the clamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mo4islona mo4islona force-pushed the fix/hotblocks-finalized-fork-floor branch from 45bf5ea to fd80630 Compare July 16, 2026 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant