Skip to content

Commit 0d8de9c

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

2 files changed

Lines changed: 81 additions & 21 deletions

File tree

crates/apollo_consensus_orchestrator/src/sequencer_consensus_context.rs

Lines changed: 31 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,25 @@ 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+
/// Drop fee_proposals older than the SNIP-35 window so only heights in
299+
/// `[current_height - FEE_PROPOSAL_WINDOW_SIZE, ..)` remain.
300+
///
301+
/// `BTreeMap::split_off(&cutoff)` is inclusive on `cutoff`: it returns the entries with
302+
/// key `>= cutoff` and leaves keys `< cutoff` in `self`. Reassigning the returned half
303+
/// back to the field keeps `[cutoff, ..)` and drops everything strictly below.
304+
fn prune_fee_proposals_window(&mut self, current_height: BlockNumber) {
305+
let window_size =
306+
u64::try_from(FEE_PROPOSAL_WINDOW_SIZE).expect("FEE_PROPOSAL_WINDOW_SIZE fits in u64");
307+
let cutoff = BlockNumber(current_height.0.saturating_sub(window_size));
308+
self.fee_proposals_window = self.fee_proposals_window.split_off(&cutoff);
301309
}
302310

303311
/// SNIP-35: bootstrap the fee_proposals window from state_sync on startup so `fee_actual` is
@@ -315,11 +323,13 @@ impl SequencerConsensusContext {
315323
u64::try_from(FEE_PROPOSAL_WINDOW_SIZE).expect("FEE_PROPOSAL_WINDOW_SIZE fits in u64");
316324
let start = height.0.saturating_sub(window_size);
317325
for h in start..height.0 {
318-
match self.deps.state_sync_client.get_block(BlockNumber(h)).await {
326+
let block_number = BlockNumber(h);
327+
match self.deps.state_sync_client.get_block(block_number).await {
319328
Ok(block) => {
320-
if let Some(fee_proposal) = block.block_header_without_hash.fee_proposal_fri {
321-
self.push_fee_proposal(fee_proposal);
322-
}
329+
self.record_fee_proposal(
330+
block_number,
331+
block.block_header_without_hash.fee_proposal_fri,
332+
);
323333
}
324334
Err(e) => {
325335
warn!(
@@ -436,9 +446,7 @@ impl SequencerConsensusContext {
436446
let DecisionReachedResponse { state_diff, central_objects } = decision_reached_response;
437447

438448
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-
}
449+
self.record_fee_proposal(height, init.fee_proposal_fri);
442450

443451
// A hash map of (possibly failed) transactions, where the key is the transaction hash
444452
// and the value is the transaction itself.
@@ -891,9 +899,10 @@ impl ConsensusContext for SequencerConsensusContext {
891899
sync_block.block_header_without_hash.next_l2_gas_price,
892900
VersionedConstants::latest_constants().min_gas_price,
893901
);
894-
if let Some(fee_proposal) = sync_block.block_header_without_hash.fee_proposal_fri {
895-
self.push_fee_proposal(fee_proposal);
896-
}
902+
self.record_fee_proposal(
903+
sync_block.block_header_without_hash.block_number,
904+
sync_block.block_header_without_hash.fee_proposal_fri,
905+
);
897906

898907
// TODO(Asmaa): validate starknet_version and parent_hash when they are stored.
899908
let block_number = sync_block.block_header_without_hash.block_number;
@@ -958,6 +967,7 @@ impl ConsensusContext for SequencerConsensusContext {
958967
}
959968
self.current_height = Some(height);
960969
self.current_round = round;
970+
self.prune_fee_proposals_window(height);
961971
self.queued_proposals.clear();
962972
// The Batcher must be told when we begin to work on a new height. The implicit model is
963973
// 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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,3 +1535,53 @@ async fn test_first_height_keeps_sync_provided_l2_gas_price() {
15351535
expected_price, context.l2_gas_price.0
15361536
);
15371537
}
1538+
1539+
#[rstest]
1540+
// FEE_PROPOSAL_WINDOW_SIZE = 10, prune at 110: cutoff = 100, keep heights >= 100.
1541+
#[case::drops_entries_below_cutoff(
1542+
(90u64..=110).collect::<Vec<_>>(),
1543+
BlockNumber(110),
1544+
(100u64..=110).collect::<Vec<_>>(),
1545+
)]
1546+
#[case::empty_when_all_entries_below_cutoff(
1547+
vec![50u64, 60, 70],
1548+
BlockNumber(200),
1549+
vec![],
1550+
)]
1551+
// `current_height < FEE_PROPOSAL_WINDOW_SIZE`: cutoff would underflow; saturating_sub clamps
1552+
// to 0, so every entry is retained.
1553+
#[case::saturates_at_zero_when_height_below_window(
1554+
vec![0u64, 1, 2],
1555+
BlockNumber(5),
1556+
vec![0u64, 1, 2],
1557+
)]
1558+
#[case::no_op_when_all_entries_within_window(
1559+
(105u64..=110).collect::<Vec<_>>(),
1560+
BlockNumber(110),
1561+
(105u64..=110).collect::<Vec<_>>(),
1562+
)]
1563+
fn test_prune_fee_proposals_window(
1564+
#[case] inserted_heights: Vec<u64>,
1565+
#[case] current_height: BlockNumber,
1566+
#[case] expected_kept_heights: Vec<u64>,
1567+
) {
1568+
let (mut deps, _network) = create_test_and_network_deps();
1569+
deps.setup_default_expectations();
1570+
let mut context = deps.build_context();
1571+
1572+
// Distinct value per height so the assertion checks identity, not just count.
1573+
for h in &inserted_heights {
1574+
context.record_fee_proposal(BlockNumber(*h), Some(GasPrice(u128::from(*h))));
1575+
}
1576+
1577+
context.prune_fee_proposals_window(current_height);
1578+
1579+
let kept_heights: Vec<u64> = context.fee_proposals_window.keys().map(|k| k.0).collect();
1580+
assert_eq!(kept_heights, expected_kept_heights);
1581+
for h in &expected_kept_heights {
1582+
assert_eq!(
1583+
context.fee_proposals_window.get(&BlockNumber(*h)),
1584+
Some(&Some(GasPrice(u128::from(*h)))),
1585+
);
1586+
}
1587+
}

0 commit comments

Comments
 (0)