@@ -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