Skip to content

Commit

Permalink
feat(starknet_l1_provider): add the L1GasPriceProvider implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
guy-starkware committed Feb 10, 2025
1 parent 81a1e77 commit 5d496df
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 7 deletions.
5 changes: 5 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/papyrus_base_layer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub trait BaseLayerContract {
}

/// A struct that holds together the data on the base layer's gas prices, for a given timestamp.
#[derive(Clone, Debug)]
pub struct PriceSample {
pub timestamp: u64,
pub base_fee_per_gas: u128,
Expand Down
5 changes: 5 additions & 0 deletions crates/starknet_l1_gas_price/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ repository.workspace = true
license.workspace = true

[dependencies]
papyrus_base_layer.workspace = true
papyrus_config.workspace = true
serde.workspace = true
starknet_api.workspace = true
thiserror.workspace = true
tokio.workspace = true
validator.workspace = true

[lints]
workspace = true
112 changes: 105 additions & 7 deletions crates/starknet_l1_gas_price/src/l1_gas_price_provider.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
use std::collections::VecDeque;

use papyrus_base_layer::PriceSample;
use serde::{Deserialize, Serialize};
use starknet_api::block::{BlockNumber, BlockTimestamp};
use thiserror::Error;
use validator::Validate;

// TODO(guyn): both these constants need to go into VersionedConstants.
pub const MEAN_NUMBER_OF_BLOCKS: u64 = 300;
pub const LAG_MARGIN_SECONDS: u64 = 60;
#[cfg(test)]
#[path = "l1_gas_price_provider_test.rs"]
pub mod l1_gas_price_provider_test;

// TODO(guyn, Gilad): consider moving this to starknet_l1_provider_types/lib.rs?
// This is an interface that allows sharing the provider with the scraper across threads.
pub trait L1GasPriceProviderClient: Send + Sync {
fn add_price_info(
&self,
height: BlockNumber,
timestamp: BlockTimestamp,
gas_price: u128,
data_gas_price: u128,
sample: PriceSample,
) -> Result<(), L1GasPriceProviderError>;

fn get_price_info(
Expand All @@ -25,7 +28,102 @@ pub trait L1GasPriceProviderClient: Send + Sync {
#[derive(Clone, Debug, Error)]
pub enum L1GasPriceProviderError {
#[error("Failed to add price info: {0}")]
AddPriceInfoError(String),
InvalidHeight(String),
#[error("Failed to add price info: {0}")]
MissingData(String),
#[error("Failed to get price info: {0}")]
GetPriceInfoError(String),
}

#[derive(Clone, Debug, Serialize, Deserialize, Validate, PartialEq)]
pub struct L1GasPriceProviderConfig {
// TODO(guyn): these two fields need to go into VersionedConstants.
pub number_of_blocks_for_mean: u64,
pub lag_margin_seconds: u64,
pub storage_limit: usize,
}

impl Default for L1GasPriceProviderConfig {
fn default() -> Self {
const MEAN_NUMBER_OF_BLOCKS: u64 = 300;
Self {
number_of_blocks_for_mean: MEAN_NUMBER_OF_BLOCKS,
lag_margin_seconds: 60,
storage_limit: usize::try_from(10 * MEAN_NUMBER_OF_BLOCKS).unwrap(),
}
}
}

#[derive(Clone, Debug)]
pub struct L1GasPriceProvider {
config: L1GasPriceProviderConfig,
data: VecDeque<(BlockNumber, PriceSample)>,
}

// TODO(guyn): remove the dead code attribute when we use this.
#[allow(dead_code)]
impl L1GasPriceProvider {
pub fn new(config: L1GasPriceProviderConfig) -> Self {
Self { config, data: VecDeque::new() }
}

pub fn add_price_info(
&mut self,
height: BlockNumber,
sample: PriceSample,
) -> Result<(), L1GasPriceProviderError> {
let last_plus_one = self.data.back().map(|(h, _)| h.0 + 1).unwrap_or(0);
if height.0 != last_plus_one {
return Err(L1GasPriceProviderError::InvalidHeight(format!(
"Block height is not consecutive: expected {}, got {}",
last_plus_one, height.0
)));
}
self.data.push_back((height, sample));
if self.data.len() > self.config.storage_limit {
self.data.pop_front();
}
Ok(())
}

pub fn get_price_info(
&self,
timestamp: BlockTimestamp,
) -> Result<(u128, u128), L1GasPriceProviderError> {
let mut gas_price = 0;
let mut data_gas_price = 0;

// This index is for the last block in the mean (inclusive).
let index_last_timestamp_rev = self.data.iter().rev().position(|(_, sample)| {
sample.timestamp <= timestamp.0 - self.config.lag_margin_seconds
});

// Could not find a block with the requested timestamp and lag.
let Some(last_index_rev) = index_last_timestamp_rev else {
return Err(L1GasPriceProviderError::MissingData(format!(
"No block price data from time {} - {}s",
timestamp.0, self.config.lag_margin_seconds
)));
};
// We need to convert the index to the forward direction.
let last_index = self.data.len() - last_index_rev;

let num_blocks = usize::try_from(self.config.number_of_blocks_for_mean)
.expect("number_of_blocks_for_mean is too large to fit into a usize");
if last_index < num_blocks {
return Err(L1GasPriceProviderError::MissingData(format!(
"Insufficient block price history: expected at least {}, found only {}",
num_blocks, last_index
)));
}
// Go over all elements between last_index-num_blocks to last_index (non-inclusive).
for (_height, sample) in self.data.iter().skip(last_index - num_blocks).take(num_blocks) {
gas_price += sample.base_fee_per_gas;
data_gas_price += sample.blob_fee;
}
Ok((
gas_price / u128::from(self.config.number_of_blocks_for_mean),
data_gas_price / u128::from(self.config.number_of_blocks_for_mean),
))
}
}
76 changes: 76 additions & 0 deletions crates/starknet_l1_gas_price/src/l1_gas_price_provider_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use papyrus_base_layer::PriceSample;
use starknet_api::block::{BlockNumber, BlockTimestamp};

use crate::l1_gas_price_provider::{L1GasPriceProvider, L1GasPriceProviderConfig};

// Make a provider with five samples.
// Timestamps are 2 seconds per block, from zero to 8.
// To average over the middle 3 samples, we should use timestamps 2, 4, and 6.
// To get the timestamp at the end of the range, we need to ask for timestamp
// of 6 + 60 (which is the lag margin).
// Returns the provider, the final timestamp, and the lag margin.
fn make_provider_with_few_samples() -> (L1GasPriceProvider, u64, u64) {
let mut provider = L1GasPriceProvider::new(L1GasPriceProviderConfig {
number_of_blocks_for_mean: 3,
..Default::default()
});

for i in 0..5 {
let block_num = i.try_into().unwrap();
let price = i.try_into().unwrap();
let time = (i * 2).try_into().unwrap();
let sample = PriceSample { timestamp: time, base_fee_per_gas: price, blob_fee: price + 1 };
provider.add_price_info(BlockNumber(block_num), sample).unwrap();
}
let lag = provider.config.lag_margin_seconds;
let final_timestamp = 6;
(provider, final_timestamp, lag)
}

#[test]
fn gas_price_provider_mean_prices() {
let (provider, final_timestamp, lag) = make_provider_with_few_samples();

// This calculation will grab 3 samples from the middle of the range.
let (gas_price, data_gas_price) =
provider.get_price_info(BlockTimestamp(final_timestamp + lag)).unwrap();
// The gas prices (set arbitrarily to equal the block number) should go from
let gas_price_calculation = (1 + 2 + 3) / 3;
// The data gas is one more than the gas price.
let data_gas_calculation = gas_price_calculation + 1;
assert_eq!(gas_price, gas_price_calculation);
assert_eq!(data_gas_price, data_gas_calculation);
}

#[test]
fn gas_price_provider_adding_samples() {
let (mut provider, final_timestamp, lag) = make_provider_with_few_samples();

let (gas_price, data_gas_price) =
provider.get_price_info(BlockTimestamp(final_timestamp + lag)).unwrap();

// Add a block to the provider.
let sample = PriceSample { timestamp: 10, base_fee_per_gas: 10, blob_fee: 11 };
provider.add_price_info(BlockNumber(5), sample).unwrap();

let (gas_price_new, data_gas_price_new) =
provider.get_price_info(BlockTimestamp(final_timestamp + lag)).unwrap();

// This should not change the results if we ask for the same timestamp.
assert_eq!(gas_price, gas_price_new);
assert_eq!(data_gas_price, data_gas_price_new);
}

#[test]
fn gas_price_provider_timestamp_changes_mean() {
let (provider, final_timestamp, lag) = make_provider_with_few_samples();

let (gas_price, data_gas_price) =
provider.get_price_info(BlockTimestamp(final_timestamp + lag)).unwrap();

// If we take a higher timestamp the gas prices should go up.
let (gas_price_new, data_gas_price_new) =
provider.get_price_info(BlockTimestamp(final_timestamp + lag + 2)).unwrap();
assert!(gas_price_new > gas_price);
assert!(data_gas_price_new > data_gas_price);
}

0 comments on commit 5d496df

Please sign in to comment.