Skip to content

Commit 172dd92

Browse files
authored
fix(storage): protect head root from state pruning (lambdaclass#396)
## Summary - Add the fork-choice head root to `prune_old_data`'s `protected_roots` so the pruner never evicts the state currently in use by fork choice. ## Problem `prune_old_data` only protected 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 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 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 around slot 2866 while a minority fork from 2 surviving aggregators kept proposing one empty block per slot, reaching slot ~15000+. On the next gossip block (slot 15010, proposer 2), 6 of 8 nodes pruned 2666 states and panicked simultaneously at `store.rs:1314` with `head state is always available`. Logs on every affected node: ``` 2026-05-28T13:15:07.133 INFO Pruned old states and blocks pruned_states=2666 pruned_blocks=0 2026-05-28T13:15:07.802 thread 'tokio-rt-worker' panicked at crates/storage/src/store.rs:1314:14: head state is always available 2026-05-28T13:15:07.802 ERROR spawned_concurrency::tasks::actor: Panic in message handler ``` ## Fix Add `self.head()` to `protected_roots`. The head is now retained regardless of its slot position relative to other branches' tips. ## Test plan - [ ] Existing `prune_old_states_*` tests still pass. - [ ] Add regression test: head root at a low slot, retention window otherwise filled by a higher-slot branch; assert head's state is preserved after `prune_old_data`. - [ ] Re-run the long-lived devnet scenario that stalled finalization and verify nodes no longer panic when fork-choice head lags behind competing branch tips.
1 parent 873ddf8 commit 172dd92

1 file changed

Lines changed: 5 additions & 1 deletion

File tree

crates/storage/src/store.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,11 @@ impl Store {
774774
/// this mid-cascade would delete states that pending children still need,
775775
/// causing infinite re-processing loops when fallback pruning is active.
776776
pub fn prune_old_data(&mut self) {
777-
let protected_roots = [self.latest_finalized().root, self.latest_justified().root];
777+
let protected_roots = [
778+
self.latest_finalized().root,
779+
self.latest_justified().root,
780+
self.head(),
781+
];
778782
let pruned_states = self.prune_old_states(&protected_roots);
779783
let pruned_blocks = self.prune_old_blocks(&protected_roots);
780784
if pruned_states > 0 || pruned_blocks > 0 {

0 commit comments

Comments
 (0)