|
select! { |
|
mailbox_message = self.mailbox.next() => { |
|
let mail = mailbox_message.expect("Finalizer mailbox closed"); |
|
match mail { |
|
FinalizerMessage::SyncerUpdate { update } => { |
|
match update { |
|
Update::Tip(_height, _digest) => { |
|
// I don't think we need this |
|
} |
|
Update::FinalizedBlock((block, finalization), ack_tx) => { |
|
self.handle_finalized_block(ack_tx, block, finalization, &mut orchestrator_mailbox, &mut last_committed_timestamp).await; |
|
} |
|
Update::NotarizedBlock(block) => { |
|
self.handle_notarized_block(block).await; |
|
} |
|
} |
|
}, |
|
FinalizerMessage::NotifyAtHeight { height, block_digest, response } => { |
|
if self.canonical_state.get_latest_height() > height { |
|
// This block proposal is trying to build a block at height + 1, |
|
// but the canonical chain is already at height + 1 (or higher), |
|
// so the proposal should be aborted. |
|
let _ = response.send(false); |
|
warn!( |
|
"Aborting height notification for height {} and digest {} at epoch {} and height {} because the height is outdated", |
|
height, |
|
block_digest, |
|
self.canonical_state.get_epoch(), |
|
self.canonical_state.get_latest_height() |
|
); |
|
} else if height == self.canonical_state.get_latest_height() { |
|
// If the height matches the height of the canonical chain, |
|
// we check if the digest matches the head of the canonical chain. |
|
// If the digests don't match, then the proposal should be aborted. |
|
if block_digest == self.canonical_state.get_head_digest() { |
|
let _ = response.send(true); |
|
} else { |
|
let _ = response.send(false); |
|
warn!( |
|
"Aborting height notification for height {} and digest {} at epoch {} and height {} because the head digest is {}", |
|
height, |
|
block_digest, |
|
self.canonical_state.get_epoch(), |
|
self.canonical_state.get_latest_height(), |
|
self.canonical_state.get_head_digest() |
|
); |
|
} |
|
} else { |
|
// If the block was already executed on one of the forks, |
|
// we send the notification immediately, otherwise we store the request |
|
if self.fork_states.get(&height) |
|
.map(|forks| forks.contains_key(&block_digest)) |
|
.unwrap_or(false) { |
|
let _ = response.send(true); |
|
} else { |
|
self.pending_height_notifys.entry((height, block_digest)).or_default().push(response); |
|
} |
|
} |
|
}, |
|
FinalizerMessage::GetAuxData { height, parent_digest, response } => { |
|
self.handle_aux_data_mailbox(height, parent_digest, response).await; |
|
}, |
|
FinalizerMessage::GetEpochGenesisHash { epoch, response } => { |
|
// The finalizer sends a message to the orchestrator to start the new epoch. |
|
// The orchestrator will start the new Simplex instance, which will then request |
|
// the epoch genesis hash from the finalizer. |
|
// Since the finalizer increments `self.canonical_state.epoch` before sending the message to the |
|
// orchestrator, the finalizer should never receive a GetEpochGenesisHash request for the wrong epoch. |
|
if epoch != self.canonical_state.get_epoch() { |
|
error!(target: "critical", "Finalizer received epoch genesis hash request from a different epoch. This should not happen and is a bug. Our epoch: {}, requested epoch {}", self.canonical_state.get_epoch(), epoch); |
|
#[cfg(feature = "prom")] |
|
counter!("critical_errors_total", "reason" => "epoch_mismatch", "severity" => "critical").increment(1); |
|
} |
|
let _ = response.send(self.canonical_state.get_epoch_genesis_hash()); |
|
}, |
|
FinalizerMessage::QueryState { request, response } => { |
|
self.handle_consensus_state_query(request, response).await; |
|
}, |
Context
Summit limits how many withdrawals are emitted in an epoch with
max_withdrawals_per_epoch. The finalizer actor also handles consensus-critical mailbox work, including finalized block updates, notarized blocks, height notifications, aux-data requests, and state queries.Claim
A normal or adversarially large set of valid withdrawal and exit requests accumulates for one epoch with size far above
max_withdrawals_per_epoch; at every epoch boundary, the finalizer performs O(backlog) queue collection, overflow rescheduling, and tree rebuild work on the consensus actor path before only a capped prefix can be emitted. The withdrawal cap limits only the withdrawals included in the block, not the finalizer work required to inspect and roll over a larger ready backlog. Epoch-final aux-data collection builds a vector for the full ready epoch before applying.take(max_withdrawals), and epoch transition later reschedules all overflow withdrawals and rebuilds the withdrawal SSZ tree.Flow
The path is reachable from accepted withdrawal state when the number of ready withdrawals for an epoch exceeds the cap. The scan did not quantify the backlog size needed for material delay or prove malicious public ingress, so a focused workload still needs to measure finalizer occupancy.
Impact
The cap can give operators a false resource bound: a large backlog can still stall finalizer mailbox service at epoch boundaries, delaying
SyncerUpdate,NotifyAtHeight, andGetAuxDatahandling. Repeated overflow can carry the same large work set forward into later epochs.Root Cause
The code applies the per-epoch cap after collecting the full ready queue and reschedules overflow by moving every remaining key plus rebuilding the complete withdrawal proof subtree.
Code
summit/finalizer/src/actor.rs
Lines 306 to 383 in ed2c5c8
take(max_withdrawals):summit/finalizer/src/actor.rs
Lines 951 to 959 in ed2c5c8
WithdrawalQueue::get_for_epochiterates and collects the full scheduled epoch queue:summit/types/src/withdrawal.rs
Lines 235 to 245 in ed2c5c8
summit/finalizer/src/actor.rs
Lines 550 to 567 in ed2c5c8
summit/types/src/withdrawal.rs
Lines 252 to 269 in ed2c5c8
rebuild_withdrawalswalks all epochs and withdrawal items:summit/types/src/consensus_state.rs
Lines 619 to 623 in ed2c5c8
summit/types/src/ssz_state_tree.rs
Lines 544 to 577 in ed2c5c8
Related Issues/PRs
The following existing items touch the same trust boundary, adjacent root cause, or likely remediation stack.
Note that #362 is overflow backlog work outside the per-epoch cap.
Fix
Select only the capped prefix without materializing the entire epoch queue, store overflow in a structure that can roll forward incrementally, and update the withdrawal proof subtree proportionally to changed entries. Add a regression benchmark for backlog sizes above the cap.