66mod sequencer_consensus_context_test;
77
88use std:: cmp:: max;
9- use std:: collections:: { BTreeMap , HashMap } ;
9+ use std:: collections:: { BTreeMap , HashMap , VecDeque } ;
1010use std:: sync:: { Arc , Mutex } ;
1111use std:: time:: Duration ;
1212
@@ -23,7 +23,7 @@ use apollo_config::behavior_mode::BehaviorMode;
2323use apollo_config_manager_types:: communication:: SharedConfigManagerClient ;
2424use apollo_consensus:: types:: { ConsensusContext , ConsensusError , ProposalCommitment , Round } ;
2525use apollo_consensus_orchestrator_config:: config:: ContextConfig ;
26- use apollo_l1_gas_price_types:: L1GasPriceProviderClient ;
26+ use apollo_l1_gas_price_types:: { L1GasPriceProviderClient , PriceOracleClientTrait } ;
2727use apollo_network:: network_manager:: { BroadcastTopicClient , BroadcastTopicClientTrait } ;
2828use apollo_protobuf:: consensus:: {
2929 BuildParam ,
@@ -77,7 +77,11 @@ use crate::cende::{
7777 InternalTransactionWithReceipt ,
7878 N_BLOCK_HASHES_BACK_IN_BLOB ,
7979} ;
80- use crate :: fee_market:: { calculate_next_l2_gas_price_for_fin, FeeMarketInfo } ;
80+ use crate :: fee_market:: {
81+ calculate_next_l2_gas_price_for_fin,
82+ FeeMarketInfo ,
83+ FEE_PROPOSAL_WINDOW_SIZE ,
84+ } ;
8185use crate :: metrics:: {
8286 record_build_proposal_failure,
8387 record_validate_proposal_failure,
@@ -228,6 +232,8 @@ pub struct SequencerConsensusContext {
228232 l2_gas_price : GasPrice ,
229233 l1_da_mode : L1DataAvailabilityMode ,
230234 previous_proposal_init : Option < PreviousProposalInitInfo > ,
235+ /// SNIP-35: sliding window of recent fee_proposal values (size from config).
236+ fee_proposals_window : VecDeque < GasPrice > ,
231237}
232238
233239#[ derive( Clone ) ]
@@ -244,6 +250,8 @@ pub struct SequencerConsensusContextDeps {
244250 // Used to broadcast votes to other consensus nodes.
245251 pub vote_broadcast_client : BroadcastTopicClient < Vote > ,
246252 pub config_manager_client : Option < SharedConfigManagerClient > ,
253+ /// STRK/USD price oracle for SNIP-35 dynamic gas pricing. None if disabled.
254+ pub strk_price_oracle : Option < Arc < dyn PriceOracleClientTrait > > ,
247255}
248256
249257#[ derive( thiserror:: Error , PartialEq , Debug ) ]
@@ -274,7 +282,16 @@ impl SequencerConsensusContext {
274282 l2_gas_price : VersionedConstants :: latest_constants ( ) . min_gas_price ,
275283 l1_da_mode,
276284 previous_proposal_init : None ,
285+ fee_proposals_window : VecDeque :: with_capacity ( FEE_PROPOSAL_WINDOW_SIZE ) ,
286+ }
287+ }
288+
289+ /// SNIP-35: push a fee_proposal into the sliding window, evicting the oldest if full.
290+ fn push_fee_proposal ( & mut self , fee_proposal : GasPrice ) {
291+ if self . fee_proposals_window . len ( ) >= FEE_PROPOSAL_WINDOW_SIZE {
292+ self . fee_proposals_window . pop_front ( ) ;
277293 }
294+ self . fee_proposals_window . push_back ( fee_proposal) ;
278295 }
279296
280297 async fn start_stream ( & mut self , stream_id : HeightAndRound ) -> StreamSender {
@@ -377,6 +394,7 @@ impl SequencerConsensusContext {
377394 let DecisionReachedResponse { state_diff, central_objects } = decision_reached_response;
378395
379396 self . update_l2_gas_price ( height, l2_gas_used) ;
397+ self . push_fee_proposal ( init. fee_proposal ) ;
380398
381399 // A hash map of (possibly failed) transactions, where the key is the transaction hash
382400 // and the value is the transaction itself.
@@ -783,6 +801,9 @@ impl ConsensusContext for SequencerConsensusContext {
783801 sync_block. block_header_without_hash . next_l2_gas_price ,
784802 VersionedConstants :: latest_constants ( ) . min_gas_price ,
785803 ) ;
804+ // SNIP-35: push fee_proposal from synced block into the sliding window.
805+ self . push_fee_proposal ( sync_block. block_header_without_hash . fee_proposal ) ;
806+
786807 // TODO(Asmaa): validate starknet_version and parent_hash when they are stored.
787808 let block_number = sync_block. block_header_without_hash . block_number ;
788809 let timestamp = sync_block. block_header_without_hash . timestamp ;
@@ -828,6 +849,21 @@ impl ConsensusContext for SequencerConsensusContext {
828849 // First or a new (higher) height.
829850 if self . current_height . is_none_or ( |h| height > h) {
830851 self . update_dynamic_config ( ) . await ;
852+ // SNIP-35: on first height (startup), backfill the fee_proposals window from stored
853+ // blocks so fee_actual can be computed immediately. Sequential to maintain insertion
854+ // order (oldest first).
855+ if self . current_height . is_none ( ) && self . fee_proposals_window . is_empty ( ) {
856+ #[ allow( clippy:: as_conversions) ] // FEE_PROPOSAL_WINDOW_SIZE is a small constant.
857+ let start = height. 0 . saturating_sub ( FEE_PROPOSAL_WINDOW_SIZE as u64 ) ;
858+ for h in start..height. 0 {
859+ match self . deps . state_sync_client . get_block ( BlockNumber ( h) ) . await {
860+ Ok ( block) => {
861+ self . push_fee_proposal ( block. block_header_without_hash . fee_proposal ) ;
862+ }
863+ Err ( _) => break ,
864+ }
865+ }
866+ }
831867 self . current_height = Some ( height) ;
832868 self . current_round = round;
833869 self . queued_proposals . clear ( ) ;
0 commit comments