Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/blockchain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ impl BlockChainServer {
while let Some(block) = queue.pop_front() {
self.process_or_pend_block(block, &mut queue);
}

// Prune old states and blocks AFTER the entire cascade completes.
// Running this mid-cascade would delete states that pending children
// still need, causing re-processing loops when fallback pruning is active.
self.store.prune_old_data();
}

/// Try to process a single block. If its parent state is missing, store it
Expand Down
63 changes: 29 additions & 34 deletions crates/storage/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,53 +470,41 @@ impl Store {
batch.put_batch(Table::Metadata, entries).expect("put");
batch.commit().expect("commit");

// Prune after successful checkpoint update
// Lightweight pruning that should happen immediately on finalization advance:
// live chain index, signatures, and attestation data. These are cheap and
// affect fork choice correctness (live chain) or attestation processing.
// Heavy state/block pruning is deferred to prune_old_data().
if let Some(finalized) = checkpoints.finalized
&& finalized.slot > old_finalized_slot
{
let pruned_chain = self.prune_live_chain(finalized.slot);

// Prune signatures and attestation data for finalized slots
let pruned_sigs = self.prune_gossip_signatures(finalized.slot);
let pruned_att_data = self.prune_attestation_data_by_root(finalized.slot);
// Prune old states before blocks: state pruning uses headers for slot lookup
let protected_roots = [finalized.root, self.latest_justified().root];
let pruned_states = self.prune_old_states(&protected_roots);
let pruned_blocks = self.prune_old_blocks(&protected_roots);

if pruned_chain > 0
|| pruned_sigs > 0
|| pruned_att_data > 0
|| pruned_states > 0
|| pruned_blocks > 0
{

if pruned_chain > 0 || pruned_sigs > 0 || pruned_att_data > 0 {
info!(
finalized_slot = finalized.slot,
pruned_chain,
pruned_sigs,
pruned_att_data,
pruned_states,
pruned_blocks,
"Pruned finalized data"
);
}
} else {
// Fallback pruning when finalization is stalled.
// When finalization doesn't advance, the normal pruning path above never
// triggers. Prune old states and blocks on every head update to keep
// storage bounded. The prune methods are no-ops when within retention limits.
let protected_roots = [self.latest_finalized().root, self.latest_justified().root];
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 {
info!(
pruned_states,
pruned_blocks, "Fallback pruning (finalization stalled)"
pruned_chain, pruned_sigs, pruned_att_data, "Pruned finalized data"
);
}
}
}

/// Prune old states and blocks to keep storage bounded.
///
/// This is separated from `update_checkpoints` so callers can defer heavy
/// pruning until after a batch of blocks has been fully processed. Running
/// 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 pruned_states = self.prune_old_states(&protected_roots);
let pruned_blocks = self.prune_old_blocks(&protected_roots);
if pruned_states > 0 || pruned_blocks > 0 {
info!(pruned_states, pruned_blocks, "Pruned old states and blocks");
}
}

// ============ Blocks ============

/// Get block data for fork choice: root -> (slot, parent_root).
Expand Down Expand Up @@ -1486,6 +1474,12 @@ mod tests {
let head_root = root(total_states as u64 - 1);
store.update_checkpoints(ForkCheckpoints::head_only(head_root));

// update_checkpoints no longer prunes states/blocks inline — the caller
// must invoke prune_old_data() separately (after a block cascade completes).
assert_eq!(count_entries(backend.as_ref(), Table::States), total_states);

store.prune_old_data();

// 3005 headers total. Top 3000 by slot are kept in the retention window,
// leaving 5 candidates. 2 are protected (finalized + justified),
// so 3 are pruned → 3005 - 3 = 3002 states remaining.
Expand Down Expand Up @@ -1530,6 +1524,7 @@ mod tests {
// Use the last inserted root as head
let head_root = root(STATES_TO_KEEP as u64 - 1);
store.update_checkpoints(ForkCheckpoints::head_only(head_root));
store.prune_old_data();

// Nothing should be pruned (within retention window)
assert_eq!(
Expand Down
Loading