Skip to content

Commit a6145b3

Browse files
apollo_consensus_orchestrator: introduce BTreeMap-keyed SNIP-35 fee_proposals window
1 parent 65d1324 commit a6145b3

2 files changed

Lines changed: 53 additions & 21 deletions

File tree

crates/apollo_consensus_orchestrator/src/sequencer_consensus_context.rs

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
mod sequencer_consensus_context_test;
77

88
use std::cmp::max;
9-
use std::collections::{BTreeMap, HashMap, VecDeque};
9+
use std::collections::{BTreeMap, HashMap};
1010
use std::sync::{Arc, Mutex};
1111
use std::time::Duration;
1212

@@ -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,16 +287,22 @@ 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+
// Per `BTreeMap::split_off` docs: "Splits the collection into two at the given key.
303+
// Returns everything after the given key, including the key." Reassigning the returned
304+
// half back keeps `[cutoff, ..)` and drops everything below.
305+
self.fee_proposals_window = self.fee_proposals_window.split_off(&cutoff);
301306
}
302307

303308
/// SNIP-35: bootstrap the fee_proposals window from state_sync on startup so `fee_actual` is
@@ -315,11 +320,13 @@ impl SequencerConsensusContext {
315320
u64::try_from(FEE_PROPOSAL_WINDOW_SIZE).expect("FEE_PROPOSAL_WINDOW_SIZE fits in u64");
316321
let start = height.0.saturating_sub(window_size);
317322
for h in start..height.0 {
318-
match self.deps.state_sync_client.get_block(BlockNumber(h)).await {
323+
let block_number = BlockNumber(h);
324+
match self.deps.state_sync_client.get_block(block_number).await {
319325
Ok(block) => {
320-
if let Some(fee_proposal) = block.block_header_without_hash.fee_proposal_fri {
321-
self.push_fee_proposal(fee_proposal);
322-
}
326+
self.record_fee_proposal(
327+
block_number,
328+
block.block_header_without_hash.fee_proposal_fri,
329+
);
323330
}
324331
Err(e) => {
325332
warn!(
@@ -436,9 +443,7 @@ impl SequencerConsensusContext {
436443
let DecisionReachedResponse { state_diff, central_objects } = decision_reached_response;
437444

438445
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-
}
446+
self.record_fee_proposal(height, init.fee_proposal_fri);
442447

443448
// A hash map of (possibly failed) transactions, where the key is the transaction hash
444449
// and the value is the transaction itself.
@@ -891,9 +896,6 @@ impl ConsensusContext for SequencerConsensusContext {
891896
sync_block.block_header_without_hash.next_l2_gas_price,
892897
VersionedConstants::latest_constants().min_gas_price,
893898
);
894-
if let Some(fee_proposal) = sync_block.block_header_without_hash.fee_proposal_fri {
895-
self.push_fee_proposal(fee_proposal);
896-
}
897899

898900
// TODO(Asmaa): validate starknet_version and parent_hash when they are stored.
899901
let block_number = sync_block.block_header_without_hash.block_number;
@@ -916,6 +918,7 @@ impl ConsensusContext for SequencerConsensusContext {
916918
);
917919
return false;
918920
}
921+
self.record_fee_proposal(height, sync_block.block_header_without_hash.fee_proposal_fri);
919922
self.previous_proposal_init =
920923
Some(previous_proposal_init_from_block_header(&sync_block.block_header_without_hash));
921924
self.interrupt_active_proposal().await;
@@ -958,6 +961,7 @@ impl ConsensusContext for SequencerConsensusContext {
958961
}
959962
self.current_height = Some(height);
960963
self.current_round = round;
964+
self.prune_fee_proposals_window(height);
961965
self.queued_proposals.clear();
962966
// The Batcher must be told when we begin to work on a new height. The implicit model is
963967
// that consensus works on a given height until it is done (either a decision is reached

crates/apollo_consensus_orchestrator/src/sequencer_consensus_context_test.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,3 +1535,31 @@ async fn test_first_height_keeps_sync_provided_l2_gas_price() {
15351535
expected_price, context.l2_gas_price.0
15361536
);
15371537
}
1538+
1539+
// Distinct value per height so map equality verifies identity, not just key set.
1540+
fn window_of(
1541+
heights: impl IntoIterator<Item = u64>,
1542+
) -> std::collections::BTreeMap<BlockNumber, Option<GasPrice>> {
1543+
heights.into_iter().map(|h| (BlockNumber(h), Some(GasPrice(u128::from(h))))).collect()
1544+
}
1545+
1546+
#[rstest]
1547+
// FEE_PROPOSAL_WINDOW_SIZE = 10, prune at 110: cutoff = 100, keep heights >= 100.
1548+
#[case::drops_entries_below_cutoff(window_of(90..=110), BlockNumber(110), window_of(100..=110))]
1549+
#[case::empty_when_all_entries_below_cutoff(window_of([50, 60, 70]), BlockNumber(200), window_of([]))]
1550+
// `current_height < FEE_PROPOSAL_WINDOW_SIZE`: cutoff would underflow; saturating_sub clamps
1551+
// to 0, so every entry is retained.
1552+
#[case::saturates_at_zero_when_height_below_window(window_of(0..=2), BlockNumber(5), window_of(0..=2))]
1553+
#[case::no_op_when_all_entries_within_window(window_of(105..=110), BlockNumber(110), window_of(105..=110))]
1554+
fn test_prune_fee_proposals_window(
1555+
#[case] initial_window: std::collections::BTreeMap<BlockNumber, Option<GasPrice>>,
1556+
#[case] current_height: BlockNumber,
1557+
#[case] expected_window: std::collections::BTreeMap<BlockNumber, Option<GasPrice>>,
1558+
) {
1559+
let (mut deps, _network) = create_test_and_network_deps();
1560+
deps.setup_default_expectations();
1561+
let mut context = deps.build_context();
1562+
context.fee_proposals_window = initial_window;
1563+
context.prune_fee_proposals_window(current_height);
1564+
assert_eq!(context.fee_proposals_window, expected_window);
1565+
}

0 commit comments

Comments
 (0)