From 1afa81aee9301039ff0dbdf8415c5f99290491e3 Mon Sep 17 00:00:00 2001 From: Vaclav Barta Date: Thu, 27 Jun 2024 21:27:16 +0200 Subject: [PATCH] feat: hardhat_reset (#302) --- SUPPORTED_APIS.md | 2 +- src/fork.rs | 111 +++++++++++++-- src/http_fork_source.rs | 4 + src/main.rs | 3 +- src/namespaces/hardhat.rs | 31 ++++ src/namespaces/mod.rs | 2 +- src/node/hardhat.rs | 11 +- src/node/in_memory.rs | 287 ++++++++++++++++++++++++-------------- src/node/in_memory_ext.rs | 67 ++++++++- src/testing.rs | 7 +- 10 files changed, 402 insertions(+), 123 deletions(-) diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md index f1d56666..221ff473 100644 --- a/SUPPORTED_APIS.md +++ b/SUPPORTED_APIS.md @@ -99,7 +99,7 @@ The `status` options are: | `HARDHAT` | `hardhat_getAutomine` | `NOT IMPLEMENTED` | Returns `true` if automatic mining is enabled, and `false` otherwise | | `HARDHAT` | `hardhat_metadata` | `NOT IMPLEMENTED` | Returns the metadata of the current network | | [`HARDHAT`](#hardhat-namespace) | [`hardhat_mine`](#hardhat_mine) | Mine any number of blocks at once, in constant time | -| `HARDHAT` | `hardhat_reset` | `NOT IMPLEMENTED` | Resets the state of the network | +| [`HARDHAT`](#hardhat-namespace) | [`hardhat_reset`] | `PARTIALLY` | Resets the state of the network; cannot revert to past block numbers, unless they're in a fork | | [`HARDHAT`](#hardhat-namespace) | [`hardhat_setBalance`](#hardhat_setbalance) | `SUPPORTED` | Modifies the balance of an account | | [`HARDHAT`](#hardhat-namespace) | [`hardhat_setCode`](#hardhat_setcode) | `SUPPORTED` | Sets the bytecode of a given account | | `HARDHAT` | `hardhat_setCoinbase` | `NOT IMPLEMENTED` | Sets the coinbase address | diff --git a/src/fork.rs b/src/fork.rs index efa71846..e652268f 100644 --- a/src/fork.rs +++ b/src/fork.rs @@ -6,7 +6,9 @@ use std::{ collections::HashMap, convert::{TryFrom, TryInto}, + fmt, future::Future, + marker::PhantomData, str::FromStr, sync::{Arc, RwLock}, }; @@ -109,12 +111,14 @@ pub struct ForkStorageInner { pub factory_dep_cache: HashMap>>, // If set - it hold the necessary information on where to fetch the data. // If not set - it will simply read from underlying storage. - pub fork: Option>, + pub fork: Option, + // ForkSource type no longer needed but retained to keep the old interface. + pub dummy: PhantomData, } impl ForkStorage { pub fn new( - fork: Option>, + fork: Option, system_contracts_options: &system_contracts::Options, ) -> Self { let chain_id = fork @@ -133,11 +137,40 @@ impl ForkStorage { value_read_cache: Default::default(), fork, factory_dep_cache: Default::default(), + dummy: Default::default(), })), chain_id, } } + pub fn get_cache_config(&self) -> Result { + let reader = self + .inner + .read() + .map_err(|e| format!("Failed to acquire read lock: {}", e))?; + let cache_config = if let Some(ref fork_details) = reader.fork { + fork_details.cache_config.clone() + } else { + CacheConfig::default() + }; + Ok(cache_config) + } + + pub fn get_fork_url(&self) -> Result { + let reader = self + .inner + .read() + .map_err(|e| format!("Failed to acquire read lock: {}", e))?; + if let Some(ref fork_details) = reader.fork { + fork_details + .fork_source + .get_fork_url() + .map_err(|e| e.to_string()) + } else { + Err("not forked".to_string()) + } + } + pub fn read_value_internal( &self, key: &StorageKey, @@ -269,6 +302,9 @@ impl ForkStorage { /// forking a remote chain. /// The method signatures are similar to methods from ETHNamespace and ZKNamespace. pub trait ForkSource { + /// Returns the forked URL. + fn get_fork_url(&self) -> eyre::Result; + /// Returns the Storage value at a given index for given address. fn get_storage_at( &self, @@ -347,11 +383,9 @@ pub trait ForkSource { } /// Holds the information about the original chain. -/// "S" is the implementation of the ForkSource. -#[derive(Debug, Clone)] -pub struct ForkDetails { - // Source of the fork data (for example HTTPForkSource) - pub fork_source: S, +pub struct ForkDetails { + // Source of the fork data (for example HttpForkSource) + pub fork_source: Box, // Block number at which we forked (the next block to create is l1_block + 1) pub l1_block: L1BatchNumber, // The actual L2 block @@ -367,6 +401,7 @@ pub struct ForkDetails { /// The factor by which to scale the gasLimit. pub estimate_gas_scale_factor: f32, pub fee_params: Option, + pub cache_config: CacheConfig, } const SUPPORTED_VERSIONS: &[ProtocolVersionId] = &[ @@ -401,7 +436,22 @@ pub fn supported_versions_to_string() -> String { versions.join(", ") } -impl ForkDetails { +impl fmt::Debug for ForkDetails { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ForkDetails") + .field("l1_block", &self.l1_block) + .field("l2_block", &self.l2_block) + .field("l2_miniblock", &self.l2_miniblock) + .field("l2_miniblock_hash", &self.l2_miniblock_hash) + .field("block_timestamp", &self.block_timestamp) + .field("overwrite_chain_id", &self.overwrite_chain_id) + .field("l1_gas_price", &self.l1_gas_price) + .field("l2_fair_gas_price", &self.l2_fair_gas_price) + .finish() + } +} + +impl ForkDetails { pub async fn from_network_and_miniblock_and_chain( network: ForkNetwork, client: Client, @@ -454,7 +504,7 @@ impl ForkDetails { let fee_params = client.get_fee_params().await.ok(); ForkDetails { - fork_source: HttpForkSource::new(url.to_owned(), cache_config), + fork_source: Box::new(HttpForkSource::new(url.to_owned(), cache_config.clone())), l1_block: l1_batch_number, l2_block: block, block_timestamp: block_details.base.timestamp, @@ -466,6 +516,7 @@ impl ForkDetails { estimate_gas_price_scale_factor, estimate_gas_scale_factor, fee_params, + cache_config, } } /// Create a fork from a given network at a given height. @@ -509,9 +560,40 @@ impl ForkDetails { ) .await } -} -impl ForkDetails { + /// Return URL and HTTP client for `hardhat_reset`. + pub fn from_url( + url: String, + fork_at: Option, + cache_config: CacheConfig, + ) -> eyre::Result { + let parsed_url = SensitiveUrl::from_str(&url)?; + let builder = match Client::http(parsed_url) { + Ok(b) => b, + Err(error) => { + return Err(eyre::Report::msg(error)); + } + }; + let client = builder.build(); + + block_on(async move { + let l2_miniblock = if let Some(fork_at) = fork_at { + fork_at + } else { + client.get_block_number().await?.as_u64() + }; + + Ok(Self::from_network_and_miniblock_and_chain( + ForkNetwork::Other(url), + client, + l2_miniblock, + None, + cache_config, + ) + .await) + }) + } + /// Return [`ForkNetwork`] and HTTP client for a given fork name. pub fn fork_network_and_client(fork: &str) -> (ForkNetwork, Client) { let network = match fork { @@ -571,6 +653,7 @@ mod tests { use zksync_types::{api::TransactionVariant, StorageKey}; use crate::{ + cache::CacheConfig, deps::InMemoryStorage, node::{ DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR, DEFAULT_ESTIMATE_GAS_SCALE_FACTOR, @@ -598,7 +681,7 @@ mod tests { let options = system_contracts::Options::default(); let fork_details = ForkDetails { - fork_source: &external_storage, + fork_source: Box::new(external_storage), l1_block: L1BatchNumber(1), l2_block: zksync_types::api::Block::::default(), l2_miniblock: 1, @@ -610,9 +693,11 @@ mod tests { estimate_gas_price_scale_factor: DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR, estimate_gas_scale_factor: DEFAULT_ESTIMATE_GAS_SCALE_FACTOR, fee_params: None, + cache_config: CacheConfig::None, }; - let mut fork_storage = ForkStorage::new(Some(fork_details), &options); + let mut fork_storage: ForkStorage = + ForkStorage::new(Some(fork_details), &options); assert!(fork_storage.is_write_initial(&never_written_key)); assert!(!fork_storage.is_write_initial(&key_with_some_value)); diff --git a/src/http_fork_source.rs b/src/http_fork_source.rs index 5880b848..275b239d 100644 --- a/src/http_fork_source.rs +++ b/src/http_fork_source.rs @@ -47,6 +47,10 @@ impl HttpForkSource { } impl ForkSource for HttpForkSource { + fn get_fork_url(&self) -> eyre::Result { + Ok(self.fork_url.clone()) + } + fn get_storage_at( &self, address: zksync_basic_types::Address, diff --git a/src/main.rs b/src/main.rs index 284850c6..0130af5b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use crate::cache::CacheConfig; +use crate::http_fork_source::HttpForkSource; use crate::node::{InMemoryNodeConfig, ShowGasDetails, ShowStorageLogs, ShowVMDetails}; use crate::observability::Observability; use crate::utils::to_human_size; @@ -395,7 +396,7 @@ async fn main() -> anyhow::Result<()> { ), } - let node = InMemoryNode::new( + let node: InMemoryNode = InMemoryNode::new( fork_details, Some(observability), InMemoryNodeConfig { diff --git a/src/namespaces/hardhat.rs b/src/namespaces/hardhat.rs index 08d2a87c..06ce26a5 100644 --- a/src/namespaces/hardhat.rs +++ b/src/namespaces/hardhat.rs @@ -1,8 +1,27 @@ use jsonrpc_derive::rpc; +use serde::{Deserialize, Serialize}; use zksync_basic_types::{Address, U256, U64}; use super::RpcResult; +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResetRequestForking { + pub json_rpc_url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub block_number: Option, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct ResetRequest { + /// The block number to reset the state to. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub to: Option, + // Forking to a specified URL. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub forking: Option, +} + #[rpc] pub trait HardhatNamespaceT { /// Sets the balance of the given address to the given balance. @@ -46,6 +65,18 @@ pub trait HardhatNamespaceT { #[rpc(name = "hardhat_mine")] fn hardhat_mine(&self, num_blocks: Option, interval: Option) -> RpcResult; + /// Reset the state of the network back to a fresh forked state, or disable forking. + /// + /// # Arguments + /// + /// * `reset_spec` - The requested state, defaults to resetting the current network. + /// + /// # Returns + /// + /// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation. + #[rpc(name = "hardhat_reset")] + fn reset_network(&self, reset_spec: Option) -> RpcResult; + /// Hardhat Network allows you to send transactions impersonating specific account and contract addresses. /// To impersonate an account use this method, passing the address to impersonate as its parameter. /// After calling this method, any transactions with this sender will be executed without verification. diff --git a/src/namespaces/mod.rs b/src/namespaces/mod.rs index 75fe8e83..760140fe 100644 --- a/src/namespaces/mod.rs +++ b/src/namespaces/mod.rs @@ -13,7 +13,7 @@ pub use debug::DebugNamespaceT; pub use eth::EthNamespaceT; pub use eth_test::EthTestNodeNamespaceT; pub use evm::EvmNamespaceT; -pub use hardhat::HardhatNamespaceT; +pub use hardhat::{HardhatNamespaceT, ResetRequest}; pub use net::NetNamespaceT; pub use web3::Web3NamespaceT; pub use zks::ZksNamespaceT; diff --git a/src/node/hardhat.rs b/src/node/hardhat.rs index fd2a66f1..2d931b03 100644 --- a/src/node/hardhat.rs +++ b/src/node/hardhat.rs @@ -3,7 +3,7 @@ use zksync_web3_decl::error::Web3Error; use crate::{ fork::ForkSource, - namespaces::{HardhatNamespaceT, RpcResult}, + namespaces::{HardhatNamespaceT, ResetRequest, RpcResult}, node::InMemoryNode, utils::{into_jsrpc_error, into_jsrpc_error_message, IntoBoxedFuture}, }; @@ -38,6 +38,15 @@ impl HardhatNam .into_boxed_future() } + fn reset_network(&self, reset_spec: Option) -> RpcResult { + self.reset_network(reset_spec) + .map_err(|err| { + tracing::error!("failed reset: {:?}", err); + into_jsrpc_error(Web3Error::InternalError(err)) + }) + .into_boxed_future() + } + fn impersonate_account(&self, address: Address) -> RpcResult { self.impersonate_account(address) .map_err(|err| { diff --git a/src/node/in_memory.rs b/src/node/in_memory.rs index d9c54864..3e149d7e 100644 --- a/src/node/in_memory.rs +++ b/src/node/in_memory.rs @@ -1,6 +1,7 @@ //! In-memory node, that supports forking other networks. use crate::{ bootloader_debug::{BootloaderDebug, BootloaderDebugTracer}, + cache::CacheConfig, console_log::ConsoleLogHandler, deps::{storage_view::StorageView, InMemoryStorage}, filters::EthFilters, @@ -333,6 +334,113 @@ type L2TxResult = ( ); impl InMemoryNodeInner { + /// Create the state to be used implementing [InMemoryNode]. + pub fn new( + fork: Option, + observability: Option, + config: InMemoryNodeConfig, + ) -> Self { + let default_l1_gas_price = if let Some(f) = &fork { + f.l1_gas_price + } else { + DEFAULT_L1_GAS_PRICE + }; + let l1_gas_price = if let Some(custom_l1_gas_price) = config.l1_gas_price { + tracing::info!( + "L1 gas price set to {} (overridden from {})", + to_human_size(custom_l1_gas_price.into()), + to_human_size(default_l1_gas_price.into()) + ); + custom_l1_gas_price + } else { + default_l1_gas_price + }; + + if let Some(f) = &fork { + let mut block_hashes = HashMap::::new(); + block_hashes.insert(f.l2_block.number.as_u64(), f.l2_block.hash); + let mut blocks = HashMap::>::new(); + blocks.insert(f.l2_block.hash, f.l2_block.clone()); + + let mut fee_input_provider = if let Some(params) = f.fee_params { + TestNodeFeeInputProvider::from_fee_params_and_estimate_scale_factors( + params, + f.estimate_gas_price_scale_factor, + f.estimate_gas_scale_factor, + ) + } else { + TestNodeFeeInputProvider::from_estimate_scale_factors( + f.estimate_gas_price_scale_factor, + f.estimate_gas_scale_factor, + ) + }; + fee_input_provider.l1_gas_price = l1_gas_price; + fee_input_provider.l2_gas_price = config.l2_fair_gas_price; + InMemoryNodeInner { + current_timestamp: f.block_timestamp, + current_batch: f.l1_block.0, + current_miniblock: f.l2_miniblock, + current_miniblock_hash: f.l2_miniblock_hash, + fee_input_provider, + tx_results: Default::default(), + blocks, + block_hashes, + filters: Default::default(), + fork_storage: ForkStorage::new(fork, &config.system_contracts_options), + show_calls: config.show_calls, + show_outputs: config.show_outputs, + show_storage_logs: config.show_storage_logs, + show_vm_details: config.show_vm_details, + show_gas_details: config.show_gas_details, + resolve_hashes: config.resolve_hashes, + console_log_handler: ConsoleLogHandler::default(), + system_contracts: SystemContracts::from_options(&config.system_contracts_options), + impersonated_accounts: Default::default(), + rich_accounts: HashSet::new(), + previous_states: Default::default(), + observability, + } + } else { + let mut block_hashes = HashMap::::new(); + let block_hash = compute_hash(0, H256::zero()); + block_hashes.insert(0, block_hash); + let mut blocks = HashMap::>::new(); + blocks.insert( + block_hash, + create_empty_block(0, NON_FORK_FIRST_BLOCK_TIMESTAMP, 0, None), + ); + + let fee_input_provider = TestNodeFeeInputProvider { + l1_gas_price, + ..Default::default() + }; + InMemoryNodeInner { + current_timestamp: NON_FORK_FIRST_BLOCK_TIMESTAMP, + current_batch: 0, + current_miniblock: 0, + current_miniblock_hash: block_hash, + fee_input_provider, + tx_results: Default::default(), + blocks, + block_hashes, + filters: Default::default(), + fork_storage: ForkStorage::new(fork, &config.system_contracts_options), + show_calls: config.show_calls, + show_outputs: config.show_outputs, + show_storage_logs: config.show_storage_logs, + show_vm_details: config.show_vm_details, + show_gas_details: config.show_gas_details, + resolve_hashes: config.resolve_hashes, + console_log_handler: ConsoleLogHandler::default(), + system_contracts: SystemContracts::from_options(&config.system_contracts_options), + impersonated_accounts: Default::default(), + rich_accounts: HashSet::new(), + previous_states: Default::default(), + observability, + } + } + } + /// Create [L1BatchEnv] to be used in the VM. /// /// We compute l1/l2 block details from storage to support fork testing, where the storage @@ -920,6 +1028,8 @@ pub struct InMemoryNode { inner: Arc>>, /// List of snapshots of the [InMemoryNodeInner]. This is bounded at runtime by [MAX_SNAPSHOTS]. pub(crate) snapshots: Arc>>, + /// Configuration option that survives reset. + system_contracts_options: system_contracts::Options, } fn contract_address_from_tx_result(execution_result: &VmExecutionResultAndLogs) -> Option { @@ -941,114 +1051,16 @@ impl Default for InMemoryNode { impl InMemoryNode { pub fn new( - fork: Option>, + fork: Option, observability: Option, config: InMemoryNodeConfig, ) -> Self { - let default_l1_gas_price = if let Some(f) = &fork { - f.l1_gas_price - } else { - DEFAULT_L1_GAS_PRICE - }; - let l1_gas_price = if let Some(custom_l1_gas_price) = config.l1_gas_price { - tracing::info!( - "L1 gas price set to {} (overridden from {})", - to_human_size(custom_l1_gas_price.into()), - to_human_size(default_l1_gas_price.into()) - ); - custom_l1_gas_price - } else { - default_l1_gas_price - }; - - let inner = if let Some(f) = &fork { - let mut block_hashes = HashMap::::new(); - block_hashes.insert(f.l2_block.number.as_u64(), f.l2_block.hash); - let mut blocks = HashMap::>::new(); - blocks.insert(f.l2_block.hash, f.l2_block.clone()); - - let mut fee_input_provider = if let Some(params) = f.fee_params { - TestNodeFeeInputProvider::from_fee_params_and_estimate_scale_factors( - params, - f.estimate_gas_price_scale_factor, - f.estimate_gas_scale_factor, - ) - } else { - TestNodeFeeInputProvider::from_estimate_scale_factors( - f.estimate_gas_price_scale_factor, - f.estimate_gas_scale_factor, - ) - }; - fee_input_provider.l1_gas_price = l1_gas_price; - fee_input_provider.l2_gas_price = config.l2_fair_gas_price; - - InMemoryNodeInner { - current_timestamp: f.block_timestamp, - current_batch: f.l1_block.0, - current_miniblock: f.l2_miniblock, - current_miniblock_hash: f.l2_miniblock_hash, - fee_input_provider, - tx_results: Default::default(), - blocks, - block_hashes, - filters: Default::default(), - fork_storage: ForkStorage::new(fork, &config.system_contracts_options), - show_calls: config.show_calls, - show_outputs: config.show_outputs, - show_storage_logs: config.show_storage_logs, - show_vm_details: config.show_vm_details, - show_gas_details: config.show_gas_details, - resolve_hashes: config.resolve_hashes, - console_log_handler: ConsoleLogHandler::default(), - system_contracts: SystemContracts::from_options(&config.system_contracts_options), - impersonated_accounts: Default::default(), - rich_accounts: HashSet::new(), - previous_states: Default::default(), - observability, - } - } else { - let mut block_hashes = HashMap::::new(); - let block_hash = compute_hash(0, H256::zero()); - block_hashes.insert(0, block_hash); - let mut blocks = HashMap::>::new(); - blocks.insert( - block_hash, - create_empty_block(0, NON_FORK_FIRST_BLOCK_TIMESTAMP, 0, None), - ); - - let fee_input_provider = TestNodeFeeInputProvider { - l1_gas_price, - ..Default::default() - }; - InMemoryNodeInner { - current_timestamp: NON_FORK_FIRST_BLOCK_TIMESTAMP, - current_batch: 0, - current_miniblock: 0, - current_miniblock_hash: block_hash, - fee_input_provider, - tx_results: Default::default(), - blocks, - block_hashes, - filters: Default::default(), - fork_storage: ForkStorage::new(fork, &config.system_contracts_options), - show_calls: config.show_calls, - show_outputs: config.show_outputs, - show_storage_logs: config.show_storage_logs, - show_vm_details: config.show_vm_details, - show_gas_details: config.show_gas_details, - resolve_hashes: config.resolve_hashes, - console_log_handler: ConsoleLogHandler::default(), - system_contracts: SystemContracts::from_options(&config.system_contracts_options), - impersonated_accounts: Default::default(), - rich_accounts: HashSet::new(), - previous_states: Default::default(), - observability, - } - }; - + let system_contracts_options = config.system_contracts_options.clone(); + let inner = InMemoryNodeInner::new(fork, observability, config); InMemoryNode { inner: Arc::new(RwLock::new(inner)), snapshots: Default::default(), + system_contracts_options, } } @@ -1056,6 +1068,72 @@ impl InMemoryNode { self.inner.clone() } + pub fn get_cache_config(&self) -> Result { + let inner = self + .inner + .read() + .map_err(|e| format!("Failed to acquire read lock: {}", e))?; + inner.fork_storage.get_cache_config() + } + + pub fn get_fork_url(&self) -> Result { + let inner = self + .inner + .read() + .map_err(|e| format!("Failed to acquire read lock: {}", e))?; + inner.fork_storage.get_fork_url() + } + + fn get_config(&self, l2_gas_price_override: Option) -> Result { + let inner = self + .inner + .read() + .map_err(|e| format!("Failed to acquire read lock: {}", e))?; + let l2_gas_price = if let Some(l2_gas_price) = l2_gas_price_override { + l2_gas_price + } else { + inner.fee_input_provider.l2_gas_price + }; + Ok(InMemoryNodeConfig { + l1_gas_price: Some(inner.fee_input_provider.l1_gas_price), + l2_fair_gas_price: l2_gas_price, + show_calls: inner.show_calls.clone(), + show_outputs: inner.show_outputs, + show_storage_logs: inner.show_storage_logs.clone(), + show_vm_details: inner.show_vm_details.clone(), + show_gas_details: inner.show_gas_details.clone(), + resolve_hashes: inner.resolve_hashes, + system_contracts_options: self.system_contracts_options.clone(), + }) + } + + pub fn reset(&self, fork: Option) -> Result<(), String> { + let observability = self + .inner + .read() + .map_err(|e| format!("Failed to acquire read lock: {}", e))? + .observability + .clone(); + + let l2_gas_price = fork.as_ref().map(|f| f.l2_fair_gas_price); + let config = self.get_config(l2_gas_price)?; + + let inner = InMemoryNodeInner::new(fork, observability, config); + + let mut writer = self + .snapshots + .write() + .map_err(|e| format!("Failed to acquire write lock: {}", e))?; + writer.clear(); + + let mut guard = self + .inner + .write() + .map_err(|e| format!("Failed to acquire write lock: {}", e))?; + *guard = inner; + Ok(()) + } + /// Applies multiple transactions - but still one per L1 batch. pub fn apply_txs(&self, txs: Vec) -> Result<(), String> { tracing::info!("Running {:?} transactions (one per batch)", txs.len()); @@ -1937,9 +2015,9 @@ mod tests { let mock_db = testing::ExternalStorage { raw_storage: external_storage.inner.read().unwrap().raw_storage.clone(), }; - let node = InMemoryNode::new( + let node: InMemoryNode = InMemoryNode::new( Some(ForkDetails { - fork_source: &mock_db, + fork_source: Box::new(mock_db), l1_block: L1BatchNumber(1), l2_block: Block::default(), l2_miniblock: 2, @@ -1951,6 +2029,7 @@ mod tests { fee_params: None, estimate_gas_price_scale_factor: DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR, estimate_gas_scale_factor: DEFAULT_ESTIMATE_GAS_SCALE_FACTOR, + cache_config: CacheConfig::default(), }), None, Default::default(), diff --git a/src/node/in_memory_ext.rs b/src/node/in_memory_ext.rs index 899d6d29..d3326367 100644 --- a/src/node/in_memory_ext.rs +++ b/src/node/in_memory_ext.rs @@ -8,7 +8,8 @@ use zksync_types::{ use zksync_utils::{h256_to_u256, u256_to_h256}; use crate::{ - fork::ForkSource, + fork::{ForkDetails, ForkSource}, + namespaces::ResetRequest, node::InMemoryNode, utils::{self, bytecode_to_factory_dep}, }; @@ -268,6 +269,53 @@ impl InMemoryNo }) } + pub fn reset_network(&self, reset_spec: Option) -> Result { + let (opt_url, block_number) = if let Some(spec) = reset_spec { + if let Some(to) = spec.to { + if spec.forking.is_some() { + return Err(anyhow!( + "Only one of 'to' and 'forking' attributes can be specified" + )); + } + let url = match self.get_fork_url() { + Ok(url) => url, + Err(error) => { + tracing::error!("For returning to past local state, mark it with `evm_snapshot`, then revert to it with `evm_revert`."); + return Err(anyhow!(error.to_string())); + } + }; + (Some(url), Some(to.as_u64())) + } else if let Some(forking) = spec.forking { + let block_number = forking.block_number.map(|n| n.as_u64()); + (Some(forking.json_rpc_url), block_number) + } else { + (None, None) + } + } else { + (None, None) + }; + + let fork_details = if let Some(url) = opt_url { + let cache_config = self.get_cache_config().map_err(|err| anyhow!(err))?; + match ForkDetails::from_url(url, block_number, cache_config) { + Ok(fd) => Some(fd), + Err(error) => { + return Err(anyhow!(error.to_string())); + } + } + } else { + None + }; + + match self.reset(fork_details) { + Ok(()) => { + tracing::info!("👷 Network reset"); + Ok(true) + } + Err(error) => Err(anyhow!(error.to_string())), + } + } + pub fn impersonate_account(&self, address: Address) -> Result { self.get_inner() .write() @@ -449,6 +497,23 @@ mod tests { } } + #[tokio::test] + async fn test_reset() { + let address = Address::from_str("0x36615Cf349d7F6344891B1e7CA7C72883F5dc049").unwrap(); + let node = InMemoryNode::::default(); + + let nonce_before = node.get_transaction_count(address, None).await.unwrap(); + + let set_result = node.set_nonce(address, U256::from(1337)).unwrap(); + assert!(set_result); + + let reset_result = node.reset_network(None).unwrap(); + assert!(reset_result); + + let nonce_after = node.get_transaction_count(address, None).await.unwrap(); + assert_eq!(nonce_before, nonce_after); + } + #[tokio::test] async fn test_impersonate_account() { let node = InMemoryNode::::default(); diff --git a/src/testing.rs b/src/testing.rs index 00a57eec..4431b5b7 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -11,6 +11,7 @@ use crate::{fork::ForkSource, node::compute_hash}; use ethabi::{ParamType, Token}; use ethers::contract; +use eyre::eyre; use httptest::{ matchers::{eq, json_decoded, request}, responders::json_encoded, @@ -729,7 +730,11 @@ pub struct ExternalStorage { pub raw_storage: InMemoryStorage, } -impl ForkSource for &ExternalStorage { +impl ForkSource for ExternalStorage { + fn get_fork_url(&self) -> eyre::Result { + Err(eyre!("Not implemented")) + } + fn get_storage_at( &self, address: H160,