Skip to content

Commit 3f243cb

Browse files
apollo_consensus_orchestrator: rework SNIP-35 fee_proposals window for state_sync resilience
1 parent 8b9dba2 commit 3f243cb

1 file changed

Lines changed: 36 additions & 37 deletions

File tree

crates/apollo_consensus_orchestrator/src/sequencer_consensus_context.rs

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,8 @@ pub struct SequencerConsensusContext {
235235
l2_gas_price: GasPrice,
236236
l1_da_mode: L1DataAvailabilityMode,
237237
previous_proposal_init: Option<PreviousProposalInitInfo>,
238-
/// SNIP-35: sliding window of recent fee_proposal values (size from
239-
/// `FEE_PROPOSAL_WINDOW_SIZE`).
240-
fee_proposals_window: VecDeque<GasPrice>,
238+
/// SNIP-35 fee_proposals window. `None` = pre-V0_14_3.
239+
fee_proposals_window: BTreeMap<BlockNumber, Option<GasPrice>>,
241240
}
242241

243242
#[derive(Clone)]
@@ -288,46 +287,46 @@ impl SequencerConsensusContext {
288287
l2_gas_price: VersionedConstants::latest_constants().min_gas_price,
289288
l1_da_mode,
290289
previous_proposal_init: None,
291-
fee_proposals_window: VecDeque::with_capacity(FEE_PROPOSAL_WINDOW_SIZE),
290+
fee_proposals_window: BTreeMap::new(),
292291
}
293292
}
294293

295-
/// FIFO append into the SNIP-35 sliding window.
296-
fn push_fee_proposal(&mut self, fee_proposal: GasPrice) {
297-
if self.fee_proposals_window.len() >= FEE_PROPOSAL_WINDOW_SIZE {
298-
self.fee_proposals_window.pop_front();
299-
}
300-
self.fee_proposals_window.push_back(fee_proposal);
294+
fn record_fee_proposal(&mut self, height: BlockNumber, fee_proposal_fri: Option<GasPrice>) {
295+
self.fee_proposals_window.insert(height, fee_proposal_fri);
296+
}
297+
298+
fn prune_fee_proposals_window(&mut self, current_height: BlockNumber) {
299+
let window_size =
300+
u64::try_from(FEE_PROPOSAL_WINDOW_SIZE).expect("FEE_PROPOSAL_WINDOW_SIZE fits in u64");
301+
let cutoff = BlockNumber(current_height.0.saturating_sub(window_size));
302+
self.fee_proposals_window = self.fee_proposals_window.split_off(&cutoff);
301303
}
302304

303-
/// SNIP-35: bootstrap the fee_proposals window from state_sync on startup so `fee_actual` is
304-
/// available immediately. Pre-V0_14_3 blocks return `Ok` with `fee_proposal_fri == None` and
305-
/// contribute nothing; an `Err` from state_sync means the block could not be retrieved, so we
306-
/// log and stop bootstrapping.
307-
// TODO(AndrewL): On `Err`, state_sync may simply not have downloaded the block yet rather than
308-
// the block being absent. Returning a partial window here is non-deterministic across nodes
309-
// (different local sync progress → different windows → different `fee_actual`). Resolving this
310-
// requires a startup gate that guarantees state_sync is caught up to `height - 1` before
311-
// consensus enters `set_height_and_round`, since blocking inside this call to retry is unsafe
312-
// (peers may already be voting at this height). Track and fix in a follow-up PR.
305+
/// Fill `[height - WINDOW, height)` from state_sync, retrying missing heights until the
306+
/// window is complete.
307+
// TODO(AndrewL): blocking here is unsafe; peers may already be voting at this height while
308+
// we wait. Replace with observer mode during warmup: receive proposals/votes without
309+
// broadcasting our own until the window is complete.
313310
async fn bootstrap_fee_proposals_window(&mut self, height: BlockNumber) {
314311
let window_size =
315312
u64::try_from(FEE_PROPOSAL_WINDOW_SIZE).expect("FEE_PROPOSAL_WINDOW_SIZE fits in u64");
316313
let start = height.0.saturating_sub(window_size);
317-
for h in start..height.0 {
318-
match self.deps.state_sync_client.get_block(BlockNumber(h)).await {
319-
Ok(block) => {
320-
if let Some(fee_proposal) = block.block_header_without_hash.fee_proposal_fri {
321-
self.push_fee_proposal(fee_proposal);
322-
}
323-
}
314+
let retry_interval = Duration::from_millis(500);
315+
let mut pending: VecDeque<BlockNumber> = (start..height.0).map(BlockNumber).collect();
316+
while let Some(block_number) = pending.pop_front() {
317+
match self.deps.state_sync_client.get_block(block_number).await {
318+
Ok(block) => self.record_fee_proposal(
319+
block_number,
320+
block.block_header_without_hash.fee_proposal_fri,
321+
),
324322
Err(e) => {
325323
warn!(
326-
"SNIP-35 bootstrap failed at block {h}: {e:?}. Window has {} / \
327-
{FEE_PROPOSAL_WINDOW_SIZE} entries.",
328-
self.fee_proposals_window.len()
324+
"SNIP-35 bootstrap: state_sync get_block({block_number}) failed: {e:?}. \
325+
{} pending, retrying in {retry_interval:?}.",
326+
pending.len() + 1,
329327
);
330-
break;
328+
pending.push_back(block_number);
329+
sleep(retry_interval).await;
331330
}
332331
}
333332
}
@@ -436,9 +435,7 @@ impl SequencerConsensusContext {
436435
let DecisionReachedResponse { state_diff, central_objects } = decision_reached_response;
437436

438437
self.update_l2_gas_price(height, l2_gas_used);
439-
if let Some(fee_proposal) = init.fee_proposal_fri {
440-
self.push_fee_proposal(fee_proposal);
441-
}
438+
self.record_fee_proposal(height, init.fee_proposal_fri);
442439

443440
// A hash map of (possibly failed) transactions, where the key is the transaction hash
444441
// and the value is the transaction itself.
@@ -891,9 +888,10 @@ impl ConsensusContext for SequencerConsensusContext {
891888
sync_block.block_header_without_hash.next_l2_gas_price,
892889
VersionedConstants::latest_constants().min_gas_price,
893890
);
894-
if let Some(fee_proposal) = sync_block.block_header_without_hash.fee_proposal_fri {
895-
self.push_fee_proposal(fee_proposal);
896-
}
891+
self.record_fee_proposal(
892+
sync_block.block_header_without_hash.block_number,
893+
sync_block.block_header_without_hash.fee_proposal_fri,
894+
);
897895

898896
// TODO(Asmaa): validate starknet_version and parent_hash when they are stored.
899897
let block_number = sync_block.block_header_without_hash.block_number;
@@ -958,6 +956,7 @@ impl ConsensusContext for SequencerConsensusContext {
958956
}
959957
self.current_height = Some(height);
960958
self.current_round = round;
959+
self.prune_fee_proposals_window(height);
961960
self.queued_proposals.clear();
962961
// The Batcher must be told when we begin to work on a new height. The implicit model is
963962
// that consensus works on a given height until it is done (either a decision is reached

0 commit comments

Comments
 (0)