|
6 | 6 | mod sequencer_consensus_context_test; |
7 | 7 |
|
8 | 8 | use std::cmp::max; |
9 | | -use std::collections::{BTreeMap, HashMap}; |
| 9 | +use std::collections::{BTreeMap, HashMap, VecDeque}; |
10 | 10 | use std::sync::{Arc, Mutex}; |
11 | 11 | use std::time::Duration; |
12 | 12 |
|
@@ -91,6 +91,7 @@ use crate::metrics::{ |
91 | 91 | register_metrics, |
92 | 92 | CONSENSUS_L2_GAS_PRICE, |
93 | 93 | }; |
| 94 | +use crate::snip35::FEE_PROPOSAL_WINDOW_SIZE; |
94 | 95 | use crate::utils::{ |
95 | 96 | convert_to_sn_api_block_info, |
96 | 97 | make_gas_price_params, |
@@ -234,6 +235,8 @@ pub struct SequencerConsensusContext { |
234 | 235 | l2_gas_price: GasPrice, |
235 | 236 | l1_da_mode: L1DataAvailabilityMode, |
236 | 237 | previous_proposal_init: Option<PreviousProposalInitInfo>, |
| 238 | + /// SNIP-35: sliding window of recent fee_proposal values (size from config). |
| 239 | + fee_proposals_window: VecDeque<GasPrice>, |
237 | 240 | } |
238 | 241 |
|
239 | 242 | #[derive(Clone)] |
@@ -282,9 +285,21 @@ impl SequencerConsensusContext { |
282 | 285 | l2_gas_price: VersionedConstants::latest_constants().min_gas_price, |
283 | 286 | l1_da_mode, |
284 | 287 | previous_proposal_init: None, |
| 288 | + fee_proposals_window: VecDeque::with_capacity(FEE_PROPOSAL_WINDOW_SIZE), |
285 | 289 | } |
286 | 290 | } |
287 | 291 |
|
| 292 | + /// SNIP-35: FIFO append into the fee_proposal sliding window. When the window is at |
| 293 | + /// `FEE_PROPOSAL_WINDOW_SIZE` the oldest entry is evicted so the window always holds the |
| 294 | + /// `FEE_PROPOSAL_WINDOW_SIZE` most-recently-committed `fee_proposal` values; the median of |
| 295 | + /// this window is the `fee_actual` that both proposer and validator use. |
| 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); |
| 301 | + } |
| 302 | + |
288 | 303 | async fn start_stream(&mut self, stream_id: HeightAndRound) -> StreamSender { |
289 | 304 | let (proposal_sender, proposal_receiver) = |
290 | 305 | mpsc::channel(self.config.static_config.proposal_buffer_size); |
@@ -387,6 +402,9 @@ impl SequencerConsensusContext { |
387 | 402 | let DecisionReachedResponse { state_diff, central_objects } = decision_reached_response; |
388 | 403 |
|
389 | 404 | self.update_l2_gas_price(height, l2_gas_used); |
| 405 | + if let Some(fee_proposal) = init.fee_proposal_fri { |
| 406 | + self.push_fee_proposal(fee_proposal); |
| 407 | + } |
390 | 408 |
|
391 | 409 | // A hash map of (possibly failed) transactions, where the key is the transaction hash |
392 | 410 | // and the value is the transaction itself. |
@@ -839,6 +857,12 @@ impl ConsensusContext for SequencerConsensusContext { |
839 | 857 | sync_block.block_header_without_hash.next_l2_gas_price, |
840 | 858 | VersionedConstants::latest_constants().min_gas_price, |
841 | 859 | ); |
| 860 | + // SNIP-35: push fee_proposal from synced block into the sliding window. |
| 861 | + // Pre-V0_14_3 blocks don't carry a fee_proposal; skip them. |
| 862 | + if let Some(fee_proposal) = sync_block.block_header_without_hash.fee_proposal_fri { |
| 863 | + self.push_fee_proposal(fee_proposal); |
| 864 | + } |
| 865 | + |
842 | 866 | // TODO(Asmaa): validate starknet_version and parent_hash when they are stored. |
843 | 867 | let block_number = sync_block.block_header_without_hash.block_number; |
844 | 868 | let timestamp = sync_block.block_header_without_hash.timestamp; |
@@ -895,6 +919,37 @@ impl ConsensusContext for SequencerConsensusContext { |
895 | 919 | let gas_price_u64 = u64::try_from(self.l2_gas_price.0).unwrap_or(u64::MAX); |
896 | 920 | CONSENSUS_L2_GAS_PRICE.set_lossy(gas_price_u64); |
897 | 921 | } |
| 922 | + // SNIP-35: on first height (startup), backfill the fee_proposals window from stored |
| 923 | + // blocks so fee_actual can be computed immediately. Sequential to maintain insertion |
| 924 | + // order (oldest first). |
| 925 | + if self.current_height.is_none() && self.fee_proposals_window.is_empty() { |
| 926 | + let window_size = u64::try_from(FEE_PROPOSAL_WINDOW_SIZE) |
| 927 | + .expect("FEE_PROPOSAL_WINDOW_SIZE fits in u64"); |
| 928 | + let start = height.0.saturating_sub(window_size); |
| 929 | + for h in start..height.0 { |
| 930 | + match self.deps.state_sync_client.get_block(BlockNumber(h)).await { |
| 931 | + Ok(block) => { |
| 932 | + if let Some(fee_proposal) = |
| 933 | + block.block_header_without_hash.fee_proposal_fri |
| 934 | + { |
| 935 | + self.push_fee_proposal(fee_proposal); |
| 936 | + } |
| 937 | + } |
| 938 | + Err(e) => { |
| 939 | + // Stop backfilling on the first error (likely `BlockNotFound` for |
| 940 | + // heights before the chain was SNIP-35-enabled). The remaining window |
| 941 | + // slots stay empty and `fee_actual` will be `None` until enough blocks |
| 942 | + // are committed, falling back to `l2_gas_price`. |
| 943 | + warn!( |
| 944 | + "SNIP-35 backfill stopped at block {h}: {e:?}. Window has {} / \ |
| 945 | + {FEE_PROPOSAL_WINDOW_SIZE} entries.", |
| 946 | + self.fee_proposals_window.len() |
| 947 | + ); |
| 948 | + break; |
| 949 | + } |
| 950 | + } |
| 951 | + } |
| 952 | + } |
898 | 953 | self.current_height = Some(height); |
899 | 954 | self.current_round = round; |
900 | 955 | self.queued_proposals.clear(); |
|
0 commit comments