Repository navigation
[F23] Stale Block Wishlist Entries Persist After Pruning Due to Missing Removal Delta #4986
Description
⚠️ Warning — This finding was routed to an issue rather than a PR because it touches consensus / block-graph processing or block/operation validity rules. Changing what a node accepts, rejects, or how it advances state can make patched and unpatched nodes diverge (a network split), so it requires careful design and a coordinated, version-gated (MIP / hard-fork) rollout rather than a self-contained PR.
Source
AI security-scan Finding 23 (severity: minor). Tracked in SUBMISSIONS.md as F23.
Affected file: massa-consensus-worker/src/state/process.rs
Summary
ConsensusState::block_db_changed() updates and publishes the block wishlist before pruning stale waiting blocks. At that point, mark_final_blocks() has already moved latest_final_blocks_periods forward, and block_db_changed() has already informed the pool about the newer finality floor. Because pruning has not run yet, stale waiting blocks can still add their missing dependencies to new_wishlist, and those block ids remain in both self.wishlist and the protocol retrieval wishlist. If ConsensusState::prune() later removes those waiting blocks without sending a removal delta, protocol may keep requesting blocks that consensus has already discarded as unreachable.
Root cause
The issue comes from the order of operations in ConsensusState::block_db_changed(). This function computes and publishes new_wishlist before any prune pass runs, even though mark_final_blocks() has already advanced latest_final_blocks_periods and block_db_changed() has already notified the pool about the newer finality floor.
Because of that ordering, a waiting block that is already stale under the updated latest_final_blocks_periods can still participate in wishlist computation inside ConsensusState::block_db_changed(). Its missing dependencies are added to new_wishlist, and the related ids remain stored in self.wishlist and in the protocol retrieval wishlist.
A second root cause is in the later cleanup path. When the timer-driven ConsensusState::prune() eventually removes that stale waiting block, it does not emit a follow-up send_wishlist_delta(remove=...) for the ids that became invalid. As a result, the protocol-side wishlist is not synchronized when pruning removes blocks that had previously contributed entries.
Affected code
pub fn block_db_changed(&mut self) -> Result<(), ConsensusError> {
...
let new_wishlist = self.get_block_wishlist()?;
let new_blocks: PreHashMap<BlockId, Option<SecuredHeader>> = new_wishlist
.iter()
.filter_map(|(id, header)| {
if !self.wishlist.contains_key(id) {
Some((*id, header.clone()))
} else {
None
}
})
.collect();
let remove_blocks: PreHashSet<BlockId> = self
.wishlist
.iter()
.filter_map(|(id, _)| {
if !new_wishlist.contains_key(id) {
Some(*id)
} else {
None
}
})
.collect();
if !new_blocks.is_empty() || !remove_blocks.is_empty() {
self.channels
.protocol_controller
.send_wishlist_delta(new_blocks, remove_blocks)?;
self.wishlist = new_wishlist;
}
...
if self.save_final_periods != latest_final_periods {
self.channels
.pool_controller
.notify_final_cs_periods(&latest_final_periods);
self.save_final_periods = latest_final_periods;
}
Ok(())
}
pub fn prune(&mut self) -> Result<(), ConsensusError> {
self.prune_active()?;
self.prune_slot_waiting();
self.prune_waiting_for_dependencies()?;
self.prune_discarded()?;
// … (truncated)Recommendation
Reorder ConsensusState::block_db_changed() so the wishlist is computed and published only after all pruning that depends on the updated latest_final_blocks_periods has completed. This ensures stale waiting blocks cannot contribute missing dependencies once they are already unreachable under the new finality floor.
Additionally, guarantee that every path that removes or invalidates waiting blocks, including ConsensusState::prune(), synchronizes both self.wishlist and the protocol retrieval wishlist by emitting the appropriate removal delta for any entries that were previously advertised. As a safeguard, enforce an invariant that the published wishlist is always derived exclusively from currently retained, reachable waiting blocks.
Why P0 / breaking
This change affects consensus, wire/serialization format, signatures, economics, block/tx validity, or otherwise requires a coordinated (versioned / hard-fork) rollout. It is therefore filed as an issue for design discussion rather than a direct PR. See SUBMISSIONS.md Part D.
Activity