Skip to content

Commit 69fb04b

Browse files
apollo_consensus_orchestrator: add SNIP-35 fee_proposals sliding window
1 parent e3b70b5 commit 69fb04b

4 files changed

Lines changed: 69 additions & 12 deletions

File tree

crates/apollo_consensus_manager/src/consensus_manager.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ impl ConsensusManager {
304304
outbound_proposal_sender: outbound_internal_sender,
305305
vote_broadcast_client: votes_broadcast_channels.broadcast_topic_client.clone(),
306306
config_manager_client: Some(Arc::clone(&config_manager_client)),
307+
strk_price_oracle: None,
307308
},
308309
)
309310
}

crates/apollo_consensus_orchestrator/src/fee_market/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ const FRI_DECIMALS_SCALE: u128 = 10u128.pow(18);
2828
/// Denominator for parts-per-thousand calculations in SNIP-35 fee_proposal bounds.
2929
pub(crate) const PPT_DENOMINATOR: u128 = 1000;
3030

31+
/// Number of fee_proposal values used to compute fee_actual (SNIP-35).
32+
pub(crate) const FEE_PROPOSAL_WINDOW_SIZE: usize = 10;
33+
3134
/// Fee market information for the next block.
3235
#[derive(Debug, Default, Serialize)]
3336
pub struct FeeMarketInfo {

crates/apollo_consensus_orchestrator/src/sequencer_consensus_context.rs

Lines changed: 39 additions & 3 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};
9+
use std::collections::{BTreeMap, HashMap, VecDeque};
1010
use std::sync::{Arc, Mutex};
1111
use std::time::Duration;
1212

@@ -23,7 +23,7 @@ use apollo_config::behavior_mode::BehaviorMode;
2323
use apollo_config_manager_types::communication::SharedConfigManagerClient;
2424
use apollo_consensus::types::{ConsensusContext, ConsensusError, ProposalCommitment, Round};
2525
use apollo_consensus_orchestrator_config::config::ContextConfig;
26-
use apollo_l1_gas_price_types::L1GasPriceProviderClient;
26+
use apollo_l1_gas_price_types::{L1GasPriceProviderClient, PriceOracleClientTrait};
2727
use apollo_network::network_manager::{BroadcastTopicClient, BroadcastTopicClientTrait};
2828
use 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+
};
8185
use 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();

crates/apollo_consensus_orchestrator/src/test_utils.rs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ pub(crate) struct TestDeps {
127127
pub clock: Arc<dyn Clock>,
128128
pub outbound_proposal_sender: mpsc::Sender<(HeightAndRound, mpsc::Receiver<ProposalPart>)>,
129129
pub vote_broadcast_client: BroadcastTopicClient<Vote>,
130+
pub strk_price_oracle: Option<Arc<dyn apollo_l1_gas_price_types::PriceOracleClientTrait>>,
130131
}
131132

132133
impl From<TestDeps> for SequencerConsensusContextDeps {
@@ -141,6 +142,7 @@ impl From<TestDeps> for SequencerConsensusContextDeps {
141142
outbound_proposal_sender: deps.outbound_proposal_sender,
142143
vote_broadcast_client: deps.vote_broadcast_client,
143144
config_manager_client: None,
145+
strk_price_oracle: deps.strk_price_oracle,
144146
}
145147
}
146148
}
@@ -170,6 +172,7 @@ impl TestDeps {
170172
self.setup_default_transaction_converter();
171173
self.setup_default_cende_ambassador();
172174
self.setup_default_gas_price_provider();
175+
self.setup_default_state_sync_get_block();
173176
}
174177

175178
pub(crate) fn setup_deps_for_build(&mut self, args: SetupDepsArgs) {
@@ -321,24 +324,37 @@ impl TestDeps {
321324
self.l1_gas_price_provider.expect_get_price().return_const(Ok(ETH_TO_FRI_RATE));
322325
}
323326

327+
/// Default get_block returns NotFound for all blocks. Used by SNIP-35 backfill on startup.
328+
fn setup_default_state_sync_get_block(&mut self) {
329+
self.state_sync_client.expect_get_block().returning(|block_number| {
330+
Err(apollo_state_sync_types::communication::StateSyncClientError::StateSyncError(
331+
apollo_state_sync_types::errors::StateSyncError::BlockNotFound(block_number),
332+
))
333+
});
334+
}
335+
324336
pub(crate) fn setup_default_batcher_get_block_hash(&mut self) {
325337
self.batcher.expect_get_block_hash().returning(|block_number| {
326338
Err(BatcherClientError::BatcherError(BatcherError::BlockHashNotFound(block_number)))
327339
});
328340
}
329341

330342
pub(crate) fn build_context(self) -> SequencerConsensusContext {
331-
SequencerConsensusContext::new(
332-
ContextConfig {
333-
static_config: ContextStaticConfig {
334-
proposal_buffer_size: CHANNEL_SIZE,
335-
chain_id: CHAIN_ID,
336-
..Default::default()
337-
},
343+
self.build_context_with_config(ContextConfig {
344+
static_config: ContextStaticConfig {
345+
proposal_buffer_size: CHANNEL_SIZE,
346+
chain_id: CHAIN_ID,
338347
..Default::default()
339348
},
340-
self.into(),
341-
)
349+
..Default::default()
350+
})
351+
}
352+
353+
pub(crate) fn build_context_with_config(
354+
self,
355+
config: ContextConfig,
356+
) -> SequencerConsensusContext {
357+
SequencerConsensusContext::new(config, self.into())
342358
}
343359
}
344360

@@ -367,6 +383,7 @@ pub(crate) fn create_test_and_network_deps() -> (TestDeps, NetworkDependencies)
367383
clock,
368384
outbound_proposal_sender,
369385
vote_broadcast_client: votes_topic_client,
386+
strk_price_oracle: None,
370387
};
371388

372389
let network_deps =

0 commit comments

Comments
 (0)