Skip to content

Commit

Permalink
feat(starknet_l1_provider): add L1 gas price scraper
Browse files Browse the repository at this point in the history
  • Loading branch information
guy-starkware committed Feb 9, 2025
1 parent 28a2f0c commit 88a3d8b
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/starknet_l1_gas_price/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ repository.workspace = true
license.workspace = true

[dependencies]
async-trait.workspace = true
papyrus_config.workspace = true
papyrus_base_layer.workspace = true
serde.workspace = true
starknet_api.workspace = true
starknet_sequencer_infra.workspace = true
thiserror.workspace = true
tokio.workspace = true
tracing.workspace = true
validator.workspace = true

[lints]
Expand Down
146 changes: 146 additions & 0 deletions crates/starknet_l1_gas_price/src/l1_gas_price_scraper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use std::any::type_name;
use std::cmp::max;
use std::sync::Arc;
use std::time::Duration;

use async_trait::async_trait;
use papyrus_base_layer::BaseLayerContract;
use papyrus_config::converters::deserialize_float_seconds_to_duration;
use papyrus_config::validators::validate_ascii;
use serde::{Deserialize, Serialize};
use starknet_api::block::{BlockNumber, BlockTimestamp};
use starknet_api::core::ChainId;
use starknet_sequencer_infra::component_client::ClientError;
use starknet_sequencer_infra::component_definitions::ComponentStarter;
use starknet_sequencer_infra::errors::ComponentError;
use thiserror::Error;
use tracing::{error, info};
use validator::Validate;

use crate::l1_gas_price_provider::{L1GasPriceProviderClient, L1GasPriceProviderError};

#[cfg(test)]
#[path = "l1_gas_price_scraper_test.rs"]
pub mod l1_gas_price_scraper_test;

type L1GasPriceScraperResult<T, B> = Result<T, L1GasPriceScraperError<B>>;
pub type SharedL1GasPriceProvider = Arc<dyn L1GasPriceProviderClient>;

// TODO(guyn): find a way to synchronize the value of number_of_blocks_for_mean
// with the one in L1GasPriceProviderConfig. In the end they should not be config
// items but values drawn from VersionedConstants.
#[derive(Clone, Debug, Serialize, Deserialize, Validate, PartialEq)]
pub struct L1GasPriceScraperConfig {
pub l1_block_to_start_scraping_from: u64,
#[validate(custom = "validate_ascii")]
pub chain_id: ChainId,
pub finality: u64,
#[serde(deserialize_with = "deserialize_float_seconds_to_duration")]
pub polling_interval: Duration,
pub number_of_blocks_for_mean: u64,
}

impl Default for L1GasPriceScraperConfig {
fn default() -> Self {
Self {
l1_block_to_start_scraping_from: 0,
chain_id: ChainId::Mainnet,
finality: 0,
polling_interval: Duration::from_secs(1),
number_of_blocks_for_mean: 300,
}
}
}
pub struct L1GasPriceScraper<B: BaseLayerContract> {
pub config: L1GasPriceScraperConfig,
pub base_layer: B,
pub next_block_number_to_fetch: u64,
pub l1_gas_price_provider: SharedL1GasPriceProvider,
}

impl<B: BaseLayerContract + Send + Sync> L1GasPriceScraper<B> {
pub fn new(
config: L1GasPriceScraperConfig,
l1_gas_price_provider: SharedL1GasPriceProvider,
base_layer: B,
) -> Self {
Self {
l1_gas_price_provider,
base_layer,
next_block_number_to_fetch: config.l1_block_to_start_scraping_from,
config,
}
}

async fn run(&mut self) -> L1GasPriceScraperResult<(), B> {
loop {
self.update_prices().await?;
tokio::time::sleep(self.config.polling_interval).await;
}
}

async fn update_prices(&mut self) -> L1GasPriceScraperResult<(), B> {
let latest_l1_block_number = self
.base_layer
.latest_l1_block_number(self.config.finality)
.await
.map_err(L1GasPriceScraperError::BaseLayerError)?;

if let Some(latest_l1_block_number) = latest_l1_block_number {
if self.next_block_number_to_fetch > latest_l1_block_number {
// We are already up to date.
return Ok(());
}
// Choose the oldest block we need to fetch.
// It is either next_block_number_to_fetch, or the current head of the chain,
// minus 2 * MEAN_NUMBER_OF_BLOCKS. Note that this minus can be less than zero
// for short chains, hence the saturating_sub.
let oldest_l1_block_number = max(
self.next_block_number_to_fetch,
latest_l1_block_number.saturating_sub(2 * self.config.number_of_blocks_for_mean),
);
for block_number in oldest_l1_block_number..=latest_l1_block_number {
if let Some(sample) = self
.base_layer
.get_price_sample(block_number)
.await
.map_err(L1GasPriceScraperError::BaseLayerError)?
{
self.l1_gas_price_provider
.add_price_info(
BlockNumber(block_number),
BlockTimestamp(sample.timestamp),
sample.base_fee_per_gas,
sample.blob_fee,
)
.map_err(L1GasPriceScraperError::GasPriceProviderError)?;

self.next_block_number_to_fetch = latest_l1_block_number + 1;
}
}
} else {
error!("Failed to get latest L1 block number, finality too high.");
}

Ok(())
}
}

#[async_trait]
impl<B: BaseLayerContract + Send + Sync> ComponentStarter for L1GasPriceScraper<B> {
async fn start(&mut self) -> Result<(), ComponentError> {
info!("Starting component {}.", type_name::<Self>());
self.run().await.map_err(|_| ComponentError::InternalComponentError)
}
}

#[derive(Error, Debug)]
pub enum L1GasPriceScraperError<T: BaseLayerContract + Send + Sync> {
#[error("Base layer error: {0}")]
BaseLayerError(T::Error),
#[error("Could not update gas price provider: {0}")]
GasPriceProviderError(L1GasPriceProviderError),
// Leaky abstraction, these errors should not propagate here.
#[error(transparent)]
NetworkError(ClientError),
}
208 changes: 208 additions & 0 deletions crates/starknet_l1_gas_price/src/l1_gas_price_scraper_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
use std::ops::RangeInclusive;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use async_trait::async_trait;
use papyrus_base_layer::{
BaseLayerContract,
L1BlockNumber,
L1BlockReference,
L1Event,
PriceSample,
};
use starknet_api::block::{BlockHashAndNumber, BlockNumber, BlockTimestamp};

use crate::l1_gas_price_provider::{L1GasPriceProviderClient, L1GasPriceProviderError};
use crate::l1_gas_price_scraper::{L1GasPriceScraper, L1GasPriceScraperConfig};

const BLOCK_TIME: u64 = 2;
const GAS_PRICE: u128 = 42;
const DATA_PRICE: u128 = 137;

#[derive(thiserror::Error, Debug)]
pub enum FakeBaseLayerError {}

#[derive(Debug, Clone)]
struct FakeInternalBaseLayerData {
time_between_blocks: u64,
gas_price: u128,
data_price: u128,
last_block_num: L1BlockNumber,
}

impl Default for FakeInternalBaseLayerData {
fn default() -> Self {
Self {
time_between_blocks: BLOCK_TIME,
gas_price: GAS_PRICE,
data_price: DATA_PRICE,
last_block_num: 0,
}
}
}

#[derive(Debug, Clone)]
struct FakeBaseLayerContract {
data: Arc<Mutex<FakeInternalBaseLayerData>>,
}

impl Default for FakeBaseLayerContract {
fn default() -> Self {
Self { data: Arc::new(Mutex::new(FakeInternalBaseLayerData::default())) }
}
}

#[async_trait]
impl BaseLayerContract for FakeBaseLayerContract {
type Error = FakeBaseLayerError;
async fn get_price_sample(
&self,
block_num: L1BlockNumber,
) -> Result<Option<PriceSample>, Self::Error> {
let data = self.data.lock().unwrap();
Ok(Some(PriceSample {
timestamp: block_num * data.time_between_blocks,
base_fee_per_gas: data.gas_price,
blob_fee: data.data_price,
}))
}

async fn latest_l1_block_number(&self, _: u64) -> Result<Option<L1BlockNumber>, Self::Error> {
Ok(Some(self.data.lock().unwrap().last_block_num))
}

async fn get_proved_block_at(
&self,
_: L1BlockNumber,
) -> Result<BlockHashAndNumber, Self::Error> {
todo!();
}

async fn latest_proved_block(&self, _: u64) -> Result<Option<BlockHashAndNumber>, Self::Error> {
todo!();
}

async fn latest_l1_block(&self, _: u64) -> Result<Option<L1BlockReference>, Self::Error> {
todo!();
}

async fn l1_block_at(&self, _: L1BlockNumber) -> Result<Option<L1BlockReference>, Self::Error> {
todo!();
}

/// Get specific events from the Starknet base contract between two L1 block numbers.
async fn events(
&self,
_: RangeInclusive<L1BlockNumber>,
_: &[&str],
) -> Result<Vec<L1Event>, Self::Error> {
todo!();
}
}

#[allow(clippy::type_complexity)]
#[derive(Debug, Default)]
struct FakeL1GasPriceProvider {
data: Arc<Mutex<Vec<(BlockNumber, BlockTimestamp, u128, u128)>>>,
}

impl L1GasPriceProviderClient for FakeL1GasPriceProvider {
fn add_price_info(
&self,
height: BlockNumber,
timestamp: BlockTimestamp,
gas_price: u128,
data_gas_price: u128,
) -> Result<(), L1GasPriceProviderError> {
self.data.lock().unwrap().push((height, timestamp, gas_price, data_gas_price));
Ok(())
}

fn get_price_info(
&self,
timestamp: BlockTimestamp,
) -> Result<(u128, u128), L1GasPriceProviderError> {
let vector = self.data.lock().unwrap();
let index = vector.iter().position(|(_, ts, _, _)| ts.0 >= timestamp.0).unwrap();
Ok((vector[index].2, vector[index].3))
}
}

#[tokio::test]
#[allow(clippy::as_conversions)]
async fn run_l1_gas_price_scraper() {
let fake_contract = FakeBaseLayerContract::default();
let fake_provider = Arc::new(FakeL1GasPriceProvider::default());

let mut scraper = L1GasPriceScraper::new(
L1GasPriceScraperConfig {
polling_interval: Duration::from_millis(1),
..Default::default()
},
fake_provider.clone(),
fake_contract.clone(),
);

// Run the scraper as a separate task in the background.
let _future_handle = tokio::spawn(async move {
scraper.run().await.unwrap();
});

// Let the scraper have some time to work.
tokio::time::sleep(Duration::from_millis(50)).await;

// There is only block zero on the contract.
assert_eq!(fake_provider.data.lock().unwrap().len(), 1);
{
let data = fake_provider.data.lock().unwrap();
assert_eq!(data[0].0.0, 0);
assert_eq!(data[0].1.0, 0);
assert_eq!(data[0].2, GAS_PRICE);
assert_eq!(data[0].3, DATA_PRICE);
} // Inner scope ends here to release the lock.

// Add a few blocks to the contract.
let number = 10;
{
fake_contract.data.lock().unwrap().last_block_num = number;
} // Inner scope ends here to release the lock.

// Let the scraper have some time to work.
tokio::time::sleep(Duration::from_millis(50)).await;

// The provider should have received the blocks.
{
let data = fake_provider.data.lock().unwrap();
let block_numbers = data.iter().map(|(x, _, _, _)| x.0).collect::<Vec<u64>>();
let timestamps = data.iter().map(|(_, x, _, _)| x.0).collect::<Vec<u64>>();
let gas_prices = data.iter().map(|(_, _, x, _)| *x).collect::<Vec<u128>>();
let data_prices = data.iter().map(|(_, _, _, x)| *x).collect::<Vec<u128>>();

assert_eq!(block_numbers, (0..=number).collect::<Vec<u64>>());
assert_eq!(timestamps, (0..=number).map(|x| x * BLOCK_TIME).collect::<Vec<u64>>());
assert_eq!(gas_prices, vec![GAS_PRICE; number as usize + 1]);
assert_eq!(data_prices, vec![DATA_PRICE; number as usize + 1]);
} // Inner scope ends here to release the lock.

// Change the pricing and add one more block.
{
let mut contract_data = fake_contract.data.lock().unwrap();
contract_data.gas_price = 100;
contract_data.data_price = 200;
contract_data.last_block_num = number + 1;
} // Inner scope ends here to release the lock.

// Let the scraper have some time to work.
tokio::time::sleep(Duration::from_millis(50)).await;

// The provider should have received the new block.
{
let data = fake_provider.data.lock().unwrap();
let block_numbers = data.iter().map(|(x, _, _, _)| x.0).collect::<Vec<u64>>();
let last_timestamp = data[(number + 1) as usize].1.0;
assert_eq!(block_numbers, (0..=number + 1).collect::<Vec<u64>>());
assert_eq!(last_timestamp, (number + 1) * BLOCK_TIME);
assert_eq!(data[(number + 1) as usize].2, 100);
assert_eq!(data[(number + 1) as usize].3, 200);
} // Inner scope ends here to release the lock. (in case the test is extended later)
}
1 change: 1 addition & 0 deletions crates/starknet_l1_gas_price/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod l1_gas_price_provider;
pub mod l1_gas_price_scraper;

0 comments on commit 88a3d8b

Please sign in to comment.