diff --git a/crates/katana/cli/src/args.rs b/crates/katana/cli/src/args.rs index c2e5524f9b..bf19d87dd4 100644 --- a/crates/katana/cli/src/args.rs +++ b/crates/katana/cli/src/args.rs @@ -4,12 +4,12 @@ use std::collections::HashSet; use std::path::PathBuf; use alloy_primitives::U256; -use anyhow::{Context, Result}; +use anyhow::{Context, Ok, Result}; use clap::Parser; use katana_core::constants::DEFAULT_SEQUENCER_ADDRESS; use katana_core::service::messaging::MessagingConfig; use katana_node::config::db::DbConfig; -use katana_node::config::dev::{DevConfig, FixedL1GasPriceConfig}; +use katana_node::config::dev::{DevConfig, FixedL1GasPriceConfig, GasPriceWorkerConfig}; use katana_node::config::execution::ExecutionConfig; use katana_node::config::fork::ForkingConfig; use katana_node::config::metrics::MetricsConfig; @@ -22,6 +22,7 @@ use serde::{Deserialize, Serialize}; use tracing::{info, Subscriber}; use tracing_log::LogTracer; use tracing_subscriber::{fmt, EnvFilter}; +use url::Url; use crate::file::NodeArgsConfig; use crate::options::*; @@ -68,6 +69,15 @@ pub struct NodeArgs { #[arg(value_parser = katana_core::service::messaging::MessagingConfig::parse)] pub messaging: Option, + #[arg(long)] + #[arg(conflicts_with = "l1_provider_url")] + #[arg(help = "Disable L1 gas sampling and use hardcoded values.")] + pub no_sampling: bool, + + #[arg(long = "l1.provider", value_name = "URL", alias = "l1-provider")] + #[arg(help = "The Ethereum RPC provider to sample the gas prices from.")] + pub l1_provider_url: Option, + #[command(flatten)] pub logging: LoggingOptions, @@ -170,8 +180,20 @@ impl NodeArgs { let execution = self.execution_config(); let sequencing = self.sequencer_config(); let messaging = self.messaging.clone(); - - Ok(Config { metrics, db, dev, rpc, chain, execution, sequencing, messaging, forking }) + let gas_price_worker = self.gas_price_worker_config(); + + Ok(Config { + metrics, + db, + dev, + rpc, + chain, + execution, + sequencing, + messaging, + forking, + gas_price_worker, + }) } fn sequencer_config(&self) -> SequencingConfig { @@ -258,6 +280,7 @@ impl NodeArgs { fixed_gas_prices, fee: !self.development.no_fee, account_validation: !self.development.no_account_validation, + l1_worker: self.gas_price_worker_config(), } } @@ -357,6 +380,13 @@ impl NodeArgs { Ok(self) } + + fn gas_price_worker_config(&self) -> Option { + self.l1_provider_url.clone().map(|url| GasPriceWorkerConfig { + l1_provider_url: Some(url), + no_sampling: self.no_sampling, + }) + } } #[cfg(test)] diff --git a/crates/katana/core/src/backend/gas_oracle.rs b/crates/katana/core/src/backend/gas_oracle.rs index 1822612380..47d5cbe598 100644 --- a/crates/katana/core/src/backend/gas_oracle.rs +++ b/crates/katana/core/src/backend/gas_oracle.rs @@ -1,25 +1,187 @@ +use std::collections::VecDeque; +use std::fmt::Debug; +use std::future::IntoFuture; +use std::pin::Pin; + +use alloy_provider::{Provider, ProviderBuilder}; +use alloy_rpc_types_eth::BlockNumberOrTag; +use alloy_transport::Transport; +use anyhow::{Context, Ok}; +use futures::Future; use katana_primitives::block::GasPrices; +use tokio::time::Duration; +use url::Url; + +const BUFFER_SIZE: usize = 60; +const INTERVAL: Duration = Duration::from_secs(60); +const ONE_GWEI: u128 = 1_000_000_000; // TODO: implement a proper gas oracle function - sample the l1 gas and data gas prices // currently this just return the hardcoded value set from the cli or if not set, the default value. #[derive(Debug)] -pub struct L1GasOracle { +pub enum L1GasOracle { + Fixed(FixedL1GasOracle), + Sampled(SampledL1GasOracle), +} + +#[derive(Debug)] +pub struct FixedL1GasOracle { gas_prices: GasPrices, data_gas_prices: GasPrices, } +#[derive(Debug, Default)] +pub struct SampledL1GasOracle { + gas_prices: GasPrices, + data_gas_prices: GasPrices, +} + +#[derive(Debug)] +pub struct GasOracleWorker { + pub l1_oracle: SampledL1GasOracle, + pub l1_provider_url: Option, +} + impl L1GasOracle { pub fn fixed(gas_prices: GasPrices, data_gas_prices: GasPrices) -> Self { - Self { gas_prices, data_gas_prices } + L1GasOracle::Fixed(FixedL1GasOracle { gas_prices, data_gas_prices }) + } + + pub fn sampled() -> Self { + L1GasOracle::Sampled(SampledL1GasOracle { + gas_prices: GasPrices::default(), + data_gas_prices: GasPrices::default(), + }) } /// Returns the current gas prices. pub fn current_gas_prices(&self) -> GasPrices { - self.gas_prices.clone() + match self { + L1GasOracle::Fixed(fixed) => fixed.gas_prices.clone(), + L1GasOracle::Sampled(sampled) => sampled.gas_prices.clone(), + } } /// Returns the current data gas prices. + pub fn current_data_gas_prices(&self) -> GasPrices { + match self { + L1GasOracle::Fixed(fixed) => fixed.data_gas_prices.clone(), + L1GasOracle::Sampled(sampled) => sampled.data_gas_prices.clone(), + } + } +} + +impl SampledL1GasOracle { pub fn current_data_gas_prices(&self) -> GasPrices { self.data_gas_prices.clone() } + + pub fn current_gas_prices(&self) -> GasPrices { + self.gas_prices.clone() + } +} + +impl FixedL1GasOracle { + pub fn current_data_gas_prices(&self) -> GasPrices { + self.data_gas_prices.clone() + } + + pub fn current_gas_prices(&self) -> GasPrices { + self.gas_prices.clone() + } +} + +async fn update_gas_price, T: Transport + Clone>( + l1_oracle: &mut SampledL1GasOracle, + provider: P, + buffer: &mut GasPriceBuffer, +) -> anyhow::Result<()> { + // Attempt to get the gas price from L1 + let last_block_number = provider.get_block_number().await?; + let fee_history = + provider.get_fee_history(1, BlockNumberOrTag::Number(last_block_number), &[]).await?; + + let latest_gas_price = fee_history.base_fee_per_gas.last().context("Getting eth gas price")?; + buffer.add_sample(*latest_gas_price); + + let blob_fee_history = fee_history.base_fee_per_blob_gas; + let avg_blob_base_fee = blob_fee_history.iter().last().context("Getting blob gas price")?; + + let avg_blob_fee_eth = *avg_blob_base_fee; + let avg_blob_fee_strk = *avg_blob_base_fee + ONE_GWEI; + + let avg_gas_price = GasPrices { + eth: buffer.average(), + // The price of gas on Starknet is set to the average of the last 60 gas price samples, plus + // 1 gwei. + strk: buffer.average() + ONE_GWEI, + }; + let avg_blob_price = GasPrices { eth: avg_blob_fee_eth, strk: avg_blob_fee_strk }; + + l1_oracle.gas_prices = avg_gas_price; + l1_oracle.data_gas_prices = avg_blob_price; + Ok(()) +} + +impl GasOracleWorker { + pub fn new(l1_provider_url: Option) -> Self { + Self { l1_oracle: SampledL1GasOracle::default(), l1_provider_url } + } + + pub async fn run(&mut self) -> anyhow::Result<()> { + let mut buffer = GasPriceBuffer::new(); + let provider = ProviderBuilder::new() + .on_http(self.l1_provider_url.clone().expect("gas_oracle.rs #133")); + // every 60 seconds, Starknet samples the base price of gas and data gas on L1 + let mut interval = tokio::time::interval(INTERVAL); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + loop { + tokio::select! { + // tick every 60 seconds + _ = interval.tick() => { + if let Err(e) = update_gas_price(&mut self.l1_oracle, provider.clone(), &mut buffer).await { + eprintln!("Error updating gas price: {}", e); + } + } + } + } + } +} + +impl IntoFuture for GasOracleWorker { + type Output = anyhow::Result<()>; + type IntoFuture = Pin + Send>>; + + fn into_future(mut self) -> Self::IntoFuture { + Box::pin(async move { self.run().await }) + } +} + +// Buffer to store the last 60 gas price samples +#[derive(Debug)] +pub struct GasPriceBuffer { + buffer: VecDeque, +} + +impl GasPriceBuffer { + fn new() -> Self { + Self { buffer: VecDeque::with_capacity(BUFFER_SIZE) } + } + + fn add_sample(&mut self, sample: u128) { + if self.buffer.len() == BUFFER_SIZE { + // remove oldest sample if buffer is full + self.buffer.pop_front(); + } + self.buffer.push_back(sample); + } + + fn average(&self) -> u128 { + if self.buffer.is_empty() { + return 0; + } + let sum: u128 = self.buffer.iter().sum(); + sum / self.buffer.len() as u128 + } } diff --git a/crates/katana/core/src/backend/mod.rs b/crates/katana/core/src/backend/mod.rs index 7d8d74a142..83cc49cf2c 100644 --- a/crates/katana/core/src/backend/mod.rs +++ b/crates/katana/core/src/backend/mod.rs @@ -18,6 +18,7 @@ use katana_trie::compute_merkle_root; use parking_lot::RwLock; use starknet::macros::short_string; use starknet_types_core::hash::{self, StarkHash}; + use tracing::info; pub mod contract; @@ -117,6 +118,7 @@ impl Backend { pub fn update_block_gas_prices(&self, block_env: &mut BlockEnv) { block_env.l1_gas_prices = self.gas_oracle.current_gas_prices(); block_env.l1_data_gas_prices = self.gas_oracle.current_data_gas_prices(); + } pub fn mine_empty_block( @@ -278,4 +280,4 @@ where class_trie_root, ]) } -} +} \ No newline at end of file diff --git a/crates/katana/node/src/config/mod.rs b/crates/katana/node/src/config/mod.rs index b79ae25a97..69ac11fe60 100644 --- a/crates/katana/node/src/config/mod.rs +++ b/crates/katana/node/src/config/mod.rs @@ -6,7 +6,7 @@ pub mod metrics; pub mod rpc; use db::DbConfig; -use dev::DevConfig; +use dev::{DevConfig, GasPriceWorkerConfig}; use execution::ExecutionConfig; use fork::ForkingConfig; use katana_core::service::messaging::MessagingConfig; @@ -45,6 +45,9 @@ pub struct Config { /// Development options. pub dev: DevConfig, + + /// Gas L1 sampling options. + pub gas_price_worker: Option, } /// Configurations related to block production. diff --git a/crates/katana/node/src/lib.rs b/crates/katana/node/src/lib.rs index 233aa5d242..7027e36dee 100644 --- a/crates/katana/node/src/lib.rs +++ b/crates/katana/node/src/lib.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Result; +use config::dev::GasPriceWorkerConfig; use config::metrics::MetricsConfig; use config::rpc::{ApiKind, RpcConfig}; use config::{Config, SequencingConfig}; @@ -19,7 +20,7 @@ use hyper::{Method, Uri}; use jsonrpsee::server::middleware::proxy_get_request::ProxyGetRequestLayer; use jsonrpsee::server::{AllowHosts, ServerBuilder, ServerHandle}; use jsonrpsee::RpcModule; -use katana_core::backend::gas_oracle::L1GasOracle; +use katana_core::backend::gas_oracle::{GasOracleWorker, L1GasOracle}; use katana_core::backend::storage::Blockchain; use katana_core::backend::Backend; use katana_core::constants::{ @@ -94,6 +95,7 @@ pub struct Node { pub metrics_config: Option, pub sequencing_config: SequencingConfig, pub messaging_config: Option, + pub gas_price_worker_config: Option, forked_client: Option, } @@ -151,6 +153,22 @@ impl Node { let node_components = (pool, backend, block_producer, validator, self.forked_client.take()); let rpc = spawn(node_components, self.rpc_config.clone()).await?; + // --- build and start the gas oracle worker task + + // if the Option is none, default to no sampling + if !self.gas_price_worker_config.as_ref().map_or(true, |config| config.no_sampling) { + let gas_oracle: GasOracleWorker = GasOracleWorker::new( + self.gas_price_worker_config.clone().expect("lib.src #160").l1_provider_url, + ); + + self.task_manager + .task_spawner() + .build_task() + .graceful_shutdown() + .name("L1 Gas oracle worker") + .spawn(async move { gas_oracle.into_future().await }); + } + Ok(LaunchedNode { node: self, rpc }) } } @@ -206,18 +224,20 @@ pub async fn build(mut config: Config) -> Result { // --- build l1 gas oracle // Check if the user specify a fixed gas price in the dev config. + // cases to cover: + // 1. Fixed price by user + // 2. No fixed price by user and no sampling + // 3. Sampling with user input provider url let gas_oracle = if let Some(fixed_prices) = config.dev.fixed_gas_prices { L1GasOracle::fixed(fixed_prices.gas_price, fixed_prices.data_gas_price) - } - // TODO: for now we just use the default gas prices, but this should be a proper oracle in the - // future that can perform actual sampling. - else { + } else if config.gas_price_worker.as_ref().map_or(false, |worker| worker.no_sampling) { L1GasOracle::fixed( GasPrices { eth: DEFAULT_ETH_L1_GAS_PRICE, strk: DEFAULT_STRK_L1_GAS_PRICE }, GasPrices { eth: DEFAULT_ETH_L1_DATA_GAS_PRICE, strk: DEFAULT_STRK_L1_DATA_GAS_PRICE }, ) + } else { + L1GasOracle::sampled() }; - let block_context_generator = BlockContextGenerator::default().into(); let backend = Arc::new(Backend { gas_oracle, @@ -255,6 +275,7 @@ pub async fn build(mut config: Config) -> Result { messaging_config: config.messaging, sequencing_config: config.sequencing, task_manager: TaskManager::current(), + gas_price_worker_config: config.gas_price_worker, }; Ok(node)