diff --git a/crates/apollo_consensus_manager/src/consensus_manager.rs b/crates/apollo_consensus_manager/src/consensus_manager.rs index 322601cf202..81b60aa6cb4 100644 --- a/crates/apollo_consensus_manager/src/consensus_manager.rs +++ b/crates/apollo_consensus_manager/src/consensus_manager.rs @@ -308,7 +308,6 @@ impl ConsensusManager { outbound_proposal_sender: outbound_internal_sender, vote_broadcast_client: votes_broadcast_channels.broadcast_topic_client.clone(), config_manager_client: Some(Arc::clone(&config_manager_client)), - strk_to_usd_oracle: None, }, ) } diff --git a/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context.rs b/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context.rs index ea2c646a00e..37b8dbe6e4e 100644 --- a/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context.rs +++ b/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context.rs @@ -23,7 +23,7 @@ use apollo_config::behavior_mode::BehaviorMode; use apollo_config_manager_types::communication::SharedConfigManagerClient; use apollo_consensus::types::{ConsensusContext, ConsensusError, ProposalCommitment, Round}; use apollo_consensus_orchestrator_config::config::ContextConfig; -use apollo_l1_gas_price_types::{ExchangeRateOracleClientTrait, L1GasPriceProviderClient}; +use apollo_l1_gas_price_types::L1GasPriceProviderClient; use apollo_network::network_manager::{BroadcastTopicClient, BroadcastTopicClientTrait}; use apollo_protobuf::consensus::{ BuildParam, @@ -75,7 +75,7 @@ use tokio::task::JoinHandle; use tokio::time::sleep; use tokio_util::sync::CancellationToken; use tokio_util::task::AbortOnDropHandle; -use tracing::{debug, error, error_span, info, instrument, trace, warn, Instrument}; +use tracing::{error, error_span, info, instrument, trace, warn, Instrument}; use crate::build_proposal::{build_proposal, BuildProposalError, ProposalBuildArguments}; use crate::cende::{ @@ -269,10 +269,6 @@ pub struct SequencerConsensusContextDeps { // Used to broadcast votes to other consensus nodes. pub vote_broadcast_client: BroadcastTopicClient, pub config_manager_client: Option, - /// STRK/USD oracle for SNIP-35 dynamic gas pricing. The underlying trait is generic; this - /// field's name labels the rate semantics. The instance must be configured with STRK/USD - /// URLs at construction time. None if disabled. - pub strk_to_usd_oracle: Option>, } #[derive(thiserror::Error, PartialEq, Debug)] @@ -457,23 +453,18 @@ impl SequencerConsensusContext { }; SNIP35_FEE_ACTUAL.set_lossy(fee_actual.0); - let fee_target = match &self.deps.strk_to_usd_oracle { - Some(oracle) => match oracle.fetch_rate(timestamp).await { - Ok(rate) => { - let target = compute_fee_target(TARGET_ATTO_USD_PER_L2_GAS, rate); - match target { - Some(t) => SNIP35_FEE_TARGET.set_lossy(t.0), - None => warn!("STRK/USD oracle returned zero rate, freezing fee_proposal"), - } - target - } - Err(e) => { - warn!("STRK/USD oracle error: {e:?}, freezing fee_proposal"); - None + let fee_target = match self.deps.l1_gas_price_provider.get_strk_to_usd_rate(timestamp).await + { + Ok(rate) => { + let target = compute_fee_target(TARGET_ATTO_USD_PER_L2_GAS, rate); + match target { + Some(t) => SNIP35_FEE_TARGET.set_lossy(t.0), + None => warn!("STRK/USD oracle returned zero rate, freezing fee_proposal"), } - }, - None => { - debug!("No STRK/USD oracle configured, freezing fee_proposal"); + target + } + Err(e) => { + warn!("STRK/USD oracle error: {e:?}, freezing fee_proposal"); None } }; diff --git a/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context_test.rs b/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context_test.rs index 1f3de9046ee..ad9a93e6c2e 100644 --- a/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context_test.rs +++ b/crates/apollo_consensus_orchestrator/src/sequencer_consensus_context_test.rs @@ -27,11 +27,7 @@ use apollo_l1_gas_price_types::errors::{ L1GasPriceClientError, L1GasPriceProviderError, }; -use apollo_l1_gas_price_types::{ - MockExchangeRateOracleClientTrait, - MockL1GasPriceProviderClient, - PriceInfo, -}; +use apollo_l1_gas_price_types::{MockL1GasPriceProviderClient, PriceInfo}; use apollo_protobuf::consensus::{ BuildParam, CommitmentParts, @@ -1619,11 +1615,9 @@ async fn test_initialize_fee_proposals_window( #[derive(Clone)] enum OracleBehavior { - /// No oracle is configured (`strk_to_usd_oracle = None`). - NotConfigured, - /// Oracle is configured and `fetch_rate` returns `Ok(rate)`. + /// Provider returns `Ok(rate)` for the STRK/USD query. Ok(u128), - /// Oracle is configured and `fetch_rate` returns `Err(_)`. + /// Provider returns `Err(_)` for the STRK/USD query. Err, } @@ -1631,16 +1625,10 @@ enum OracleBehavior { #[rstest] #[case::no_fee_actual_freezes_at_l2_gas_price( None, - OracleBehavior::NotConfigured, + OracleBehavior::Ok(0), GasPrice(7_000_000_000), GasPrice(7_000_000_000) )] -#[case::no_oracle_freezes_at_fee_actual( - Some(GasPrice(10_000_000_000)), - OracleBehavior::NotConfigured, - GasPrice(7_000_000_000), - GasPrice(10_000_000_000) -)] #[case::oracle_zero_rate_freezes_at_fee_actual( Some(GasPrice(10_000_000_000)), OracleBehavior::Ok(0), @@ -1679,22 +1667,21 @@ async fn test_compute_snip35_fee_proposal( #[case] expected_fee_proposal: GasPrice, ) { let (mut deps, _network) = create_test_and_network_deps(); - deps.setup_default_expectations(); - deps.strk_to_usd_oracle = match oracle_behavior { - OracleBehavior::NotConfigured => None, + // Register the specific strk_to_usd_rate expectation BEFORE setup_default_expectations so + // the catch-all default installed there does not shadow it (mockall matches oldest first). + match oracle_behavior { OracleBehavior::Ok(rate) => { - let mut mock = MockExchangeRateOracleClientTrait::new(); - mock.expect_fetch_rate().returning(move |_| Ok(rate)); - Some(Arc::new(mock)) + deps.l1_gas_price_provider.expect_get_strk_to_usd_rate().returning(move |_| Ok(rate)); } OracleBehavior::Err => { - let mut mock = MockExchangeRateOracleClientTrait::new(); - mock.expect_fetch_rate().returning(|_| { - Err(ExchangeRateOracleClientError::RequestError("test".to_string())) + deps.l1_gas_price_provider.expect_get_strk_to_usd_rate().returning(|_| { + Err(L1GasPriceClientError::ExchangeRateOracleClientError( + ExchangeRateOracleClientError::RequestError("test".to_string()), + )) }); - Some(Arc::new(mock)) } - }; + } + deps.setup_default_expectations(); let mut context = deps.build_context(); context.l2_gas_price = l2_gas_price; @@ -1713,16 +1700,17 @@ async fn test_compute_snip35_fee_proposal_converges_to_oracle_target() { ]; let (mut deps, _network) = create_test_and_network_deps(); - deps.setup_default_expectations(); - let mut mock = MockExchangeRateOracleClientTrait::new(); + // Register per-phase strk_to_usd_rate expectations BEFORE setup_default_expectations so they + // are not shadowed by the catch-all default (mockall matches oldest first). let mut seq = mockall::Sequence::new(); for &(rate, _, n_blocks) in &phases { - mock.expect_fetch_rate() + deps.l1_gas_price_provider + .expect_get_strk_to_usd_rate() .times(usize::try_from(n_blocks).unwrap()) .in_sequence(&mut seq) .returning(move |_| Ok(rate)); } - deps.strk_to_usd_oracle = Some(Arc::new(mock)); + deps.setup_default_expectations(); let mut context = deps.build_context(); // Bootstrap the window with 75 gwei (the $0.04 target). diff --git a/crates/apollo_consensus_orchestrator/src/test_utils.rs b/crates/apollo_consensus_orchestrator/src/test_utils.rs index 8c9dc9b60db..e755c5e8c5b 100644 --- a/crates/apollo_consensus_orchestrator/src/test_utils.rs +++ b/crates/apollo_consensus_orchestrator/src/test_utils.rs @@ -93,6 +93,10 @@ pub(crate) const CHAIN_ID: ChainId = ChainId::Mainnet; // values here. pub(crate) const ETH_TO_FRI_RATE: u128 = 2 * u128::pow(10, 18); +// A STRK/USD rate that yields an in-bounds SNIP-35 fee target in proposal-building tests +// where the specific value doesn't matter. +pub(crate) const DEFAULT_STRK_TO_USD_RATE: u128 = 300_000_000_000_000_000; + pub(crate) static TX_BATCH: LazyLock> = LazyLock::new(|| (0..3).map(generate_invoke_tx).collect()); @@ -129,8 +133,6 @@ pub(crate) struct TestDeps { pub clock: Arc, pub outbound_proposal_sender: mpsc::Sender<(HeightAndRound, mpsc::Receiver)>, pub vote_broadcast_client: BroadcastTopicClient, - pub strk_to_usd_oracle: - Option>, } impl From for SequencerConsensusContextDeps { @@ -145,7 +147,6 @@ impl From for SequencerConsensusContextDeps { outbound_proposal_sender: deps.outbound_proposal_sender, vote_broadcast_client: deps.vote_broadcast_client, config_manager_client: None, - strk_to_usd_oracle: deps.strk_to_usd_oracle, } } } @@ -325,6 +326,9 @@ impl TestDeps { blob_fee: GasPrice(TEMP_ETH_BLOB_GAS_FEE_IN_WEI), })); self.l1_gas_price_provider.expect_get_rate().return_const(Ok(ETH_TO_FRI_RATE)); + self.l1_gas_price_provider + .expect_get_strk_to_usd_rate() + .return_const(Ok(DEFAULT_STRK_TO_USD_RATE)); } fn setup_default_state_sync_get_block(&mut self) { @@ -381,7 +385,6 @@ pub(crate) fn create_test_and_network_deps() -> (TestDeps, NetworkDependencies) clock, outbound_proposal_sender, vote_broadcast_client: votes_topic_client, - strk_to_usd_oracle: None, }; let network_deps = diff --git a/crates/apollo_dashboard/resources/dev_grafana.json b/crates/apollo_dashboard/resources/dev_grafana.json index 8b56dc9d0e5..7d3da3d27d3 100644 --- a/crates/apollo_dashboard/resources/dev_grafana.json +++ b/crates/apollo_dashboard/resources/dev_grafana.json @@ -3478,6 +3478,32 @@ ], "extra_params": {} }, + { + "title": "get_strk_to_usd_rate (client-side)", + "description": "client-side infra metrics for request type get_strk_to_usd_rate", + "type": "timeseries", + "exprs": [ + "histogram_quantile(0.50,label_replace(sum by (le) (rate(l1_gas_price_local_response_times_secs_bucket{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\", request_variant=\"get_strk_to_usd_rate\"}[5m])), \"label_name\", \"0.50 l1_gas_price_local_response_times_secs\", \"le\", \".*\"))", + "histogram_quantile(0.95,label_replace(sum by (le) (rate(l1_gas_price_local_response_times_secs_bucket{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\", request_variant=\"get_strk_to_usd_rate\"}[5m])), \"label_name\", \"0.95 l1_gas_price_local_response_times_secs\", \"le\", \".*\"))", + "histogram_quantile(0.50,label_replace(sum by (le) (rate(l1_gas_price_remote_response_times_secs_bucket{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\", request_variant=\"get_strk_to_usd_rate\"}[5m])), \"label_name\", \"0.50 l1_gas_price_remote_response_times_secs\", \"le\", \".*\"))", + "histogram_quantile(0.95,label_replace(sum by (le) (rate(l1_gas_price_remote_response_times_secs_bucket{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\", request_variant=\"get_strk_to_usd_rate\"}[5m])), \"label_name\", \"0.95 l1_gas_price_remote_response_times_secs\", \"le\", \".*\"))", + "histogram_quantile(0.50,label_replace(sum by (le) (rate(l1_gas_price_remote_client_communication_failure_times_secs_bucket{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\", request_variant=\"get_strk_to_usd_rate\"}[5m])), \"label_name\", \"0.50 l1_gas_price_remote_client_communication_failure_times_secs\", \"le\", \".*\"))", + "histogram_quantile(0.95,label_replace(sum by (le) (rate(l1_gas_price_remote_client_communication_failure_times_secs_bucket{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\", request_variant=\"get_strk_to_usd_rate\"}[5m])), \"label_name\", \"0.95 l1_gas_price_remote_client_communication_failure_times_secs\", \"le\", \".*\"))" + ], + "extra_params": {} + }, + { + "title": "get_strk_to_usd_rate (server-side)", + "description": "server-side infra metrics for request type get_strk_to_usd_rate", + "type": "timeseries", + "exprs": [ + "histogram_quantile(0.50,label_replace(sum by (le) (rate(l1_gas_price_processing_times_secs_bucket{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\", request_variant=\"get_strk_to_usd_rate\"}[5m])), \"label_name\", \"0.50 l1_gas_price_processing_times_secs\", \"le\", \".*\"))", + "histogram_quantile(0.95,label_replace(sum by (le) (rate(l1_gas_price_processing_times_secs_bucket{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\", request_variant=\"get_strk_to_usd_rate\"}[5m])), \"label_name\", \"0.95 l1_gas_price_processing_times_secs\", \"le\", \".*\"))", + "histogram_quantile(0.50,label_replace(sum by (le) (rate(l1_gas_price_queueing_times_secs_bucket{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\", request_variant=\"get_strk_to_usd_rate\"}[5m])), \"label_name\", \"0.50 l1_gas_price_queueing_times_secs\", \"le\", \".*\"))", + "histogram_quantile(0.95,label_replace(sum by (le) (rate(l1_gas_price_queueing_times_secs_bucket{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\", request_variant=\"get_strk_to_usd_rate\"}[5m])), \"label_name\", \"0.95 l1_gas_price_queueing_times_secs\", \"le\", \".*\"))" + ], + "extra_params": {} + }, { "title": "initialize (client-side)", "description": "client-side infra metrics for request type initialize", diff --git a/crates/apollo_integration_tests/src/flow_test_setup.rs b/crates/apollo_integration_tests/src/flow_test_setup.rs index 745f6daf861..14c16cc3c74 100644 --- a/crates/apollo_integration_tests/src/flow_test_setup.rs +++ b/crates/apollo_integration_tests/src/flow_test_setup.rs @@ -278,6 +278,11 @@ impl FlowSequencerSetup { let (eth_to_strk_oracle_url_headers, _join_handle) = spawn_local_eth_to_strk_oracle(available_ports.get_next_port()); let eth_to_strk_oracle_config = ExchangeRateOracleConfig { + url_header_list: Some(vec![eth_to_strk_oracle_url_headers.clone().into()]), + ..Default::default() + }; + // Reuse the same dummy oracle endpoint for STRK/USD; the handler returns a constant. + let strk_to_usd_oracle_config = ExchangeRateOracleConfig { url_header_list: Some(vec![eth_to_strk_oracle_url_headers.into()]), ..Default::default() }; @@ -309,6 +314,7 @@ impl FlowSequencerSetup { state_sync_config, consensus_manager_config, eth_to_strk_oracle_config, + strk_to_usd_oracle_config, mempool_p2p_config, monitoring_endpoint_config, component_config, diff --git a/crates/apollo_integration_tests/src/integration_test_manager.rs b/crates/apollo_integration_tests/src/integration_test_manager.rs index 8bbda3d05d4..dd743469d8a 100644 --- a/crates/apollo_integration_tests/src/integration_test_manager.rs +++ b/crates/apollo_integration_tests/src/integration_test_manager.rs @@ -1367,6 +1367,11 @@ async fn get_sequencer_setup_configs( url_header_list: Some(vec![eth_to_strk_oracle_url.clone().into()]), ..Default::default() }; + // Reuse the same dummy oracle endpoint for STRK/USD; the handler returns a constant. + let strk_to_usd_oracle_config = ExchangeRateOracleConfig { + url_header_list: Some(vec![eth_to_strk_oracle_url.clone().into()]), + ..Default::default() + }; let validator_id = set_validator_id(&mut consensus_manager_config, node_index); let chain_info = chain_info.clone(); @@ -1397,6 +1402,7 @@ async fn get_sequencer_setup_configs( state_sync_config.clone(), consensus_manager_config.clone(), eth_to_strk_oracle_config.clone(), + strk_to_usd_oracle_config.clone(), mempool_p2p_config.clone(), monitoring_endpoint_config, executable_component_config.clone(), diff --git a/crates/apollo_integration_tests/src/utils.rs b/crates/apollo_integration_tests/src/utils.rs index 11d3f4f401b..15394b5265f 100644 --- a/crates/apollo_integration_tests/src/utils.rs +++ b/crates/apollo_integration_tests/src/utils.rs @@ -267,6 +267,7 @@ pub fn create_node_config( mut state_sync_config: StateSyncConfig, mut consensus_manager_config: ConsensusManagerConfig, eth_to_strk_oracle_config: ExchangeRateOracleConfig, + strk_to_usd_oracle_config: ExchangeRateOracleConfig, mempool_p2p_config: MempoolP2pConfig, monitoring_endpoint_config: MonitoringEndpointConfig, components: ComponentConfig, @@ -313,6 +314,7 @@ pub fn create_node_config( // Use newly minted blocks on Anvil to be used for gas price calculations. lag_margin_seconds: Duration::from_secs(0), eth_to_strk_oracle_config, + strk_to_usd_oracle_config, ..Default::default() }; let http_server_config = diff --git a/crates/apollo_l1_gas_price/src/communication.rs b/crates/apollo_l1_gas_price/src/communication.rs index 7e3a5e0d6fc..549a18a394e 100644 --- a/crates/apollo_l1_gas_price/src/communication.rs +++ b/crates/apollo_l1_gas_price/src/communication.rs @@ -32,6 +32,9 @@ impl ComponentRequestHandler for L1GasPri L1GasPriceRequest::GetEthToFriRate(timestamp) => { L1GasPriceResponse::GetEthToFriRate(self.eth_to_fri_rate(timestamp).await) } + L1GasPriceRequest::GetStrkToUsdRate(timestamp) => { + L1GasPriceResponse::GetStrkToUsdRate(self.strk_to_usd_rate(timestamp).await) + } } } } diff --git a/crates/apollo_l1_gas_price_types/src/lib.rs b/crates/apollo_l1_gas_price_types/src/lib.rs index fd80ce444df..80a495e3756 100644 --- a/crates/apollo_l1_gas_price_types/src/lib.rs +++ b/crates/apollo_l1_gas_price_types/src/lib.rs @@ -71,6 +71,7 @@ pub enum L1GasPriceRequest { GetGasPrice(BlockTimestamp), AddGasPrice(GasPriceData), GetEthToFriRate(u64), + GetStrkToUsdRate(u64), } impl_debug_for_infra_requests_and_responses!(L1GasPriceRequest); impl_labeled_request!(L1GasPriceRequest, L1GasPriceRequestLabelValue); @@ -82,6 +83,7 @@ pub enum L1GasPriceResponse { GetGasPrice(L1GasPriceProviderResult), AddGasPrice(L1GasPriceProviderResult<()>), GetEthToFriRate(L1GasPriceProviderResult), + GetStrkToUsdRate(L1GasPriceProviderResult), } impl_debug_for_infra_requests_and_responses!(L1GasPriceResponse); @@ -99,7 +101,11 @@ pub trait L1GasPriceProviderClient: Send + Sync { timestamp: BlockTimestamp, ) -> L1GasPriceProviderClientResult; + /// ETH/FRI rate as 18-decimal fixed-point integer (e.g. 5000 STRK per ETH → `5000 * 10^18`). async fn get_rate(&self, timestamp: u64) -> L1GasPriceProviderClientResult; + + /// STRK/USD rate as 18-decimal fixed-point integer (e.g. 24.5 STRK per USD → `24.5 * 10^18`). + async fn get_strk_to_usd_rate(&self, timestamp: u64) -> L1GasPriceProviderClientResult; } #[cfg_attr(any(feature = "testing", test), automock)] @@ -171,6 +177,19 @@ where Direct ) } + #[instrument(skip(self))] + async fn get_strk_to_usd_rate(&self, timestamp: u64) -> L1GasPriceProviderClientResult { + let request = L1GasPriceRequest::GetStrkToUsdRate(timestamp); + handle_all_response_variants!( + self, + request, + L1GasPriceResponse, + GetStrkToUsdRate, + L1GasPriceClientError, + L1GasPriceProviderError, + Direct + ) + } } generate_permutation_labels! {