From e3f78042b93b25d609e5767e2ba76502ede84415 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Fri, 9 Aug 2024 12:04:40 +0200 Subject: [PATCH] feat: Allow tracking l2 fees for L2-based chains (#2563) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ In this PR we add changes to the GasAdjuster needed to track the fees on L2-based chains. The PR aims to preserve type-safety for places that should work when L1 interface is available only, so `DynClient` is left in a lot of places and generally `L1` is the default network to be used. However, some methods were extended to work with any network. Overall what was added: - A new resource for accessing L2-specific endpoints. - A new field is returned from `fee_history`. Clients should not rely on this field. However, we added it so we could test things. - Some refactoring - While some new new config fields are added into the Rust code, they are not added to the config so that in case something has to be changed, we do not introduce legacy (esp. in protobuf) - Into protobuf we only added `RelayedL2Calldata` pubdata type, which would symbolize that the calldata is being relayed to L1 through another L2. I am pretty confident that this type will be needed anyway. ## Why ❔ ## Checklist - [ ] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [ ] Tests for the changes have been added / updated. - [ ] Documentation comments have been added / updated. - [ ] Code has been formatted via `zk fmt` and `zk lint`. --- core/bin/block_reverter/src/main.rs | 4 +- core/bin/external_node/src/node_builder.rs | 2 + core/bin/external_node/src/tests/framework.rs | 8 +- core/bin/external_node/src/tests/utils.rs | 4 +- core/bin/zksync_server/src/node_builder.rs | 13 +- core/lib/basic_types/src/lib.rs | 1 + core/lib/basic_types/src/settlement.rs | 16 ++ core/lib/config/src/configs/eth_sender.rs | 8 +- core/lib/config/src/testonly.rs | 2 + ...7517f5d61f7f1472e7d152f575ffd56ad8633.json | 47 ++++ ...534014b9ab9ca7725e14fb17aa050d9f35eb8.json | 23 -- core/lib/dal/src/blocks_web3_dal.rs | 32 ++- core/lib/env_config/src/eth_sender.rs | 1 + core/lib/eth_client/src/clients/http/decl.rs | 4 +- core/lib/eth_client/src/clients/http/mod.rs | 1 + core/lib/eth_client/src/clients/http/query.rs | 249 ++++++++++++----- .../eth_client/src/clients/http/signing.rs | 6 +- core/lib/eth_client/src/clients/mock.rs | 261 ++++++++++++------ core/lib/eth_client/src/clients/mod.rs | 2 +- core/lib/eth_client/src/lib.rs | 31 ++- core/lib/eth_client/src/types.rs | 7 +- .../structures/commit_batch_info.rs | 10 +- core/lib/protobuf_config/src/eth.rs | 4 + .../src/proto/config/eth_sender.proto | 1 + core/lib/types/src/api/mod.rs | 11 + core/lib/types/src/pubdata_da.rs | 3 + core/lib/web3_decl/src/client/boxed.rs | 4 +- core/lib/web3_decl/src/client/mock.rs | 4 +- core/lib/web3_decl/src/client/mod.rs | 6 +- core/lib/web3_decl/src/client/network.rs | 16 +- core/lib/web3_decl/src/namespaces/debug.rs | 6 +- core/lib/web3_decl/src/namespaces/en.rs | 6 +- core/lib/web3_decl/src/namespaces/eth.rs | 13 +- core/lib/web3_decl/src/namespaces/net.rs | 6 +- .../lib/web3_decl/src/namespaces/snapshots.rs | 6 +- core/lib/web3_decl/src/namespaces/unstable.rs | 6 +- core/lib/web3_decl/src/namespaces/web3.rs | 6 +- core/lib/web3_decl/src/namespaces/zks.rs | 6 +- .../web3/backend_jsonrpsee/namespaces/eth.rs | 6 +- .../api_server/src/web3/namespaces/eth.rs | 25 +- core/node/block_reverter/src/lib.rs | 9 +- .../src/validation_task.rs | 12 +- core/node/consistency_checker/src/lib.rs | 2 +- .../node/consistency_checker/src/tests/mod.rs | 10 +- .../eth_sender/src/abstract_l1_interface.rs | 9 +- core/node/eth_sender/src/eth_tx_aggregator.rs | 2 +- core/node/eth_sender/src/tester.rs | 17 +- core/node/eth_watch/src/client.rs | 2 +- .../src/l1_gas_price/gas_adjuster/metrics.rs | 1 + .../src/l1_gas_price/gas_adjuster/mod.rs | 140 +++++++--- .../src/l1_gas_price/gas_adjuster/tests.rs | 163 ++++++++--- core/node/fee_model/src/l1_gas_price/mod.rs | 5 +- core/node/fee_model/src/lib.rs | 14 +- .../implementations/layers/gas_adjuster.rs | 13 +- .../layers/query_eth_client.rs | 53 +++- .../resources/eth_interface.rs | 16 +- core/node/node_framework/src/service/mod.rs | 2 +- .../src/tree_data_fetcher/provider/mod.rs | 12 +- core/node/state_keeper/src/io/tests/tester.rs | 15 +- .../src/account/tx_command_executor.rs | 1 - core/tests/loadnext/src/executor.rs | 2 +- core/tests/loadnext/src/sdk/ethereum/mod.rs | 2 +- .../src/sdk/operations/deploy_contract.rs | 3 +- 63 files changed, 959 insertions(+), 413 deletions(-) create mode 100644 core/lib/basic_types/src/settlement.rs create mode 100644 core/lib/dal/.sqlx/query-1bfcc02ac79958dcbd20f3680df7517f5d61f7f1472e7d152f575ffd56ad8633.json delete mode 100644 core/lib/dal/.sqlx/query-83a931ceddf34e1c760649d613f534014b9ab9ca7725e14fb17aa050d9f35eb8.json diff --git a/core/bin/block_reverter/src/main.rs b/core/bin/block_reverter/src/main.rs index f7a9ca9f9def..65810a6e9b67 100644 --- a/core/bin/block_reverter/src/main.rs +++ b/core/bin/block_reverter/src/main.rs @@ -8,7 +8,7 @@ use tokio::{ }; use zksync_block_reverter::{ eth_client::{ - clients::{Client, PKSigningClient}, + clients::{Client, PKSigningClient, L1}, EthInterface, }, BlockReverter, BlockReverterEthConfig, NodeRole, @@ -251,7 +251,7 @@ async fn main() -> anyhow::Result<()> { json, operator_address, } => { - let eth_client = Client::http(l1_secrets.l1_rpc_url.clone()) + let eth_client = Client::::http(l1_secrets.l1_rpc_url.clone()) .context("Ethereum client")? .build(); diff --git a/core/bin/external_node/src/node_builder.rs b/core/bin/external_node/src/node_builder.rs index 2234e64482db..1a7991b48a71 100644 --- a/core/bin/external_node/src/node_builder.rs +++ b/core/bin/external_node/src/node_builder.rs @@ -169,6 +169,8 @@ impl ExternalNodeBuilder { let query_eth_client_layer = QueryEthClientLayer::new( self.config.required.settlement_layer_id(), self.config.required.eth_client_url.clone(), + // TODO(EVM-676): add this config for external node + Default::default(), ); self.node.add_layer(query_eth_client_layer); Ok(self) diff --git a/core/bin/external_node/src/tests/framework.rs b/core/bin/external_node/src/tests/framework.rs index 71a6afe503a7..e9667f2c05db 100644 --- a/core/bin/external_node/src/tests/framework.rs +++ b/core/bin/external_node/src/tests/framework.rs @@ -127,7 +127,13 @@ impl WiringLayer for MockL1ClientLayer { fn layer_name(&self) -> &'static str { // We don't care about values, we just want to hijack the layer name. - QueryEthClientLayer::new(SLChainId(1), "https://example.com".parse().unwrap()).layer_name() + // TODO(EVM-676): configure the `settlement_mode` here + QueryEthClientLayer::new( + SLChainId(1), + "https://example.com".parse().unwrap(), + Default::default(), + ) + .layer_name() } async fn wire(self, _: Self::Input) -> Result { diff --git a/core/bin/external_node/src/tests/utils.rs b/core/bin/external_node/src/tests/utils.rs index 3784fea4763b..ee92a4b802a6 100644 --- a/core/bin/external_node/src/tests/utils.rs +++ b/core/bin/external_node/src/tests/utils.rs @@ -1,7 +1,7 @@ use tempfile::TempDir; use zksync_dal::CoreDal; use zksync_db_connection::connection_pool::TestTemplate; -use zksync_eth_client::clients::MockEthereum; +use zksync_eth_client::clients::MockSettlementLayer; use zksync_node_genesis::{insert_genesis_batch, GenesisBatchParams, GenesisParams}; use zksync_types::{ api, block::L2BlockHeader, ethabi, Address, L2BlockNumber, ProtocolVersionId, H256, @@ -119,7 +119,7 @@ pub(super) fn expected_health_components(components: &ComponentsToRun) -> Vec<&' } pub(super) fn mock_eth_client(diamond_proxy_addr: Address) -> MockClient { - let mock = MockEthereum::builder().with_call_handler(move |call, _| { + let mock = MockSettlementLayer::builder().with_call_handler(move |call, _| { tracing::info!("L1 call: {call:?}"); if call.to == Some(diamond_proxy_addr) { let packed_semver = ProtocolVersionId::latest().into_packed_semver_with_patch(0); diff --git a/core/bin/zksync_server/src/node_builder.rs b/core/bin/zksync_server/src/node_builder.rs index 648050a08ebc..9fafacb7055e 100644 --- a/core/bin/zksync_server/src/node_builder.rs +++ b/core/bin/zksync_server/src/node_builder.rs @@ -67,7 +67,7 @@ use zksync_node_framework::{ }, service::{ZkStackService, ZkStackServiceBuilder}, }; -use zksync_types::SHARED_BRIDGE_ETHER_TOKEN_ADDRESS; +use zksync_types::{settlement::SettlementMode, SHARED_BRIDGE_ETHER_TOKEN_ADDRESS}; use zksync_vlog::prometheus::PrometheusExporterConfig; /// Macro that looks into a path to fetch an optional config, @@ -153,8 +153,15 @@ impl MainNodeBuilder { fn add_query_eth_client_layer(mut self) -> anyhow::Result { let genesis = self.genesis_config.clone(); let eth_config = try_load_config!(self.secrets.l1); - let query_eth_client_layer = - QueryEthClientLayer::new(genesis.settlement_layer_id(), eth_config.l1_rpc_url); + let query_eth_client_layer = QueryEthClientLayer::new( + genesis.settlement_layer_id(), + eth_config.l1_rpc_url, + self.configs + .eth + .as_ref() + .and_then(|x| Some(x.gas_adjuster?.settlement_mode)) + .unwrap_or(SettlementMode::SettlesToL1), + ); self.node.add_layer(query_eth_client_layer); Ok(self) } diff --git a/core/lib/basic_types/src/lib.rs b/core/lib/basic_types/src/lib.rs index a9522407222c..a25f740ed444 100644 --- a/core/lib/basic_types/src/lib.rs +++ b/core/lib/basic_types/src/lib.rs @@ -26,6 +26,7 @@ pub mod commitment; pub mod network; pub mod protocol_version; pub mod prover_dal; +pub mod settlement; pub mod tee_types; pub mod url; pub mod vm_version; diff --git a/core/lib/basic_types/src/settlement.rs b/core/lib/basic_types/src/settlement.rs new file mode 100644 index 000000000000..4fd921957a23 --- /dev/null +++ b/core/lib/basic_types/src/settlement.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +/// An enum which is used to describe whether a zkSync network settles to L1 or to the gateway. +/// Gateway is an Ethereum-compatible L2 and so it requires different treatment with regards to DA handling. +#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum SettlementMode { + #[default] + SettlesToL1, + Gateway, +} + +impl SettlementMode { + pub fn is_gateway(self) -> bool { + matches!(self, Self::Gateway) + } +} diff --git a/core/lib/config/src/configs/eth_sender.rs b/core/lib/config/src/configs/eth_sender.rs index c0e14dd68a87..e932cd9819b9 100644 --- a/core/lib/config/src/configs/eth_sender.rs +++ b/core/lib/config/src/configs/eth_sender.rs @@ -2,7 +2,7 @@ use std::time::Duration; use anyhow::Context as _; use serde::Deserialize; -use zksync_basic_types::H256; +use zksync_basic_types::{settlement::SettlementMode, H256}; use zksync_crypto_primitives::K256PrivateKey; use crate::EthWatchConfig; @@ -54,6 +54,7 @@ impl EthConfig { num_samples_for_blob_base_fee_estimate: 10, internal_pubdata_pricing_multiplier: 1.0, max_blob_base_fee: None, + settlement_mode: Default::default(), }), watcher: Some(EthWatchConfig { confirmations_for_eth_event: None, @@ -82,6 +83,7 @@ pub enum PubdataSendingMode { Calldata, Blobs, Custom, + RelayedL2Calldata, } #[derive(Debug, Deserialize, Clone, PartialEq)] @@ -181,6 +183,10 @@ pub struct GasAdjusterConfig { pub internal_pubdata_pricing_multiplier: f64, /// Max blob base fee that is allowed to be used. pub max_blob_base_fee: Option, + /// Whether the gas adjuster should require that the L2 node is used as a settlement layer. + /// It offers a runtime check for correctly provided values. + #[serde(default)] + pub settlement_mode: SettlementMode, } impl GasAdjusterConfig { diff --git a/core/lib/config/src/testonly.rs b/core/lib/config/src/testonly.rs index 2d6f02e1f6c5..6cf512ec5d10 100644 --- a/core/lib/config/src/testonly.rs +++ b/core/lib/config/src/testonly.rs @@ -397,6 +397,8 @@ impl Distribution for EncodeDist { num_samples_for_blob_base_fee_estimate: self.sample(rng), internal_pubdata_pricing_multiplier: self.sample(rng), max_blob_base_fee: self.sample(rng), + // TODO(EVM-676): generate it randomly once this value is used + settlement_mode: Default::default(), } } } diff --git a/core/lib/dal/.sqlx/query-1bfcc02ac79958dcbd20f3680df7517f5d61f7f1472e7d152f575ffd56ad8633.json b/core/lib/dal/.sqlx/query-1bfcc02ac79958dcbd20f3680df7517f5d61f7f1472e7d152f575ffd56ad8633.json new file mode 100644 index 000000000000..8ae53dc7e6f4 --- /dev/null +++ b/core/lib/dal/.sqlx/query-1bfcc02ac79958dcbd20f3680df7517f5d61f7f1472e7d152f575ffd56ad8633.json @@ -0,0 +1,47 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n base_fee_per_gas,\n l2_fair_gas_price,\n fair_pubdata_price,\n protocol_version,\n l1_gas_price\n FROM\n miniblocks\n WHERE\n number <= $1\n ORDER BY\n number DESC\n LIMIT\n $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "base_fee_per_gas", + "type_info": "Numeric" + }, + { + "ordinal": 1, + "name": "l2_fair_gas_price", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "fair_pubdata_price", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "protocol_version", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "l1_gas_price", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + true, + true, + false + ] + }, + "hash": "1bfcc02ac79958dcbd20f3680df7517f5d61f7f1472e7d152f575ffd56ad8633" +} diff --git a/core/lib/dal/.sqlx/query-83a931ceddf34e1c760649d613f534014b9ab9ca7725e14fb17aa050d9f35eb8.json b/core/lib/dal/.sqlx/query-83a931ceddf34e1c760649d613f534014b9ab9ca7725e14fb17aa050d9f35eb8.json deleted file mode 100644 index 8d9458dce0a4..000000000000 --- a/core/lib/dal/.sqlx/query-83a931ceddf34e1c760649d613f534014b9ab9ca7725e14fb17aa050d9f35eb8.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n base_fee_per_gas\n FROM\n miniblocks\n WHERE\n number <= $1\n ORDER BY\n number DESC\n LIMIT\n $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "base_fee_per_gas", - "type_info": "Numeric" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "83a931ceddf34e1c760649d613f534014b9ab9ca7725e14fb17aa050d9f35eb8" -} diff --git a/core/lib/dal/src/blocks_web3_dal.rs b/core/lib/dal/src/blocks_web3_dal.rs index 2957701f9e23..13fa9070f828 100644 --- a/core/lib/dal/src/blocks_web3_dal.rs +++ b/core/lib/dal/src/blocks_web3_dal.rs @@ -5,6 +5,7 @@ use zksync_db_connection::{ use zksync_system_constants::EMPTY_UNCLES_HASH; use zksync_types::{ api, + fee_model::BatchFeeInput, l2_to_l1_log::L2ToL1Log, vm_trace::Call, web3::{BlockHeader, Bytes}, @@ -564,17 +565,21 @@ impl BlocksWeb3Dal<'_, '_> { .collect()) } - /// Returns `base_fee_per_gas` for L2 block range [min(newest_block - block_count + 1, 0), newest_block] + /// Returns `base_fee_per_gas` and `fair_pubdata_price` for L2 block range [min(newest_block - block_count + 1, 0), newest_block] /// in descending order of L2 block numbers. pub async fn get_fee_history( &mut self, newest_block: L2BlockNumber, block_count: u64, - ) -> DalResult> { + ) -> DalResult<(Vec, Vec)> { let result: Vec<_> = sqlx::query!( r#" SELECT - base_fee_per_gas + base_fee_per_gas, + l2_fair_gas_price, + fair_pubdata_price, + protocol_version, + l1_gas_price FROM miniblocks WHERE @@ -593,10 +598,27 @@ impl BlocksWeb3Dal<'_, '_> { .fetch_all(self.storage) .await? .into_iter() - .map(|row| bigdecimal_to_u256(row.base_fee_per_gas)) + .map(|row| { + let fee_input = BatchFeeInput::for_protocol_version( + row.protocol_version + .map(|x| (x as u16).try_into().unwrap()) + .unwrap_or_else(ProtocolVersionId::last_potentially_undefined), + row.l2_fair_gas_price as u64, + row.fair_pubdata_price.map(|x| x as u64), + row.l1_gas_price as u64, + ); + + ( + bigdecimal_to_u256(row.base_fee_per_gas), + U256::from(fee_input.fair_pubdata_price()), + ) + }) .collect(); - Ok(result) + let (base_fee_per_gas, effective_pubdata_price): (Vec, Vec) = + result.into_iter().unzip(); + + Ok((base_fee_per_gas, effective_pubdata_price)) } pub async fn get_block_details( diff --git a/core/lib/env_config/src/eth_sender.rs b/core/lib/env_config/src/eth_sender.rs index bd48f80609e8..18a661099b61 100644 --- a/core/lib/env_config/src/eth_sender.rs +++ b/core/lib/env_config/src/eth_sender.rs @@ -84,6 +84,7 @@ mod tests { num_samples_for_blob_base_fee_estimate: 10, internal_pubdata_pricing_multiplier: 1.0, max_blob_base_fee: None, + settlement_mode: Default::default(), }), watcher: Some(EthWatchConfig { confirmations_for_eth_event: Some(0), diff --git a/core/lib/eth_client/src/clients/http/decl.rs b/core/lib/eth_client/src/clients/http/decl.rs index 77cc1c841e1b..3805bc5e7df5 100644 --- a/core/lib/eth_client/src/clients/http/decl.rs +++ b/core/lib/eth_client/src/clients/http/decl.rs @@ -1,9 +1,9 @@ use jsonrpsee::proc_macros::rpc; use zksync_types::{web3, Address, H256, U256, U64}; -use zksync_web3_decl::client::{ForNetwork, L1}; +use zksync_web3_decl::client::ForWeb3Network; /// Subset of the L1 `eth` namespace used by the L1 client. -#[rpc(client, namespace = "eth", client_bounds(Self: ForNetwork))] +#[rpc(client, namespace = "eth", client_bounds(Self: ForWeb3Network))] pub(super) trait L1EthNamespace { #[method(name = "chainId")] async fn chain_id(&self) -> RpcResult; diff --git a/core/lib/eth_client/src/clients/http/mod.rs b/core/lib/eth_client/src/clients/http/mod.rs index 2d1ed244afd4..111507c65f05 100644 --- a/core/lib/eth_client/src/clients/http/mod.rs +++ b/core/lib/eth_client/src/clients/http/mod.rs @@ -32,6 +32,7 @@ enum Method { #[metrics(name = "sign_prepared_tx_for_addr")] SignPreparedTx, Allowance, + L2FeeHistory, } #[derive(Debug, Metrics)] diff --git a/core/lib/eth_client/src/clients/http/query.rs b/core/lib/eth_client/src/clients/http/query.rs index 65387ff00779..54419f3b5626 100644 --- a/core/lib/eth_client/src/clients/http/query.rs +++ b/core/lib/eth_client/src/clients/http/query.rs @@ -3,14 +3,20 @@ use std::fmt; use async_trait::async_trait; use jsonrpsee::core::ClientError; use zksync_types::{web3, Address, SLChainId, H256, U256, U64}; -use zksync_web3_decl::error::{ClientRpcContext, EnrichedClientError, EnrichedClientResult}; +use zksync_web3_decl::{ + client::{DynClient, ForWeb3Network, MockClient, L1, L2}, + error::{ClientRpcContext, EnrichedClientError, EnrichedClientResult}, + namespaces::EthNamespaceClient, +}; use super::{decl::L1EthNamespaceClient, Method, COUNTERS, LATENCIES}; use crate::{ types::{ExecutedTxStatus, FailureInfo}, - BaseFees, EthInterface, RawTransactionBytes, + BaseFees, EthFeeInterface, EthInterface, RawTransactionBytes, }; +const FEE_HISTORY_MAX_REQUEST_CHUNK: usize = 1024; + #[async_trait] impl EthInterface for T where @@ -74,73 +80,6 @@ where Ok(tx) } - async fn base_fee_history( - &self, - upto_block: usize, - block_count: usize, - ) -> EnrichedClientResult> { - // Non-panicking conversion to u64. - fn cast_to_u64(value: U256, tag: &str) -> EnrichedClientResult { - u64::try_from(value).map_err(|_| { - let err = ClientError::Custom(format!("{tag} value does not fit in u64")); - EnrichedClientError::new(err, "cast_to_u64").with_arg("value", &value) - }) - } - - const MAX_REQUEST_CHUNK: usize = 1024; - - COUNTERS.call[&(Method::BaseFeeHistory, self.component())].inc(); - let latency = LATENCIES.direct[&Method::BaseFeeHistory].start(); - let mut history = Vec::with_capacity(block_count); - let from_block = upto_block.saturating_sub(block_count); - - // Here we are requesting `fee_history` from blocks - // `(from_block; upto_block)` in chunks of size `MAX_REQUEST_CHUNK` - // starting from the oldest block. - for chunk_start in (from_block..=upto_block).step_by(MAX_REQUEST_CHUNK) { - let chunk_end = (chunk_start + MAX_REQUEST_CHUNK).min(upto_block); - let chunk_size = chunk_end - chunk_start; - - let fee_history = self - .fee_history( - U64::from(chunk_size), - web3::BlockNumber::from(chunk_end), - None, - ) - .rpc_context("fee_history") - .with_arg("chunk_size", &chunk_size) - .with_arg("block", &chunk_end) - .await?; - - // Check that the lengths are the same. - // Per specification, the values should always be provided, and must be 0 for blocks - // prior to EIP-4844. - // https://ethereum.github.io/execution-apis/api-documentation/ - if fee_history.base_fee_per_gas.len() != fee_history.base_fee_per_blob_gas.len() { - tracing::error!( - "base_fee_per_gas and base_fee_per_blob_gas have different lengths: {} and {}", - fee_history.base_fee_per_gas.len(), - fee_history.base_fee_per_blob_gas.len() - ); - } - - for (base, blob) in fee_history - .base_fee_per_gas - .into_iter() - .zip(fee_history.base_fee_per_blob_gas) - { - let fees = BaseFees { - base_fee_per_gas: cast_to_u64(base, "base_fee_per_gas")?, - base_fee_per_blob_gas: blob, - }; - history.push(fees) - } - } - - latency.observe(); - Ok(history) - } - async fn get_pending_block_base_fee_per_gas(&self) -> EnrichedClientResult { COUNTERS.call[&(Method::PendingBlockBaseFee, self.component())].inc(); let latency = LATENCIES.direct[&Method::PendingBlockBaseFee].start(); @@ -354,6 +293,178 @@ where } } +async fn l1_base_fee_history( + client: &T, + upto_block: usize, + block_count: usize, +) -> EnrichedClientResult> +where + T: ForWeb3Network + L1EthNamespaceClient + Send + Sync, +{ + COUNTERS.call[&(Method::BaseFeeHistory, client.component())].inc(); + let latency = LATENCIES.direct[&Method::BaseFeeHistory].start(); + let mut history = Vec::with_capacity(block_count); + let from_block = upto_block.saturating_sub(block_count); + + // Here we are requesting `fee_history` from blocks + // `(from_block; upto_block)` in chunks of size `MAX_REQUEST_CHUNK` + // starting from the oldest block. + for chunk_start in (from_block..=upto_block).step_by(FEE_HISTORY_MAX_REQUEST_CHUNK) { + let chunk_end = (chunk_start + FEE_HISTORY_MAX_REQUEST_CHUNK).min(upto_block); + let chunk_size = chunk_end - chunk_start; + + let fee_history = client + .fee_history( + U64::from(chunk_size), + web3::BlockNumber::from(chunk_end), + None, + ) + .rpc_context("fee_history") + .with_arg("chunk_size", &chunk_size) + .with_arg("block", &chunk_end) + .await?; + + // Check that the lengths are the same. + // Per specification, the values should always be provided, and must be 0 for blocks + // prior to EIP-4844. + // https://ethereum.github.io/execution-apis/api-documentation/ + if fee_history.base_fee_per_gas.len() != fee_history.base_fee_per_blob_gas.len() { + tracing::error!( + "base_fee_per_gas and base_fee_per_blob_gas have different lengths: {} and {}", + fee_history.base_fee_per_gas.len(), + fee_history.base_fee_per_blob_gas.len() + ); + } + + for (base, blob) in fee_history + .base_fee_per_gas + .into_iter() + .zip(fee_history.base_fee_per_blob_gas) + { + let fees = BaseFees { + base_fee_per_gas: cast_to_u64(base, "base_fee_per_gas")?, + base_fee_per_blob_gas: blob, + l2_pubdata_price: 0.into(), + }; + history.push(fees) + } + } + + latency.observe(); + Ok(history) +} + +#[async_trait::async_trait] +impl EthFeeInterface for Box> { + async fn base_fee_history( + &self, + upto_block: usize, + block_count: usize, + ) -> EnrichedClientResult> { + l1_base_fee_history(self, upto_block, block_count).await + } +} + +#[async_trait::async_trait] +impl EthFeeInterface for MockClient { + async fn base_fee_history( + &self, + upto_block: usize, + block_count: usize, + ) -> EnrichedClientResult> { + l1_base_fee_history(self, upto_block, block_count).await + } +} + +async fn l2_base_fee_history( + client: &T, + upto_block: usize, + block_count: usize, +) -> EnrichedClientResult> +where + T: ForWeb3Network + EthNamespaceClient + Send + Sync, +{ + COUNTERS.call[&(Method::L2FeeHistory, client.component())].inc(); + let latency = LATENCIES.direct[&Method::BaseFeeHistory].start(); + let mut history = Vec::with_capacity(block_count); + let from_block = upto_block.saturating_sub(block_count); + + // Here we are requesting `fee_history` from blocks + // `(from_block; upto_block)` in chunks of size `FEE_HISTORY_MAX_REQUEST_CHUNK` + // starting from the oldest block. + for chunk_start in (from_block..=upto_block).step_by(FEE_HISTORY_MAX_REQUEST_CHUNK) { + let chunk_end = (chunk_start + FEE_HISTORY_MAX_REQUEST_CHUNK).min(upto_block); + let chunk_size = chunk_end - chunk_start; + + let fee_history = EthNamespaceClient::fee_history( + client, + U64::from(chunk_size), + zksync_types::api::BlockNumber::from(chunk_end), + vec![], + ) + .rpc_context("fee_history") + .with_arg("chunk_size", &chunk_size) + .with_arg("block", &chunk_end) + .await?; + + // Check that the lengths are the same. + if fee_history.inner.base_fee_per_gas.len() != fee_history.l2_pubdata_price.len() { + tracing::error!( + "base_fee_per_gas and pubdata_price have different lengths: {} and {}", + fee_history.inner.base_fee_per_gas.len(), + fee_history.l2_pubdata_price.len() + ); + } + + for (base, l2_pubdata_price) in fee_history + .inner + .base_fee_per_gas + .into_iter() + .zip(fee_history.l2_pubdata_price) + { + let fees = BaseFees { + base_fee_per_gas: cast_to_u64(base, "base_fee_per_gas")?, + base_fee_per_blob_gas: 0.into(), + l2_pubdata_price, + }; + history.push(fees) + } + } + + latency.observe(); + Ok(history) +} + +#[async_trait::async_trait] +impl EthFeeInterface for Box> { + async fn base_fee_history( + &self, + upto_block: usize, + block_count: usize, + ) -> EnrichedClientResult> { + l2_base_fee_history(self, upto_block, block_count).await + } +} + +#[async_trait::async_trait] +impl EthFeeInterface for MockClient { + async fn base_fee_history( + &self, + upto_block: usize, + block_count: usize, + ) -> EnrichedClientResult> { + l2_base_fee_history(self, upto_block, block_count).await + } +} + +/// Non-panicking conversion to u64. +fn cast_to_u64(value: U256, tag: &str) -> EnrichedClientResult { + u64::try_from(value).map_err(|_| { + let err = ClientError::Custom(format!("{tag} value does not fit in u64")); + EnrichedClientError::new(err, "cast_to_u64").with_arg("value", &value) + }) +} + #[cfg(test)] mod tests { use zksync_web3_decl::client::{Client, L1}; diff --git a/core/lib/eth_client/src/clients/http/signing.rs b/core/lib/eth_client/src/clients/http/signing.rs index 542b42420ae3..e602f98a35e9 100644 --- a/core/lib/eth_client/src/clients/http/signing.rs +++ b/core/lib/eth_client/src/clients/http/signing.rs @@ -74,9 +74,9 @@ impl fmt::Debug for SigningClient { } } -impl AsRef> for SigningClient { - fn as_ref(&self) -> &DynClient { - self.query_client.as_ref() +impl AsRef for SigningClient { + fn as_ref(&self) -> &(dyn EthInterface + 'static) { + &self.query_client } } diff --git a/core/lib/eth_client/src/clients/mock.rs b/core/lib/eth_client/src/clients/mock.rs index d2d6b2108f53..46ad5dc5310e 100644 --- a/core/lib/eth_client/src/clients/mock.rs +++ b/core/lib/eth_client/src/clients/mock.rs @@ -1,20 +1,22 @@ use std::{ collections::{BTreeMap, HashMap}, fmt, + marker::PhantomData, sync::{Arc, RwLock, RwLockWriteGuard}, }; use jsonrpsee::{core::ClientError, types::ErrorObject}; use zksync_types::{ + api::FeeHistory, ethabi, web3::{self, contract::Tokenize, BlockId}, - Address, SLChainId, EIP_4844_TX_TYPE, H160, H256, U256, U64, + Address, L1ChainId, L2ChainId, SLChainId, EIP_4844_TX_TYPE, H160, H256, U256, U64, }; -use zksync_web3_decl::client::{DynClient, MockClient, L1}; +use zksync_web3_decl::client::{MockClient, MockClientBuilder, Network, L1, L2}; use crate::{ types::{ContractCallError, SignedCallResult, SigningError}, - BaseFees, BoundEthInterface, Options, RawTransactionBytes, + BaseFees, BoundEthInterface, EthInterface, Options, RawTransactionBytes, }; #[derive(Debug, Clone)] @@ -82,9 +84,9 @@ struct MockExecutedTx { success: bool, } -/// Mutable part of [`MockEthereum`] that needs to be synchronized via an `RwLock`. +/// Mutable part of [`MockSettlementLayer`] that needs to be synchronized via an `RwLock`. #[derive(Debug, Default)] -struct MockEthereumInner { +struct MockSettlementLayerInner { block_number: u64, executed_txs: HashMap, sent_txs: HashMap, @@ -93,7 +95,7 @@ struct MockEthereumInner { nonces: BTreeMap, } -impl MockEthereumInner { +impl MockSettlementLayerInner { fn execute_tx( &mut self, tx_hash: H256, @@ -131,7 +133,7 @@ impl MockEthereumInner { } fn get_transaction_count(&self, address: Address, block: web3::BlockNumber) -> U256 { - if address != MockEthereum::SENDER_ACCOUNT { + if address != MOCK_SENDER_ACCOUNT { unimplemented!("Getting nonce for custom account is not supported"); } @@ -206,7 +208,7 @@ impl MockEthereumInner { #[derive(Debug)] pub struct MockExecutedTxHandle<'a> { - inner: RwLockWriteGuard<'a, MockEthereumInner>, + inner: RwLockWriteGuard<'a, MockSettlementLayerInner>, tx_hash: H256, } @@ -221,22 +223,27 @@ impl MockExecutedTxHandle<'_> { type CallHandler = dyn Fn(&web3::CallRequest, BlockId) -> Result + Send + Sync; -/// Builder for [`MockEthereum`] client. -pub struct MockEthereumBuilder { +pub trait SupportedMockSLNetwork: Network { + fn build_client(builder: MockSettlementLayerBuilder) -> MockClient; +} + +/// Builder for [`MockSettlementLayer`] client. +pub struct MockSettlementLayerBuilder { max_fee_per_gas: U256, max_priority_fee_per_gas: U256, base_fee_history: Vec, /// If true, the mock will not check the ordering nonces of the transactions. /// This is useful for testing the cases when the transactions are executed out of order. non_ordering_confirmations: bool, - inner: Arc>, + inner: Arc>, call_handler: Box, + _network: PhantomData, } -impl fmt::Debug for MockEthereumBuilder { +impl fmt::Debug for MockSettlementLayerBuilder { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter - .debug_struct("MockEthereumBuilder") + .debug_struct("MockSettlementLayerBuilder") .field("max_fee_per_gas", &self.max_fee_per_gas) .field("max_priority_fee_per_gas", &self.max_priority_fee_per_gas) .field("base_fee_history", &self.base_fee_history) @@ -249,7 +256,7 @@ impl fmt::Debug for MockEthereumBuilder { } } -impl Default for MockEthereumBuilder { +impl Default for MockSettlementLayerBuilder { fn default() -> Self { Self { max_fee_per_gas: 100.into(), @@ -260,11 +267,12 @@ impl Default for MockEthereumBuilder { call_handler: Box::new(|call, block_id| { panic!("Unexpected eth_call: {call:?}, {block_id:?}"); }), + _network: PhantomData, } } } -impl MockEthereumBuilder { +impl MockSettlementLayerBuilder { /// Sets fee history for each block in the mocked Ethereum network, starting from the 0th block. pub fn with_fee_history(self, history: Vec) -> Self { Self { @@ -327,14 +335,11 @@ impl MockEthereumBuilder { }) } - fn build_client(self) -> MockClient { - const CHAIN_ID: SLChainId = SLChainId(9); - - let base_fee_history = self.base_fee_history.clone(); + fn build_client_inner(self, chaind_id: u64, network: Net) -> MockClientBuilder { let call_handler = self.call_handler; - MockClient::builder(CHAIN_ID.into()) - .method("eth_chainId", || Ok(U64::from(CHAIN_ID.0))) + MockClient::builder(network) + .method("eth_chainId", move || Ok(U64::from(chaind_id))) .method("eth_blockNumber", { let inner = self.inner.clone(); move || Ok(U64::from(inner.read().unwrap().block_number)) @@ -355,30 +360,6 @@ impl MockEthereumBuilder { } }) .method("eth_gasPrice", move || Ok(self.max_fee_per_gas)) - .method( - "eth_feeHistory", - move |block_count: U64, newest_block: web3::BlockNumber, _: Option>| { - let web3::BlockNumber::Number(from_block) = newest_block else { - panic!("Non-numeric newest block in `eth_feeHistory`"); - }; - let from_block = from_block.as_usize(); - let start_block = from_block.saturating_sub(block_count.as_usize() - 1); - Ok(web3::FeeHistory { - oldest_block: start_block.into(), - base_fee_per_gas: base_fee_history[start_block..=from_block] - .iter() - .map(|fee| U256::from(fee.base_fee_per_gas)) - .collect(), - base_fee_per_blob_gas: base_fee_history[start_block..=from_block] - .iter() - .map(|fee| fee.base_fee_per_blob_gas) - .collect(), - gas_used_ratio: vec![], // not used - blob_gas_used_ratio: vec![], // not used - reward: None, - }) - }, - ) .method("eth_call", { let inner = self.inner.clone(); move |req, block| { @@ -410,43 +391,120 @@ impl MockEthereumBuilder { Ok(status.map(|status| status.receipt.clone())) } }) - .build() } - /// Builds a mock Ethereum client. - pub fn build(self) -> MockEthereum { - MockEthereum { + pub fn build(self) -> MockSettlementLayer { + MockSettlementLayer { max_fee_per_gas: self.max_fee_per_gas, max_priority_fee_per_gas: self.max_priority_fee_per_gas, non_ordering_confirmations: self.non_ordering_confirmations, inner: self.inner.clone(), - client: self.build_client(), + client: Net::build_client(self), } } } -/// Mock Ethereum client. +fn l2_eth_fee_history( + base_fee_history: &[BaseFees], + block_count: U64, + newest_block: web3::BlockNumber, +) -> FeeHistory { + let web3::BlockNumber::Number(from_block) = newest_block else { + panic!("Non-numeric newest block in `eth_feeHistory`"); + }; + let from_block = from_block.as_usize(); + let start_block = from_block.saturating_sub(block_count.as_usize() - 1); + + FeeHistory { + inner: web3::FeeHistory { + oldest_block: start_block.into(), + base_fee_per_gas: base_fee_history[start_block..=from_block] + .iter() + .map(|fee| U256::from(fee.base_fee_per_gas)) + .collect(), + base_fee_per_blob_gas: base_fee_history[start_block..=from_block] + .iter() + .map(|fee| fee.base_fee_per_blob_gas) + .collect(), + gas_used_ratio: vec![], // not used + blob_gas_used_ratio: vec![], // not used + reward: None, + }, + l2_pubdata_price: base_fee_history[start_block..=from_block] + .iter() + .map(|fee| fee.l2_pubdata_price) + .collect(), + } +} + +impl SupportedMockSLNetwork for L1 { + fn build_client(builder: MockSettlementLayerBuilder) -> MockClient { + const CHAIN_ID: L1ChainId = L1ChainId(9); + + let base_fee_history = builder.base_fee_history.clone(); + + builder + .build_client_inner(CHAIN_ID.0, CHAIN_ID.into()) + .method( + "eth_feeHistory", + move |block_count: U64, newest_block: web3::BlockNumber, _: Option>| { + Ok(l2_eth_fee_history(&base_fee_history, block_count, newest_block).inner) + }, + ) + .build() + } +} + +impl SupportedMockSLNetwork for L2 { + fn build_client(builder: MockSettlementLayerBuilder) -> MockClient { + let chain_id: L2ChainId = 9u64.try_into().unwrap(); + + let base_fee_history = builder.base_fee_history.clone(); + + builder + .build_client_inner(chain_id.as_u64(), chain_id.into()) + .method( + "eth_feeHistory", + move |block_count: U64, newest_block: web3::BlockNumber, _: Option>| { + Ok(l2_eth_fee_history( + &base_fee_history, + block_count, + newest_block, + )) + }, + ) + .build() + } +} + +/// Mock settlement layer client. #[derive(Debug, Clone)] -pub struct MockEthereum { +pub struct MockSettlementLayer { max_fee_per_gas: U256, max_priority_fee_per_gas: U256, non_ordering_confirmations: bool, - inner: Arc>, - client: MockClient, + inner: Arc>, + client: MockClient, +} + +impl Default for MockSettlementLayer { + fn default() -> Self { + Self::builder().build() + } } -impl Default for MockEthereum { +impl Default for MockSettlementLayer { fn default() -> Self { Self::builder().build() } } -impl MockEthereum { - const SENDER_ACCOUNT: Address = Address::repeat_byte(0x11); +const MOCK_SENDER_ACCOUNT: Address = Address::repeat_byte(0x11); - /// Initializes a builder for a [`MockEthereum`] instance. - pub fn builder() -> MockEthereumBuilder { - MockEthereumBuilder::default() +impl MockSettlementLayer { + /// Initializes a builder for a [`MockSettlementLayer`] instance. + pub fn builder() -> MockSettlementLayerBuilder { + MockSettlementLayerBuilder::default() } /// A fake `sha256` hasher, which calculates an `std::hash` instead. @@ -524,19 +582,21 @@ impl MockEthereum { } /// Converts this client into an immutable / contract-agnostic client. - pub fn into_client(self) -> MockClient { + pub fn into_client(self) -> MockClient { self.client } } -impl AsRef> for MockEthereum { - fn as_ref(&self) -> &DynClient { +impl AsRef for MockSettlementLayer { + fn as_ref(&self) -> &(dyn EthInterface + 'static) { &self.client } } #[async_trait::async_trait] -impl BoundEthInterface for MockEthereum { +impl BoundEthInterface + for MockSettlementLayer +{ fn clone_boxed(&self) -> Box { Box::new(self.clone()) } @@ -558,7 +618,7 @@ impl BoundEthInterface for MockEthereum { } fn sender_account(&self) -> Address { - Self::SENDER_ACCOUNT + MOCK_SENDER_ACCOUNT } async fn sign_prepared_tx_for_addr( @@ -586,24 +646,25 @@ mod tests { use zksync_types::{commitment::L1BatchCommitmentMode, ProtocolVersionId}; use super::*; - use crate::{CallFunctionArgs, EthInterface}; + use crate::{CallFunctionArgs, EthFeeInterface, EthInterface}; - fn base_fees(block: u64, blob: u64) -> BaseFees { + fn base_fees(block: u64, blob: u64, pubdata_price: u64) -> BaseFees { BaseFees { base_fee_per_gas: block, base_fee_per_blob_gas: U256::from(blob), + l2_pubdata_price: U256::from(pubdata_price), } } #[tokio::test] async fn managing_block_number() { - let mock = MockEthereum::builder() + let mock = MockSettlementLayer::::builder() .with_fee_history(vec![ - base_fees(0, 4), - base_fees(1, 3), - base_fees(2, 2), - base_fees(3, 1), - base_fees(4, 0), + base_fees(0, 4, 0), + base_fees(1, 3, 0), + base_fees(2, 2, 0), + base_fees(3, 1, 0), + base_fees(4, 0, 0), ]) .build(); let block_number = mock.client.block_number().await.unwrap(); @@ -628,7 +689,7 @@ mod tests { #[tokio::test] async fn getting_chain_id() { - let mock = MockEthereum::builder().build(); + let mock = MockSettlementLayer::::builder().build(); let chain_id = mock.client.fetch_chain_id().await.unwrap(); assert_eq!(chain_id, SLChainId(9)); } @@ -636,28 +697,50 @@ mod tests { #[tokio::test] async fn managing_fee_history() { let initial_fee_history = vec![ - base_fees(1, 4), - base_fees(2, 3), - base_fees(3, 2), - base_fees(4, 1), - base_fees(5, 0), + base_fees(1, 4, 0), + base_fees(2, 3, 0), + base_fees(3, 2, 0), + base_fees(4, 1, 0), + base_fees(5, 0, 0), + ]; + let client = MockSettlementLayer::::builder() + .with_fee_history(initial_fee_history.clone()) + .build(); + client.advance_block_number(4); + + let fee_history = client.client.base_fee_history(4, 4).await.unwrap(); + assert_eq!(fee_history, initial_fee_history[1..=4]); + let fee_history = client.client.base_fee_history(2, 2).await.unwrap(); + assert_eq!(fee_history, initial_fee_history[1..=2]); + let fee_history = client.client.base_fee_history(3, 2).await.unwrap(); + assert_eq!(fee_history, initial_fee_history[2..=3]); + } + + #[tokio::test] + async fn managing_fee_history_l2() { + let initial_fee_history = vec![ + base_fees(1, 0, 11), + base_fees(2, 0, 12), + base_fees(3, 0, 13), + base_fees(4, 0, 14), + base_fees(5, 0, 15), ]; - let client = MockEthereum::builder() + let client = MockSettlementLayer::::builder() .with_fee_history(initial_fee_history.clone()) .build(); client.advance_block_number(4); - let fee_history = client.as_ref().base_fee_history(4, 4).await.unwrap(); - assert_eq!(fee_history, &initial_fee_history[1..=4]); - let fee_history = client.as_ref().base_fee_history(2, 2).await.unwrap(); - assert_eq!(fee_history, &initial_fee_history[1..=2]); - let fee_history = client.as_ref().base_fee_history(3, 2).await.unwrap(); - assert_eq!(fee_history, &initial_fee_history[2..=3]); + let fee_history = client.client.base_fee_history(4, 4).await.unwrap(); + assert_eq!(fee_history, initial_fee_history[1..=4]); + let fee_history = client.client.base_fee_history(2, 2).await.unwrap(); + assert_eq!(fee_history, initial_fee_history[1..=2]); + let fee_history = client.client.base_fee_history(3, 2).await.unwrap(); + assert_eq!(fee_history, initial_fee_history[2..=3]); } #[tokio::test] async fn managing_transactions() { - let client = MockEthereum::builder() + let client = MockSettlementLayer::::builder() .with_non_ordering_confirmation(true) .build(); client.advance_block_number(2); @@ -710,7 +793,7 @@ mod tests { #[tokio::test] async fn calling_contracts() { - let client = MockEthereum::builder() + let client = MockSettlementLayer::::builder() .with_call_handler(|req, _block_id| { let packed_semver = ProtocolVersionId::latest().into_packed_semver_with_patch(0); let call_signature = &req.data.as_ref().unwrap().0[..4]; @@ -759,7 +842,7 @@ mod tests { #[tokio::test] async fn getting_transaction_failure_reason() { - let client = MockEthereum::default(); + let client = MockSettlementLayer::::default(); let signed_tx = client .sign_prepared_tx( vec![1, 2, 3], diff --git a/core/lib/eth_client/src/clients/mod.rs b/core/lib/eth_client/src/clients/mod.rs index 05b7f852f391..b08f92115492 100644 --- a/core/lib/eth_client/src/clients/mod.rs +++ b/core/lib/eth_client/src/clients/mod.rs @@ -7,5 +7,5 @@ pub use zksync_web3_decl::client::{Client, DynClient, L1}; pub use self::{ http::{PKSigningClient, SigningClient}, - mock::{MockEthereum, MockEthereumBuilder}, + mock::{MockSettlementLayer, MockSettlementLayerBuilder}, }; diff --git a/core/lib/eth_client/src/lib.rs b/core/lib/eth_client/src/lib.rs index 3e8641845c61..64fa30481118 100644 --- a/core/lib/eth_client/src/lib.rs +++ b/core/lib/eth_client/src/lib.rs @@ -10,7 +10,6 @@ use zksync_types::{ }, Address, SLChainId, H160, H256, U256, U64, }; -use zksync_web3_decl::client::{DynClient, L1}; pub use zksync_web3_decl::{ error::{EnrichedClientError, EnrichedClientResult}, jsonrpsee::core::ClientError, @@ -69,7 +68,10 @@ impl Options { #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct BaseFees { pub base_fee_per_gas: u64, + // Base fee per blob gas. It is zero on networks that do not support blob transactions (e.g. L2s). pub base_fee_per_blob_gas: U256, + // The price (in wei) for relaying the pubdata to L1. It is non-zero only for L2 settlement layers. + pub l2_pubdata_price: U256, } /// Common Web3 interface, as seen by the core applications. @@ -83,7 +85,7 @@ pub struct BaseFees { /// If you want to add a method to this trait, make sure that it doesn't depend on any particular /// contract or account address. For that, you can use the `BoundEthInterface` trait. #[async_trait] -pub trait EthInterface: Sync + Send { +pub trait EthInterface: Sync + Send + fmt::Debug { /// Fetches the L1 chain ID (in contrast to [`BoundEthInterface::chain_id()`] which returns /// the *expected* L1 chain ID). async fn fetch_chain_id(&self) -> EnrichedClientResult; @@ -95,16 +97,6 @@ pub trait EthInterface: Sync + Send { block: BlockNumber, ) -> EnrichedClientResult; - /// Collects the base fee history for the specified block range. - /// - /// Returns 1 value for each block in range, assuming that these blocks exist. - /// Will return an error if the `from_block + block_count` is beyond the head block. - async fn base_fee_history( - &self, - from_block: usize, - block_count: usize, - ) -> EnrichedClientResult>; - /// Returns the `base_fee_per_gas` value for the currently pending L1 block. async fn get_pending_block_base_fee_per_gas(&self) -> EnrichedClientResult; @@ -154,6 +146,19 @@ pub trait EthInterface: Sync + Send { async fn block(&self, block_id: BlockId) -> EnrichedClientResult>>; } +#[async_trait::async_trait] +pub trait EthFeeInterface: EthInterface { + /// Collects the base fee history for the specified block range. + /// + /// Returns 1 value for each block in range, assuming that these blocks exist. + /// Will return an error if the `from_block + block_count` is beyond the head block. + async fn base_fee_history( + &self, + from_block: usize, + block_count: usize, + ) -> EnrichedClientResult>; +} + /// An extension of `EthInterface` trait, which is used to perform queries that are bound to /// a certain contract and account. /// @@ -168,7 +173,7 @@ pub trait EthInterface: Sync + Send { /// 2. Consider adding the "unbound" version to the `EthInterface` trait and create a default method /// implementation that invokes `contract` / `contract_addr` / `sender_account` methods. #[async_trait] -pub trait BoundEthInterface: AsRef> + 'static + Sync + Send + fmt::Debug { +pub trait BoundEthInterface: 'static + Sync + Send + fmt::Debug + AsRef { /// Clones this client. fn clone_boxed(&self) -> Box; diff --git a/core/lib/eth_client/src/types.rs b/core/lib/eth_client/src/types.rs index 8ac5ff427fb8..59fb1cdeddcc 100644 --- a/core/lib/eth_client/src/types.rs +++ b/core/lib/eth_client/src/types.rs @@ -8,10 +8,7 @@ use zksync_types::{ }, Address, EIP_4844_TX_TYPE, H256, U256, }; -use zksync_web3_decl::{ - client::{DynClient, L1}, - error::EnrichedClientError, -}; +use zksync_web3_decl::error::EnrichedClientError; use crate::EthInterface; @@ -81,7 +78,7 @@ impl ContractCall<'_> { pub async fn call( &self, - client: &DynClient, + client: &dyn EthInterface, ) -> Result { let func = self .contract_abi diff --git a/core/lib/l1_contract_interface/src/i_executor/structures/commit_batch_info.rs b/core/lib/l1_contract_interface/src/i_executor/structures/commit_batch_info.rs index b5d77ff60c16..179c04748d3b 100644 --- a/core/lib/l1_contract_interface/src/i_executor/structures/commit_batch_info.rs +++ b/core/lib/l1_contract_interface/src/i_executor/structures/commit_batch_info.rs @@ -202,7 +202,10 @@ impl Tokenizable for CommitBatchInfo<'_> { } else { tokens.push(Token::Bytes(match (self.mode, self.pubdata_da) { // Here we're not pushing any pubdata on purpose; no pubdata is sent in Validium mode. - (L1BatchCommitmentMode::Validium, PubdataDA::Calldata) => { + ( + L1BatchCommitmentMode::Validium, + PubdataDA::Calldata | PubdataDA::RelayedL2Calldata, + ) => { vec![PUBDATA_SOURCE_CALLDATA] } (L1BatchCommitmentMode::Validium, PubdataDA::Blobs) => { @@ -216,7 +219,10 @@ impl Tokenizable for CommitBatchInfo<'_> { vec![PUBDATA_SOURCE_CUSTOM] } - (L1BatchCommitmentMode::Rollup, PubdataDA::Calldata) => { + ( + L1BatchCommitmentMode::Rollup, + PubdataDA::Calldata | PubdataDA::RelayedL2Calldata, + ) => { // We compute and add the blob commitment to the pubdata payload so that we can verify the proof // even if we are not using blobs. let pubdata = self.pubdata_input(); diff --git a/core/lib/protobuf_config/src/eth.rs b/core/lib/protobuf_config/src/eth.rs index b713e650d019..c605e6d2cccb 100644 --- a/core/lib/protobuf_config/src/eth.rs +++ b/core/lib/protobuf_config/src/eth.rs @@ -31,6 +31,7 @@ impl proto::PubdataSendingMode { From::Calldata => Self::Calldata, From::Blobs => Self::Blobs, From::Custom => Self::Custom, + From::RelayedL2Calldata => Self::RelayedL2Calldata, } } @@ -40,6 +41,7 @@ impl proto::PubdataSendingMode { Self::Calldata => To::Calldata, Self::Blobs => To::Blobs, Self::Custom => To::Custom, + Self::RelayedL2Calldata => To::RelayedL2Calldata, } } } @@ -174,6 +176,8 @@ impl ProtoRepr for proto::GasAdjuster { ) .context("internal_pubdata_pricing_multiplier")?, max_blob_base_fee: self.max_blob_base_fee, + // TODO(EVM-676): support this field + settlement_mode: Default::default(), }) } diff --git a/core/lib/protobuf_config/src/proto/config/eth_sender.proto b/core/lib/protobuf_config/src/proto/config/eth_sender.proto index 839c7f65b973..536ac216863e 100644 --- a/core/lib/protobuf_config/src/proto/config/eth_sender.proto +++ b/core/lib/protobuf_config/src/proto/config/eth_sender.proto @@ -24,6 +24,7 @@ enum PubdataSendingMode { CALLDATA = 0; BLOBS = 1; CUSTOM = 2; + RELAYED_L2_CALLDATA = 3; } message Sender { diff --git a/core/lib/types/src/api/mod.rs b/core/lib/types/src/api/mod.rs index e6c84ecec537..0210a28f2a2e 100644 --- a/core/lib/types/src/api/mod.rs +++ b/core/lib/types/src/api/mod.rs @@ -847,6 +847,17 @@ pub struct TransactionExecutionInfo { pub execution_info: Value, } +/// The fee history type returned from `eth_feeHistory` call. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FeeHistory { + #[serde(flatten)] + pub inner: zksync_basic_types::web3::FeeHistory, + /// An array of effective pubdata prices. Note, that this field is L2-specific and only provided by L2 nodes. + #[serde(default)] + pub l2_pubdata_price: Vec, +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/lib/types/src/pubdata_da.rs b/core/lib/types/src/pubdata_da.rs index 6705fdc29530..bc7dc55e53de 100644 --- a/core/lib/types/src/pubdata_da.rs +++ b/core/lib/types/src/pubdata_da.rs @@ -15,6 +15,8 @@ pub enum PubdataDA { Blobs, /// Pubdata is sent to the external storage (GCS/DA layers) or not sent at all. Custom, + /// Pubdata is sent to an L2 to be eventually relayed to L1. + RelayedL2Calldata, } impl From for PubdataDA { @@ -23,6 +25,7 @@ impl From for PubdataDA { PubdataSendingMode::Calldata => PubdataDA::Calldata, PubdataSendingMode::Blobs => PubdataDA::Blobs, PubdataSendingMode::Custom => PubdataDA::Custom, + PubdataSendingMode::RelayedL2Calldata => PubdataDA::RelayedL2Calldata, } } } diff --git a/core/lib/web3_decl/src/client/boxed.rs b/core/lib/web3_decl/src/client/boxed.rs index c49e8aed721c..53def182c932 100644 --- a/core/lib/web3_decl/src/client/boxed.rs +++ b/core/lib/web3_decl/src/client/boxed.rs @@ -9,7 +9,7 @@ use jsonrpsee::core::{ }; use serde::de::DeserializeOwned; -use super::{ForNetwork, Network, TaggedClient}; +use super::{ForWeb3Network, Network, TaggedClient}; #[derive(Debug)] pub struct RawParams(pub(super) Option>); @@ -30,7 +30,7 @@ impl ToRpcParams for RawParams { // The implementation is fairly straightforward: [`RawParams`] is used as a catch-all params type, // and `serde_json::Value` is used as a catch-all response type. #[async_trait] -pub trait ObjectSafeClient: 'static + Send + Sync + fmt::Debug + ForNetwork { +pub trait ObjectSafeClient: 'static + Send + Sync + fmt::Debug + ForWeb3Network { /// Tags this client as working for a specific component. The component name can be used in logging, /// metrics etc. fn for_component(self: Box, component_name: &'static str) -> Box>; diff --git a/core/lib/web3_decl/src/client/mock.rs b/core/lib/web3_decl/src/client/mock.rs index 75bd037049d4..2dcb30094aae 100644 --- a/core/lib/web3_decl/src/client/mock.rs +++ b/core/lib/web3_decl/src/client/mock.rs @@ -14,7 +14,7 @@ use jsonrpsee::{ }; use serde::{de::DeserializeOwned, Serialize}; -use super::{boxed::RawParams, ForNetwork, Network, TaggedClient}; +use super::{boxed::RawParams, ForWeb3Network, Network, TaggedClient}; /// Object-safe counterpart to [`Handler`]. We need it because async closures aren't available on stable Rust. #[async_trait] @@ -177,7 +177,7 @@ impl MockClient { } } -impl ForNetwork for MockClient { +impl ForWeb3Network for MockClient { type Net = Net; fn network(&self) -> Self::Net { diff --git a/core/lib/web3_decl/src/client/mod.rs b/core/lib/web3_decl/src/client/mod.rs index ca861e77fdfe..a8246216eca3 100644 --- a/core/lib/web3_decl/src/client/mod.rs +++ b/core/lib/web3_decl/src/client/mod.rs @@ -37,8 +37,8 @@ use zksync_types::url::SensitiveUrl; use self::metrics::{L2ClientMetrics, METRICS}; pub use self::{ boxed::{DynClient, ObjectSafeClient}, - mock::MockClient, - network::{ForNetwork, Network, TaggedClient, L1, L2}, + mock::{MockClient, MockClientBuilder}, + network::{ForWeb3Network, Network, TaggedClient, L1, L2}, shared::Shared, }; @@ -227,7 +227,7 @@ impl Client { } } -impl ForNetwork for Client { +impl ForWeb3Network for Client { type Net = Net; fn network(&self) -> Self::Net { diff --git a/core/lib/web3_decl/src/client/network.rs b/core/lib/web3_decl/src/client/network.rs index 82136689d1d0..d0cb09299385 100644 --- a/core/lib/web3_decl/src/client/network.rs +++ b/core/lib/web3_decl/src/client/network.rs @@ -2,7 +2,7 @@ use std::fmt; -use zksync_types::{L2ChainId, SLChainId}; +use zksync_types::{L1ChainId, L2ChainId, SLChainId}; /// Marker trait for networks. Two standard network kinds are [`L1`] and [`L2`]. /// @@ -33,6 +33,12 @@ impl From for L1 { } } +impl From for L1 { + fn from(chain_id: L1ChainId) -> Self { + Self(Some(chain_id.into())) + } +} + /// L2 network. #[derive(Debug, Clone, Copy, Default)] pub struct L2(Option); @@ -55,7 +61,7 @@ impl From for L2 { /// Associates a type with a particular type of RPC networks, such as Ethereum or ZKsync Era. RPC traits created using `jsonrpsee::rpc` /// can use `ForNetwork` as a client boundary to restrict which implementations can call their methods. -pub trait ForNetwork { +pub trait ForWeb3Network { /// Network that the type is associated with. type Net: Network; @@ -67,7 +73,7 @@ pub trait ForNetwork { fn component(&self) -> &'static str; } -impl ForNetwork for &T { +impl ForWeb3Network for &T { type Net = T::Net; fn network(&self) -> Self::Net { @@ -79,7 +85,7 @@ impl ForNetwork for &T { } } -impl ForNetwork for Box { +impl ForWeb3Network for Box { type Net = T::Net; fn network(&self) -> Self::Net { @@ -92,7 +98,7 @@ impl ForNetwork for Box { } /// Client that can be tagged with the component using it. -pub trait TaggedClient: ForNetwork { +pub trait TaggedClient: ForWeb3Network { /// Tags this client as working for a specific component. fn set_component(&mut self, component_name: &'static str); } diff --git a/core/lib/web3_decl/src/namespaces/debug.rs b/core/lib/web3_decl/src/namespaces/debug.rs index b06560b47c32..1fbe3237104b 100644 --- a/core/lib/web3_decl/src/namespaces/debug.rs +++ b/core/lib/web3_decl/src/namespaces/debug.rs @@ -8,17 +8,17 @@ use zksync_types::{ }; use crate::{ - client::{ForNetwork, L2}, + client::{ForWeb3Network, L2}, types::H256, }; #[cfg_attr( feature = "server", - rpc(server, client, namespace = "debug", client_bounds(Self: ForNetwork)) + rpc(server, client, namespace = "debug", client_bounds(Self: ForWeb3Network)) )] #[cfg_attr( not(feature = "server"), - rpc(client, namespace = "debug", client_bounds(Self: ForNetwork)) + rpc(client, namespace = "debug", client_bounds(Self: ForWeb3Network)) )] pub trait DebugNamespace { #[method(name = "traceBlockByNumber")] diff --git a/core/lib/web3_decl/src/namespaces/en.rs b/core/lib/web3_decl/src/namespaces/en.rs index 7bb80bccd1c4..dac774dd7bdf 100644 --- a/core/lib/web3_decl/src/namespaces/en.rs +++ b/core/lib/web3_decl/src/namespaces/en.rs @@ -4,15 +4,15 @@ use jsonrpsee::proc_macros::rpc; use zksync_config::{configs::EcosystemContracts, GenesisConfig}; use zksync_types::{api::en, tokens::TokenInfo, Address, L2BlockNumber}; -use crate::client::{ForNetwork, L2}; +use crate::client::{ForWeb3Network, L2}; #[cfg_attr( feature = "server", - rpc(server, client, namespace = "en", client_bounds(Self: ForNetwork)) + rpc(server, client, namespace = "en", client_bounds(Self: ForWeb3Network)) )] #[cfg_attr( not(feature = "server"), - rpc(client, namespace = "en", client_bounds(Self: ForNetwork)) + rpc(client, namespace = "en", client_bounds(Self: ForWeb3Network)) )] pub trait EnNamespace { #[method(name = "syncL2Block")] diff --git a/core/lib/web3_decl/src/namespaces/eth.rs b/core/lib/web3_decl/src/namespaces/eth.rs index 10443443958b..9f271d80cbcf 100644 --- a/core/lib/web3_decl/src/namespaces/eth.rs +++ b/core/lib/web3_decl/src/namespaces/eth.rs @@ -3,28 +3,27 @@ use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; use zksync_types::{ api::{ - state_override::StateOverride, BlockId, BlockIdVariant, BlockNumber, Transaction, - TransactionVariant, + state_override::StateOverride, BlockId, BlockIdVariant, BlockNumber, FeeHistory, + Transaction, TransactionVariant, }, transaction_request::CallRequest, Address, H256, }; use crate::{ - client::{ForNetwork, L2}, + client::{ForWeb3Network, L2}, types::{ - Block, Bytes, FeeHistory, Filter, FilterChanges, Index, Log, SyncState, TransactionReceipt, - U256, U64, + Block, Bytes, Filter, FilterChanges, Index, Log, SyncState, TransactionReceipt, U256, U64, }, }; #[cfg_attr( feature = "server", - rpc(server, client, namespace = "eth", client_bounds(Self: ForNetwork)) + rpc(server, client, namespace = "eth", client_bounds(Self: ForWeb3Network)) )] #[cfg_attr( not(feature = "server"), - rpc(client, namespace = "eth", client_bounds(Self: ForNetwork)) + rpc(client, namespace = "eth", client_bounds(Self: ForWeb3Network)) )] pub trait EthNamespace { #[method(name = "blockNumber")] diff --git a/core/lib/web3_decl/src/namespaces/net.rs b/core/lib/web3_decl/src/namespaces/net.rs index 21e6548e5341..eebe503ea7a2 100644 --- a/core/lib/web3_decl/src/namespaces/net.rs +++ b/core/lib/web3_decl/src/namespaces/net.rs @@ -3,15 +3,15 @@ use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; use zksync_types::U256; -use crate::client::{ForNetwork, L2}; +use crate::client::{ForWeb3Network, L2}; #[cfg_attr( feature = "server", - rpc(server, client, namespace = "net", client_bounds(Self: ForNetwork)) + rpc(server, client, namespace = "net", client_bounds(Self: ForWeb3Network)) )] #[cfg_attr( not(feature = "server"), - rpc(client, namespace = "net", client_bounds(Self: ForNetwork)) + rpc(client, namespace = "net", client_bounds(Self: ForWeb3Network)) )] pub trait NetNamespace { #[method(name = "version")] diff --git a/core/lib/web3_decl/src/namespaces/snapshots.rs b/core/lib/web3_decl/src/namespaces/snapshots.rs index 6b82d5f590d8..5d1ac36d95c9 100644 --- a/core/lib/web3_decl/src/namespaces/snapshots.rs +++ b/core/lib/web3_decl/src/namespaces/snapshots.rs @@ -6,15 +6,15 @@ use zksync_types::{ L1BatchNumber, }; -use crate::client::{ForNetwork, L2}; +use crate::client::{ForWeb3Network, L2}; #[cfg_attr( feature = "server", - rpc(server, client, namespace = "snapshots", client_bounds(Self: ForNetwork)) + rpc(server, client, namespace = "snapshots", client_bounds(Self: ForWeb3Network)) )] #[cfg_attr( not(feature = "server"), - rpc(client, namespace = "snapshots", client_bounds(Self: ForNetwork)) + rpc(client, namespace = "snapshots", client_bounds(Self: ForWeb3Network)) )] pub trait SnapshotsNamespace { #[method(name = "getAllSnapshots")] diff --git a/core/lib/web3_decl/src/namespaces/unstable.rs b/core/lib/web3_decl/src/namespaces/unstable.rs index d2a6e29c7083..e6b36dd26846 100644 --- a/core/lib/web3_decl/src/namespaces/unstable.rs +++ b/core/lib/web3_decl/src/namespaces/unstable.rs @@ -7,16 +7,16 @@ use zksync_types::{ L1BatchNumber, H256, }; -use crate::client::{ForNetwork, L2}; +use crate::client::{ForWeb3Network, L2}; /// RPCs in this namespace are experimental, and their interface is unstable, and it WILL change. #[cfg_attr( feature = "server", - rpc(server, client, namespace = "unstable", client_bounds(Self: ForNetwork)) + rpc(server, client, namespace = "unstable", client_bounds(Self: ForWeb3Network)) )] #[cfg_attr( not(feature = "server"), - rpc(client, namespace = "unstable", client_bounds(Self: ForNetwork)) + rpc(client, namespace = "unstable", client_bounds(Self: ForWeb3Network)) )] pub trait UnstableNamespace { #[method(name = "getTransactionExecutionInfo")] diff --git a/core/lib/web3_decl/src/namespaces/web3.rs b/core/lib/web3_decl/src/namespaces/web3.rs index 8851f6d0c3be..d9d417a49d47 100644 --- a/core/lib/web3_decl/src/namespaces/web3.rs +++ b/core/lib/web3_decl/src/namespaces/web3.rs @@ -2,15 +2,15 @@ use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; -use crate::client::{ForNetwork, L2}; +use crate::client::{ForWeb3Network, L2}; #[cfg_attr( feature = "server", - rpc(server, client, namespace = "web3", client_bounds(Self: ForNetwork)) + rpc(server, client, namespace = "web3", client_bounds(Self: ForWeb3Network)) )] #[cfg_attr( not(feature = "server"), - rpc(client, namespace = "web3", client_bounds(Self: ForNetwork)) + rpc(client, namespace = "web3", client_bounds(Self: ForWeb3Network)) )] pub trait Web3Namespace { #[method(name = "clientVersion")] diff --git a/core/lib/web3_decl/src/namespaces/zks.rs b/core/lib/web3_decl/src/namespaces/zks.rs index 6f443dbded6a..47aae2a0835e 100644 --- a/core/lib/web3_decl/src/namespaces/zks.rs +++ b/core/lib/web3_decl/src/namespaces/zks.rs @@ -15,17 +15,17 @@ use zksync_types::{ }; use crate::{ - client::{ForNetwork, L2}, + client::{ForWeb3Network, L2}, types::{Bytes, Token}, }; #[cfg_attr( feature = "server", - rpc(server, client, namespace = "zks", client_bounds(Self: ForNetwork)) + rpc(server, client, namespace = "zks", client_bounds(Self: ForWeb3Network)) )] #[cfg_attr( not(feature = "server"), - rpc(client, namespace = "zks", client_bounds(Self: ForNetwork)) + rpc(client, namespace = "zks", client_bounds(Self: ForWeb3Network)) )] pub trait ZksNamespace { #[method(name = "estimateFee")] diff --git a/core/node/api_server/src/web3/backend_jsonrpsee/namespaces/eth.rs b/core/node/api_server/src/web3/backend_jsonrpsee/namespaces/eth.rs index ff8ce0356a05..15528c5b309b 100644 --- a/core/node/api_server/src/web3/backend_jsonrpsee/namespaces/eth.rs +++ b/core/node/api_server/src/web3/backend_jsonrpsee/namespaces/eth.rs @@ -1,10 +1,10 @@ use zksync_types::{ api::{ - state_override::StateOverride, Block, BlockId, BlockIdVariant, BlockNumber, Log, - Transaction, TransactionId, TransactionReceipt, TransactionVariant, + state_override::StateOverride, Block, BlockId, BlockIdVariant, BlockNumber, FeeHistory, + Log, Transaction, TransactionId, TransactionReceipt, TransactionVariant, }, transaction_request::CallRequest, - web3::{Bytes, FeeHistory, Index, SyncState}, + web3::{Bytes, Index, SyncState}, Address, H256, U256, U64, }; use zksync_web3_decl::{ diff --git a/core/node/api_server/src/web3/namespaces/eth.rs b/core/node/api_server/src/web3/namespaces/eth.rs index 68030763fd60..c3bed64a1468 100644 --- a/core/node/api_server/src/web3/namespaces/eth.rs +++ b/core/node/api_server/src/web3/namespaces/eth.rs @@ -3,13 +3,13 @@ use zksync_dal::{CoreDal, DalError}; use zksync_system_constants::DEFAULT_L2_TX_GAS_PER_PUBDATA_BYTE; use zksync_types::{ api::{ - state_override::StateOverride, BlockId, BlockNumber, GetLogsFilter, Transaction, - TransactionId, TransactionReceipt, TransactionVariant, + state_override::StateOverride, BlockId, BlockNumber, FeeHistory, GetLogsFilter, + Transaction, TransactionId, TransactionReceipt, TransactionVariant, }, l2::{L2Tx, TransactionType}, transaction_request::CallRequest, utils::decompose_full_nonce, - web3::{self, Bytes, FeeHistory, SyncInfo, SyncState}, + web3::{self, Bytes, SyncInfo, SyncState}, AccountTreeId, L2BlockNumber, StorageKey, H256, L2_BASE_TOKEN_ADDRESS, U256, }; use zksync_utils::u256_to_h256; @@ -678,13 +678,15 @@ impl EthNamespace { .await?; self.set_block_diff(newest_l2_block); - let mut base_fee_per_gas = connection + let (mut base_fee_per_gas, mut effective_pubdata_price_history) = connection .blocks_web3_dal() .get_fee_history(newest_l2_block, block_count) .await .map_err(DalError::generalize)?; + // DAL method returns fees in DESC order while we need ASC. base_fee_per_gas.reverse(); + effective_pubdata_price_history.reverse(); let oldest_block = newest_l2_block.0 + 1 - base_fee_per_gas.len() as u32; // We do not store gas used ratio for blocks, returns array of zeroes as a placeholder. @@ -702,12 +704,15 @@ impl EthNamespace { // `base_fee_per_gas` for next L2 block cannot be calculated, appending last fee as a placeholder. base_fee_per_gas.push(*base_fee_per_gas.last().unwrap()); Ok(FeeHistory { - oldest_block: web3::BlockNumber::Number(oldest_block.into()), - base_fee_per_gas, - gas_used_ratio, - reward, - base_fee_per_blob_gas, - blob_gas_used_ratio, + inner: web3::FeeHistory { + oldest_block: zksync_types::web3::BlockNumber::Number(oldest_block.into()), + base_fee_per_gas, + gas_used_ratio, + reward, + base_fee_per_blob_gas, + blob_gas_used_ratio, + }, + l2_pubdata_price: effective_pubdata_price_history, }) } diff --git a/core/node/block_reverter/src/lib.rs b/core/node/block_reverter/src/lib.rs index 466b5f3c69f7..c7397ee475a7 100644 --- a/core/node/block_reverter/src/lib.rs +++ b/core/node/block_reverter/src/lib.rs @@ -8,10 +8,7 @@ use zksync_contracts::hyperchain_contract; use zksync_dal::{ConnectionPool, Core, CoreDal}; // Public re-export to simplify the API use. pub use zksync_eth_client as eth_client; -use zksync_eth_client::{ - clients::{DynClient, L1}, - BoundEthInterface, CallFunctionArgs, EthInterface, Options, -}; +use zksync_eth_client::{BoundEthInterface, CallFunctionArgs, EthInterface, Options}; use zksync_merkle_tree::domain::ZkSyncTree; use zksync_object_store::{ObjectStore, ObjectStoreError}; use zksync_state::RocksdbStorage; @@ -538,7 +535,7 @@ impl BlockReverter { #[tracing::instrument(err)] async fn get_l1_batch_number_from_contract( - eth_client: &DynClient, + eth_client: &dyn EthInterface, contract_address: Address, op: AggregatedActionType, ) -> anyhow::Result { @@ -560,7 +557,7 @@ impl BlockReverter { /// Returns suggested values for a reversion. pub async fn suggested_values( &self, - eth_client: &DynClient, + eth_client: &dyn EthInterface, eth_config: &BlockReverterEthConfig, reverter_address: Address, ) -> anyhow::Result { diff --git a/core/node/commitment_generator/src/validation_task.rs b/core/node/commitment_generator/src/validation_task.rs index a28eeabfd0fc..639bb79baf97 100644 --- a/core/node/commitment_generator/src/validation_task.rs +++ b/core/node/commitment_generator/src/validation_task.rs @@ -3,7 +3,7 @@ use std::time::Duration; use tokio::sync::watch; use zksync_eth_client::{ clients::{DynClient, L1}, - CallFunctionArgs, ClientError, ContractCallError, + CallFunctionArgs, ClientError, ContractCallError, EthInterface, }; use zksync_types::{commitment::L1BatchCommitmentMode, Address}; @@ -46,9 +46,9 @@ impl L1BatchCommitmentModeValidationTask { async fn validate_commitment_mode(self) -> anyhow::Result<()> { let expected_mode = self.expected_mode; let diamond_proxy_address = self.diamond_proxy_address; - let eth_client = self.eth_client.as_ref(); loop { - let result = Self::get_pubdata_pricing_mode(diamond_proxy_address, eth_client).await; + let result = + Self::get_pubdata_pricing_mode(diamond_proxy_address, &self.eth_client).await; match result { Ok(mode) => { anyhow::ensure!( @@ -91,7 +91,7 @@ impl L1BatchCommitmentModeValidationTask { async fn get_pubdata_pricing_mode( diamond_proxy_address: Address, - eth_client: &DynClient, + eth_client: &dyn EthInterface, ) -> Result { CallFunctionArgs::new("getPubdataPricingMode", ()) .for_contract( @@ -124,7 +124,7 @@ impl L1BatchCommitmentModeValidationTask { mod tests { use std::{mem, sync::Mutex}; - use zksync_eth_client::clients::MockEthereum; + use zksync_eth_client::clients::MockSettlementLayer; use zksync_types::{ethabi, U256}; use zksync_web3_decl::{client::MockClient, jsonrpsee::types::ErrorObject}; @@ -132,7 +132,7 @@ mod tests { fn mock_ethereum(token: ethabi::Token, err: Option) -> MockClient { let err_mutex = Mutex::new(err); - MockEthereum::builder() + MockSettlementLayer::builder() .with_fallible_call_handler(move |_, _| { let err = mem::take(&mut *err_mutex.lock().unwrap()); if let Some(err) = err { diff --git a/core/node/consistency_checker/src/lib.rs b/core/node/consistency_checker/src/lib.rs index 29b020b18607..20ba43a4166e 100644 --- a/core/node/consistency_checker/src/lib.rs +++ b/core/node/consistency_checker/src/lib.rs @@ -518,7 +518,7 @@ impl ConsistencyChecker { let version: U256 = CallFunctionArgs::new("getProtocolVersion", ()) .for_contract(address, &self.contract) - .call(self.l1_client.as_ref()) + .call(&self.l1_client) .await?; tracing::info!("Checked diamond proxy {address:?} (protocol version: {version})"); Ok(()) diff --git a/core/node/consistency_checker/src/tests/mod.rs b/core/node/consistency_checker/src/tests/mod.rs index 914e21069bdd..40c447071cf4 100644 --- a/core/node/consistency_checker/src/tests/mod.rs +++ b/core/node/consistency_checker/src/tests/mod.rs @@ -7,7 +7,7 @@ use test_casing::{test_casing, Product}; use tokio::sync::mpsc; use zksync_config::GenesisConfig; use zksync_dal::Connection; -use zksync_eth_client::{clients::MockEthereum, Options}; +use zksync_eth_client::{clients::MockSettlementLayer, Options}; use zksync_l1_contract_interface::{i_executor::methods::CommitBatches, Tokenizable, Tokenize}; use zksync_node_genesis::{insert_genesis_batch, mock_genesis_config, GenesisParams}; use zksync_node_test_utils::{ @@ -92,7 +92,7 @@ pub(crate) fn build_commit_tx_input_data( } pub(crate) fn create_mock_checker( - client: MockEthereum, + client: MockSettlementLayer, pool: ConnectionPool, commitment_mode: L1BatchCommitmentMode, ) -> ConsistencyChecker { @@ -111,8 +111,8 @@ pub(crate) fn create_mock_checker( } } -fn create_mock_ethereum() -> MockEthereum { - let mock = MockEthereum::builder().with_call_handler(|call, _block_id| { +fn create_mock_ethereum() -> MockSettlementLayer { + let mock = MockSettlementLayer::builder().with_call_handler(|call, _block_id| { assert_eq!(call.to, Some(DIAMOND_PROXY_ADDR)); let packed_semver = ProtocolVersionId::latest().into_packed_semver_with_patch(0); let contract = zksync_contracts::hyperchain_contract(); @@ -650,7 +650,7 @@ impl IncorrectDataKind { async fn apply( self, - client: &MockEthereum, + client: &MockSettlementLayer, l1_batch: &L1BatchWithMetadata, commitment_mode: L1BatchCommitmentMode, ) -> H256 { diff --git a/core/node/eth_sender/src/abstract_l1_interface.rs b/core/node/eth_sender/src/abstract_l1_interface.rs index 9c9af82553e9..1f1956c9dd84 100644 --- a/core/node/eth_sender/src/abstract_l1_interface.rs +++ b/core/node/eth_sender/src/abstract_l1_interface.rs @@ -3,7 +3,6 @@ use std::fmt; use async_trait::async_trait; use vise::{EncodeLabelSet, EncodeLabelValue}; use zksync_eth_client::{ - clients::{DynClient, L1}, BoundEthInterface, EnrichedClientResult, EthInterface, ExecutedTxStatus, FailureInfo, Options, RawTransactionBytes, SignedCallResult, }; @@ -91,11 +90,14 @@ pub(super) struct RealL1Interface { } impl RealL1Interface { - pub(crate) fn query_client(&self) -> &DynClient { + pub(crate) fn query_client(&self) -> &dyn EthInterface { self.ethereum_gateway().as_ref() } - pub(crate) fn query_client_for_operator(&self, operator_type: OperatorType) -> &DynClient { + pub(crate) fn query_client_for_operator( + &self, + operator_type: OperatorType, + ) -> &dyn EthInterface { if operator_type == OperatorType::Blob { self.ethereum_gateway_blobs().unwrap().as_ref() } else { @@ -103,6 +105,7 @@ impl RealL1Interface { } } } + #[async_trait] impl AbstractL1Interface for RealL1Interface { async fn failure_reason(&self, tx_hash: H256) -> Option { diff --git a/core/node/eth_sender/src/eth_tx_aggregator.rs b/core/node/eth_sender/src/eth_tx_aggregator.rs index 89533432ef83..9ec79dfc300b 100644 --- a/core/node/eth_sender/src/eth_tx_aggregator.rs +++ b/core/node/eth_sender/src/eth_tx_aggregator.rs @@ -2,7 +2,7 @@ use tokio::sync::watch; use zksync_config::configs::eth_sender::SenderConfig; use zksync_contracts::BaseSystemContractsHashes; use zksync_dal::{Connection, ConnectionPool, Core, CoreDal}; -use zksync_eth_client::{BoundEthInterface, CallFunctionArgs, EthInterface}; +use zksync_eth_client::{BoundEthInterface, CallFunctionArgs}; use zksync_l1_contract_interface::{ i_executor::{ commit::kzg::{KzgInfo, ZK_SYNC_BYTES_PER_BLOB}, diff --git a/core/node/eth_sender/src/tester.rs b/core/node/eth_sender/src/tester.rs index 7cad69c5a9a3..5bd5181ed8c7 100644 --- a/core/node/eth_sender/src/tester.rs +++ b/core/node/eth_sender/src/tester.rs @@ -5,9 +5,9 @@ use zksync_config::{ ContractsConfig, EthConfig, GasAdjusterConfig, }; use zksync_dal::{Connection, ConnectionPool, Core, CoreDal}; -use zksync_eth_client::{clients::MockEthereum, BaseFees, BoundEthInterface}; +use zksync_eth_client::{clients::MockSettlementLayer, BaseFees, BoundEthInterface}; use zksync_l1_contract_interface::i_executor::methods::{ExecuteBatches, ProveBatches}; -use zksync_node_fee_model::l1_gas_price::GasAdjuster; +use zksync_node_fee_model::l1_gas_price::{GasAdjuster, GasAdjusterClient}; use zksync_node_test_utils::{create_l1_batch, l1_batch_metadata_to_commitment_artifacts}; use zksync_object_store::MockObjectStore; use zksync_types::{ @@ -109,8 +109,8 @@ impl TestL1Batch { #[derive(Debug)] pub(crate) struct EthSenderTester { pub conn: ConnectionPool, - pub gateway: Box, - pub gateway_blobs: Box, + pub gateway: Box, + pub gateway_blobs: Box, pub manager: MockEthTxManager, pub aggregator: EthTxAggregator, pub gas_adjuster: Arc, @@ -152,14 +152,16 @@ impl EthSenderTester { .map(|base_fee_per_gas| BaseFees { base_fee_per_gas, base_fee_per_blob_gas: 0.into(), + l2_pubdata_price: 0.into(), }) .collect(); - let gateway = MockEthereum::builder() + let gateway = MockSettlementLayer::builder() .with_fee_history( std::iter::repeat_with(|| BaseFees { base_fee_per_gas: 0, base_fee_per_blob_gas: 0.into(), + l2_pubdata_price: 0.into(), }) .take(Self::WAIT_CONFIRMATIONS as usize) .chain(history.clone()) @@ -174,11 +176,12 @@ impl EthSenderTester { gateway.advance_block_number(Self::WAIT_CONFIRMATIONS); let gateway = Box::new(gateway); - let gateway_blobs = MockEthereum::builder() + let gateway_blobs = MockSettlementLayer::builder() .with_fee_history( std::iter::repeat_with(|| BaseFees { base_fee_per_gas: 0, base_fee_per_blob_gas: 0.into(), + l2_pubdata_price: 0.into(), }) .take(Self::WAIT_CONFIRMATIONS as usize) .chain(history) @@ -195,7 +198,7 @@ impl EthSenderTester { let gas_adjuster = Arc::new( GasAdjuster::new( - Box::new(gateway.clone().into_client()), + GasAdjusterClient::from_l1(Box::new(gateway.clone().into_client())), GasAdjusterConfig { max_base_fee_samples: Self::MAX_BASE_FEE_SAMPLES, pricing_formula_parameter_a: 3.0, diff --git a/core/node/eth_watch/src/client.rs b/core/node/eth_watch/src/client.rs index 39b9b5e9f6b1..8be556b42889 100644 --- a/core/node/eth_watch/src/client.rs +++ b/core/node/eth_watch/src/client.rs @@ -122,7 +122,7 @@ impl EthClient for EthHttpQueryClient { // New verifier returns the hash of the verification key. CallFunctionArgs::new("verificationKeyHash", ()) .for_contract(verifier_address, &self.verifier_contract_abi) - .call(self.client.as_ref()) + .call(&self.client) .await } diff --git a/core/node/fee_model/src/l1_gas_price/gas_adjuster/metrics.rs b/core/node/fee_model/src/l1_gas_price/gas_adjuster/metrics.rs index 0a671179de39..f75c1796037f 100644 --- a/core/node/fee_model/src/l1_gas_price/gas_adjuster/metrics.rs +++ b/core/node/fee_model/src/l1_gas_price/gas_adjuster/metrics.rs @@ -7,6 +7,7 @@ use vise::{Gauge, Metrics}; pub(super) struct GasAdjusterMetrics { pub current_base_fee_per_gas: Gauge, pub current_blob_base_fee: Gauge, + pub current_l2_pubdata_price: Gauge, pub median_base_fee_per_gas: Gauge, pub median_blob_base_fee_per_gas: Gauge, pub median_blob_base_fee: Gauge, diff --git a/core/node/fee_model/src/l1_gas_price/gas_adjuster/mod.rs b/core/node/fee_model/src/l1_gas_price/gas_adjuster/mod.rs index 2032cb9c89fd..244220da026f 100644 --- a/core/node/fee_model/src/l1_gas_price/gas_adjuster/mod.rs +++ b/core/node/fee_model/src/l1_gas_price/gas_adjuster/mod.rs @@ -7,9 +7,9 @@ use std::{ use tokio::sync::watch; use zksync_config::{configs::eth_sender::PubdataSendingMode, GasAdjusterConfig}; -use zksync_eth_client::EthInterface; +use zksync_eth_client::EthFeeInterface; use zksync_types::{commitment::L1BatchCommitmentMode, L1_GAS_PER_PUBDATA_BYTE, U256}; -use zksync_web3_decl::client::{DynClient, L1}; +use zksync_web3_decl::client::{DynClient, L1, L2}; use self::metrics::METRICS; use super::L1TxParamsProvider; @@ -18,6 +18,40 @@ mod metrics; #[cfg(test)] mod tests; +#[derive(Debug)] +pub struct GasAdjusterClient { + gateway_mode: bool, + inner: Box, +} + +impl GasAdjusterClient { + pub fn from_l1(inner: Box>) -> Self { + Self { + inner: Box::new(inner.for_component("gas_adjuster")), + gateway_mode: false, + } + } + + pub fn from_l2(inner: Box>) -> Self { + Self { + inner: Box::new(inner.for_component("gas_adjuster")), + gateway_mode: true, + } + } +} + +impl From>> for GasAdjusterClient { + fn from(inner: Box>) -> Self { + Self::from_l1(inner) + } +} + +impl From>> for GasAdjusterClient { + fn from(inner: Box>) -> Self { + Self::from_l2(inner) + } +} + /// This component keeps track of the median `base_fee` from the last `max_base_fee_samples` blocks /// and of the median `blob_base_fee` from the last `max_blob_base_fee_sample` blocks. /// It is used to adjust the base_fee of transactions sent to L1. @@ -27,31 +61,53 @@ pub struct GasAdjuster { // Type for blob base fee is chosen to be `U256`. // In practice, it's very unlikely to overflow `u64` (if `blob_base_fee_statistics` = 10 ^ 18, then price for one blob is 2 ^ 17 ETH). // But it's still possible and code shouldn't panic if that happens. One more argument is that geth uses big int type for blob prices. + // + // Note, that for L2-based chains it will contains only zeroes. pub(super) blob_base_fee_statistics: GasStatistics, + // Note, that for L1-based chains the following field contains only zeroes. + pub(super) l2_pubdata_price_statistics: GasStatistics, + pub(super) config: GasAdjusterConfig, pubdata_sending_mode: PubdataSendingMode, - eth_client: Box>, + client: GasAdjusterClient, commitment_mode: L1BatchCommitmentMode, } impl GasAdjuster { pub async fn new( - eth_client: Box>, + client: GasAdjusterClient, config: GasAdjusterConfig, pubdata_sending_mode: PubdataSendingMode, commitment_mode: L1BatchCommitmentMode, ) -> anyhow::Result { - let eth_client = eth_client.for_component("gas_adjuster"); + // A runtime check to ensure consistent config. + if config.settlement_mode.is_gateway() { + anyhow::ensure!(client.gateway_mode, "Must be L2 client in L2 mode"); + + anyhow::ensure!( + matches!(pubdata_sending_mode, PubdataSendingMode::RelayedL2Calldata), + "Only relayed L2 calldata is available for L2 mode" + ); + } else { + anyhow::ensure!(!client.gateway_mode, "Must be L1 client in L1 mode"); + + anyhow::ensure!( + !matches!(pubdata_sending_mode, PubdataSendingMode::RelayedL2Calldata), + "Relayed L2 calldata is only available in L2 mode" + ); + } // Subtracting 1 from the "latest" block number to prevent errors in case // the info about the latest block is not yet present on the node. // This sometimes happens on Infura. - let current_block = eth_client + let current_block = client + .inner .block_number() .await? .as_usize() .saturating_sub(1); - let fee_history = eth_client + let fee_history = client + .inner .base_fee_history(current_block, config.max_base_fee_samples) .await?; @@ -67,12 +123,19 @@ impl GasAdjuster { fee_history.iter().map(|fee| fee.base_fee_per_blob_gas), ); + let l2_pubdata_price_statistics = GasStatistics::new( + config.num_samples_for_blob_base_fee_estimate, + current_block, + fee_history.iter().map(|fee| fee.l2_pubdata_price), + ); + Ok(Self { base_fee_statistics, blob_base_fee_statistics, + l2_pubdata_price_statistics, config, pubdata_sending_mode, - eth_client, + client, commitment_mode, }) } @@ -84,7 +147,8 @@ impl GasAdjuster { // the info about the latest block is not yet present on the node. // This sometimes happens on Infura. let current_block = self - .eth_client + .client + .inner .block_number() .await? .as_usize() @@ -94,26 +158,27 @@ impl GasAdjuster { if current_block > last_processed_block { let n_blocks = current_block - last_processed_block; - let base_fees = self - .eth_client + let fee_data = self + .client + .inner .base_fee_history(current_block, n_blocks) .await?; // We shouldn't rely on L1 provider to return consistent results, so we check that we have at least one new sample. - if let Some(current_base_fee_per_gas) = base_fees.last().map(|fee| fee.base_fee_per_gas) + if let Some(current_base_fee_per_gas) = fee_data.last().map(|fee| fee.base_fee_per_gas) { METRICS .current_base_fee_per_gas .set(current_base_fee_per_gas); } self.base_fee_statistics - .add_samples(base_fees.iter().map(|fee| fee.base_fee_per_gas)); + .add_samples(fee_data.iter().map(|fee| fee.base_fee_per_gas)); if let Some(current_blob_base_fee) = - base_fees.last().map(|fee| fee.base_fee_per_blob_gas) + fee_data.last().map(|fee| fee.base_fee_per_blob_gas) { // Blob base fee overflows `u64` only in very extreme cases. - // It doesn't worth to observe exact value with metric because anyway values that can be used + // It isn't worth to observe exact value with metric because anyway values that can be used // are capped by `self.config.max_blob_base_fee()` of `u64` type. if current_blob_base_fee > U256::from(u64::MAX) { tracing::error!("Failed to report current_blob_base_fee = {current_blob_base_fee}, it exceeds u64::MAX"); @@ -124,7 +189,23 @@ impl GasAdjuster { } } self.blob_base_fee_statistics - .add_samples(base_fees.iter().map(|fee| fee.base_fee_per_blob_gas)); + .add_samples(fee_data.iter().map(|fee| fee.base_fee_per_blob_gas)); + + if let Some(current_l2_pubdata_price) = fee_data.last().map(|fee| fee.l2_pubdata_price) + { + // L2 pubdata price overflows `u64` only in very extreme cases. + // It isn't worth to observe exact value with metric because anyway values that can be used + // are capped by `self.config.max_blob_base_fee()` of `u64` type. + if current_l2_pubdata_price > U256::from(u64::MAX) { + tracing::error!("Failed to report current_l2_pubdata_price = {current_l2_pubdata_price}, it exceeds u64::MAX"); + } else { + METRICS + .current_l2_pubdata_price + .set(current_l2_pubdata_price.as_u64()); + } + } + self.l2_pubdata_price_statistics + .add_samples(fee_data.iter().map(|fee| fee.l2_pubdata_price)); } Ok(()) } @@ -197,36 +278,33 @@ impl GasAdjuster { * BLOB_GAS_PER_BYTE as f64 * self.config.internal_pubdata_pricing_multiplier; - self.bound_blob_base_fee(calculated_price) - } - PubdataSendingMode::Calldata => { - self.estimate_effective_gas_price() * self.pubdata_byte_gas() + self.cap_pubdata_fee(calculated_price) } + PubdataSendingMode::Calldata => self.cap_pubdata_fee( + (self.estimate_effective_gas_price() * L1_GAS_PER_PUBDATA_BYTE as u64) as f64, + ), PubdataSendingMode::Custom => { // Fix this when we have a better understanding of dynamic pricing for custom DA layers. // GitHub issue: https://github.com/matter-labs/zksync-era/issues/2105 0 } + PubdataSendingMode::RelayedL2Calldata => { + self.cap_pubdata_fee(self.l2_pubdata_price_statistics.median().as_u64() as f64) + } } } - fn pubdata_byte_gas(&self) -> u64 { - match self.commitment_mode { - L1BatchCommitmentMode::Validium => 0, - L1BatchCommitmentMode::Rollup => L1_GAS_PER_PUBDATA_BYTE.into(), - } - } - - fn bound_blob_base_fee(&self, blob_base_fee: f64) -> u64 { + fn cap_pubdata_fee(&self, pubdata_fee: f64) -> u64 { + // We will treat the max blob base fee as the maximal fee that we can take for each byte of pubdata. let max_blob_base_fee = self.config.max_blob_base_fee(); match self.commitment_mode { L1BatchCommitmentMode::Validium => 0, L1BatchCommitmentMode::Rollup => { - if blob_base_fee > max_blob_base_fee as f64 { - tracing::error!("Blob base fee is too high: {blob_base_fee}, using max allowed: {max_blob_base_fee}"); + if pubdata_fee > max_blob_base_fee as f64 { + tracing::error!("Blob base fee is too high: {pubdata_fee}, using max allowed: {max_blob_base_fee}"); return max_blob_base_fee; } - blob_base_fee as u64 + pubdata_fee as u64 } } } diff --git a/core/node/fee_model/src/l1_gas_price/gas_adjuster/tests.rs b/core/node/fee_model/src/l1_gas_price/gas_adjuster/tests.rs index 200903b6deda..2643e4b3c424 100644 --- a/core/node/fee_model/src/l1_gas_price/gas_adjuster/tests.rs +++ b/core/node/fee_model/src/l1_gas_price/gas_adjuster/tests.rs @@ -2,10 +2,12 @@ use std::{collections::VecDeque, sync::RwLockReadGuard}; use test_casing::test_casing; use zksync_config::{configs::eth_sender::PubdataSendingMode, GasAdjusterConfig}; -use zksync_eth_client::{clients::MockEthereum, BaseFees}; -use zksync_types::commitment::L1BatchCommitmentMode; +use zksync_eth_client::{clients::MockSettlementLayer, BaseFees}; +use zksync_types::{commitment::L1BatchCommitmentMode, settlement::SettlementMode}; +use zksync_web3_decl::client::L2; use super::{GasAdjuster, GasStatistics, GasStatisticsInner}; +use crate::l1_gas_price::GasAdjusterClient; /// Check that we compute the median correctly #[test] @@ -28,57 +30,78 @@ fn samples_queue() { assert_eq!(stats.samples, VecDeque::from([4, 5, 18, 18, 18])); } +const TEST_BLOCK_FEES: [u64; 10] = [0, 4, 6, 8, 7, 5, 5, 8, 10, 9]; +const TEST_BLOB_FEES: [u64; 10] = [ + 0, + 393216, + 393216, + 393216 * 2, + 393216, + 393216 * 2, + 393216 * 2, + 393216 * 3, + 393216 * 4, + 393216, +]; +const TEST_PUBDATA_PRICES: [u64; 10] = [ + 0, + 493216, + 493216, + 493216 * 2, + 493216, + 493216 * 2, + 493216 * 2, + 493216 * 3, + 493216 * 4, + 493216, +]; + +fn test_config(settlement_mode: SettlementMode) -> GasAdjusterConfig { + GasAdjusterConfig { + default_priority_fee_per_gas: 5, + max_base_fee_samples: 5, + pricing_formula_parameter_a: 1.5, + pricing_formula_parameter_b: 1.0005, + internal_l1_pricing_multiplier: 0.8, + internal_enforced_l1_gas_price: None, + internal_enforced_pubdata_price: None, + poll_period: 5, + max_l1_gas_price: None, + num_samples_for_blob_base_fee_estimate: 3, + internal_pubdata_pricing_multiplier: 1.0, + max_blob_base_fee: None, + settlement_mode, + } +} + +/// Helper function to read a value from adjuster +fn read(statistics: &GasStatistics) -> RwLockReadGuard> { + statistics.0.read().unwrap() +} + /// Check that we properly fetch base fees as block are mined #[test_casing(2, [L1BatchCommitmentMode::Rollup, L1BatchCommitmentMode::Validium])] #[tokio::test] async fn kept_updated(commitment_mode: L1BatchCommitmentMode) { - // Helper function to read a value from adjuster - fn read(statistics: &GasStatistics) -> RwLockReadGuard> { - statistics.0.read().unwrap() - } - - let block_fees = vec![0, 4, 6, 8, 7, 5, 5, 8, 10, 9]; - let blob_fees = vec![ - 0, - 393216, - 393216, - 393216 * 2, - 393216, - 393216 * 2, - 393216 * 2, - 393216 * 3, - 393216 * 4, - 393216, - ]; - let base_fees = block_fees + let base_fees = TEST_BLOCK_FEES .into_iter() - .zip(blob_fees) + .zip(TEST_BLOB_FEES) .map(|(block, blob)| BaseFees { base_fee_per_gas: block, base_fee_per_blob_gas: blob.into(), + l2_pubdata_price: 0.into(), }) .collect(); - let eth_client = MockEthereum::builder().with_fee_history(base_fees).build(); + let eth_client = MockSettlementLayer::builder() + .with_fee_history(base_fees) + .build(); // 5 sampled blocks + additional block to account for latest block subtraction eth_client.advance_block_number(6); - let config = GasAdjusterConfig { - default_priority_fee_per_gas: 5, - max_base_fee_samples: 5, - pricing_formula_parameter_a: 1.5, - pricing_formula_parameter_b: 1.0005, - internal_l1_pricing_multiplier: 0.8, - internal_enforced_l1_gas_price: None, - internal_enforced_pubdata_price: None, - poll_period: 5, - max_l1_gas_price: None, - num_samples_for_blob_base_fee_estimate: 3, - internal_pubdata_pricing_multiplier: 1.0, - max_blob_base_fee: None, - }; + let config = test_config(SettlementMode::SettlesToL1); let adjuster = GasAdjuster::new( - Box::new(eth_client.clone().into_client()), + GasAdjusterClient::from_l1(Box::new(eth_client.clone().into_client())), config, PubdataSendingMode::Calldata, commitment_mode, @@ -119,3 +142,67 @@ async fn kept_updated(commitment_mode: L1BatchCommitmentMode) { expected_median_blob_base_fee.into() ); } + +/// Check that we properly fetch base fees as block are mined +#[test_casing(2, [L1BatchCommitmentMode::Rollup, L1BatchCommitmentMode::Validium])] +#[tokio::test] +async fn kept_updated_l2(commitment_mode: L1BatchCommitmentMode) { + let base_fees = TEST_BLOCK_FEES + .into_iter() + .zip(TEST_PUBDATA_PRICES) + .map(|(block, pubdata)| BaseFees { + base_fee_per_gas: block, + base_fee_per_blob_gas: 0.into(), + l2_pubdata_price: pubdata.into(), + }) + .collect(); + + let eth_client = MockSettlementLayer::::builder() + .with_fee_history(base_fees) + .build(); + // 5 sampled blocks + additional block to account for latest block subtraction + eth_client.advance_block_number(6); + + let config = test_config(SettlementMode::Gateway); + let adjuster = GasAdjuster::new( + GasAdjusterClient::from_l2(Box::new(eth_client.clone().into_client())), + config, + PubdataSendingMode::RelayedL2Calldata, + commitment_mode, + ) + .await + .unwrap(); + + assert_eq!( + read(&adjuster.base_fee_statistics).samples.len(), + config.max_base_fee_samples + ); + assert_eq!(read(&adjuster.base_fee_statistics).median(), 6); + + eprintln!("{:?}", read(&adjuster.l2_pubdata_price_statistics).samples); + let expected_median_blob_base_fee = 493216 * 2; + assert_eq!( + read(&adjuster.l2_pubdata_price_statistics).samples.len(), + config.num_samples_for_blob_base_fee_estimate + ); + assert_eq!( + read(&adjuster.l2_pubdata_price_statistics).median(), + expected_median_blob_base_fee.into() + ); + + eth_client.advance_block_number(3); + adjuster.keep_updated().await.unwrap(); + + assert_eq!( + read(&adjuster.base_fee_statistics).samples.len(), + config.max_base_fee_samples + ); + assert_eq!(read(&adjuster.base_fee_statistics).median(), 7); + + let expected_median_blob_base_fee = 493216 * 3; + assert_eq!(read(&adjuster.l2_pubdata_price_statistics).samples.len(), 3); + assert_eq!( + read(&adjuster.l2_pubdata_price_statistics).median(), + expected_median_blob_base_fee.into() + ); +} diff --git a/core/node/fee_model/src/l1_gas_price/mod.rs b/core/node/fee_model/src/l1_gas_price/mod.rs index 0dab2d921c40..29db21bc1733 100644 --- a/core/node/fee_model/src/l1_gas_price/mod.rs +++ b/core/node/fee_model/src/l1_gas_price/mod.rs @@ -3,13 +3,12 @@ use std::fmt; pub use self::{ - gas_adjuster::GasAdjuster, main_node_fetcher::MainNodeFeeParamsFetcher, - singleton::GasAdjusterSingleton, + gas_adjuster::{GasAdjuster, GasAdjusterClient}, + main_node_fetcher::MainNodeFeeParamsFetcher, }; mod gas_adjuster; mod main_node_fetcher; -mod singleton; /// Abstraction that provides parameters to set the fee for an L1 transaction, taking the desired /// mining time into account. diff --git a/core/node/fee_model/src/lib.rs b/core/node/fee_model/src/lib.rs index 66a1c07a1c64..f65239912523 100644 --- a/core/node/fee_model/src/lib.rs +++ b/core/node/fee_model/src/lib.rs @@ -286,9 +286,10 @@ impl BatchFeeModelInputProvider for MockBatchFeeParamsProvider { mod tests { use std::num::NonZeroU64; + use l1_gas_price::GasAdjusterClient; use zksync_base_token_adjuster::NoOpRatioProvider; use zksync_config::{configs::eth_sender::PubdataSendingMode, GasAdjusterConfig}; - use zksync_eth_client::{clients::MockEthereum, BaseFees}; + use zksync_eth_client::{clients::MockSettlementLayer, BaseFees}; use zksync_types::{commitment::L1BatchCommitmentMode, fee_model::BaseTokenConversionRatio}; use super::*; @@ -744,19 +745,20 @@ mod tests { } // Helper function to create BaseFees. - fn base_fees(block: u64, blob: U256) -> BaseFees { + fn test_base_fees(block: u64, blob: U256, pubdata: U256) -> BaseFees { BaseFees { base_fee_per_gas: block, base_fee_per_blob_gas: blob, + l2_pubdata_price: pubdata, } } // Helper function to setup the GasAdjuster. async fn setup_gas_adjuster(l1_gas_price: u64, l1_pubdata_price: u64) -> GasAdjuster { - let mock = MockEthereum::builder() + let mock = MockSettlementLayer::builder() .with_fee_history(vec![ - base_fees(0, U256::from(4)), - base_fees(1, U256::from(3)), + test_base_fees(0, U256::from(4), U256::from(0)), + test_base_fees(1, U256::from(3), U256::from(0)), ]) .build(); mock.advance_block_number(2); // Ensure we have enough blocks for the fee history @@ -770,7 +772,7 @@ mod tests { }; GasAdjuster::new( - Box::new(mock.into_client()), + GasAdjusterClient::from_l1(Box::new(mock.into_client())), gas_adjuster_config, PubdataSendingMode::Blobs, L1BatchCommitmentMode::Rollup, diff --git a/core/node/node_framework/src/implementations/layers/gas_adjuster.rs b/core/node/node_framework/src/implementations/layers/gas_adjuster.rs index 71a4e56bbbae..229700289a71 100644 --- a/core/node/node_framework/src/implementations/layers/gas_adjuster.rs +++ b/core/node/node_framework/src/implementations/layers/gas_adjuster.rs @@ -6,7 +6,8 @@ use zksync_node_fee_model::l1_gas_price::GasAdjuster; use crate::{ implementations::resources::{ - eth_interface::EthInterfaceResource, gas_adjuster::GasAdjusterResource, + eth_interface::{EthInterfaceResource, L2InterfaceResource}, + gas_adjuster::GasAdjusterResource, }, service::StopReceiver, task::{Task, TaskId}, @@ -26,7 +27,8 @@ pub struct GasAdjusterLayer { #[derive(Debug, FromContext)] #[context(crate = crate)] pub struct Input { - pub eth_client: EthInterfaceResource, + pub eth_interface_client: EthInterfaceResource, + pub l2_inteface_client: Option, } #[derive(Debug, IntoContext)] @@ -62,7 +64,12 @@ impl WiringLayer for GasAdjusterLayer { } async fn wire(self, input: Self::Input) -> Result { - let client = input.eth_client.0; + let client = if self.gas_adjuster_config.settlement_mode.is_gateway() { + input.l2_inteface_client.unwrap().0.into() + } else { + input.eth_interface_client.0.into() + }; + let adjuster = GasAdjuster::new( client, self.gas_adjuster_config, diff --git a/core/node/node_framework/src/implementations/layers/query_eth_client.rs b/core/node/node_framework/src/implementations/layers/query_eth_client.rs index b3a9c7d4b275..116823d92d8a 100644 --- a/core/node/node_framework/src/implementations/layers/query_eth_client.rs +++ b/core/node/node_framework/src/implementations/layers/query_eth_client.rs @@ -1,10 +1,11 @@ use anyhow::Context; -use zksync_types::{url::SensitiveUrl, SLChainId}; +use zksync_types::{settlement::SettlementMode, url::SensitiveUrl, L2ChainId, SLChainId}; use zksync_web3_decl::client::Client; use crate::{ - implementations::resources::eth_interface::EthInterfaceResource, + implementations::resources::eth_interface::{EthInterfaceResource, L2InterfaceResource}, wiring_layer::{WiringError, WiringLayer}, + IntoContext, }; /// Wiring layer for Ethereum client. @@ -12,28 +13,58 @@ use crate::{ pub struct QueryEthClientLayer { chain_id: SLChainId, web3_url: SensitiveUrl, + settlement_mode: SettlementMode, } impl QueryEthClientLayer { - pub fn new(chain_id: SLChainId, web3_url: SensitiveUrl) -> Self { - Self { chain_id, web3_url } + pub fn new( + chain_id: SLChainId, + web3_url: SensitiveUrl, + settlement_mode: SettlementMode, + ) -> Self { + Self { + chain_id, + web3_url, + settlement_mode, + } } } +#[derive(Debug, IntoContext)] +#[context(crate = crate)] +pub struct Output { + query_client_l1: EthInterfaceResource, + query_client_l2: Option, +} + #[async_trait::async_trait] impl WiringLayer for QueryEthClientLayer { type Input = (); - type Output = EthInterfaceResource; + type Output = Output; fn layer_name(&self) -> &'static str { "query_eth_client_layer" } - async fn wire(self, _input: Self::Input) -> Result { - let query_client = Client::http(self.web3_url.clone()) - .context("Client::new()")? - .for_network(self.chain_id.into()) - .build(); - Ok(EthInterfaceResource(Box::new(query_client))) + async fn wire(self, _input: Self::Input) -> Result { + // Both the L1 and L2 client have the same URL, but provide different type guarantees. + Ok(Output { + query_client_l1: EthInterfaceResource(Box::new( + Client::http(self.web3_url.clone()) + .context("Client::new()")? + .for_network(self.chain_id.into()) + .build(), + )), + query_client_l2: if self.settlement_mode.is_gateway() { + Some(L2InterfaceResource(Box::new( + Client::http(self.web3_url.clone()) + .context("Client::new()")? + .for_network(L2ChainId::try_from(self.chain_id.0).unwrap().into()) + .build(), + ))) + } else { + None + }, + }) } } diff --git a/core/node/node_framework/src/implementations/resources/eth_interface.rs b/core/node/node_framework/src/implementations/resources/eth_interface.rs index cf470c0379da..5879610b75ed 100644 --- a/core/node/node_framework/src/implementations/resources/eth_interface.rs +++ b/core/node/node_framework/src/implementations/resources/eth_interface.rs @@ -1,5 +1,5 @@ use zksync_eth_client::BoundEthInterface; -use zksync_web3_decl::client::{DynClient, L1}; +use zksync_web3_decl::client::{DynClient, L1, L2}; use crate::resource::Resource; @@ -13,6 +13,20 @@ impl Resource for EthInterfaceResource { } } +/// A resource that provides L2 interface object to the service. +/// It is expected to have the same URL as the `EthInterfaceResource`, but have different capabilities. +/// +/// This resource is provided separately from `EthInterfaceResource`, to provide type safety in places, where the +/// component must work with L1-interface only and should use `EthInterfaceResource` instead. +#[derive(Debug, Clone)] +pub struct L2InterfaceResource(pub Box>); + +impl Resource for L2InterfaceResource { + fn name() -> String { + "common/l2_interface".into() + } +} + /// A resource that provides L1 interface with signing capabilities to the service. #[derive(Debug, Clone)] pub struct BoundEthInterfaceResource(pub Box); diff --git a/core/node/node_framework/src/service/mod.rs b/core/node/node_framework/src/service/mod.rs index b6bbaa2e4d28..b4cb5857bbab 100644 --- a/core/node/node_framework/src/service/mod.rs +++ b/core/node/node_framework/src/service/mod.rs @@ -153,7 +153,7 @@ impl ZkStackService { tracing::info!("Exiting the service"); - if let Some(mut observability_guard) = observability_guard.into() { + if let Some(observability_guard) = &mut observability_guard.into() { // Make sure that the shutdown happens in the `tokio` context. let _guard = self.runtime.enter(); observability_guard.shutdown(); diff --git a/core/node/node_sync/src/tree_data_fetcher/provider/mod.rs b/core/node/node_sync/src/tree_data_fetcher/provider/mod.rs index 96819101fa2b..e4f68cade6a4 100644 --- a/core/node/node_sync/src/tree_data_fetcher/provider/mod.rs +++ b/core/node/node_sync/src/tree_data_fetcher/provider/mod.rs @@ -135,7 +135,7 @@ impl L1DataProvider { /// Guesses the number of an L1 block with a `BlockCommit` event for the specified L1 batch. /// The guess is based on the L1 batch seal timestamp. async fn guess_l1_commit_block_number( - eth_client: &DynClient, + eth_client: &dyn EthInterface, l1_batch_seal_timestamp: u64, ) -> EnrichedClientResult<(U64, usize)> { let l1_batch_seal_timestamp = U256::from(l1_batch_seal_timestamp); @@ -171,7 +171,7 @@ impl L1DataProvider { /// Gets a block that should be present on L1. async fn get_block( - eth_client: &DynClient, + eth_client: &dyn EthInterface, number: web3::BlockNumber, ) -> EnrichedClientResult<(U64, U256)> { let block = eth_client.block(number.into()).await?.ok_or_else(|| { @@ -218,11 +218,9 @@ impl TreeDataProvider for L1DataProvider { let from_block = match from_block { Some(number) => number, None => { - let (approximate_block, steps) = Self::guess_l1_commit_block_number( - self.eth_client.as_ref(), - l1_batch_seal_timestamp, - ) - .await?; + let (approximate_block, steps) = + Self::guess_l1_commit_block_number(&self.eth_client, l1_batch_seal_timestamp) + .await?; tracing::debug!( number = number.0, "Guessed L1 block number for L1 batch #{number} commit in {steps} binary search steps: {approximate_block}" diff --git a/core/node/state_keeper/src/io/tests/tester.rs b/core/node/state_keeper/src/io/tests/tester.rs index 28fcbd51822e..dc5e5f345d5a 100644 --- a/core/node/state_keeper/src/io/tests/tester.rs +++ b/core/node/state_keeper/src/io/tests/tester.rs @@ -9,9 +9,12 @@ use zksync_config::{ }; use zksync_contracts::BaseSystemContracts; use zksync_dal::{ConnectionPool, Core, CoreDal}; -use zksync_eth_client::{clients::MockEthereum, BaseFees}; +use zksync_eth_client::{clients::MockSettlementLayer, BaseFees}; use zksync_multivm::vm_latest::constants::BATCH_COMPUTATIONAL_GAS_LIMIT; -use zksync_node_fee_model::{l1_gas_price::GasAdjuster, MainNodeFeeInputProvider}; +use zksync_node_fee_model::{ + l1_gas_price::{GasAdjuster, GasAdjusterClient}, + MainNodeFeeInputProvider, +}; use zksync_node_genesis::create_genesis_l1_batch; use zksync_node_test_utils::{ create_l1_batch, create_l2_block, create_l2_transaction, execute_l2_transaction, @@ -54,9 +57,12 @@ impl Tester { .map(|base_fee_per_gas| BaseFees { base_fee_per_gas, base_fee_per_blob_gas: 1.into(), // Not relevant for the test + l2_pubdata_price: 0.into(), // Not relevant for the test }) .collect(); - let eth_client = MockEthereum::builder().with_fee_history(base_fees).build(); + let eth_client = MockSettlementLayer::builder() + .with_fee_history(base_fees) + .build(); let gas_adjuster_config = GasAdjusterConfig { default_priority_fee_per_gas: 10, @@ -71,10 +77,11 @@ impl Tester { num_samples_for_blob_base_fee_estimate: 10, internal_pubdata_pricing_multiplier: 1.0, max_blob_base_fee: None, + settlement_mode: Default::default(), }; GasAdjuster::new( - Box::new(eth_client.into_client()), + GasAdjusterClient::from_l1(Box::new(eth_client.into_client())), gas_adjuster_config, PubdataSendingMode::Calldata, self.commitment_mode, diff --git a/core/tests/loadnext/src/account/tx_command_executor.rs b/core/tests/loadnext/src/account/tx_command_executor.rs index b085219060b7..2a916564fd61 100644 --- a/core/tests/loadnext/src/account/tx_command_executor.rs +++ b/core/tests/loadnext/src/account/tx_command_executor.rs @@ -1,6 +1,5 @@ use std::time::Instant; -use zksync_eth_client::EthInterface; use zksync_system_constants::MAX_L1_TRANSACTION_GAS_LIMIT; use zksync_types::{ api::{BlockNumber, TransactionReceipt}, diff --git a/core/tests/loadnext/src/executor.rs b/core/tests/loadnext/src/executor.rs index 48d90f19c1d7..a573583ed318 100644 --- a/core/tests/loadnext/src/executor.rs +++ b/core/tests/loadnext/src/executor.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::anyhow; use futures::{channel::mpsc, future, SinkExt}; -use zksync_eth_client::{EthInterface, Options}; +use zksync_eth_client::Options; use zksync_eth_signer::PrivateKeySigner; use zksync_system_constants::MAX_L1_TRANSACTION_GAS_LIMIT; use zksync_types::{ diff --git a/core/tests/loadnext/src/sdk/ethereum/mod.rs b/core/tests/loadnext/src/sdk/ethereum/mod.rs index 31fcc5269774..4b7bb00a3080 100644 --- a/core/tests/loadnext/src/sdk/ethereum/mod.rs +++ b/core/tests/loadnext/src/sdk/ethereum/mod.rs @@ -131,7 +131,7 @@ impl EthereumProvider { &self.eth_client } - pub fn query_client(&self) -> &DynClient { + pub fn query_client(&self) -> &dyn EthInterface { self.eth_client.as_ref() } diff --git a/core/tests/loadnext/src/sdk/operations/deploy_contract.rs b/core/tests/loadnext/src/sdk/operations/deploy_contract.rs index 3b4c7a5eb53f..161d156a53e9 100644 --- a/core/tests/loadnext/src/sdk/operations/deploy_contract.rs +++ b/core/tests/loadnext/src/sdk/operations/deploy_contract.rs @@ -3,10 +3,11 @@ use zksync_types::{ l2::L2Tx, transaction_request::PaymasterParams, Execute, Nonce, CONTRACT_DEPLOYER_ADDRESS, U256, }; use zksync_utils::bytecode::hash_bytecode; +use zksync_web3_decl::namespaces::EthNamespaceClient; use crate::sdk::{ error::ClientError, operations::SyncTransactionHandle, wallet::Wallet, zksync_types::fee::Fee, - EthNamespaceClient, ZksNamespaceClient, + ZksNamespaceClient, }; pub struct DeployContractBuilder<'a, S: EthereumSigner, P> {