From 687742b7e8cdf77af3de9cbb5d2eb5e54b14d587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 28 May 2026 11:45:08 -0300 Subject: [PATCH] fix(storage): protect head root from state pruning ## Problem `prune_old_data` only protects the latest finalized and justified block roots: ```rust let protected_roots = [self.latest_finalized().root, self.latest_justified().root]; ``` `prune_old_states` then keeps the top `STATES_TO_KEEP = 3_000` entries sorted by **slot** (descending) and deletes the rest of the `States` table, ignoring fork-choice membership. When finalization stalls and a competing branch keeps producing blocks on top of an unfinalized region, that branch's high-slot headers can fill the retention window even though it isn't the fork-choice head. The actual head can fall outside the top 3000 by slot, and its state row is deleted. The very next call into: ```rust pub fn head_state(&self) -> State { self.get_state(&self.head()) .expect("head state is always available") } ``` panics, taking down the blockchain actor (the container stays up, P2P keeps running, but every gossip message logs `err=Actor stopped`). Observed in a stalled 8-node devnet: finalization froze with head at slot ~2891 while a minority fork advanced past slot 15_000. On receiving the next gossip block, six nodes pruned 2666 states and panicked simultaneously at `store.rs:1314`. ## Fix Add `self.head()` to `protected_roots` so the pruner never deletes the state currently in use by fork choice, regardless of its slot position relative to other branches' tips. --- crates/storage/src/store.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 8ea1f850..1ba86503 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -774,7 +774,11 @@ impl Store { /// this mid-cascade would delete states that pending children still need, /// causing infinite re-processing loops when fallback pruning is active. pub fn prune_old_data(&mut self) { - let protected_roots = [self.latest_finalized().root, self.latest_justified().root]; + let protected_roots = [ + self.latest_finalized().root, + self.latest_justified().root, + self.head(), + ]; let pruned_states = self.prune_old_states(&protected_roots); let pruned_blocks = self.prune_old_blocks(&protected_roots); if pruned_states > 0 || pruned_blocks > 0 {