-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(client): l1 gas price #219
Conversation
…ed to state_update.rs
infinite_loop: bool, | ||
) -> anyhow::Result<()> { | ||
let poll_time = eth_client.gas_price_poll_ms.unwrap_or(DEFAULT_GAS_PRICE_POLL_MS); | ||
update_last_update_timestamp().await; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this incorrect? i can see the timestamp is updated inside update_gas_price
and it should only be there from what I understand
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is for the first time, time stamp is defined in the lazy static, and when the gas worker
is called, it will update it for the first time and then for the next time it will get updated in the update_gas_price
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is removed because we are calling the update_gas_price while creating the sync service
); | ||
} | ||
|
||
if !infinite_loop { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the purpose of the infinite_loop
variable was to call this function once at the start of the node without looping to set the initial gas price, I think we should do that as well here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this function would look better if it was split into two:
the loop body is a function, which corresponds to when the current infinite_loop
false when called directly, and a separate function for the infinite task which calls the first function
|
||
let avg_blob_base_fee = blob_fee_history_one_hour.iter().sum::<u128>() / blob_fee_history_one_hour.len() as u128; | ||
|
||
let eth_gas_price = fee_history.base_fee_per_blob_gas.last().context("Setting l1 last confirmed block number")?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let eth_gas_price = fee_history.base_fee_per_blob_gas.last().context("Setting l1 last confirmed block number")?; | |
let eth_gas_price = fee_history.base_fee_gas.last().context("Getting eth gas price")?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
anyway we can ensure this in test cases? I think anvil allows you to set a custom gas price when you start it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah that's true, I think we can achieve this using mocks right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
eth_client.l1_block_metrics.l1_block_number.set(latest_block_number as f64); | ||
eth_client.l1_block_metrics.l1_gas_price_wei.set(eth_gas_price as f64); | ||
|
||
// We're ignoring l1_gas_price_strk |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we import the strk code as well? i remember pragma did a PR. or do we want to do it in another issue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we would need an oracle for that right? I will check for the PR, it can be another PR with the scope of adding oracle
let l1_data_provider: Arc<dyn L1DataProvider> = Arc::new(GasPriceProvider::new()); | ||
|
||
// Run the worker for a short time | ||
let worker_handle = tokio::spawn(gas_price_worker(eth_client, l1_data_provider.clone(), false)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- if
infinite_loop
is false, do we need to spawn tokio? we can just await here right? - we should also write a case for infinite loop true
- how does the current code work without a delay?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- yeah I think that should work, using the await only
- will do, we have that when we are testing for the fail case but will create for this as well
- there is a delay, it's in the eth_client
let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as u128; | ||
|
||
let last_update_timestamp = get_last_update_timestamp().await; | ||
assert!(last_update_timestamp > now - 60, "Last update timestamp should be within the last minute"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the timestamp when the test starts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it get's updated in the gas worker
crates/client/mempool/src/l1.rs
Outdated
#[async_trait::async_trait] | ||
impl L1DataProvider for GasPriceProvider { | ||
async fn get_gas_prices(&self) -> GasPrices { | ||
(*self.gas_prices.lock().await).clone() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's mention that it's 4 u128
so it's ok to clone (unless we think it isn't?)
|
||
/// The L1 rpc endpoint url for state verification. | ||
#[clap(long, value_parser = parse_url, value_name = "ETHEREUM RPC URL")] | ||
pub l1_endpoint: Option<Url>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can enforce in clippy that sync_l1_disabled
false requires l1_endpoint
afaik
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
crates/node/src/service/l1.rs
Outdated
let l1_endpoint = if !config.sync_l1_disabled { | ||
if let Some(l1_rpc_url) = &config.l1_endpoint { | ||
Some(l1_rpc_url.clone()) | ||
} else { | ||
return Err(anyhow::anyhow!( | ||
"❗ No L1 endpoint provided. You must provide one in order to verify the synced state." | ||
)); | ||
} | ||
} else { | ||
None | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let l1_endpoint = if !config.sync_l1_disabled { | |
if let Some(l1_rpc_url) = &config.l1_endpoint { | |
Some(l1_rpc_url.clone()) | |
} else { | |
return Err(anyhow::anyhow!( | |
"❗ No L1 endpoint provided. You must provide one in order to verify the synced state." | |
)); | |
} | |
} else { | |
None | |
}; | |
let l1_endpoint = config.sync_l1_disabled | |
.not() | |
.then_some(config.l1_endpoint.clone() | |
.context("❗ No L1 endpoint provided. You must provide one in order to verify the synced state.")? | |
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
crates/node/src/service/l1.rs
Outdated
}; | ||
|
||
let core_address = Address::from_slice(l1_core_address.as_bytes()); | ||
let eth_client = EthereumClient::new(l1_endpoint.unwrap(), core_address, metrics_handle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let eth_client = EthereumClient::new(l1_endpoint.unwrap(), core_address, metrics_handle) | |
let eth_client = EthereumClient::new(l1_endpoint?, core_address, metrics_handle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if this pull request:
- fetches gas prices everytime a transaction is added to the mempool / a block is produced -- the L1DataProvider functions are async
- or is an infinite task running in the background, watching for l1 gas prices and updating a mutex that is shared with the mempool through the L1DataProvider abstraction -- the L1DataProvider functions are sync
which of the two does this pr implement?
I don't like the first one as:
- the mempool needs L1 prices for mempool validation
- the block production task needs L1 prices to produce blocks
- i don't like the idea of l1 gas prices request being initiated by a add_transaction rpc
- i don't like having the block production be slowed down by l1, as it's potentially time critical
The second approach has a major drawback that i'm realizing now:
- we shouldnt accept any mempool transaction before the first gas prices arrive.
- we cannot produce any block before the first gas prices arrive otherwise we could have a huge problem
I think the easiest way to solve it is:
- have L1DataProvider look like this
pub struct NewBlockL1Info {
pub data_availability_mode: L1DataAvailabilityMode, // blob or calldata
pub gas_prices: GasPrices,
}
trait L1DataProvider {
/// This returns immediately when GasPrices are available,
/// or will wait until the L1 watcher has started up and got the first prices.
async fn get_gas_prices(&self) -> NewBlockL1Info;
}
// tokio watch channel
struct ProvideL1Data(tokio::sync::watch::Receiver<Option<NewBlockL1Info>>);
impl L1DataProvider for ProvideL1Data {
async fn get_gas_prices(&self) -> NewBlockL1Info {
self.0.wait_for(Option::is_some).await.expect("recv error").clone()
}
}
async fn l1_gas_prices_task(set_gas_prices: tokio::sync::watch::Sender<Option<NewBlockL1Info>>) -> anyhow::Result<()> {
loop {
get prices
if set_gas_prices.send(Some(gas_prices)).is_err() {
// channel is closed, meaning no one is listening to gas prices
return Ok(());
}
interval sleep
}
}
use std::time::{SystemTime, UNIX_EPOCH}; | ||
|
||
lazy_static::lazy_static! { | ||
static ref LAST_UPDATE_TIMESTAMP: Arc<Mutex<u128>> = Arc::new(Mutex::new(0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this be inside the service instead of global state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also use a SystemTime directly instead of an untyped integer unix epoch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
crates/node/src/service/l1.rs
Outdated
anyhow::anyhow!("EthereumClient is required to start the l1 sync service but not provided.") | ||
})?; | ||
// running at-least once before the block production service | ||
let _ = futures::executor::block_on(dc_eth::l1_gas_price::gas_price_worker( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is that block_on? this looks suspicious
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is to fetch the gas price before returning the sync service, it make sure we have gas price
|
||
/// Disable the gas price sync service. The sync service is responsible to fetch the fee history from the ethereum. | ||
#[clap(long, alias = "no-gas-price-sync")] | ||
pub gas_price_sync_disabled: bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i like it when everything is configurable thank you :)
crates/node/src/service/sync.rs
Outdated
}; | ||
|
||
let core_address = Address::from_slice(config.network.l1_core_address().as_bytes()); | ||
let eth_client = EthereumClient::new(l1_endpoint.unwrap(), core_address, metrics_handle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when making #221 i found out that you broke sync_l1_disabled
on main in the last pr due to this unwrap - i fixed it there for now but is it fixed in this pr?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked that, and yeah it's resolved in this PR
crates/client/eth/src/sync.rs
Outdated
gas_price_sync_disabled: bool, | ||
gas_price_poll_ms: u64, | ||
) -> anyhow::Result<()> { | ||
let state_update_fut = async { state_update_worker(backend, eth_client, chain_id).await }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this func can be simplified to
tokio::try_join!(
state_update_worker(backend,eth_client, chain_id),
async {
if !gas_price_sync_disabled {
gas_price_worker(eth_client, l1_data_provider, true, gas_price_poll_ms).await?;
}
Ok(())
},
)?;
crates/client/mempool/src/l1.rs
Outdated
pub trait L1DataProvider: Send + Sync { | ||
/// Get L1 data gas prices. This needs an oracle for STRK prices. | ||
fn get_gas_prices(&self) -> GasPrices; | ||
async fn get_gas_prices(&self) -> GasPrices; | ||
async fn set_gas_prices(&self, new_prices: GasPrices); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you remove those setters from the trait? this is an internal impl detail that other crate shouldn't have access
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
crates/client/mempool/src/lib.rs
Outdated
@@ -52,7 +52,7 @@ impl Mempool { | |||
Mempool { backend, l1_data_provider, inner: Default::default() } | |||
} | |||
|
|||
pub fn accept_account_tx( | |||
pub async fn accept_account_tx( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mmh i don't like that being async i need to think about this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
.as_millis(); | ||
|
||
if current_timestamp - last_update_timestamp > 10 * gas_price_poll_ms as u128 { | ||
panic!( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why panic here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in case we are not able to fetch the latest gas prices, it was present in the madara code as well. I believe it exist to make sure we don't have gas fees deviation. but we can have a flag for this as well
); | ||
} | ||
|
||
if !infinite_loop { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this function would look better if it was split into two:
the loop body is a function, which corresponds to when the current infinite_loop
false when called directly, and a separate function for the infinite task which calls the first function
return Ok(()); | ||
} | ||
|
||
sleep(Duration::from_millis(gas_price_poll_ms)).await; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use
let mut interval = tokio::time::interval(pending_block_poll_interval);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
// work
interval.tick().await;
}
instead of sleep for more accurate intervals and tick skipping behavior in case the node is overloaded
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually, infinite loops should look like this:
let mut interval = tokio::time::interval(pending_block_poll_interval);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
while wait_or_graceful_shutdown(interval.tick()).await.is_some() {
to handle graceful shutdown of the node
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved
it implements the second one, the functions are async because reading from mutex is async and hence all the functions inside the block production service which requires the gas fees needs to be async
that's why we are getting the gas price in the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good to me :)
Pull Request type
What is the current behavior?
Resolves: #205
What is the new behavior?
Does this introduce a breaking change?
No
Other Information
In the current scope of the PR, we are not aiming to add oracle to fetch the STRK <-> ETH price for the strk conversion of the fees.
Current Progress