From 761bda19844fb3935f8a57c47df39010f88ef9dc Mon Sep 17 00:00:00 2001 From: Agustin Aon <21188659+aon@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:25:16 -0300 Subject: [PATCH] feat: add state override for gas estimates (#1358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ - Adds state override for gas estimates ## Why ❔ - Solves #947 - Feature parity with geth https://github.com/ethereum/go-ethereum/issues/27800 ## Checklist - [x] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [x] Tests for the changes have been added / updated. - [x] Documentation comments have been added / updated. - [x] Code has been formatted via `zk fmt` and `zk lint`. - [x] Spellcheck has been run via `zk spellcheck`. - [x] Linkcheck has been run via `zk linkcheck`. --------- Co-authored-by: Juan Rigada <62958725+Jrigada@users.noreply.github.com> Co-authored-by: Jrigada Co-authored-by: Danil --- core/lib/state/src/lib.rs | 9 + core/lib/state/src/storage_overrides.rs | 150 +++++++++++ core/lib/state/src/storage_view.rs | 10 +- core/lib/types/src/api/mod.rs | 1 + core/lib/types/src/api/state_override.rs | 70 +++++ core/lib/types/src/transaction_request.rs | 4 +- core/lib/vm_utils/src/lib.rs | 9 +- core/lib/web3_decl/src/namespaces/eth.rs | 19 +- core/lib/web3_decl/src/namespaces/zks.rs | 16 +- .../api_server/src/execution_sandbox/apply.rs | 35 ++- .../src/execution_sandbox/execute.rs | 5 + .../api_server/src/execution_sandbox/tests.rs | 1 + .../src/execution_sandbox/validate.rs | 1 + core/node/api_server/src/tx_sender/mod.rs | 40 ++- .../web3/backend_jsonrpsee/namespaces/eth.rs | 22 +- .../web3/backend_jsonrpsee/namespaces/zks.rs | 21 +- .../api_server/src/web3/namespaces/debug.rs | 1 + .../api_server/src/web3/namespaces/eth.rs | 15 +- .../api_server/src/web3/namespaces/zks.rs | 28 +- core/node/api_server/src/web3/tests/vm.rs | 134 +++++++++- .../src/sdk/operations/deploy_contract.rs | 2 +- .../src/sdk/operations/execute_contract.rs | 2 +- .../loadnext/src/sdk/operations/transfer.rs | 2 +- core/tests/loadnext/src/sdk/wallet.rs | 2 +- .../state-override/StateOverrideTest.sol | 28 ++ .../ts-integration/tests/api/web3.test.ts | 251 +++++++++++++++++- 26 files changed, 803 insertions(+), 75 deletions(-) create mode 100644 core/lib/state/src/storage_overrides.rs create mode 100644 core/lib/types/src/api/state_override.rs create mode 100644 core/tests/ts-integration/contracts/state-override/StateOverrideTest.sol diff --git a/core/lib/state/src/lib.rs b/core/lib/state/src/lib.rs index 66577841fd4..74c60e4a369 100644 --- a/core/lib/state/src/lib.rs +++ b/core/lib/state/src/lib.rs @@ -12,6 +12,7 @@ use std::{cell::RefCell, collections::HashMap, fmt, rc::Rc}; use zksync_types::{ + api::state_override::StateOverride, get_known_code_key, storage::{StorageKey, StorageValue}, H256, @@ -29,6 +30,7 @@ pub use self::{ }, shadow_storage::ShadowStorage, storage_factory::{BatchDiff, PgOrRocksdbStorage, ReadStorageFactory, RocksdbWithMemory}, + storage_overrides::StorageOverrides, storage_view::{StorageView, StorageViewCache, StorageViewMetrics}, witness::WitnessStorage, }; @@ -40,6 +42,7 @@ mod postgres; mod rocksdb; mod shadow_storage; mod storage_factory; +mod storage_overrides; mod storage_view; #[cfg(test)] mod test_utils; @@ -89,3 +92,9 @@ pub trait WriteStorage: ReadStorage { /// Smart pointer to [`WriteStorage`]. pub type StoragePtr = Rc>; + +/// Functionality to override the storage state. +pub trait OverrideStorage { + /// Apply state override to the storage. + fn apply_state_override(&mut self, overrides: &StateOverride); +} diff --git a/core/lib/state/src/storage_overrides.rs b/core/lib/state/src/storage_overrides.rs new file mode 100644 index 00000000000..f45dd6d3382 --- /dev/null +++ b/core/lib/state/src/storage_overrides.rs @@ -0,0 +1,150 @@ +use std::{cell::RefCell, collections::HashMap, fmt, rc::Rc}; + +use zksync_types::{ + api::state_override::{OverrideState, StateOverride}, + get_code_key, get_nonce_key, + utils::{decompose_full_nonce, nonces_to_full_nonce, storage_key_for_eth_balance}, + AccountTreeId, StorageKey, StorageValue, H256, U256, +}; +use zksync_utils::{bytecode::hash_bytecode, h256_to_u256, u256_to_h256}; + +use crate::{OverrideStorage, ReadStorage}; + +/// A storage view that allows to override some of the storage values. +#[derive(Debug)] +pub struct StorageOverrides { + storage_handle: S, + overridden_factory_deps: HashMap>, + overridden_account_state: HashMap>, + overridden_account_state_diff: HashMap>, + overridden_balance: HashMap, + overridden_nonce: HashMap, + overridden_code: HashMap, +} + +impl StorageOverrides { + /// Creates a new storage view based on the underlying storage. + pub fn new(storage: S) -> Self { + Self { + storage_handle: storage, + overridden_factory_deps: HashMap::new(), + overridden_account_state: HashMap::new(), + overridden_account_state_diff: HashMap::new(), + overridden_balance: HashMap::new(), + overridden_nonce: HashMap::new(), + overridden_code: HashMap::new(), + } + } + + /// Overrides a factory dependency code. + pub fn store_factory_dep(&mut self, hash: H256, code: Vec) { + self.overridden_factory_deps.insert(hash, code); + } + + /// Overrides an account entire state. + pub fn override_account_state(&mut self, account: AccountTreeId, state: HashMap) { + self.overridden_account_state.insert(account, state); + } + + /// Overrides an account state diff. + pub fn override_account_state_diff( + &mut self, + account: AccountTreeId, + state_diff: HashMap, + ) { + self.overridden_account_state_diff + .insert(account, state_diff); + } + + /// Make a Rc RefCell ptr to the storage + pub fn to_rc_ptr(self) -> Rc> { + Rc::new(RefCell::new(self)) + } +} + +impl ReadStorage for StorageOverrides { + fn read_value(&mut self, key: &StorageKey) -> StorageValue { + if let Some(balance) = self.overridden_balance.get(key) { + return u256_to_h256(*balance); + } + if let Some(code) = self.overridden_code.get(key) { + return *code; + } + + if let Some(nonce) = self.overridden_nonce.get(key) { + return u256_to_h256(*nonce); + } + + if let Some(account_state) = self.overridden_account_state.get(key.account()) { + if let Some(value) = account_state.get(key.key()) { + return *value; + } + return H256::zero(); + } + + if let Some(account_state_diff) = self.overridden_account_state_diff.get(key.account()) { + if let Some(value) = account_state_diff.get(key.key()) { + return *value; + } + } + + self.storage_handle.read_value(key) + } + + fn is_write_initial(&mut self, key: &StorageKey) -> bool { + self.storage_handle.is_write_initial(key) + } + + fn load_factory_dep(&mut self, hash: H256) -> Option> { + self.overridden_factory_deps + .get(&hash) + .cloned() + .or_else(|| self.storage_handle.load_factory_dep(hash)) + } + + fn get_enumeration_index(&mut self, key: &StorageKey) -> Option { + self.storage_handle.get_enumeration_index(key) + } +} + +impl OverrideStorage for StorageOverrides { + fn apply_state_override(&mut self, state_override: &StateOverride) { + for (account, overrides) in state_override.iter() { + if let Some(balance) = overrides.balance { + let balance_key = storage_key_for_eth_balance(account); + self.overridden_balance.insert(balance_key, balance); + } + + if let Some(nonce) = overrides.nonce { + let nonce_key = get_nonce_key(account); + let full_nonce = self.read_value(&nonce_key); + let (_, deployment_nonce) = decompose_full_nonce(h256_to_u256(full_nonce)); + let new_full_nonce = nonces_to_full_nonce(nonce, deployment_nonce); + self.overridden_nonce.insert(nonce_key, new_full_nonce); + } + + if let Some(code) = &overrides.code { + let code_key = get_code_key(account); + let code_hash = hash_bytecode(&code.0); + self.overridden_code.insert(code_key, code_hash); + self.store_factory_dep(code_hash, code.0.clone()); + } + + match &overrides.state { + Some(OverrideState::State(state)) => { + self.override_account_state(AccountTreeId::new(*account), state.clone()); + } + Some(OverrideState::StateDiff(state_diff)) => { + for (key, value) in state_diff { + let account_state = self + .overridden_account_state_diff + .entry(AccountTreeId::new(*account)) + .or_default(); + account_state.insert(*key, *value); + } + } + None => {} + } + } + } +} diff --git a/core/lib/state/src/storage_view.rs b/core/lib/state/src/storage_view.rs index 7dcfda2ba40..4d79298101f 100644 --- a/core/lib/state/src/storage_view.rs +++ b/core/lib/state/src/storage_view.rs @@ -6,9 +6,9 @@ use std::{ time::{Duration, Instant}, }; -use zksync_types::{StorageKey, StorageValue, H256}; +use zksync_types::{api::state_override::StateOverride, StorageKey, StorageValue, H256}; -use crate::{ReadStorage, WriteStorage}; +use crate::{OverrideStorage, ReadStorage, WriteStorage}; /// Metrics for [`StorageView`]. #[derive(Debug, Default, Clone, Copy)] @@ -224,6 +224,12 @@ impl WriteStorage for StorageView { } } +impl OverrideStorage for StorageView { + fn apply_state_override(&mut self, state_override: &StateOverride) { + self.storage_handle.apply_state_override(state_override); + } +} + #[cfg(test)] mod test { use zksync_types::{AccountTreeId, Address, H256}; diff --git a/core/lib/types/src/api/mod.rs b/core/lib/types/src/api/mod.rs index a0039ba0567..751de9bd704 100644 --- a/core/lib/types/src/api/mod.rs +++ b/core/lib/types/src/api/mod.rs @@ -18,6 +18,7 @@ use crate::{ }; pub mod en; +pub mod state_override; /// Block Number #[derive(Copy, Clone, Debug, PartialEq, Display)] diff --git a/core/lib/types/src/api/state_override.rs b/core/lib/types/src/api/state_override.rs new file mode 100644 index 00000000000..5c2395ae4bf --- /dev/null +++ b/core/lib/types/src/api/state_override.rs @@ -0,0 +1,70 @@ +use std::{collections::HashMap, ops::Deref}; + +use serde::{Deserialize, Deserializer, Serialize}; +use zksync_basic_types::{web3::Bytes, H256, U256}; + +use crate::Address; + +/// Collection of overridden accounts +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StateOverride(HashMap); + +/// Account override for `eth_estimateGas`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OverrideAccount { + pub balance: Option, + pub nonce: Option, + pub code: Option, + #[serde(flatten, deserialize_with = "state_deserializer")] + pub state: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum OverrideState { + State(HashMap), + StateDiff(HashMap), +} + +fn state_deserializer<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let val = serde_json::Value::deserialize(deserializer)?; + let state: Option> = match val.get("state") { + Some(val) => serde_json::from_value(val.clone()).map_err(serde::de::Error::custom)?, + None => None, + }; + let state_diff: Option> = match val.get("stateDiff") { + Some(val) => serde_json::from_value(val.clone()).map_err(serde::de::Error::custom)?, + None => None, + }; + + match (state, state_diff) { + (Some(state), None) => Ok(Some(OverrideState::State(state))), + (None, Some(state_diff)) => Ok(Some(OverrideState::StateDiff(state_diff))), + (None, None) => Ok(None), + _ => Err(serde::de::Error::custom( + "Both 'state' and 'stateDiff' cannot be set simultaneously", + )), + } +} + +impl StateOverride { + pub fn new(state: HashMap) -> Self { + Self(state) + } + + pub fn get(&self, address: &Address) -> Option<&OverrideAccount> { + self.0.get(address) + } +} + +impl Deref for StateOverride { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/core/lib/types/src/transaction_request.rs b/core/lib/types/src/transaction_request.rs index a59b21409cd..887dfcbff37 100644 --- a/core/lib/types/src/transaction_request.rs +++ b/core/lib/types/src/transaction_request.rs @@ -400,7 +400,9 @@ impl TransactionRequest { } // returns packed eth signature if it is present - fn get_packed_signature(&self) -> Result { + pub fn get_packed_signature( + &self, + ) -> Result { let packed_v = self .v .ok_or(SerializationTransactionError::IncompleteSignature)? diff --git a/core/lib/vm_utils/src/lib.rs b/core/lib/vm_utils/src/lib.rs index 9cec0e13be8..b970d1a8c6b 100644 --- a/core/lib/vm_utils/src/lib.rs +++ b/core/lib/vm_utils/src/lib.rs @@ -8,14 +8,14 @@ use zksync_multivm::{ vm_latest::HistoryEnabled, VmInstance, }; -use zksync_state::{PostgresStorage, StoragePtr, StorageView, WriteStorage}; +use zksync_state::{PostgresStorage, StorageOverrides, StoragePtr, StorageView, WriteStorage}; use zksync_types::{L1BatchNumber, L2ChainId, Transaction}; use crate::storage::L1BatchParamsProvider; pub type VmAndStorage<'a> = ( - VmInstance>, HistoryEnabled>, - StoragePtr>>, + VmInstance>>, HistoryEnabled>, + StoragePtr>>>, ); pub fn create_vm( @@ -52,7 +52,8 @@ pub fn create_vm( let storage_l2_block_number = first_l2_block_in_batch.number() - 1; let pg_storage = PostgresStorage::new(rt_handle.clone(), connection, storage_l2_block_number, true); - let storage_view = StorageView::new(pg_storage).to_rc_ptr(); + let storage_overrides = StorageOverrides::new(pg_storage); + let storage_view = StorageView::new(storage_overrides).to_rc_ptr(); let vm = VmInstance::new(l1_batch_env, system_env, storage_view.clone()); Ok((vm, storage_view)) diff --git a/core/lib/web3_decl/src/namespaces/eth.rs b/core/lib/web3_decl/src/namespaces/eth.rs index b0e311d339b..10443443958 100644 --- a/core/lib/web3_decl/src/namespaces/eth.rs +++ b/core/lib/web3_decl/src/namespaces/eth.rs @@ -2,7 +2,10 @@ use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; use zksync_types::{ - api::{BlockId, BlockIdVariant, BlockNumber, Transaction, TransactionVariant}, + api::{ + state_override::StateOverride, BlockId, BlockIdVariant, BlockNumber, Transaction, + TransactionVariant, + }, transaction_request::CallRequest, Address, H256, }; @@ -31,10 +34,20 @@ pub trait EthNamespace { async fn chain_id(&self) -> RpcResult; #[method(name = "call")] - async fn call(&self, req: CallRequest, block: Option) -> RpcResult; + async fn call( + &self, + req: CallRequest, + block: Option, + state_override: Option, + ) -> RpcResult; #[method(name = "estimateGas")] - async fn estimate_gas(&self, req: CallRequest, _block: Option) -> RpcResult; + async fn estimate_gas( + &self, + req: CallRequest, + _block: Option, + state_override: Option, + ) -> RpcResult; #[method(name = "gasPrice")] async fn gas_price(&self) -> RpcResult; diff --git a/core/lib/web3_decl/src/namespaces/zks.rs b/core/lib/web3_decl/src/namespaces/zks.rs index b6861a9d2dd..6f443dbded6 100644 --- a/core/lib/web3_decl/src/namespaces/zks.rs +++ b/core/lib/web3_decl/src/namespaces/zks.rs @@ -5,8 +5,8 @@ use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; use zksync_types::{ api::{ - BlockDetails, BridgeAddresses, L1BatchDetails, L2ToL1LogProof, Proof, ProtocolVersion, - TransactionDetailedResult, TransactionDetails, + state_override::StateOverride, BlockDetails, BridgeAddresses, L1BatchDetails, + L2ToL1LogProof, Proof, ProtocolVersion, TransactionDetailedResult, TransactionDetails, }, fee::Fee, fee_model::{FeeParams, PubdataIndependentBatchFeeModelInput}, @@ -29,10 +29,18 @@ use crate::{ )] pub trait ZksNamespace { #[method(name = "estimateFee")] - async fn estimate_fee(&self, req: CallRequest) -> RpcResult; + async fn estimate_fee( + &self, + req: CallRequest, + state_override: Option, + ) -> RpcResult; #[method(name = "estimateGasL1ToL2")] - async fn estimate_gas_l1_to_l2(&self, req: CallRequest) -> RpcResult; + async fn estimate_gas_l1_to_l2( + &self, + req: CallRequest, + state_override: Option, + ) -> RpcResult; #[method(name = "getBridgehubContract")] async fn get_bridgehub_contract(&self) -> RpcResult>; diff --git a/core/node/api_server/src/execution_sandbox/apply.rs b/core/node/api_server/src/execution_sandbox/apply.rs index 0d607311a44..c30e5bc36c8 100644 --- a/core/node/api_server/src/execution_sandbox/apply.rs +++ b/core/node/api_server/src/execution_sandbox/apply.rs @@ -17,13 +17,16 @@ use zksync_multivm::{ vm_latest::{constants::BATCH_COMPUTATIONAL_GAS_LIMIT, HistoryDisabled}, VmInstance, }; -use zksync_state::{PostgresStorage, ReadStorage, StoragePtr, StorageView, WriteStorage}; +use zksync_state::{ + OverrideStorage, PostgresStorage, ReadStorage, StorageOverrides, StoragePtr, StorageView, + WriteStorage, +}; use zksync_system_constants::{ SYSTEM_CONTEXT_ADDRESS, SYSTEM_CONTEXT_CURRENT_L2_BLOCK_INFO_POSITION, SYSTEM_CONTEXT_CURRENT_TX_ROLLING_HASH_POSITION, ZKPORTER_IS_AVAILABLE, }; use zksync_types::{ - api, + api::{self, state_override::StateOverride}, block::{pack_block_info, unpack_block_info, L2BlockHasher}, fee_model::BatchFeeInput, get_nonce_key, @@ -38,7 +41,8 @@ use super::{ BlockArgs, TxExecutionArgs, TxSharedArgs, VmPermit, }; -type BoxedVm<'a> = Box>, HistoryDisabled>>; +type BoxedVm<'a> = + Box>>, HistoryDisabled>>; #[derive(Debug)] struct Sandbox<'a> { @@ -46,7 +50,7 @@ struct Sandbox<'a> { l1_batch_env: L1BatchEnv, execution_args: &'a TxExecutionArgs, l2_block_info_to_reset: Option, - storage_view: StorageView>, + storage_view: StorageView>>, } impl<'a> Sandbox<'a> { @@ -90,7 +94,9 @@ impl<'a> Sandbox<'a> { .context("cannot create `PostgresStorage`")? .with_caches(shared_args.caches.clone()); - let storage_view = StorageView::new(storage); + let storage_overrides = StorageOverrides::new(storage); + + let storage_view = StorageView::new(storage_overrides); let (system_env, l1_batch_env) = Self::prepare_env( shared_args, execution_args, @@ -259,7 +265,16 @@ impl<'a> Sandbox<'a> { mut self, tx: &Transaction, adjust_pubdata_price: bool, - ) -> (BoxedVm<'a>, StoragePtr>>) { + state_override: Option, + ) -> ( + BoxedVm<'a>, + StoragePtr>>>, + ) { + // Apply state override + if let Some(state_override) = state_override { + // Apply the state override + self.storage_view.apply_state_override(&state_override); + } self.setup_storage_view(tx); let protocol_version = self.system_env.version; if adjust_pubdata_price { @@ -294,9 +309,10 @@ pub(super) fn apply_vm_in_sandbox( execution_args: &TxExecutionArgs, connection_pool: &ConnectionPool, tx: Transaction, - block_args: BlockArgs, + block_args: BlockArgs, // Block arguments for the transaction. + state_override: Option, apply: impl FnOnce( - &mut VmInstance>, HistoryDisabled>, + &mut VmInstance>>, HistoryDisabled>, Transaction, ProtocolVersionId, ) -> T, @@ -321,7 +337,7 @@ pub(super) fn apply_vm_in_sandbox( block_args, ))?; let protocol_version = sandbox.system_env.version; - let (mut vm, storage_view) = sandbox.into_vm(&tx, adjust_pubdata_price); + let (mut vm, storage_view) = sandbox.into_vm(&tx, adjust_pubdata_price, state_override); SANDBOX_METRICS.sandbox[&SandboxStage::Initialization].observe(stage_started_at.elapsed()); span.exit(); @@ -331,6 +347,7 @@ pub(super) fn apply_vm_in_sandbox( tx.initiator_account(), tx.nonce().unwrap_or(Nonce(0)) ); + let execution_latency = SANDBOX_METRICS.sandbox[&SandboxStage::Execution].start(); let result = apply(&mut vm, tx, protocol_version); let vm_execution_took = execution_latency.observe(); diff --git a/core/node/api_server/src/execution_sandbox/execute.rs b/core/node/api_server/src/execution_sandbox/execute.rs index d15cf7a9143..f633b133ab0 100644 --- a/core/node/api_server/src/execution_sandbox/execute.rs +++ b/core/node/api_server/src/execution_sandbox/execute.rs @@ -17,6 +17,7 @@ use super::{ apply, testonly::MockTransactionExecutor, vm_metrics, ApiTracer, BlockArgs, TxSharedArgs, VmPermit, }; +use crate::execution_sandbox::api::state_override::StateOverride; #[derive(Debug)] pub(crate) struct TxExecutionArgs { @@ -111,6 +112,7 @@ impl TransactionExecutor { connection_pool: ConnectionPool, tx: Transaction, block_args: BlockArgs, + state_override: Option, custom_tracers: Vec, ) -> anyhow::Result { if let Self::Mock(mock_executor) = self { @@ -129,6 +131,7 @@ impl TransactionExecutor { &connection_pool, tx, block_args, + state_override, |vm, tx, _| { let storage_invocation_tracer = StorageInvocations::new(execution_args.missed_storage_invocation_limit); @@ -170,6 +173,7 @@ impl TransactionExecutor { block_args: BlockArgs, vm_execution_cache_misses_limit: Option, custom_tracers: Vec, + state_override: Option, ) -> anyhow::Result { let execution_args = TxExecutionArgs::for_eth_call( call_overrides.enforced_base_fee, @@ -189,6 +193,7 @@ impl TransactionExecutor { connection_pool, tx.into(), block_args, + state_override, custom_tracers, ) .await?; diff --git a/core/node/api_server/src/execution_sandbox/tests.rs b/core/node/api_server/src/execution_sandbox/tests.rs index e479066cacc..0a8af35597b 100644 --- a/core/node/api_server/src/execution_sandbox/tests.rs +++ b/core/node/api_server/src/execution_sandbox/tests.rs @@ -195,6 +195,7 @@ async fn test_instantiating_vm(pool: ConnectionPool, block_args: BlockArgs &pool, transaction.clone(), block_args, + None, |_, received_tx, _| { assert_eq!(received_tx, transaction); }, diff --git a/core/node/api_server/src/execution_sandbox/validate.rs b/core/node/api_server/src/execution_sandbox/validate.rs index 958fbc8a074..5e958cada66 100644 --- a/core/node/api_server/src/execution_sandbox/validate.rs +++ b/core/node/api_server/src/execution_sandbox/validate.rs @@ -72,6 +72,7 @@ impl TransactionExecutor { &connection_pool, tx, block_args, + None, |vm, tx, protocol_version| { let stage_latency = SANDBOX_METRICS.sandbox[&SandboxStage::Validation].start(); let span = tracing::debug_span!("validation").entered(); diff --git a/core/node/api_server/src/tx_sender/mod.rs b/core/node/api_server/src/tx_sender/mod.rs index 50b0be541bf..15f9271d642 100644 --- a/core/node/api_server/src/tx_sender/mod.rs +++ b/core/node/api_server/src/tx_sender/mod.rs @@ -24,6 +24,7 @@ use zksync_state_keeper::{ SequencerSealer, }; use zksync_types::{ + api::state_override::StateOverride, fee::{Fee, TransactionExecutionMetrics}, fee_model::BatchFeeInput, get_code_key, get_intrinsic_constants, @@ -385,6 +386,7 @@ impl TxSender { self.0.replica_connection_pool.clone(), tx.clone().into(), block_args, + None, vec![], ) .await?; @@ -656,6 +658,7 @@ impl TxSender { block_args: BlockArgs, base_fee: u64, vm_version: VmVersion, + state_override: Option, ) -> anyhow::Result<(VmExecutionResultAndLogs, TransactionExecutionMetrics)> { let gas_limit_with_overhead = tx_gas_limit + derive_overhead( @@ -703,6 +706,7 @@ impl TxSender { self.0.replica_connection_pool.clone(), tx.clone(), block_args, + state_override, vec![], ) .await?; @@ -733,6 +737,7 @@ impl TxSender { mut tx: Transaction, estimated_fee_scale_factor: f64, acceptable_overestimation: u64, + state_override: Option, ) -> Result { let estimation_started_at = Instant::now(); @@ -786,17 +791,25 @@ impl TxSender { ) })?; - if !tx.is_l1() - && account_code_hash == H256::zero() - && tx.execute.value > self.get_balance(&tx.initiator_account()).await? - { - tracing::info!( - "fee estimation failed on validation step. - account: {} does not have enough funds for for transferring tx.value: {}.", - &tx.initiator_account(), - tx.execute.value - ); - return Err(SubmitTxError::InsufficientFundsForTransfer); + if !tx.is_l1() && account_code_hash == H256::zero() { + let balance = match state_override + .as_ref() + .and_then(|overrides| overrides.get(&tx.initiator_account())) + .and_then(|account| account.balance) + { + Some(balance) => balance.to_owned(), + None => self.get_balance(&tx.initiator_account()).await?, + }; + + if tx.execute.value > balance { + tracing::info!( + "fee estimation failed on validation step. + account: {} does not have enough funds for for transferring tx.value: {}.", + &tx.initiator_account(), + tx.execute.value + ); + return Err(SubmitTxError::InsufficientFundsForTransfer); + } } // For L2 transactions we need a properly formatted signature @@ -836,6 +849,7 @@ impl TxSender { block_args, base_fee, protocol_version.into(), + state_override.clone(), ) .await .context("estimate_gas step failed")?; @@ -871,6 +885,7 @@ impl TxSender { block_args, base_fee, protocol_version.into(), + state_override.clone(), ) .await .context("estimate_gas step failed")?; @@ -903,6 +918,7 @@ impl TxSender { block_args, base_fee, protocol_version.into(), + state_override, ) .await .context("final estimate_gas step failed")?; @@ -973,6 +989,7 @@ impl TxSender { block_args: BlockArgs, call_overrides: CallOverrides, tx: L2Tx, + state_override: Option, ) -> Result, SubmitTxError> { let vm_permit = self.0.vm_concurrency_limiter.acquire().await; let vm_permit = vm_permit.ok_or(SubmitTxError::ServerShuttingDown)?; @@ -989,6 +1006,7 @@ impl TxSender { block_args, vm_execution_cache_misses_limit, vec![], + state_override, ) .await? .into_api_call_result() 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 c4a16b13242..ff8ce0356a0 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,7 +1,7 @@ use zksync_types::{ api::{ - Block, BlockId, BlockIdVariant, BlockNumber, Log, Transaction, TransactionId, - TransactionReceipt, TransactionVariant, + state_override::StateOverride, Block, BlockId, BlockIdVariant, BlockNumber, Log, + Transaction, TransactionId, TransactionReceipt, TransactionVariant, }, transaction_request::CallRequest, web3::{Bytes, FeeHistory, Index, SyncState}, @@ -27,14 +27,24 @@ impl EthNamespaceServer for EthNamespace { Ok(self.chain_id_impl()) } - async fn call(&self, req: CallRequest, block: Option) -> RpcResult { - self.call_impl(req, block.map(Into::into)) + async fn call( + &self, + req: CallRequest, + block: Option, + state_override: Option, + ) -> RpcResult { + self.call_impl(req, block.map(Into::into), state_override) .await .map_err(|err| self.current_method().map_err(err)) } - async fn estimate_gas(&self, req: CallRequest, block: Option) -> RpcResult { - self.estimate_gas_impl(req, block) + async fn estimate_gas( + &self, + req: CallRequest, + block: Option, + state_override: Option, + ) -> RpcResult { + self.estimate_gas_impl(req, block, state_override) .await .map_err(|err| self.current_method().map_err(err)) } diff --git a/core/node/api_server/src/web3/backend_jsonrpsee/namespaces/zks.rs b/core/node/api_server/src/web3/backend_jsonrpsee/namespaces/zks.rs index 45cb312dde6..16bbde13509 100644 --- a/core/node/api_server/src/web3/backend_jsonrpsee/namespaces/zks.rs +++ b/core/node/api_server/src/web3/backend_jsonrpsee/namespaces/zks.rs @@ -3,8 +3,9 @@ use std::collections::HashMap; use itertools::Itertools; use zksync_types::{ api::{ - ApiStorageLog, BlockDetails, BridgeAddresses, L1BatchDetails, L2ToL1LogProof, Log, Proof, - ProtocolVersion, TransactionDetailedResult, TransactionDetails, + state_override::StateOverride, ApiStorageLog, BlockDetails, BridgeAddresses, + L1BatchDetails, L2ToL1LogProof, Log, Proof, ProtocolVersion, TransactionDetailedResult, + TransactionDetails, }, fee::Fee, fee_model::{FeeParams, PubdataIndependentBatchFeeModelInput}, @@ -22,14 +23,22 @@ use crate::web3::ZksNamespace; #[async_trait] impl ZksNamespaceServer for ZksNamespace { - async fn estimate_fee(&self, req: CallRequest) -> RpcResult { - self.estimate_fee_impl(req) + async fn estimate_fee( + &self, + req: CallRequest, + state_override: Option, + ) -> RpcResult { + self.estimate_fee_impl(req, state_override) .await .map_err(|err| self.current_method().map_err(err)) } - async fn estimate_gas_l1_to_l2(&self, req: CallRequest) -> RpcResult { - self.estimate_l1_to_l2_gas_impl(req) + async fn estimate_gas_l1_to_l2( + &self, + req: CallRequest, + state_override: Option, + ) -> RpcResult { + self.estimate_l1_to_l2_gas_impl(req, state_override) .await .map_err(|err| self.current_method().map_err(err)) } diff --git a/core/node/api_server/src/web3/namespaces/debug.rs b/core/node/api_server/src/web3/namespaces/debug.rs index a2e6e2782ac..2f2d1d44cba 100644 --- a/core/node/api_server/src/web3/namespaces/debug.rs +++ b/core/node/api_server/src/web3/namespaces/debug.rs @@ -197,6 +197,7 @@ impl DebugNamespace { block_args, self.sender_config().vm_execution_cache_misses_limit, custom_tracers, + None, ) .await?; diff --git a/core/node/api_server/src/web3/namespaces/eth.rs b/core/node/api_server/src/web3/namespaces/eth.rs index 7b4710d1cd4..68030763fd6 100644 --- a/core/node/api_server/src/web3/namespaces/eth.rs +++ b/core/node/api_server/src/web3/namespaces/eth.rs @@ -3,8 +3,8 @@ use zksync_dal::{CoreDal, DalError}; use zksync_system_constants::DEFAULT_L2_TX_GAS_PER_PUBDATA_BYTE; use zksync_types::{ api::{ - BlockId, BlockNumber, GetLogsFilter, Transaction, TransactionId, TransactionReceipt, - TransactionVariant, + state_override::StateOverride, BlockId, BlockNumber, GetLogsFilter, Transaction, + TransactionId, TransactionReceipt, TransactionVariant, }, l2::{L2Tx, TransactionType}, transaction_request::CallRequest, @@ -55,6 +55,7 @@ impl EthNamespace { &self, mut request: CallRequest, block_id: Option, + state_override: Option, ) -> Result { let block_id = block_id.unwrap_or(BlockId::Number(BlockNumber::Pending)); self.current_method().set_block_id(block_id); @@ -88,7 +89,7 @@ impl EthNamespace { let call_result: Vec = self .state .tx_sender - .eth_call(block_args, call_overrides, tx) + .eth_call(block_args, call_overrides, tx, state_override) .await?; Ok(call_result.into()) } @@ -97,6 +98,7 @@ impl EthNamespace { &self, request: CallRequest, _block: Option, + state_override: Option, ) -> Result { let mut request_with_gas_per_pubdata_overridden = request; self.state @@ -138,7 +140,12 @@ impl EthNamespace { let fee = self .state .tx_sender - .get_txs_fee_in_wei(tx.into(), scale_factor, acceptable_overestimation as u64) + .get_txs_fee_in_wei( + tx.into(), + scale_factor, + acceptable_overestimation as u64, + state_override, + ) .await?; Ok(fee.gas_limit) } diff --git a/core/node/api_server/src/web3/namespaces/zks.rs b/core/node/api_server/src/web3/namespaces/zks.rs index 2b3fbbcd55c..4f88eb17e23 100644 --- a/core/node/api_server/src/web3/namespaces/zks.rs +++ b/core/node/api_server/src/web3/namespaces/zks.rs @@ -8,8 +8,8 @@ use zksync_multivm::interface::VmExecutionResultAndLogs; use zksync_system_constants::DEFAULT_L2_TX_GAS_PER_PUBDATA_BYTE; use zksync_types::{ api::{ - BlockDetails, BridgeAddresses, GetLogsFilter, L1BatchDetails, L2ToL1LogProof, Proof, - ProtocolVersion, StorageProof, TransactionDetails, + state_override::StateOverride, BlockDetails, BridgeAddresses, GetLogsFilter, + L1BatchDetails, L2ToL1LogProof, Proof, ProtocolVersion, StorageProof, TransactionDetails, }, fee::Fee, fee_model::{FeeParams, PubdataIndependentBatchFeeModelInput}, @@ -48,7 +48,11 @@ impl ZksNamespace { &self.state.current_method } - pub async fn estimate_fee_impl(&self, request: CallRequest) -> Result { + pub async fn estimate_fee_impl( + &self, + request: CallRequest, + state_override: Option, + ) -> Result { let mut request_with_gas_per_pubdata_overridden = request; self.state .set_nonce_for_call_request(&mut request_with_gas_per_pubdata_overridden) @@ -67,12 +71,13 @@ impl ZksNamespace { // not consider provided ones. tx.common_data.fee.max_priority_fee_per_gas = 0u64.into(); tx.common_data.fee.gas_per_pubdata_limit = U256::from(DEFAULT_L2_TX_GAS_PER_PUBDATA_BYTE); - self.estimate_fee(tx.into()).await + self.estimate_fee(tx.into(), state_override).await } pub async fn estimate_l1_to_l2_gas_impl( &self, request: CallRequest, + state_override: Option, ) -> Result { let mut request_with_gas_per_pubdata_overridden = request; // When we're estimating fee, we are trying to deduce values related to fee, so we should @@ -87,11 +92,15 @@ impl ZksNamespace { .try_into() .map_err(Web3Error::SerializationError)?; - let fee = self.estimate_fee(tx.into()).await?; + let fee = self.estimate_fee(tx.into(), state_override).await?; Ok(fee.gas_limit) } - async fn estimate_fee(&self, tx: Transaction) -> Result { + async fn estimate_fee( + &self, + tx: Transaction, + state_override: Option, + ) -> Result { let scale_factor = self.state.api_config.estimate_gas_scale_factor; let acceptable_overestimation = self.state.api_config.estimate_gas_acceptable_overestimation; @@ -99,7 +108,12 @@ impl ZksNamespace { Ok(self .state .tx_sender - .get_txs_fee_in_wei(tx, scale_factor, acceptable_overestimation as u64) + .get_txs_fee_in_wei( + tx, + scale_factor, + acceptable_overestimation as u64, + state_override, + ) .await?) } diff --git a/core/node/api_server/src/web3/tests/vm.rs b/core/node/api_server/src/web3/tests/vm.rs index 1bce1b732b1..61c24bcf900 100644 --- a/core/node/api_server/src/web3/tests/vm.rs +++ b/core/node/api_server/src/web3/tests/vm.rs @@ -2,6 +2,7 @@ use std::sync::atomic::{AtomicU32, Ordering}; +use api::state_override::{OverrideAccount, StateOverride}; use itertools::Itertools; use zksync_multivm::{ interface::{ExecutionResult, VmRevertReason}, @@ -63,7 +64,9 @@ impl HttpTest for CallTest { client: &DynClient, _pool: &ConnectionPool, ) -> anyhow::Result<()> { - let call_result = client.call(Self::call_request(b"pending"), None).await?; + let call_result = client + .call(Self::call_request(b"pending"), None, None) + .await?; assert_eq!(call_result.0, b"output"); let valid_block_numbers_and_calldata = [ @@ -74,7 +77,7 @@ impl HttpTest for CallTest { for (number, calldata) in valid_block_numbers_and_calldata { let number = api::BlockIdVariant::BlockNumber(number); let call_result = client - .call(Self::call_request(calldata), Some(number)) + .call(Self::call_request(calldata), Some(number), None) .await?; assert_eq!(call_result.0, b"output"); } @@ -82,7 +85,7 @@ impl HttpTest for CallTest { let invalid_block_number = api::BlockNumber::from(100); let number = api::BlockIdVariant::BlockNumber(invalid_block_number); let error = client - .call(Self::call_request(b"100"), Some(number)) + .call(Self::call_request(b"100"), Some(number), None) .await .unwrap_err(); if let ClientError::Call(error) = error { @@ -120,7 +123,7 @@ impl HttpTest for CallTestAfterSnapshotRecovery { _pool: &ConnectionPool, ) -> anyhow::Result<()> { let call_result = client - .call(CallTest::call_request(b"pending"), None) + .call(CallTest::call_request(b"pending"), None, None) .await?; assert_eq!(call_result.0, b"output"); let pending_block_number = api::BlockIdVariant::BlockNumber(api::BlockNumber::Pending); @@ -128,6 +131,7 @@ impl HttpTest for CallTestAfterSnapshotRecovery { .call( CallTest::call_request(b"pending"), Some(pending_block_number), + None, ) .await?; assert_eq!(call_result.0, b"output"); @@ -137,7 +141,7 @@ impl HttpTest for CallTestAfterSnapshotRecovery { for number in pruned_block_numbers { let number = api::BlockIdVariant::BlockNumber(number.into()); let error = client - .call(CallTest::call_request(b"pruned"), Some(number)) + .call(CallTest::call_request(b"pruned"), Some(number), None) .await .unwrap_err(); assert_pruned_block_error(&error, first_local_l2_block); @@ -147,7 +151,7 @@ impl HttpTest for CallTestAfterSnapshotRecovery { for number in first_l2_block_numbers { let number = api::BlockIdVariant::BlockNumber(number); let call_result = client - .call(CallTest::call_request(b"first"), Some(number)) + .call(CallTest::call_request(b"first"), Some(number), None) .await?; assert_eq!(call_result.0, b"output"); } @@ -499,7 +503,7 @@ impl HttpTest for TraceCallTestAfterSnapshotRecovery { for number in pruned_block_numbers { let number = api::BlockIdVariant::BlockNumber(number.into()); let error = client - .call(CallTest::call_request(b"pruned"), Some(number)) + .call(CallTest::call_request(b"pruned"), Some(number), None) .await .unwrap_err(); assert_pruned_block_error(&error, first_local_l2_block); @@ -579,7 +583,7 @@ impl HttpTest for EstimateGasTest { for threshold in [10_000, 50_000, 100_000, 1_000_000] { self.gas_limit_threshold.store(threshold, Ordering::Relaxed); let output = client - .estimate_gas(l2_transaction.clone().into(), None) + .estimate_gas(l2_transaction.clone().into(), None, None) .await?; assert!( output >= U256::from(threshold), @@ -604,10 +608,15 @@ impl HttpTest for EstimateGasTest { let mut call_request = CallRequest::from(l2_transaction); call_request.from = Some(SendRawTransactionTest::private_key().address()); call_request.value = Some(1_000_000.into()); - client.estimate_gas(call_request.clone(), None).await?; + client + .estimate_gas(call_request.clone(), None, None) + .await?; call_request.value = Some(U256::max_value()); - let error = client.estimate_gas(call_request, None).await.unwrap_err(); + let error = client + .estimate_gas(call_request, None, None) + .await + .unwrap_err(); if let ClientError::Call(error) = error { let error_msg = error.message(); assert!( @@ -630,3 +639,108 @@ async fn estimate_gas_basics() { async fn estimate_gas_after_snapshot_recovery() { test_http_server(EstimateGasTest::new(true)).await; } + +#[derive(Debug)] +struct EstimateGasWithStateOverrideTest { + gas_limit_threshold: Arc, + snapshot_recovery: bool, +} + +impl EstimateGasWithStateOverrideTest { + fn new(snapshot_recovery: bool) -> Self { + Self { + gas_limit_threshold: Arc::default(), + snapshot_recovery, + } + } +} + +#[async_trait] +impl HttpTest for EstimateGasWithStateOverrideTest { + fn storage_initialization(&self) -> StorageInitialization { + let snapshot_recovery = self.snapshot_recovery; + SendRawTransactionTest { snapshot_recovery }.storage_initialization() + } + + fn transaction_executor(&self) -> MockTransactionExecutor { + let mut tx_executor = MockTransactionExecutor::default(); + let pending_block_number = if self.snapshot_recovery { + StorageInitialization::SNAPSHOT_RECOVERY_BLOCK + 2 + } else { + L2BlockNumber(1) + }; + let gas_limit_threshold = self.gas_limit_threshold.clone(); + tx_executor.set_call_responses(move |tx, block_args| { + assert_eq!(tx.execute.calldata(), [] as [u8; 0]); + assert_eq!(tx.nonce(), Some(Nonce(0))); + assert_eq!(block_args.resolved_block_number(), pending_block_number); + + let gas_limit_threshold = gas_limit_threshold.load(Ordering::SeqCst); + if tx.gas_limit() >= U256::from(gas_limit_threshold) { + ExecutionResult::Success { output: vec![] } + } else { + ExecutionResult::Revert { + output: VmRevertReason::VmError, + } + } + }); + tx_executor + } + + async fn test( + &self, + client: &DynClient, + _pool: &ConnectionPool, + ) -> anyhow::Result<()> { + // Transaction with balance override + let l2_transaction = create_l2_transaction(10, 100); + let mut call_request = CallRequest::from(l2_transaction); + call_request.from = Some(Address::random()); + call_request.value = Some(1_000_000.into()); + + let mut state_override_map = HashMap::new(); + state_override_map.insert( + call_request.from.unwrap(), + OverrideAccount { + balance: Some(U256::max_value()), + nonce: None, + code: None, + state: None, + }, + ); + let state_override = StateOverride::new(state_override_map); + + client + .estimate_gas(call_request.clone(), None, Some(state_override)) + .await?; + + // Transaction that should fail without balance override + let l2_transaction = create_l2_transaction(10, 100); + let mut call_request = CallRequest::from(l2_transaction); + call_request.from = Some(Address::random()); + call_request.value = Some(1_000_000.into()); + + let error = client + .estimate_gas(call_request.clone(), None, None) + .await + .unwrap_err(); + + if let ClientError::Call(error) = error { + let error_msg = error.message(); + assert!( + error_msg + .to_lowercase() + .contains("insufficient balance for transfer"), + "{error_msg}" + ); + } else { + panic!("Unexpected error: {error:?}"); + } + Ok(()) + } +} + +#[tokio::test] +async fn estimate_gas_with_state_override() { + test_http_server(EstimateGasWithStateOverrideTest::new(false)).await; +} diff --git a/core/tests/loadnext/src/sdk/operations/deploy_contract.rs b/core/tests/loadnext/src/sdk/operations/deploy_contract.rs index af621249ed8..3b4c7a5eb53 100644 --- a/core/tests/loadnext/src/sdk/operations/deploy_contract.rs +++ b/core/tests/loadnext/src/sdk/operations/deploy_contract.rs @@ -155,7 +155,7 @@ where ); self.wallet .provider - .estimate_fee(l2_tx.into()) + .estimate_fee(l2_tx.into(), None) .await .map_err(Into::into) } diff --git a/core/tests/loadnext/src/sdk/operations/execute_contract.rs b/core/tests/loadnext/src/sdk/operations/execute_contract.rs index 18b93008a73..d5fe57c7b79 100644 --- a/core/tests/loadnext/src/sdk/operations/execute_contract.rs +++ b/core/tests/loadnext/src/sdk/operations/execute_contract.rs @@ -155,7 +155,7 @@ where ); self.wallet .provider - .estimate_fee(execute.into()) + .estimate_fee(execute.into(), None) .await .map_err(Into::into) } diff --git a/core/tests/loadnext/src/sdk/operations/transfer.rs b/core/tests/loadnext/src/sdk/operations/transfer.rs index 34bab615c7c..94ee3aeb608 100644 --- a/core/tests/loadnext/src/sdk/operations/transfer.rs +++ b/core/tests/loadnext/src/sdk/operations/transfer.rs @@ -181,7 +181,7 @@ where }; self.wallet .provider - .estimate_fee(l2_tx.into()) + .estimate_fee(l2_tx.into(), None) .await .map_err(Into::into) } diff --git a/core/tests/loadnext/src/sdk/wallet.rs b/core/tests/loadnext/src/sdk/wallet.rs index c46431f70f4..9d3bd73a9bf 100644 --- a/core/tests/loadnext/src/sdk/wallet.rs +++ b/core/tests/loadnext/src/sdk/wallet.rs @@ -96,7 +96,7 @@ where }; let bytes = self .provider - .call(req, Some(BlockIdVariant::BlockNumber(block_number))) + .call(req, Some(BlockIdVariant::BlockNumber(block_number)), None) .await?; if bytes.0.len() == 32 { U256::from_big_endian(&bytes.0) diff --git a/core/tests/ts-integration/contracts/state-override/StateOverrideTest.sol b/core/tests/ts-integration/contracts/state-override/StateOverrideTest.sol new file mode 100644 index 00000000000..e8d02737cc1 --- /dev/null +++ b/core/tests/ts-integration/contracts/state-override/StateOverrideTest.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +pragma solidity ^0.8.0; + +contract StateOverrideTest { + uint256 public someValue; + uint256 public anotherValue; + uint256 public initialValue = 100; + + function setValue(uint256 value) public { + someValue = value; + } + + function setAnotherValue(uint256 value) public { + anotherValue = value; + } + + function increment(uint256 value) public view returns (uint256) { + require(someValue > 0, "Initial state not set"); + return someValue + value; + } + + function sumValues() public view returns (uint256) { + require(someValue > 0 && anotherValue > 0, "Initial state not set"); + return someValue + anotherValue + initialValue; + } +} diff --git a/core/tests/ts-integration/tests/api/web3.test.ts b/core/tests/ts-integration/tests/api/web3.test.ts index 9b334488fcb..e78ec452b2f 100644 --- a/core/tests/ts-integration/tests/api/web3.test.ts +++ b/core/tests/ts-integration/tests/api/web3.test.ts @@ -19,7 +19,8 @@ const contracts = { counter: getTestContract('Counter'), events: getTestContract('Emitter'), outer: getTestContract('Outer'), - inner: getTestContract('Inner') + inner: getTestContract('Inner'), + stateOverride: getTestContract('StateOverrideTest') }; describe('web3 API compatibility tests', () => { @@ -679,13 +680,20 @@ describe('web3 API compatibility tests', () => { // There are around `0.5 * maxLogsLimit` logs in [tx1Receipt.blockNumber, tx1Receipt.blockNumber] range, // so query with such filter should succeed. - await expect(alice.provider.getLogs({ fromBlock: tx1Receipt.blockNumber, toBlock: tx1Receipt.blockNumber })) - .resolves; + await expect( + alice.provider.getLogs({ + fromBlock: tx1Receipt.blockNumber, + toBlock: tx1Receipt.blockNumber + }) + ).resolves; // There are at least `1.5 * maxLogsLimit` logs in [tx1Receipt.blockNumber, tx3Receipt.blockNumber] range, // so query with such filter should fail. await expect( - alice.provider.getLogs({ fromBlock: tx1Receipt.blockNumber, toBlock: tx3Receipt.blockNumber }) + alice.provider.getLogs({ + fromBlock: tx1Receipt.blockNumber, + toBlock: tx3Receipt.blockNumber + }) ).rejects.toThrow(`Query returned more than ${maxLogsLimit} results.`); }); @@ -961,6 +969,241 @@ describe('web3 API compatibility tests', () => { expect(txFromApi.signature.v! === 27 || 28); }); + describe('Storage override', () => { + test('Should be able to estimate_gas overriding the balance of the sender', async () => { + const balance = await alice.getBalance(); + const amount = balance + 1n; + + // Expect the transaction to be reverted without the overridden balance + await expect( + alice.provider.estimateGas({ + from: alice.address, + to: alice.address, + value: amount.toString() + }) + ).toBeRejected(); + + // Call estimate_gas overriding the balance of the sender using the eth_estimateGas endpoint + const response = await alice.provider.send('eth_estimateGas', [ + { + from: alice.address, + to: alice.address, + value: amount.toString() + }, + 'latest', + //override with the balance needed to send the transaction + { + [alice.address]: { + balance: amount.toString() + } + } + ]); + + // Assert that the response is successful + expect(response).toEqual(expect.stringMatching(HEX_VALUE_REGEX)); + }); + test('Should be able to estimate_gas overriding contract code', async () => { + // Deploy the first contract + const contract1 = await deployContract(alice, contracts.events, []); + const contract1Address = await contract1.getAddress(); + + // Deploy the second contract to extract the code that we are overriding the estimation with + const contract2 = await deployContract(alice, contracts.counter, []); + const contract2Address = await contract2.getAddress(); + + // Get the code of contract2 + const code = await alice.provider.getCode(contract2Address); + + // Get the calldata of the increment function of contract2 + const incrementFunctionData = contract2.interface.encodeFunctionData('increment', [1]); + + // Assert that the estimation fails because the increment function is not present in contract1 + expect( + alice.provider.estimateGas({ + to: contract1Address.toString(), + data: incrementFunctionData + }) + ).toBeRejected(); + + // Call estimate_gas overriding the code of contract1 with the code of contract2 using the eth_estimateGas endpoint + const response = await alice.provider.send('eth_estimateGas', [ + { + from: alice.address, + to: contract1Address.toString(), + data: incrementFunctionData + }, + 'latest', + { [contract1Address.toString()]: { code: code } } + ]); + + // Assert that the response is successful + expect(response).toEqual(expect.stringMatching(HEX_VALUE_REGEX)); + }); + + test('Should estimate gas by overriding state with State', async () => { + const contract = await deployContract(alice, contracts.stateOverride, []); + const contractAddress = await contract.getAddress(); + + const sumValuesFunctionData = contract.interface.encodeFunctionData('sumValues', []); + + // Ensure that the initial gas estimation fails due to contract requirements + await expect( + alice.provider.estimateGas({ + to: contractAddress.toString(), + data: sumValuesFunctionData + }) + ).toBeRejected(); + + // Override the entire contract state using State + const state = { + [contractAddress.toString()]: { + state: { + '0x0000000000000000000000000000000000000000000000000000000000000000': + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000001': + '0x0000000000000000000000000000000000000000000000000000000000000002' + } + } + }; + + const response = await alice.provider.send('eth_estimateGas', [ + { + from: alice.address, + to: contractAddress.toString(), + data: sumValuesFunctionData + }, + 'latest', + state + ]); + + expect(response).toEqual(expect.stringMatching(HEX_VALUE_REGEX)); + }); + + test('Should estimate gas by overriding state with StateDiff', async () => { + const contract = await deployContract(alice, contracts.stateOverride, []); + const contractAddress = await contract.getAddress(); + const incrementFunctionData = contract.interface.encodeFunctionData('increment', [1]); + + // Ensure that the initial gas estimation fails due to contract requirements + await expect( + alice.provider.estimateGas({ + to: contractAddress.toString(), + data: incrementFunctionData + }) + ).toBeRejected(); + + // Override the contract state using StateDiff + const stateDiff = { + [contractAddress.toString()]: { + stateDiff: { + '0x0000000000000000000000000000000000000000000000000000000000000000': + '0x0000000000000000000000000000000000000000000000000000000000000001' + } + } + }; + + const response = await alice.provider.send('eth_estimateGas', [ + { + from: alice.address, + to: contractAddress.toString(), + data: incrementFunctionData + }, + 'latest', + stateDiff + ]); + + expect(response).toEqual(expect.stringMatching(HEX_VALUE_REGEX)); + }); + + test('Should call and succeed with overriding state with State', async () => { + const contract = await deployContract(alice, contracts.stateOverride, []); + const contractAddress = await contract.getAddress(); + const sumValuesFunctionData = contract.interface.encodeFunctionData('sumValues', []); + + // Ensure that the initial call fails due to contract requirements + await alice.provider + .call({ + to: contractAddress.toString(), + data: sumValuesFunctionData + }) + .catch((error) => { + const errorString = 'Initial state not set'; + expect(error.message).toContain(errorString); + }); + + // Override the contract state using State + const state = { + [contractAddress.toString()]: { + state: { + '0x0000000000000000000000000000000000000000000000000000000000000000': + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000001': + '0x0000000000000000000000000000000000000000000000000000000000000002' + } + } + }; + + const response = await alice.provider.send('eth_call', [ + { + from: alice.address, + to: contractAddress.toString(), + data: sumValuesFunctionData + }, + 'latest', + state + ]); + + // The state replace the entire state of the contract, so the sum now would be + // 1 (0x1) + 2 (0x2) = 3 (0x3) + expect(response).toEqual('0x0000000000000000000000000000000000000000000000000000000000000003'); + }); + + test('Should call and succeed with overriding state with StateDiff', async () => { + const contract = await deployContract(alice, contracts.stateOverride, []); + const contractAddress = await contract.getAddress(); + const sumValuesFunctionData = contract.interface.encodeFunctionData('sumValues', []); + + // Ensure that the initial call fails due to contract requirements + await alice.provider + .call({ + to: contractAddress.toString(), + data: sumValuesFunctionData + }) + .catch((error) => { + const errorString = 'Initial state not set'; + expect(error.message).toContain(errorString); + }); + + // Override the contract state using State + const stateDiff = { + [contractAddress.toString()]: { + stateDiff: { + '0x0000000000000000000000000000000000000000000000000000000000000000': + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000001': + '0x0000000000000000000000000000000000000000000000000000000000000002' + } + } + }; + + const response = await alice.provider.send('eth_call', [ + { + from: alice.address, + to: contractAddress.toString(), + data: sumValuesFunctionData + }, + 'latest', + stateDiff + ]); + + // The stateDiff only changes the specific slots provided in the override. + // The initial value of the storage slot at key 0x2 remains unchanged, which is 100 (0x64 in hex). + // Therefore, the sum of the values at the three storage slots is: + // 1 (0x1) + 2 (0x2) + 100 (0x64) = 103 (0x67 in hex). + // This is why the expected response is 0x67. + expect(response).toEqual('0x0000000000000000000000000000000000000000000000000000000000000067'); + }); + }); // We want to be sure that correct(outer) contract address is return in the transaction receipt, // when there is a contract that initializa another contract in the constructor test('Should check inner-outer contract address in the receipt of the deploy tx', async () => {