diff --git a/Cargo.lock b/Cargo.lock index 42ae4c5f..aca1d1c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,6 +436,7 @@ dependencies = [ "http 1.1.0", "jsonrpsee", "thiserror", + "tokio", "tower 0.4.13", "tower-http", "tracing", @@ -468,6 +469,7 @@ dependencies = [ "anvil_zksync_config", "anvil_zksync_types", "anyhow", + "backon", "chrono", "colored", "ethabi 16.0.0", @@ -482,6 +484,7 @@ dependencies = [ "lazy_static", "maplit", "once_cell", + "pin-project", "reqwest 0.11.27", "rustc-hash 1.1.0", "serde", @@ -808,6 +811,17 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backon" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5289ec98f68f28dd809fd601059e6aa908bb8f6108620930828283d4ee23d7" +dependencies = [ + "fastrand", + "gloo-timers 0.3.0", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -2589,7 +2603,7 @@ version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" dependencies = [ - "gloo-timers", + "gloo-timers 0.2.6", "send_wrapper 0.4.0", ] @@ -2694,6 +2708,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gloo-utils" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 536e335f..b3118a3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ itertools = "0.13" jsonrpsee = "0.23" lazy_static = "1.4" once_cell = "1.7" +pin-project = "1.1.7" rand = "0.8" reqwest = { version = "0.11", features = ["blocking"] } rustc-hash = "1.1.0" @@ -82,6 +83,7 @@ maplit = "1.0.2" zksync-web3-rs = "0.1.1" ethers = { version = "2.0.4", features = ["rustls"] } test-case = "3.3.1" +backon = "1.3.0" ######################### # Local dependencies # diff --git a/crates/api_decl/src/namespaces/anvil.rs b/crates/api_decl/src/namespaces/anvil.rs index 0f8d3ef8..201f8d99 100644 --- a/crates/api_decl/src/namespaces/anvil.rs +++ b/crates/api_decl/src/namespaces/anvil.rs @@ -18,7 +18,7 @@ pub trait AnvilNamespace { /// # Returns /// Buffer representing the chain state. #[method(name = "dumpState", aliases = ["hardhat_dumpState"])] - fn dump_state(&self, preserve_historical_states: Option) -> RpcResult; + async fn dump_state(&self, preserve_historical_states: Option) -> RpcResult; /// Append chain state buffer to current chain. Will overwrite any conflicting addresses or /// storage. @@ -30,14 +30,14 @@ pub trait AnvilNamespace { /// # Returns /// `true` if a snapshot was reverted, otherwise `false`. #[method(name = "loadState", aliases = ["hardhat_loadState"])] - fn load_state(&self, bytes: Bytes) -> RpcResult; + async fn load_state(&self, bytes: Bytes) -> RpcResult; /// Mines a single block in the same way as `evm_mine` but returns extra fields. /// /// # Returns /// Freshly mined block's representation along with extra fields. #[method(name = "mine_detailed", aliases = ["evm_mine_detailed"])] - fn mine_detailed(&self) -> RpcResult>; + async fn mine_detailed(&self) -> RpcResult>; /// Sets the fork RPC url. Assumes the underlying chain is the same as before. /// @@ -45,7 +45,7 @@ pub trait AnvilNamespace { /// /// * `url` - Fork's new URL #[method(name = "setRpcUrl")] - fn set_rpc_url(&self, url: String) -> RpcResult<()>; + async fn set_rpc_url(&self, url: String) -> RpcResult<()>; /// Sets the base fee of the next block. /// @@ -53,7 +53,7 @@ pub trait AnvilNamespace { /// /// * `base_fee` - Value to be set as base fee for the next block #[method(name = "setNextBlockBaseFeePerGas", aliases = ["hardhat_setNextBlockBaseFeePerGas"])] - fn set_next_block_base_fee_per_gas(&self, base_fee: U256) -> RpcResult<()>; + async fn set_next_block_base_fee_per_gas(&self, base_fee: U256) -> RpcResult<()>; /// Removes a transaction from the pool. /// @@ -64,11 +64,11 @@ pub trait AnvilNamespace { /// # Returns /// `Some(hash)` if transaction was in the pool before being removed, `None` otherwise #[method(name = "dropTransaction", aliases = ["hardhat_dropTransaction"])] - fn drop_transaction(&self, hash: H256) -> RpcResult>; + async fn drop_transaction(&self, hash: H256) -> RpcResult>; /// Remove all transactions from the pool. #[method(name = "dropAllTransactions", aliases = ["hardhat_dropAllTransactions"])] - fn drop_all_transactions(&self) -> RpcResult<()>; + async fn drop_all_transactions(&self) -> RpcResult<()>; /// Remove all transactions from the pool by sender address. /// @@ -76,14 +76,14 @@ pub trait AnvilNamespace { /// /// * `address` - Sender which transactions should be removed from the pool #[method(name = "removePoolTransactions")] - fn remove_pool_transactions(&self, address: Address) -> RpcResult<()>; + async fn remove_pool_transactions(&self, address: Address) -> RpcResult<()>; /// Gets node's auto mining status. /// /// # Returns /// `true` if auto mining is enabled, `false` otherwise #[method(name = "getAutomine", aliases = ["hardhat_getAutomine"])] - fn get_auto_mine(&self) -> RpcResult; + async fn get_auto_mine(&self) -> RpcResult; /// Enables or disables, based on the single boolean argument, the automatic mining of new /// blocks with each new transaction submitted to the network. @@ -92,7 +92,7 @@ pub trait AnvilNamespace { /// /// * `enable` - if `true` automatic mining will be enabled, disabled otherwise #[method(name = "setAutomine", aliases = ["evm_setAutomine"])] - fn set_auto_mine(&self, enable: bool) -> RpcResult<()>; + async fn set_auto_mine(&self, enable: bool) -> RpcResult<()>; /// Sets the mining behavior to interval with the given interval (seconds). /// @@ -100,7 +100,7 @@ pub trait AnvilNamespace { /// /// * `seconds` - Frequency of automatic block production (in seconds) #[method(name = "setIntervalMining", aliases = ["evm_setIntervalMining"])] - fn set_interval_mining(&self, seconds: u64) -> RpcResult<()>; + async fn set_interval_mining(&self, seconds: u64) -> RpcResult<()>; /// Sets the block timestamp interval. All future blocks' timestamps will /// have the provided amount of seconds in-between of them. Does not affect @@ -110,14 +110,14 @@ pub trait AnvilNamespace { /// /// * `seconds` - The interval between two consecutive blocks (in seconds) #[method(name = "setBlockTimestampInterval")] - fn set_block_timestamp_interval(&self, seconds: u64) -> RpcResult<()>; + async fn set_block_timestamp_interval(&self, seconds: u64) -> RpcResult<()>; /// Removes the block timestamp interval if it exists. /// /// # Returns /// `true` if an existing interval was removed, `false` otherwise #[method(name = "removeBlockTimestampInterval")] - fn remove_block_timestamp_interval(&self) -> RpcResult; + async fn remove_block_timestamp_interval(&self) -> RpcResult; /// Set the minimum gas price for the node. Unsupported for ZKsync as it is only relevant for /// pre-EIP1559 chains. @@ -126,7 +126,7 @@ pub trait AnvilNamespace { /// /// * `gas` - The minimum gas price to be set #[method(name = "setMinGasPrice", aliases = ["hardhat_setMinGasPrice"])] - fn set_min_gas_price(&self, gas: U256) -> RpcResult<()>; + async fn set_min_gas_price(&self, gas: U256) -> RpcResult<()>; /// Enable or disable logging. /// @@ -134,7 +134,7 @@ pub trait AnvilNamespace { /// /// * `enable` - if `true` logging will be enabled, disabled otherwise #[method(name = "setLoggingEnabled", aliases = ["hardhat_setLoggingEnabled"])] - fn set_logging_enabled(&self, enable: bool) -> RpcResult<()>; + async fn set_logging_enabled(&self, enable: bool) -> RpcResult<()>; /// Snapshot the state of the blockchain at the current block. Takes no parameters. Returns the id of the snapshot /// that was created. A snapshot can only be reverted once. After a successful `anvil_revert`, the same snapshot id cannot @@ -144,7 +144,7 @@ pub trait AnvilNamespace { /// # Returns /// The `U64` identifier for this snapshot. #[method(name = "snapshot", aliases = ["evm_snapshot"])] - fn snapshot(&self) -> RpcResult; + async fn snapshot(&self) -> RpcResult; /// Revert the state of the blockchain to a previous snapshot. Takes a single parameter, /// which is the snapshot id to revert to. This deletes the given snapshot, as well as any snapshots @@ -157,7 +157,7 @@ pub trait AnvilNamespace { /// # Returns /// `true` if a snapshot was reverted, otherwise `false`. #[method(name = "revert", aliases = ["evm_revert"])] - fn revert(&self, id: U64) -> RpcResult; + async fn revert(&self, id: U64) -> RpcResult; /// Set the current timestamp for the node. /// Warning: This will allow you to move backwards in time, which may cause new blocks to appear to be @@ -170,7 +170,7 @@ pub trait AnvilNamespace { /// # Returns /// The difference between the current timestamp and the new timestamp. #[method(name = "setTime", aliases = ["evm_setTime"])] - fn set_time(&self, timestamp: Numeric) -> RpcResult; + async fn set_time(&self, timestamp: Numeric) -> RpcResult; /// Increase the current timestamp for the node /// @@ -181,7 +181,7 @@ pub trait AnvilNamespace { /// # Returns /// The applied time delta to the current timestamp in seconds. #[method(name = "increaseTime", aliases = ["evm_increaseTime"])] - fn increase_time(&self, seconds: Numeric) -> RpcResult; + async fn increase_time(&self, seconds: Numeric) -> RpcResult; /// Set timestamp for the next block. The timestamp must be in future. /// @@ -189,7 +189,7 @@ pub trait AnvilNamespace { /// /// * `timestamp` - The timestamp to set the time to #[method(name = "setNextBlockTimestamp", aliases = ["evm_setNextBlockTimestamp"])] - fn set_next_block_timestamp(&self, timestamp: Numeric) -> RpcResult<()>; + async fn set_next_block_timestamp(&self, timestamp: Numeric) -> RpcResult<()>; /// Sets auto impersonation status. /// @@ -201,7 +201,7 @@ pub trait AnvilNamespace { /// /// A `BoxFuture` containing a `Result` representing the success of the operation. #[method(name = "autoImpersonateAccount", aliases = ["hardhat_autoImpersonateAccount"])] - fn auto_impersonate_account(&self, enabled: bool) -> RpcResult<()>; + async fn auto_impersonate_account(&self, enabled: bool) -> RpcResult<()>; /// Sets the balance of the given address to the given balance. /// @@ -214,7 +214,7 @@ pub trait AnvilNamespace { /// /// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation. #[method(name = "setBalance", aliases = ["hardhat_setBalance"])] - fn set_balance(&self, address: Address, balance: U256) -> RpcResult; + async fn set_balance(&self, address: Address, balance: U256) -> RpcResult; /// Modifies an account's nonce by overwriting it. /// @@ -227,7 +227,7 @@ pub trait AnvilNamespace { /// /// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation. #[method(name = "setNonce", aliases = ["hardhat_setNonce", "evm_setAccountNonce"])] - fn set_nonce(&self, address: Address, nonce: U256) -> RpcResult; + async fn set_nonce(&self, address: Address, nonce: U256) -> RpcResult; /// Sometimes you may want to advance the latest block number of the network by a large number of blocks. /// One way to do this would be to call the evm_mine RPC method multiple times, but this is too slow if you want to mine thousands of blocks. @@ -242,7 +242,7 @@ pub trait AnvilNamespace { /// /// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation. #[method(name = "mine", aliases = ["hardhat_mine"])] - fn anvil_mine(&self, num_blocks: Option, interval: Option) -> RpcResult<()>; + async fn anvil_mine(&self, num_blocks: Option, interval: Option) -> RpcResult<()>; /// Reset the state of the network back to a fresh forked state, or disable forking. /// @@ -254,7 +254,7 @@ pub trait AnvilNamespace { /// /// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation. #[method(name = "reset", aliases = ["hardhat_reset"])] - fn reset_network(&self, reset_spec: Option) -> RpcResult; + async fn reset_network(&self, reset_spec: Option) -> RpcResult; /// anvil-zksync allows transactions impersonating specific account and contract addresses. /// To impersonate an account use this method, passing the address to impersonate as its parameter. @@ -269,7 +269,7 @@ pub trait AnvilNamespace { /// /// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation. #[method(name = "impersonateAccount", aliases = ["hardhat_impersonateAccount"])] - fn impersonate_account(&self, address: Address) -> RpcResult<()>; + async fn impersonate_account(&self, address: Address) -> RpcResult<()>; /// Use this method to stop impersonating an account after having previously used `anvil_impersonateAccount` /// The method returns `true` if the account was being impersonated and `false` otherwise. @@ -282,7 +282,7 @@ pub trait AnvilNamespace { /// /// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation. #[method(name = "stopImpersonatingAccount", aliases = ["hardhat_stopImpersonatingAccount"])] - fn stop_impersonating_account(&self, address: Address) -> RpcResult<()>; + async fn stop_impersonating_account(&self, address: Address) -> RpcResult<()>; /// Modifies the bytecode stored at an account's address. /// @@ -295,7 +295,7 @@ pub trait AnvilNamespace { /// /// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation. #[method(name = "setCode", aliases = ["hardhat_setCode"])] - fn set_code(&self, address: Address, code: String) -> RpcResult<()>; + async fn set_code(&self, address: Address, code: String) -> RpcResult<()>; /// Directly modifies the storage of a contract at a specified slot. /// @@ -309,7 +309,7 @@ pub trait AnvilNamespace { /// /// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation. #[method(name = "setStorageAt", aliases = ["hardhat_setStorageAt"])] - fn set_storage_at(&self, address: Address, slot: U256, value: U256) -> RpcResult; + async fn set_storage_at(&self, address: Address, slot: U256, value: U256) -> RpcResult; /// Sets the chain id. /// @@ -317,5 +317,5 @@ pub trait AnvilNamespace { /// /// * `id` - The chain id to be set. #[method(name = "setChainId")] - fn set_chain_id(&self, id: u32) -> RpcResult<()>; + async fn set_chain_id(&self, id: u32) -> RpcResult<()>; } diff --git a/crates/api_decl/src/namespaces/config.rs b/crates/api_decl/src/namespaces/config.rs index 331bea01..43a13819 100644 --- a/crates/api_decl/src/namespaces/config.rs +++ b/crates/api_decl/src/namespaces/config.rs @@ -9,21 +9,21 @@ pub trait ConfigNamespace { /// # Returns /// The current `show_calls` value for the InMemoryNodeInner. #[method(name = "getShowCalls")] - fn get_show_calls(&self) -> RpcResult; + async fn get_show_calls(&self) -> RpcResult; /// Get the InMemoryNodeInner's show_outputs property as a boolean /// /// # Returns /// The current `show_outputs` value for the InMemoryNodeInner. #[method(name = "getShowOutputs")] - fn get_show_outputs(&self) -> RpcResult; + async fn get_show_outputs(&self) -> RpcResult; /// Get the InMemoryNodeInner's current_timestamp property /// /// # Returns /// The current `current_timestamp` value for the InMemoryNodeInner. #[method(name = "getCurrentTimestamp")] - fn get_current_timestamp(&self) -> RpcResult; + async fn get_current_timestamp(&self) -> RpcResult; /// Set show_calls for the InMemoryNodeInner /// @@ -33,7 +33,7 @@ pub trait ConfigNamespace { /// # Returns /// The updated/current `show_calls` value for the InMemoryNodeInner. #[method(name = "setShowCalls")] - fn set_show_calls(&self, value: ShowCalls) -> RpcResult; + async fn set_show_calls(&self, value: ShowCalls) -> RpcResult; /// Set show_outputs for the InMemoryNodeInner /// @@ -43,7 +43,7 @@ pub trait ConfigNamespace { /// # Returns /// The updated/current `show_outputs` value for the InMemoryNodeInner. #[method(name = "setShowOutputs")] - fn set_show_outputs(&self, value: bool) -> RpcResult; + async fn set_show_outputs(&self, value: bool) -> RpcResult; /// Set show_storage_logs for the InMemoryNodeInner /// @@ -53,7 +53,7 @@ pub trait ConfigNamespace { /// # Returns /// The updated/current `show_storage_logs` value for the InMemoryNodeInner. #[method(name = "setShowStorageLogs")] - fn set_show_storage_logs(&self, value: ShowStorageLogs) -> RpcResult; + async fn set_show_storage_logs(&self, value: ShowStorageLogs) -> RpcResult; /// Set show_vm_details for the InMemoryNodeInner /// @@ -63,7 +63,7 @@ pub trait ConfigNamespace { /// # Returns /// The updated/current `show_vm_details` value for the InMemoryNodeInner. #[method(name = "setShowVmDetails")] - fn set_show_vm_details(&self, value: ShowVMDetails) -> RpcResult; + async fn set_show_vm_details(&self, value: ShowVMDetails) -> RpcResult; /// Set show_gas_details for the InMemoryNodeInner /// @@ -73,7 +73,7 @@ pub trait ConfigNamespace { /// # Returns /// The updated/current `show_gas_details` value for the InMemoryNodeInner. #[method(name = "setShowGasDetails")] - fn set_show_gas_details(&self, value: ShowGasDetails) -> RpcResult; + async fn set_show_gas_details(&self, value: ShowGasDetails) -> RpcResult; /// Set resolve_hashes for the InMemoryNodeInner /// @@ -83,7 +83,7 @@ pub trait ConfigNamespace { /// # Returns /// The updated `resolve_hashes` value for the InMemoryNodeInner. #[method(name = "setResolveHashes")] - fn set_resolve_hashes(&self, value: bool) -> RpcResult; + async fn set_resolve_hashes(&self, value: bool) -> RpcResult; /// Set show_node_config for the InMemoryNodeInner /// @@ -93,7 +93,7 @@ pub trait ConfigNamespace { /// # Returns /// The updated/current `show_node_config` value for the InMemoryNodeInner. #[method(name = "setShowNodeConfig")] - fn set_show_node_config(&self, value: bool) -> RpcResult; + async fn set_show_node_config(&self, value: bool) -> RpcResult; /// Set show_tx_summary for the InMemoryNodeInner /// @@ -103,7 +103,7 @@ pub trait ConfigNamespace { /// # Returns /// The updated/current `show_tx_summary` value for the InMemoryNodeInner. #[method(name = "setShowTxSummary")] - fn set_show_tx_summary(&self, value: bool) -> RpcResult; + async fn set_show_tx_summary(&self, value: bool) -> RpcResult; /// Set show_event_logs for the InMemoryNodeInner /// @@ -113,7 +113,7 @@ pub trait ConfigNamespace { /// # Returns /// The updated/current `show_event_logs` value for the InMemoryNodeInner. #[method(name = "setShowEventLogs")] - fn set_show_event_logs(&self, value: bool) -> RpcResult; + async fn set_show_event_logs(&self, value: bool) -> RpcResult; /// Set disable_console_log for the InMemoryNodeInner /// @@ -123,7 +123,7 @@ pub trait ConfigNamespace { /// # Returns /// The updated/current `disable_console_log` value for the InMemoryNodeInner. #[method(name = "setDisableConsoleLog")] - fn set_disable_console_log(&self, value: bool) -> RpcResult; + async fn set_disable_console_log(&self, value: bool) -> RpcResult; /// Set the logging for the InMemoryNodeInner /// @@ -133,7 +133,7 @@ pub trait ConfigNamespace { /// # Returns /// `true` if the operation succeeded, `false` otherwise. #[method(name = "setLogLevel")] - fn set_log_level(&self, level: LogLevel) -> RpcResult; + async fn set_log_level(&self, level: LogLevel) -> RpcResult; /// Set the logging for the InMemoryNodeInner /// @@ -146,5 +146,5 @@ pub trait ConfigNamespace { /// # Returns /// `true` if the operation succeeded, `false` otherwise. #[method(name = "setLogging")] - fn set_logging(&self, directive: String) -> RpcResult; + async fn set_logging(&self, directive: String) -> RpcResult; } diff --git a/crates/api_decl/src/namespaces/evm.rs b/crates/api_decl/src/namespaces/evm.rs index 76bee828..600e2e0f 100644 --- a/crates/api_decl/src/namespaces/evm.rs +++ b/crates/api_decl/src/namespaces/evm.rs @@ -10,5 +10,5 @@ pub trait EvmNamespace { /// # Returns /// The string "0x0". #[method(name = "mine")] - fn mine(&self) -> RpcResult; + async fn mine(&self) -> RpcResult; } diff --git a/crates/api_server/Cargo.toml b/crates/api_server/Cargo.toml index 8e592dad..939af2d6 100644 --- a/crates/api_server/Cargo.toml +++ b/crates/api_server/Cargo.toml @@ -23,6 +23,7 @@ hex.workspace = true http.workspace = true jsonrpsee.workspace = true thiserror.workspace = true +tokio.workspace = true tower.workspace = true tower-http.workspace = true tracing.workspace = true diff --git a/crates/api_server/src/impls/anvil.rs b/crates/api_server/src/impls/anvil.rs index 43c2c9b6..58829fea 100644 --- a/crates/api_server/src/impls/anvil.rs +++ b/crates/api_server/src/impls/anvil.rs @@ -3,7 +3,7 @@ use anvil_zksync_api_decl::AnvilNamespaceServer; use anvil_zksync_core::node::InMemoryNode; use anvil_zksync_types::api::{DetailedTransaction, ResetRequest}; use anvil_zksync_types::Numeric; -use jsonrpsee::core::RpcResult; +use jsonrpsee::core::{async_trait, RpcResult}; use zksync_types::api::Block; use zksync_types::web3::Bytes; use zksync_types::{Address, H256, U256, U64}; @@ -18,157 +18,167 @@ impl AnvilNamespace { } } +#[async_trait] impl AnvilNamespaceServer for AnvilNamespace { - fn dump_state(&self, preserve_historical_states: Option) -> RpcResult { + async fn dump_state(&self, preserve_historical_states: Option) -> RpcResult { Ok(self .node .dump_state(preserve_historical_states.unwrap_or(false)) + .await .map_err(RpcError::from)?) } - fn load_state(&self, bytes: Bytes) -> RpcResult { - Ok(self.node.load_state(bytes).map_err(RpcError::from)?) + async fn load_state(&self, bytes: Bytes) -> RpcResult { + Ok(self.node.load_state(bytes).await.map_err(RpcError::from)?) } - fn mine_detailed(&self) -> RpcResult> { - Ok(self.node.mine_detailed().map_err(RpcError::from)?) + async fn mine_detailed(&self) -> RpcResult> { + Ok(self.node.mine_detailed().await.map_err(RpcError::from)?) } - fn set_rpc_url(&self, url: String) -> RpcResult<()> { - Ok(self.node.set_rpc_url(url).map_err(RpcError::from)?) + async fn set_rpc_url(&self, url: String) -> RpcResult<()> { + Ok(self.node.set_rpc_url(url).await.map_err(RpcError::from)?) } - fn set_next_block_base_fee_per_gas(&self, base_fee: U256) -> RpcResult<()> { + async fn set_next_block_base_fee_per_gas(&self, base_fee: U256) -> RpcResult<()> { Ok(self .node .set_next_block_base_fee_per_gas(base_fee) + .await .map_err(RpcError::from)?) } - fn drop_transaction(&self, hash: H256) -> RpcResult> { + async fn drop_transaction(&self, hash: H256) -> RpcResult> { Ok(self.node.drop_transaction(hash).map_err(RpcError::from)?) } - fn drop_all_transactions(&self) -> RpcResult<()> { + async fn drop_all_transactions(&self) -> RpcResult<()> { Ok(self.node.drop_all_transactions().map_err(RpcError::from)?) } - fn remove_pool_transactions(&self, address: Address) -> RpcResult<()> { + async fn remove_pool_transactions(&self, address: Address) -> RpcResult<()> { Ok(self .node .remove_pool_transactions(address) .map_err(RpcError::from)?) } - fn get_auto_mine(&self) -> RpcResult { + async fn get_auto_mine(&self) -> RpcResult { Ok(self.node.get_immediate_sealing().map_err(RpcError::from)?) } - fn set_auto_mine(&self, enable: bool) -> RpcResult<()> { + async fn set_auto_mine(&self, enable: bool) -> RpcResult<()> { Ok(self .node .set_immediate_sealing(enable) + .await .map_err(RpcError::from)?) } - fn set_interval_mining(&self, seconds: u64) -> RpcResult<()> { + async fn set_interval_mining(&self, seconds: u64) -> RpcResult<()> { Ok(self .node .set_interval_sealing(seconds) + .await .map_err(RpcError::from)?) } - fn set_block_timestamp_interval(&self, seconds: u64) -> RpcResult<()> { + async fn set_block_timestamp_interval(&self, seconds: u64) -> RpcResult<()> { Ok(self .node .set_block_timestamp_interval(seconds) + .await .map_err(RpcError::from)?) } - fn remove_block_timestamp_interval(&self) -> RpcResult { + async fn remove_block_timestamp_interval(&self) -> RpcResult { Ok(self .node .remove_block_timestamp_interval() + .await .map_err(RpcError::from)?) } - fn set_min_gas_price(&self, _gas: U256) -> RpcResult<()> { + async fn set_min_gas_price(&self, _gas: U256) -> RpcResult<()> { tracing::info!( "Setting minimum gas price is unsupported as ZKsync is a post-EIP1559 chain" ); Err(RpcError::Unsupported.into()) } - fn set_logging_enabled(&self, enable: bool) -> RpcResult<()> { + async fn set_logging_enabled(&self, enable: bool) -> RpcResult<()> { Ok(self .node .set_logging_enabled(enable) .map_err(RpcError::from)?) } - fn snapshot(&self) -> RpcResult { - Ok(self.node.snapshot().map_err(RpcError::from)?) + async fn snapshot(&self) -> RpcResult { + Ok(self.node.snapshot().await.map_err(RpcError::from)?) } - fn revert(&self, id: U64) -> RpcResult { - Ok(self.node.revert_snapshot(id).map_err(RpcError::from)?) + async fn revert(&self, id: U64) -> RpcResult { + Ok(self + .node + .revert_snapshot(id) + .await + .map_err(RpcError::from)?) } - fn set_time(&self, timestamp: Numeric) -> RpcResult { + async fn set_time(&self, timestamp: Numeric) -> RpcResult { Ok(self .node .set_time(timestamp.into()) + .await .map_err(RpcError::from)?) } - fn increase_time(&self, seconds: Numeric) -> RpcResult { + async fn increase_time(&self, seconds: Numeric) -> RpcResult { Ok(self .node .increase_time(seconds.into()) + .await .map_err(RpcError::from)?) } - fn set_next_block_timestamp(&self, timestamp: Numeric) -> RpcResult<()> { + async fn set_next_block_timestamp(&self, timestamp: Numeric) -> RpcResult<()> { Ok(self .node .set_next_block_timestamp(timestamp.into()) + .await .map_err(RpcError::from)?) } - fn auto_impersonate_account(&self, enabled: bool) -> RpcResult<()> { + async fn auto_impersonate_account(&self, enabled: bool) -> RpcResult<()> { self.node.auto_impersonate_account(enabled); Ok(()) } - fn set_balance(&self, address: Address, balance: U256) -> RpcResult { - Ok(self - .node - .set_balance(address, balance) - .map_err(RpcError::from)?) + async fn set_balance(&self, address: Address, balance: U256) -> RpcResult { + Ok(self.node.set_balance(address, balance).await) } - fn set_nonce(&self, address: Address, nonce: U256) -> RpcResult { - Ok(self - .node - .set_nonce(address, nonce) - .map_err(RpcError::from)?) + async fn set_nonce(&self, address: Address, nonce: U256) -> RpcResult { + Ok(self.node.set_nonce(address, nonce).await) } - fn anvil_mine(&self, num_blocks: Option, interval: Option) -> RpcResult<()> { + async fn anvil_mine(&self, num_blocks: Option, interval: Option) -> RpcResult<()> { Ok(self .node .mine_blocks(num_blocks, interval) + .await .map_err(RpcError::from)?) } - fn reset_network(&self, reset_spec: Option) -> RpcResult { + async fn reset_network(&self, reset_spec: Option) -> RpcResult { Ok(self .node .reset_network(reset_spec) + .await .map_err(RpcError::from)?) } - fn impersonate_account(&self, address: Address) -> RpcResult<()> { + async fn impersonate_account(&self, address: Address) -> RpcResult<()> { Ok(self .node .impersonate_account(address) @@ -176,7 +186,7 @@ impl AnvilNamespaceServer for AnvilNamespace { .map_err(RpcError::from)?) } - fn stop_impersonating_account(&self, address: Address) -> RpcResult<()> { + async fn stop_impersonating_account(&self, address: Address) -> RpcResult<()> { Ok(self .node .stop_impersonating_account(address) @@ -184,18 +194,19 @@ impl AnvilNamespaceServer for AnvilNamespace { .map_err(RpcError::from)?) } - fn set_code(&self, address: Address, code: String) -> RpcResult<()> { - Ok(self.node.set_code(address, code).map_err(RpcError::from)?) - } - - fn set_storage_at(&self, address: Address, slot: U256, value: U256) -> RpcResult { + async fn set_code(&self, address: Address, code: String) -> RpcResult<()> { Ok(self .node - .set_storage_at(address, slot, value) + .set_code(address, code) + .await .map_err(RpcError::from)?) } - fn set_chain_id(&self, id: u32) -> RpcResult<()> { - Ok(self.node.set_chain_id(id).map_err(RpcError::from)?) + async fn set_storage_at(&self, address: Address, slot: U256, value: U256) -> RpcResult { + Ok(self.node.set_storage_at(address, slot, value).await) + } + + async fn set_chain_id(&self, id: u32) -> RpcResult<()> { + Ok(self.node.set_chain_id(id).await.map_err(RpcError::from)?) } } diff --git a/crates/api_server/src/impls/config.rs b/crates/api_server/src/impls/config.rs index 10e2809a..817fafb8 100644 --- a/crates/api_server/src/impls/config.rs +++ b/crates/api_server/src/impls/config.rs @@ -2,7 +2,7 @@ use crate::error::RpcError; use anvil_zksync_api_decl::ConfigNamespaceServer; use anvil_zksync_core::node::InMemoryNode; use anvil_zksync_types::{LogLevel, ShowCalls, ShowGasDetails, ShowStorageLogs, ShowVMDetails}; -use jsonrpsee::core::RpcResult; +use jsonrpsee::core::{async_trait, RpcResult}; pub struct ConfigNamespace { node: InMemoryNode, @@ -14,88 +14,105 @@ impl ConfigNamespace { } } +#[async_trait] impl ConfigNamespaceServer for ConfigNamespace { - fn get_show_calls(&self) -> RpcResult { - Ok(self.node.get_show_calls().map_err(RpcError::from)?) + async fn get_show_calls(&self) -> RpcResult { + Ok(self.node.get_show_calls().await.map_err(RpcError::from)?) } - fn get_show_outputs(&self) -> RpcResult { - Ok(self.node.get_show_outputs().map_err(RpcError::from)?) + async fn get_show_outputs(&self) -> RpcResult { + Ok(self.node.get_show_outputs().await.map_err(RpcError::from)?) } - fn get_current_timestamp(&self) -> RpcResult { + async fn get_current_timestamp(&self) -> RpcResult { Ok(self.node.get_current_timestamp().map_err(RpcError::from)?) } - fn set_show_calls(&self, value: ShowCalls) -> RpcResult { - Ok(self.node.set_show_calls(value).map_err(RpcError::from)?) + async fn set_show_calls(&self, value: ShowCalls) -> RpcResult { + Ok(self + .node + .set_show_calls(value) + .await + .map_err(RpcError::from)?) } - fn set_show_outputs(&self, value: bool) -> RpcResult { - Ok(self.node.set_show_outputs(value).map_err(RpcError::from)?) + async fn set_show_outputs(&self, value: bool) -> RpcResult { + Ok(self + .node + .set_show_outputs(value) + .await + .map_err(RpcError::from)?) } - fn set_show_storage_logs(&self, value: ShowStorageLogs) -> RpcResult { + async fn set_show_storage_logs(&self, value: ShowStorageLogs) -> RpcResult { Ok(self .node .set_show_storage_logs(value) + .await .map_err(RpcError::from)?) } - fn set_show_vm_details(&self, value: ShowVMDetails) -> RpcResult { + async fn set_show_vm_details(&self, value: ShowVMDetails) -> RpcResult { Ok(self .node .set_show_vm_details(value) + .await .map_err(RpcError::from)?) } - fn set_show_gas_details(&self, value: ShowGasDetails) -> RpcResult { + async fn set_show_gas_details(&self, value: ShowGasDetails) -> RpcResult { Ok(self .node .set_show_gas_details(value) + .await .map_err(RpcError::from)?) } - fn set_resolve_hashes(&self, value: bool) -> RpcResult { + async fn set_resolve_hashes(&self, value: bool) -> RpcResult { Ok(self .node .set_resolve_hashes(value) + .await .map_err(RpcError::from)?) } - fn set_show_node_config(&self, value: bool) -> RpcResult { + async fn set_show_node_config(&self, value: bool) -> RpcResult { Ok(self .node .set_show_node_config(value) + .await .map_err(RpcError::from)?) } - fn set_show_tx_summary(&self, value: bool) -> RpcResult { + async fn set_show_tx_summary(&self, value: bool) -> RpcResult { Ok(self .node .set_show_tx_summary(value) + .await .map_err(RpcError::from)?) } - fn set_show_event_logs(&self, value: bool) -> RpcResult { + async fn set_show_event_logs(&self, value: bool) -> RpcResult { Ok(self .node .set_show_event_logs(value) + .await .map_err(RpcError::from)?) } - fn set_disable_console_log(&self, value: bool) -> RpcResult { + async fn set_disable_console_log(&self, value: bool) -> RpcResult { Ok(self .node .set_disable_console_log(value) + .await .map_err(RpcError::from)?) } - fn set_log_level(&self, level: LogLevel) -> RpcResult { + async fn set_log_level(&self, level: LogLevel) -> RpcResult { Ok(self.node.set_log_level(level).map_err(RpcError::from)?) } - fn set_logging(&self, directive: String) -> RpcResult { + async fn set_logging(&self, directive: String) -> RpcResult { Ok(self.node.set_logging(directive).map_err(RpcError::from)?) } } diff --git a/crates/api_server/src/impls/debug.rs b/crates/api_server/src/impls/debug.rs index fb753e1d..4e489a97 100644 --- a/crates/api_server/src/impls/debug.rs +++ b/crates/api_server/src/impls/debug.rs @@ -2,11 +2,9 @@ use crate::error::RpcError; use anvil_zksync_api_decl::DebugNamespaceServer; use anvil_zksync_core::node::InMemoryNode; use jsonrpsee::core::{async_trait, RpcResult}; -use zksync_types::api::{ - BlockId, BlockNumber, CallTracerBlockResult, CallTracerResult, TracerConfig, -}; +use zksync_types::api::{BlockNumber, CallTracerBlockResult, CallTracerResult, TracerConfig}; use zksync_types::transaction_request::CallRequest; -use zksync_types::H256; +use zksync_types::{api, H256}; pub struct DebugNamespace { node: InMemoryNode, @@ -27,7 +25,7 @@ impl DebugNamespaceServer for DebugNamespace { ) -> RpcResult { Ok(self .node - .trace_block_by_number_impl(block, options) + .trace_block_impl(api::BlockId::Number(block), options) .await .map_err(RpcError::from)?) } @@ -39,7 +37,7 @@ impl DebugNamespaceServer for DebugNamespace { ) -> RpcResult { Ok(self .node - .trace_block_by_hash_impl(hash, options) + .trace_block_impl(api::BlockId::Hash(hash), options) .await .map_err(RpcError::from)?) } @@ -47,7 +45,7 @@ impl DebugNamespaceServer for DebugNamespace { async fn trace_call( &self, request: CallRequest, - block: Option, + block: Option, options: Option, ) -> RpcResult { Ok(self diff --git a/crates/api_server/src/impls/eth.rs b/crates/api_server/src/impls/eth.rs index 7bea08c9..729dfa74 100644 --- a/crates/api_server/src/impls/eth.rs +++ b/crates/api_server/src/impls/eth.rs @@ -4,12 +4,12 @@ use anvil_zksync_core::node::InMemoryNode; use jsonrpsee::core::{async_trait, RpcResult}; use zksync_types::api::state_override::StateOverride; use zksync_types::api::{ - Block, BlockId, BlockIdVariant, BlockNumber, FeeHistory, Log, Transaction, TransactionReceipt, + Block, BlockIdVariant, BlockNumber, FeeHistory, Log, Transaction, TransactionReceipt, TransactionVariant, }; use zksync_types::transaction_request::CallRequest; use zksync_types::web3::{Bytes, Index, SyncState, U64Number}; -use zksync_types::{Address, H256, U256, U64}; +use zksync_types::{api, Address, H256, U256, U64}; use zksync_web3_decl::types::{Filter, FilterChanges}; pub struct EthNamespace { @@ -36,6 +36,7 @@ impl EthNamespaceServer for EthNamespace { Ok(self .node .get_chain_id() + .await .map(U64::from) .map_err(RpcError::from)?) } @@ -48,7 +49,7 @@ impl EthNamespaceServer for EthNamespace { // TODO: Support _state_override: Option, ) -> RpcResult { - Ok(self.node.call_impl(req).map_err(RpcError::from)?) + Ok(self.node.call_impl(req).await.map_err(RpcError::from)?) } async fn estimate_gas( @@ -144,7 +145,7 @@ impl EthNamespaceServer for EthNamespace { ) -> RpcResult>> { Ok(self .node - .get_block_by_number_impl(block_number, full_transactions) + .get_block_impl(api::BlockId::Number(block_number), full_transactions) .await .map_err(RpcError::from)?) } @@ -156,7 +157,7 @@ impl EthNamespaceServer for EthNamespace { ) -> RpcResult>> { Ok(self .node - .get_block_by_hash_impl(hash, full_transactions) + .get_block_impl(api::BlockId::Hash(hash), full_transactions) .await .map_err(RpcError::from)?) } @@ -167,14 +168,14 @@ impl EthNamespaceServer for EthNamespace { ) -> RpcResult> { Ok(self .node - .get_block_transaction_count_by_number_impl(block_number) + .get_block_transaction_count_impl(api::BlockId::Number(block_number)) .await .map_err(RpcError::from)?) } async fn get_block_receipts( &self, - _block_id: BlockId, + _block_id: api::BlockId, ) -> RpcResult>> { Err(RpcError::Unsupported.into()) } @@ -185,7 +186,7 @@ impl EthNamespaceServer for EthNamespace { ) -> RpcResult> { Ok(self .node - .get_block_transaction_count_by_hash_impl(block_hash) + .get_block_transaction_count_impl(api::BlockId::Hash(block_hash)) .await .map_err(RpcError::from)?) } @@ -238,7 +239,7 @@ impl EthNamespaceServer for EthNamespace { ) -> RpcResult> { Ok(self .node - .get_transaction_by_block_hash_and_index_impl(block_hash, index) + .get_transaction_by_block_and_index_impl(api::BlockId::Hash(block_hash), index) .await .map_err(RpcError::from)?) } @@ -250,7 +251,7 @@ impl EthNamespaceServer for EthNamespace { ) -> RpcResult> { Ok(self .node - .get_transaction_by_block_number_and_index_impl(block_number, index) + .get_transaction_by_block_and_index_impl(api::BlockId::Number(block_number), index) .await .map_err(RpcError::from)?) } diff --git a/crates/api_server/src/impls/evm.rs b/crates/api_server/src/impls/evm.rs index db257523..a3587ef8 100644 --- a/crates/api_server/src/impls/evm.rs +++ b/crates/api_server/src/impls/evm.rs @@ -1,7 +1,7 @@ use crate::error::RpcError; use anvil_zksync_api_decl::EvmNamespaceServer; use anvil_zksync_core::node::InMemoryNode; -use jsonrpsee::core::RpcResult; +use jsonrpsee::core::{async_trait, RpcResult}; pub struct EvmNamespace { node: InMemoryNode, @@ -13,9 +13,10 @@ impl EvmNamespace { } } +#[async_trait] impl EvmNamespaceServer for EvmNamespace { - fn mine(&self) -> RpcResult { - self.node.mine_block().map_err(RpcError::from)?; + async fn mine(&self) -> RpcResult { + self.node.mine_block().await.map_err(RpcError::from)?; Ok("0x0".to_string()) } } diff --git a/crates/api_server/src/impls/net.rs b/crates/api_server/src/impls/net.rs index 3c715562..70305fe4 100644 --- a/crates/api_server/src/impls/net.rs +++ b/crates/api_server/src/impls/net.rs @@ -16,7 +16,8 @@ impl NetNamespace { impl NetNamespaceServer for NetNamespace { fn version(&self) -> RpcResult { - let chain_id = self.node.get_chain_id().map_err(RpcError::from)?; + let chain_id = tokio::runtime::Handle::current() + .block_on(async { self.node.get_chain_id().await.map_err(RpcError::from) })?; Ok(chain_id.to_string()) } diff --git a/crates/api_server/src/impls/zks.rs b/crates/api_server/src/impls/zks.rs index 575d2619..4cf43fb2 100644 --- a/crates/api_server/src/impls/zks.rs +++ b/crates/api_server/src/impls/zks.rs @@ -80,6 +80,7 @@ impl ZksNamespaceServer for ZksNamespace { Ok(self .node .get_chain_id() + .await .map(U64::from) .map_err(RpcError::from)?) } diff --git a/crates/cli/src/bytecode_override.rs b/crates/cli/src/bytecode_override.rs index bc9e6d40..d3d84d1c 100644 --- a/crates/cli/src/bytecode_override.rs +++ b/crates/cli/src/bytecode_override.rs @@ -19,7 +19,7 @@ struct Bytecode { // Loads a list of bytecodes and addresses from the directory and then inserts them directly // into the Node's storage. -pub fn override_bytecodes(node: &InMemoryNode, bytecodes_dir: String) -> eyre::Result<()> { +pub async fn override_bytecodes(node: &InMemoryNode, bytecodes_dir: String) -> eyre::Result<()> { for entry in fs::read_dir(bytecodes_dir)? { let entry = entry?; let path = entry.path(); @@ -43,6 +43,7 @@ pub fn override_bytecodes(node: &InMemoryNode, bytecodes_dir: String) -> eyre::R .wrap_err(format!("Failed to parse hex from {:?}", path))?; node.override_bytecode(&address, &bytecode) + .await .expect("Failed to override bytecode"); tracing::info!("+++++ Replacing bytecode at address {:?} +++++", address); } diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 788e718c..9fb70feb 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -29,7 +29,6 @@ use std::{ pin::Pin, task::{Context, Poll}, }; -use tokio::task::spawn_blocking; use tokio::time::{Instant, Interval}; use zksync_types::{H256, U256}; @@ -553,18 +552,13 @@ impl PeriodicStateDumper { tracing::trace!(path=?dump_path, "Dumping state"); // Spawn a blocking task for state dumping - let state_bytes = - match spawn_blocking(move || node.dump_state(preserve_historical_states)).await { - Ok(Ok(bytes)) => bytes, - Ok(Err(err)) => { - tracing::error!("Failed to dump state: {:?}", err); - return; - } - Err(err) => { - tracing::error!("Failed to join blocking task: {:?}", err); - return; - } - }; + let state_bytes = match node.dump_state(preserve_historical_states).await { + Ok(bytes) => bytes, + Err(err) => { + tracing::error!("Failed to dump state: {:?}", err); + return; + } + }; let mut decoder = GzDecoder::new(&state_bytes.0[..]); let mut json_str = String::new(); @@ -635,9 +629,7 @@ mod tests { use crate::cli::PeriodicStateDumper; use super::Cli; - use anvil_zksync_core::node::{ - BlockSealer, BlockSealerMode, ImpersonationManager, InMemoryNode, TimestampManager, TxPool, - }; + use anvil_zksync_core::node::InMemoryNode; use clap::Parser; use serde_json::Value; use std::{ @@ -703,15 +695,7 @@ mod tests { ..Default::default() }; - let node = InMemoryNode::new( - None, - None, - &config, - TimestampManager::default(), - ImpersonationManager::default(), - TxPool::new(ImpersonationManager::default(), config.transaction_order), - BlockSealer::new(BlockSealerMode::noop()), - ); + let node = InMemoryNode::test_config(None, config.clone()); let mut state_dumper = PeriodicStateDumper::new( node.clone(), @@ -752,17 +736,10 @@ mod tests { ..Default::default() }; - let node = InMemoryNode::new( - None, - None, - &config, - TimestampManager::default(), - ImpersonationManager::default(), - TxPool::new(ImpersonationManager::default(), config.transaction_order), - BlockSealer::new(BlockSealerMode::noop()), - ); + let node = InMemoryNode::test_config(None, config.clone()); let test_address = H160::from_low_u64_be(12345); - node.set_rich_account(test_address, U256::from(1000000u64)); + node.set_rich_account(test_address, U256::from(1000000u64)) + .await; let mut state_dumper = PeriodicStateDumper::new( node.clone(), @@ -787,20 +764,14 @@ mod tests { std::fs::read_to_string(&state_path).expect("Expected state file to be created"); let new_config = anvil_zksync_config::TestNodeConfig::default(); - let new_node = InMemoryNode::new( - None, - None, - &new_config, - TimestampManager::default(), - ImpersonationManager::default(), - TxPool::new(ImpersonationManager::default(), config.transaction_order), - BlockSealer::new(BlockSealerMode::noop()), - ); + let new_node = InMemoryNode::test_config(None, new_config.clone()); - new_node.load_state(zksync_types::web3::Bytes(std::fs::read(&state_path)?))?; + new_node + .load_state(zksync_types::web3::Bytes(std::fs::read(&state_path)?)) + .await?; // assert the balance from the loaded state is correctly applied - let balance = new_node.get_balance_impl(test_address, None).await.unwrap(); + let balance = new_node.get_balance_impl(test_address, None).await?; assert_eq!(balance, U256::from(1000000u64)); Ok(()) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 4796cdf3..5e68ebfc 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -9,18 +9,21 @@ use anvil_zksync_config::constants::{ }; use anvil_zksync_config::types::SystemContractsOptions; use anvil_zksync_config::ForkPrintInfo; -use anvil_zksync_core::fork::ForkDetails; +use anvil_zksync_core::filters::EthFilters; +use anvil_zksync_core::node::fork::ForkDetails; use anvil_zksync_core::node::{ - BlockProducer, BlockSealer, BlockSealerMode, ImpersonationManager, InMemoryNode, - TimestampManager, TxPool, + BlockSealer, BlockSealerMode, ImpersonationManager, InMemoryNode, InMemoryNodeInner, + NodeExecutor, TestNodeFeeInputProvider, TxPool, }; use anvil_zksync_core::observability::Observability; use anvil_zksync_core::system_contracts::SystemContracts; use anyhow::{anyhow, Context}; use clap::Parser; use std::fs::File; +use std::sync::Arc; use std::time::Duration; use std::{env, net::SocketAddr, str::FromStr}; +use tokio::sync::RwLock; use tower_http::cors::AllowOrigin; use tracing_subscriber::filter::LevelFilter; use zksync_types::fee_model::{FeeModelConfigV2, FeeParams}; @@ -206,9 +209,29 @@ async fn main() -> anyhow::Result<()> { None }; - let time = TimestampManager::default(); let impersonation = ImpersonationManager::default(); + if config.enable_auto_impersonate { + // Enable auto impersonation if configured + impersonation.set_auto_impersonation(true); + } let pool = TxPool::new(impersonation.clone(), config.transaction_order); + + let fee_input_provider = TestNodeFeeInputProvider::from_fork(fork_details.as_ref()); + let filters = Arc::new(RwLock::new(EthFilters::default())); + let system_contracts = + SystemContracts::from_options(&config.system_contracts_options, config.use_evm_emulator); + + let (node_inner, _fork_storage, blockchain, time) = InMemoryNodeInner::init( + fork_details, + fee_input_provider.clone(), + filters, + config.clone(), + impersonation.clone(), + system_contracts.clone(), + ); + + let (node_executor, node_handle) = + NodeExecutor::new(node_inner.clone(), system_contracts.clone()); let sealing_mode = if config.no_mining { BlockSealerMode::noop() } else if let Some(block_time) = config.block_time { @@ -216,43 +239,51 @@ async fn main() -> anyhow::Result<()> { } else { BlockSealerMode::immediate(config.max_transactions, pool.add_tx_listener()) }; - let block_sealer = BlockSealer::new(sealing_mode); + let (block_sealer, block_sealer_state) = + BlockSealer::new(sealing_mode, pool.clone(), node_handle.clone()); let node: InMemoryNode = InMemoryNode::new( - fork_details, + node_inner, + blockchain, + node_handle, Some(observability), - &config, - time.clone(), + time, impersonation, - pool.clone(), - block_sealer.clone(), + pool, + block_sealer_state, + system_contracts, ); if let Some(ref bytecodes_dir) = config.override_bytecodes_dir { - override_bytecodes(&node, bytecodes_dir.to_string()).unwrap(); + override_bytecodes(&node, bytecodes_dir.to_string()) + .await + .unwrap(); } if !transactions_to_replay.is_empty() { - let _ = node.apply_txs(transactions_to_replay, config.max_transactions); + node.apply_txs(transactions_to_replay, config.max_transactions) + .await?; } for signer in config.genesis_accounts.iter() { let address = H160::from_slice(signer.address().as_ref()); - node.set_rich_account(address, config.genesis_balance); + node.set_rich_account(address, config.genesis_balance).await; } for signer in config.signer_accounts.iter() { let address = H160::from_slice(signer.address().as_ref()); - node.set_rich_account(address, config.genesis_balance); + node.set_rich_account(address, config.genesis_balance).await; } // sets legacy rich wallets for wallet in LEGACY_RICH_WALLETS.iter() { let address = wallet.0; - node.set_rich_account(H160::from_str(address).unwrap(), config.genesis_balance); + node.set_rich_account(H160::from_str(address).unwrap(), config.genesis_balance) + .await; } // sets additional legacy rich wallets for wallet in RICH_WALLETS.iter() { let address = wallet.0; - node.set_rich_account(H160::from_str(address).unwrap(), config.genesis_balance); + node.set_rich_account(H160::from_str(address).unwrap(), config.genesis_balance) + .await; } let mut server_builder = NodeServerBuilder::new( @@ -283,11 +314,11 @@ async fn main() -> anyhow::Result<()> { // Load state from `--load-state` if provided if let Some(ref load_state_path) = config.load_state { let bytes = std::fs::read(load_state_path).expect("Failed to read load state file"); - node.load_state(zksync_types::web3::Bytes(bytes))?; + node.load_state(zksync_types::web3::Bytes(bytes)).await?; } if let Some(ref state_path) = config.state { let bytes = std::fs::read(state_path).expect("Failed to read load state file"); - node.load_state(zksync_types::web3::Bytes(bytes))?; + node.load_state(zksync_types::web3::Bytes(bytes)).await?; } let state_path = config.dump_state.clone().or_else(|| config.state.clone()); @@ -304,10 +335,6 @@ async fn main() -> anyhow::Result<()> { preserve_historical_states, ); - let system_contracts = - SystemContracts::from_options(&config.system_contracts_options, config.use_evm_emulator); - let block_producer_handle = BlockProducer::new(node, pool, block_sealer, system_contracts); - config.print(fork_print_info.as_ref()); tokio::select! { @@ -317,8 +344,11 @@ async fn main() -> anyhow::Result<()> { _ = any_server_stopped => { tracing::trace!("node server was stopped") }, - _ = block_producer_handle => { - tracing::trace!("block producer was stopped") + _ = node_executor => { + tracing::trace!("node executor was stopped") + }, + _ = block_sealer => { + tracing::trace!("block sealer was stopped") }, _ = state_dumper => { tracing::trace!("state dumper was stopped") diff --git a/crates/cli/src/utils.rs b/crates/cli/src/utils.rs index bc25a507..fae302e8 100644 --- a/crates/cli/src/utils.rs +++ b/crates/cli/src/utils.rs @@ -1,6 +1,6 @@ use anvil_zksync_config::types::Genesis; use anvil_zksync_config::TestNodeConfig; -use anvil_zksync_core::fork::ForkDetails; +use anvil_zksync_core::node::fork::ForkDetails; use std::fs; /// Parses the genesis file from the given path. diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index ae05d588..6df7f227 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -40,6 +40,7 @@ chrono.workspace = true time.workspace = true flate2.workspace = true thiserror.workspace = true +pin-project.workspace = true [dev-dependencies] maplit.workspace = true @@ -48,3 +49,4 @@ httptest.workspace = true tempdir.workspace = true zksync-web3-rs.workspace = true test-case.workspace = true +backon.workspace = true diff --git a/crates/core/src/formatter.rs b/crates/core/src/formatter.rs index 1e2ff4f3..3b82685e 100644 --- a/crates/core/src/formatter.rs +++ b/crates/core/src/formatter.rs @@ -1,7 +1,7 @@ //! Helper methods to display transaction data in more human readable way. use crate::bootloader_debug::BootloaderDebug; -use crate::fork::block_on; use crate::resolver; +use crate::utils::block_on; use crate::utils::{calculate_eth_cost, to_human_size}; use anvil_zksync_config::utils::format_gwei; use anvil_zksync_types::ShowCalls; @@ -413,7 +413,7 @@ impl Formatter { contract_address: Option, call: &Call, is_last_sibling: bool, - show_calls: &ShowCalls, + show_calls: ShowCalls, show_outputs: bool, resolve_hashes: bool, ) { diff --git a/crates/core/src/http_fork_source.rs b/crates/core/src/http_fork_source.rs index b98be508..45b8f7e6 100644 --- a/crates/core/src/http_fork_source.rs +++ b/crates/core/src/http_fork_source.rs @@ -3,10 +3,9 @@ use std::{ sync::{Arc, RwLock}, }; -use crate::{ - cache::Cache, - fork::{block_on, ForkSource}, -}; +use crate::cache::Cache; +use crate::node::fork::ForkSource; +use crate::utils::block_on; use anvil_zksync_config::types::CacheConfig; use eyre::Context; use zksync_types::{ diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index f1549d44..674fdf15 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -45,7 +45,6 @@ pub mod bootloader_debug; pub mod console_log; pub mod deps; pub mod filters; -pub mod fork; pub mod formatter; pub mod http_fork_source; pub mod node; diff --git a/crates/core/src/node/block_producer.rs b/crates/core/src/node/block_producer.rs deleted file mode 100644 index e6f9f666..00000000 --- a/crates/core/src/node/block_producer.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::node::pool::{TxBatch, TxPool}; -use crate::node::sealer::BlockSealer; -use crate::node::InMemoryNode; -use crate::system_contracts::SystemContracts; -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; -use zksync_multivm::interface::TxExecutionMode; - -pub struct BlockProducer { - node: InMemoryNode, - pool: TxPool, - block_sealer: BlockSealer, - system_contracts: SystemContracts, -} - -impl BlockProducer { - pub fn new( - node: InMemoryNode, - pool: TxPool, - block_sealer: BlockSealer, - system_contracts: SystemContracts, - ) -> Self { - Self { - node, - pool, - block_sealer, - system_contracts, - } - } -} - -impl Future for BlockProducer { - type Output = (); - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let pin = self.get_mut(); - - while let Poll::Ready(tx_batch) = pin.block_sealer.poll(&pin.pool, cx) { - let TxBatch { impersonating, txs } = tx_batch; - - let base_system_contracts = pin - .system_contracts - .contracts(TxExecutionMode::VerifyExecute, impersonating) - .clone(); - pin.node - .seal_block(&mut pin.node.time.lock(), txs, base_system_contracts) - .expect("block sealing failed"); - } - - Poll::Pending - } -} diff --git a/crates/core/src/node/debug.rs b/crates/core/src/node/debug.rs index 2fe812b1..e85f92d6 100644 --- a/crates/core/src/node/debug.rs +++ b/crates/core/src/node/debug.rs @@ -1,6 +1,6 @@ use crate::deps::storage_view::StorageView; use crate::node::{InMemoryNode, MAX_TX_SIZE}; -use crate::utils::{create_debug_output, to_real_block_number}; +use crate::utils::create_debug_output; use itertools::Itertools; use once_cell::sync::OnceCell; use std::sync::Arc; @@ -8,80 +8,60 @@ use zksync_multivm::interface::{VmFactory, VmInterface}; use zksync_multivm::tracers::CallTracer; use zksync_multivm::vm_latest::constants::ETH_CALL_GAS_LIMIT; use zksync_multivm::vm_latest::{HistoryDisabled, ToTracerPointer, Vm}; -use zksync_types::api::{ - BlockId, BlockNumber, CallTracerBlockResult, CallTracerResult, ResultDebugCall, TracerConfig, - TransactionVariant, -}; use zksync_types::l2::L2Tx; use zksync_types::transaction_request::CallRequest; -use zksync_types::{PackedEthSignature, Transaction, H256, U64}; +use zksync_types::{api, PackedEthSignature, Transaction, H256}; use zksync_web3_decl::error::Web3Error; impl InMemoryNode { - pub async fn trace_block_by_number_impl( + pub async fn trace_block_impl( &self, - block: BlockNumber, - options: Option, - ) -> anyhow::Result { - let current_miniblock = self.read_inner()?.current_miniblock; - let number = to_real_block_number(block, U64::from(current_miniblock)).as_u64(); - let block_hash = *self - .read_inner()? - .block_hashes - .get(&number) - .ok_or_else(|| anyhow::anyhow!("Block (id={block}) not found"))?; - - self.trace_block_by_hash_impl(block_hash, options).await - } - - pub async fn trace_block_by_hash_impl( - &self, - hash: H256, - options: Option, - ) -> anyhow::Result { + block_id: api::BlockId, + options: Option, + ) -> anyhow::Result { let only_top = options.is_some_and(|o| o.tracer_config.only_top_call); - let tx_hashes = self - .read_inner()? - .blocks - .get(&hash) - .ok_or_else(|| anyhow::anyhow!("Block (hash={hash}) not found"))? - .transactions - .iter() - .map(|tx| match tx { - TransactionVariant::Full(tx) => tx.hash, - TransactionVariant::Hash(hash) => *hash, - }) - .collect_vec(); - - let debug_calls = tx_hashes - .into_iter() - .map(|tx_hash| { - Ok(self - .read_inner()? - .tx_results - .get(&tx_hash) - .ok_or_else(|| anyhow::anyhow!("Transaction (hash={tx_hash}) not found"))? - .debug_info(only_top)) + .blockchain + .inspect_block_by_id(block_id, |block| { + block + .transactions + .iter() + .map(|tx| match tx { + api::TransactionVariant::Full(tx) => tx.hash, + api::TransactionVariant::Hash(hash) => *hash, + }) + .collect_vec() }) - .collect::>>()? - .into_iter() - .map(|result| ResultDebugCall { result }) - .collect_vec(); + .await + .ok_or_else(|| anyhow::anyhow!("Block (id={block_id}) not found"))?; + + let mut debug_calls = Vec::with_capacity(tx_hashes.len()); + for tx_hash in tx_hashes { + let result = self.blockchain + .get_tx_debug_info(&tx_hash, only_top) + .await + .ok_or_else(|| { + anyhow::anyhow!( + "Unexpectedly transaction (hash={tx_hash}) belongs to a block but could not be found" + ) + })?; + debug_calls.push(api::ResultDebugCall { result }); + } - Ok(CallTracerBlockResult::CallTrace(debug_calls)) + Ok(api::CallTracerBlockResult::CallTrace(debug_calls)) } pub async fn trace_call_impl( &self, request: CallRequest, - block: Option, - options: Option, - ) -> Result { + block: Option, + options: Option, + ) -> Result { let only_top = options.is_some_and(|o| o.tracer_config.only_top_call); - let inner = self.read_inner()?; + let inner = self.inner.read().await; let system_contracts = self.system_contracts.contracts_for_l2_call(); - if block.is_some() && !matches!(block, Some(BlockId::Number(BlockNumber::Latest))) { + if block.is_some() && !matches!(block, Some(api::BlockId::Number(api::BlockNumber::Latest))) + { return Err(Web3Error::InternalError(anyhow::anyhow!( "tracing only supported at `latest` block" ))); @@ -91,15 +71,14 @@ impl InMemoryNode { let mut l2_tx = L2Tx::from_request(request.into(), MAX_TX_SIZE, allow_no_target) .map_err(Web3Error::SerializationError)?; let execution_mode = zksync_multivm::interface::TxExecutionMode::EthCall; - let storage = StorageView::new(&inner.fork_storage).into_rc_ptr(); // init vm - let (mut l1_batch_env, _block_context) = - inner.create_l1_batch_env(&self.time, storage.clone()); + let (mut l1_batch_env, _block_context) = inner.create_l1_batch_env().await; // update the enforced_base_fee within l1_batch_env to match the logic in zksync_core l1_batch_env.enforced_base_fee = Some(l2_tx.common_data.fee.max_fee_per_gas.as_u64()); let system_env = inner.create_system_env(system_contracts.clone(), execution_mode); + let storage = StorageView::new(&inner.fork_storage).into_rc_ptr(); let mut vm: Vm<_, HistoryDisabled> = Vm::new(l1_batch_env, system_env, storage); // We must inject *some* signature (otherwise bootloader code fails to generate hash). @@ -133,21 +112,20 @@ impl InMemoryNode { let debug = create_debug_output(&l2_tx, &tx_result, call_traces)?; - Ok(CallTracerResult::CallTrace(debug)) + Ok(api::CallTracerResult::CallTrace(debug)) } pub async fn trace_transaction_impl( &self, tx_hash: H256, - options: Option, - ) -> anyhow::Result> { + options: Option, + ) -> anyhow::Result> { let only_top = options.is_some_and(|o| o.tracer_config.only_top_call); - let inner = self.read_inner()?; - - Ok(inner - .tx_results - .get(&tx_hash) - .map(|tx| CallTracerResult::CallTrace(tx.debug_info(only_top)))) + Ok(self + .blockchain + .get_tx_debug_info(&tx_hash, only_top) + .await + .map(api::CallTracerResult::CallTrace)) } } @@ -156,10 +134,8 @@ mod tests { use anvil_zksync_config::constants::DEFAULT_ACCOUNT_BALANCE; use ethers::abi::{short_signature, AbiEncode, HumanReadableParser, ParamType, Token}; use zksync_types::{ - api::{Block, CallTracerConfig, SupportedTracers, TransactionReceipt}, - transaction_request::CallRequestBuilder, - utils::deployed_address_create, - Address, K256PrivateKey, Nonce, H160, U256, + transaction_request::CallRequestBuilder, utils::deployed_address_create, Address, + K256PrivateKey, L2BlockNumber, Nonce, H160, U256, }; use super::*; @@ -169,10 +145,11 @@ mod tests { testing::{self, LogBuilder}, }; - fn deploy_test_contracts(node: &InMemoryNode) -> (Address, Address) { + async fn deploy_test_contracts(node: &InMemoryNode) -> (Address, Address) { let private_key = K256PrivateKey::from_bytes(H256::repeat_byte(0xee)).unwrap(); let from_account = private_key.address(); - node.set_rich_account(from_account, U256::from(DEFAULT_ACCOUNT_BALANCE)); + node.set_rich_account(from_account, U256::from(DEFAULT_ACCOUNT_BALANCE)) + .await; // first, deploy secondary contract let secondary_bytecode = bytecode_from_slice( @@ -187,7 +164,8 @@ mod tests { secondary_bytecode, Some((U256::from(2),).encode()), Nonce(0), - ); + ) + .await; // deploy primary contract using the secondary contract address as a constructor parameter let primary_bytecode = bytecode_from_slice( @@ -202,15 +180,17 @@ mod tests { primary_bytecode, Some((secondary_deployed_address).encode()), Nonce(1), - ); + ) + .await; (primary_deployed_address, secondary_deployed_address) } #[tokio::test] async fn test_trace_deployed_contract() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); - let (primary_deployed_address, secondary_deployed_address) = deploy_test_contracts(&node); + let (primary_deployed_address, secondary_deployed_address) = + deploy_test_contracts(&node).await; // trace a call to the primary contract let func = HumanReadableParser::parse_function("calculate(uint)").unwrap(); let calldata = func.encode_input(&[Token::Uint(U256::from(42))]).unwrap(); @@ -258,9 +238,9 @@ mod tests { #[tokio::test] async fn test_trace_only_top() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); - let (primary_deployed_address, _) = deploy_test_contracts(&node); + let (primary_deployed_address, _) = deploy_test_contracts(&node).await; // trace a call to the primary contract let func = HumanReadableParser::parse_function("calculate(uint)").unwrap(); @@ -276,9 +256,9 @@ mod tests { .trace_call_impl( request, None, - Some(TracerConfig { - tracer: SupportedTracers::CallTracer, - tracer_config: CallTracerConfig { + Some(api::TracerConfig { + tracer: api::SupportedTracers::CallTracer, + tracer_config: api::CallTracerConfig { only_top_call: true, }, }), @@ -296,9 +276,9 @@ mod tests { #[tokio::test] async fn test_trace_reverts() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); - let (primary_deployed_address, _) = deploy_test_contracts(&node); + let (primary_deployed_address, _) = deploy_test_contracts(&node).await; // trace a call to the primary contract let request = CallRequestBuilder::default() @@ -332,23 +312,24 @@ mod tests { #[tokio::test] async fn test_trace_transaction_impl() { - let node = InMemoryNode::default(); - let inner = node.get_inner(); + let node = InMemoryNode::test(None); { - let mut writer = inner.write().unwrap(); - writer.tx_results.insert( - H256::repeat_byte(0x1), - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: TransactionReceipt { - logs: vec![LogBuilder::new() - .set_address(H160::repeat_byte(0xa1)) - .build()], - ..Default::default() + let mut writer = node.inner.write().await; + writer + .insert_tx_result( + H256::repeat_byte(0x1), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: api::TransactionReceipt { + logs: vec![LogBuilder::new() + .set_address(H160::repeat_byte(0xa1)) + .build()], + ..Default::default() + }, + debug: testing::default_tx_debug_info(), }, - debug: testing::default_tx_debug_info(), - }, - ); + ) + .await; } let result = node .trace_transaction_impl(H256::repeat_byte(0x1), None) @@ -361,15 +342,15 @@ mod tests { #[tokio::test] async fn test_trace_transaction_only_top() { - let node = InMemoryNode::default(); - let inner = node.get_inner(); - { - let mut writer = inner.write().unwrap(); - writer.tx_results.insert( + let node = InMemoryNode::test(None); + node.inner + .write() + .await + .insert_tx_result( H256::repeat_byte(0x1), TransactionResult { info: testing::default_tx_execution_info(), - receipt: TransactionReceipt { + receipt: api::TransactionReceipt { logs: vec![LogBuilder::new() .set_address(H160::repeat_byte(0xa1)) .build()], @@ -377,14 +358,14 @@ mod tests { }, debug: testing::default_tx_debug_info(), }, - ); - } + ) + .await; let result = node .trace_transaction_impl( H256::repeat_byte(0x1), - Some(TracerConfig { - tracer: SupportedTracers::CallTracer, - tracer_config: CallTracerConfig { + Some(api::TracerConfig { + tracer: api::SupportedTracers::CallTracer, + tracer_config: api::CallTracerConfig { only_top_call: true, }, }), @@ -398,7 +379,7 @@ mod tests { #[tokio::test] async fn test_trace_transaction_not_found() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let result = node .trace_transaction_impl(H256::repeat_byte(0x1), None) .await @@ -408,15 +389,15 @@ mod tests { #[tokio::test] async fn test_trace_block_by_hash_empty() { - let node = InMemoryNode::default(); - let inner = node.get_inner(); - { - let mut writer = inner.write().unwrap(); - let block = Block::::default(); - writer.blocks.insert(H256::repeat_byte(0x1), block); - } + let node = InMemoryNode::test(None); + let block = api::Block::::default(); + node.inner + .write() + .await + .insert_block(H256::repeat_byte(0x1), block) + .await; let result = node - .trace_block_by_hash_impl(H256::repeat_byte(0x1), None) + .trace_block_impl(api::BlockId::Hash(H256::repeat_byte(0x1)), None) .await .unwrap() .unwrap_default(); @@ -425,26 +406,27 @@ mod tests { #[tokio::test] async fn test_trace_block_by_hash_impl() { - let node = InMemoryNode::default(); - let inner = node.get_inner(); + let node = InMemoryNode::test(None); + let tx = api::Transaction::default(); + let tx_hash = tx.hash; + let mut block = api::Block::::default(); + block.transactions.push(api::TransactionVariant::Full(tx)); { - let mut writer = inner.write().unwrap(); - let tx = zksync_types::api::Transaction::default(); - let tx_hash = tx.hash; - let mut block = Block::::default(); - block.transactions.push(TransactionVariant::Full(tx)); - writer.blocks.insert(H256::repeat_byte(0x1), block); - writer.tx_results.insert( - tx_hash, - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: TransactionReceipt::default(), - debug: testing::default_tx_debug_info(), - }, - ); + let mut writer = node.inner.write().await; + writer.insert_block(H256::repeat_byte(0x1), block).await; + writer + .insert_tx_result( + tx_hash, + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: api::TransactionReceipt::default(), + debug: testing::default_tx_debug_info(), + }, + ) + .await; } let result = node - .trace_block_by_hash_impl(H256::repeat_byte(0x1), None) + .trace_block_impl(api::BlockId::Hash(H256::repeat_byte(0x1)), None) .await .unwrap() .unwrap_default(); @@ -454,28 +436,31 @@ mod tests { #[tokio::test] async fn test_trace_block_by_number_impl() { - let node = InMemoryNode::default(); - let inner = node.get_inner(); + let node = InMemoryNode::test(None); + let tx = api::Transaction::default(); + let tx_hash = tx.hash; + let mut block = api::Block::::default(); + block.transactions.push(api::TransactionVariant::Full(tx)); { - let mut writer = inner.write().unwrap(); - let tx = zksync_types::api::Transaction::default(); - let tx_hash = tx.hash; - let mut block = Block::::default(); - block.transactions.push(TransactionVariant::Full(tx)); - writer.blocks.insert(H256::repeat_byte(0x1), block); - writer.block_hashes.insert(0, H256::repeat_byte(0x1)); - writer.tx_results.insert( - tx_hash, - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: TransactionReceipt::default(), - debug: testing::default_tx_debug_info(), - }, - ); + let mut writer = node.inner.write().await; + writer.insert_block(H256::repeat_byte(0x1), block).await; + writer + .insert_block_hash(L2BlockNumber(0), H256::repeat_byte(0x1)) + .await; + writer + .insert_tx_result( + tx_hash, + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: api::TransactionReceipt::default(), + debug: testing::default_tx_debug_info(), + }, + ) + .await; } // check `latest` alias let result = node - .trace_block_by_number_impl(BlockNumber::Latest, None) + .trace_block_impl(api::BlockId::Number(api::BlockNumber::Latest), None) .await .unwrap() .unwrap_default(); @@ -484,7 +469,10 @@ mod tests { // check block number let result = node - .trace_block_by_number_impl(BlockNumber::Number(0.into()), None) + .trace_block_impl( + api::BlockId::Number(api::BlockNumber::Number(0.into())), + None, + ) .await .unwrap() .unwrap_default(); diff --git a/crates/core/src/node/eth.rs b/crates/core/src/node/eth.rs index a1bb4690..1d8934b7 100644 --- a/crates/core/src/node/eth.rs +++ b/crates/core/src/node/eth.rs @@ -5,15 +5,16 @@ use colored::Colorize; use itertools::Itertools; use zksync_multivm::interface::{ExecutionResult, TxExecutionMode}; use zksync_multivm::vm_latest::constants::ETH_CALL_GAS_LIMIT; +use zksync_types::h256_to_u256; use zksync_types::{ + api, api::{Block, BlockIdVariant, BlockNumber, TransactionVariant}, get_code_key, get_nonce_key, l2::L2Tx, transaction_request::TransactionRequest, utils::storage_key_for_standard_token_balance, - PackedEthSignature, StorageKey, L2_BASE_TOKEN_ADDRESS, MAX_L1_TRANSACTION_GAS_LIMIT, + PackedEthSignature, L2_BASE_TOKEN_ADDRESS, MAX_L1_TRANSACTION_GAS_LIMIT, }; -use zksync_types::{h256_to_u256, u256_to_h256}; use zksync_types::{ web3::{self, Bytes}, AccountTreeId, Address, H160, H256, U256, U64, @@ -26,11 +27,11 @@ use zksync_web3_decl::{ use crate::{ filters::{FilterType, LogFilter}, node::{InMemoryNode, TransactionResult, MAX_TX_SIZE, PROTOCOL_VERSION}, - utils::{self, h256_to_u64, TransparentError}, + utils::{h256_to_u64, TransparentError}, }; impl InMemoryNode { - pub fn call_impl( + pub async fn call_impl( &self, req: zksync_types::transaction_request::CallRequest, ) -> Result { @@ -41,6 +42,7 @@ impl InMemoryNode { tx.common_data.fee.gas_limit = ETH_CALL_GAS_LIMIT.into(); let call_result = self .run_l2_call(tx, system_contracts) + .await .context("Invalid data due to invalid name")?; match call_result { @@ -74,12 +76,7 @@ impl InMemoryNode { } pub async fn send_raw_transaction_impl(&self, tx_bytes: Bytes) -> Result { - let chain_id = self - .get_inner() - .read() - .map_err(|_| anyhow::anyhow!("Failed to acquire read lock for chain ID retrieval"))? - .fork_storage - .chain_id; + let chain_id = self.inner.read().await.fork_storage.chain_id; let (tx_req, hash) = TransactionRequest::from_bytes(&tx_bytes.0, chain_id)?; // Impersonation does not matter in this context so we assume the tx is not impersonated: @@ -107,7 +104,7 @@ impl InMemoryNode { tx: zksync_types::transaction_request::CallRequest, ) -> Result { let (chain_id, l2_gas_price) = { - let reader = self.read_inner()?; + let reader = self.inner.read().await; ( reader.fork_storage.chain_id, reader.fee_input_provider.gas_price(), @@ -192,51 +189,38 @@ impl InMemoryNode { &address, ); - let inner_guard = self.read_inner()?; + let inner_guard = self.inner.read().await; match inner_guard.fork_storage.read_value_internal(&balance_key) { Ok(balance) => Ok(h256_to_u256(balance)), Err(error) => Err(anyhow::anyhow!("failed to read account balance: {error}")), } } - pub async fn get_block_by_number_impl( + pub async fn get_block_impl( &self, - block_number: BlockNumber, + block_id: api::BlockId, full_transactions: bool, ) -> anyhow::Result>> { - let inner = self.get_inner().clone(); - let maybe_block = { - let reader = match inner.read() { - Ok(r) => r, - Err(_) => { - anyhow::bail!("Failed to acquire read lock for block retrieval") - } - }; - let number = - utils::to_real_block_number(block_number, U64::from(reader.current_miniblock)) - .as_u64(); - - reader - .block_hashes - .get(&number) - .and_then(|hash| reader.blocks.get(hash)) - .cloned() - .or_else(|| { - reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage") - .fork - .as_ref() - .and_then(|fork| { - fork.fork_source - .get_block_by_number(block_number, true) - .ok() - .flatten() - }) - }) + if let Some(block) = self.blockchain.get_block_by_id(block_id).await { + Some(block) + } else { + self.inner + .read() + .await + .fork_storage + .inner + .read() + .expect("failed reading fork storage") + .fork + .as_ref() + .and_then(|fork| { + fork.fork_source + .get_block_by_id(block_id, true) + .ok() + .flatten() + }) + } }; match maybe_block { @@ -275,7 +259,7 @@ impl InMemoryNode { // TODO: Support _block: Option, ) -> anyhow::Result { - let inner = self.write_inner()?; + let inner = self.inner.write().await; let code_key = get_code_key(&address); @@ -297,7 +281,7 @@ impl InMemoryNode { // TODO: Support _block: Option, ) -> anyhow::Result { - let inner = self.read_inner()?; + let inner = self.inner.read().await; let nonce_key = get_nonce_key(&address); match inner.fork_storage.read_value_internal(&nonce_key) { @@ -309,81 +293,31 @@ impl InMemoryNode { pub async fn get_transaction_receipt_impl( &self, hash: H256, - ) -> anyhow::Result> { - let inner = self.read_inner()?; - - let receipt = inner.tx_results.get(&hash).map(|info| info.receipt.clone()); - Ok(receipt) - } - - pub async fn get_block_by_hash_impl( - &self, - hash: H256, - full_transactions: bool, - ) -> anyhow::Result>> { - let reader = self.read_inner()?; - - // try retrieving block from memory, and if unavailable subsequently from the fork - let maybe_block = reader.blocks.get(&hash).cloned().or_else(|| { - reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage") - .fork - .as_ref() - .and_then(|fork| { - fork.fork_source - .get_block_by_hash(hash, true) - .ok() - .flatten() - }) - }); - - match maybe_block { - Some(mut block) => { - let block_hash = block.hash; - block.transactions = block - .transactions - .into_iter() - .map(|transaction| match &transaction { - TransactionVariant::Full(inner) => { - if full_transactions { - transaction - } else { - TransactionVariant::Hash(inner.hash) - } - } - TransactionVariant::Hash(_) => { - if full_transactions { - panic!("unexpected non full transaction for block {}", block_hash) - } else { - transaction - } - } - }) - .collect(); - - Ok(Some(block)) - } - None => Ok(None), - } + ) -> anyhow::Result> { + // TODO: Call fork if not found + Ok(self.blockchain.get_tx_receipt(&hash).await) } pub async fn get_transaction_by_hash_impl( &self, hash: H256, - ) -> anyhow::Result> { - let reader = self.read_inner()?; - + ) -> anyhow::Result> { // try retrieving transaction from memory, and if unavailable subsequently from the fork - Ok(reader - .tx_results - .get(&hash) - .and_then(|TransactionResult { info, receipt, .. }| { - let input_data = info.tx.common_data.input.clone().or(None)?; - let chain_id = info.tx.common_data.extract_chain_id().or(None)?; - Some(zksync_types::api::Transaction { + let tx = self + .blockchain + .inspect_tx(&hash, |TransactionResult { info, receipt, .. }| { + let input_data = info + .tx + .common_data + .input + .clone() + .context("tx is missing input data")?; + let chain_id = info + .tx + .common_data + .extract_chain_id() + .context("tx has malformed chain id")?; + anyhow::Ok(api::Transaction { hash, nonce: U256::from(info.tx.common_data.nonce.0), block_hash: Some(hash), @@ -421,25 +355,31 @@ impl InMemoryNode { l1_batch_tx_index: None, }) }) - .or_else(|| { - reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage") - .fork - .as_ref() - .and_then(|fork| { - fork.fork_source - .get_transaction_by_hash(hash) - .ok() - .flatten() - }) - })) + .await + .transpose()?; + match tx { + Some(tx) => Ok(Some(tx)), + None => Ok(self + .inner + .read() + .await + .fork_storage + .inner + .read() + .expect("failed reading fork storage") + .fork + .as_ref() + .and_then(|fork| { + fork.fork_source + .get_transaction_by_hash(hash) + .ok() + .flatten() + })), + } } pub async fn get_block_number_impl(&self) -> anyhow::Result { - Ok(U64::from(self.read_inner()?.current_miniblock)) + Ok(U64::from(self.blockchain.current_block_number().await.0)) } pub async fn estimate_gas_impl( @@ -448,14 +388,12 @@ impl InMemoryNode { // TODO: Support _block: Option, ) -> Result { - // TODO: Burn with fire - let time = self.time.lock(); - let fee = self.read_inner()?.estimate_gas_impl(&time, req)?; + let fee = self.inner.read().await.estimate_gas_impl(req).await?; Ok(fee.gas_limit) } pub async fn gas_price_impl(&self) -> anyhow::Result { - let fair_l2_gas_price: u64 = self.read_inner()?.fee_input_provider.gas_price(); + let fair_l2_gas_price: u64 = self.inner.read().await.fee_input_provider.gas_price(); Ok(U256::from(fair_l2_gas_price)) } @@ -476,28 +414,47 @@ impl InMemoryNode { } }) } - self.write_inner()? + self.inner + .write() + .await .filters + .write() + .await .add_log_filter(from_block, to_block, addresses, topics) .map_err(anyhow::Error::msg) } pub async fn new_block_filter_impl(&self) -> anyhow::Result { - self.write_inner()? + self.inner + .write() + .await .filters + .write() + .await .add_block_filter() .map_err(anyhow::Error::msg) } pub async fn new_pending_transaction_filter_impl(&self) -> anyhow::Result { - self.write_inner()? + self.inner + .write() + .await .filters + .write() + .await .add_pending_transaction_filter() .map_err(anyhow::Error::msg) } pub async fn uninstall_filter_impl(&self, id: U256) -> anyhow::Result { - Ok(self.write_inner()?.filters.remove_filter(id)) + Ok(self + .inner + .write() + .await + .filters + .write() + .await + .remove_filter(id)) } pub async fn get_logs_impl( @@ -521,42 +478,50 @@ impl InMemoryNode { }) } + // TODO: LogFilter should really resolve `from_block` and `to_block` during init and not + // on every `matches` call. let log_filter = LogFilter::new(from_block, to_block, addresses, topics); - - let reader = self.read_inner()?; - let latest_block_number = U64::from(reader.current_miniblock); - let logs = reader - .tx_results - .values() - .flat_map(|tx_result| { - tx_result - .receipt - .logs - .iter() - .filter(|log| log_filter.matches(log, latest_block_number)) - .cloned() + let latest_block_number = self.blockchain.current_block_number().await; + let logs = self + .blockchain + .inspect_all_txs(|tx_results| { + tx_results + .values() + .flat_map(|tx_result| { + tx_result + .receipt + .logs + .iter() + .filter(|log| log_filter.matches(log, U64::from(latest_block_number.0))) + .cloned() + }) + .collect_vec() }) - .collect_vec(); + .await; Ok(logs) } pub async fn get_filter_logs_impl(&self, id: U256) -> anyhow::Result { - let reader = self.read_inner()?; - let latest_block_number = U64::from(reader.current_miniblock); - let logs = match reader.filters.get_filter(id) { - Some(FilterType::Log(f)) => reader - .tx_results - .values() - .flat_map(|tx_result| { - tx_result - .receipt - .logs - .iter() - .filter(|log| f.matches(log, latest_block_number)) - .cloned() - }) - .collect_vec(), + let latest_block_number = self.blockchain.current_block_number().await; + let logs = match self.inner.read().await.filters.read().await.get_filter(id) { + Some(FilterType::Log(f)) => { + self.blockchain + .inspect_all_txs(|tx_results| { + tx_results + .values() + .flat_map(|tx_result| { + tx_result + .receipt + .logs + .iter() + .filter(|log| f.matches(log, U64::from(latest_block_number.0))) + .cloned() + }) + .collect_vec() + }) + .await + } _ => { anyhow::bail!("Failed to acquire read lock for filter logs.") } @@ -566,75 +531,46 @@ impl InMemoryNode { } pub async fn get_filter_changes_impl(&self, id: U256) -> anyhow::Result { - self.write_inner()? + self.inner + .write() + .await .filters + .write() + .await .get_new_changes(id) .map_err(anyhow::Error::msg) } - pub async fn get_block_transaction_count_by_number_impl( - &self, - block_number: BlockNumber, - ) -> Result, Web3Error> { - let reader = self.read_inner()?; - let number = - utils::to_real_block_number(block_number, U64::from(reader.current_miniblock)).as_u64(); - - let maybe_result = reader - .block_hashes - .get(&number) - .and_then(|hash| reader.blocks.get(hash)) - .map(|block| U256::from(block.transactions.len())) - .or_else(|| { - reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage") - .fork - .as_ref() - .and_then(|fork| { - fork.fork_source - .get_block_transaction_count_by_number(block_number) - .ok() - .flatten() - }) - }); - - match maybe_result { - Some(value) => Ok(Some(value)), - None => Err(Web3Error::NoBlock), - } - } - - pub async fn get_block_transaction_count_by_hash_impl( + pub async fn get_block_transaction_count_impl( &self, - block_hash: H256, + block_id: api::BlockId, ) -> Result, Web3Error> { - let reader = self.read_inner()?; - - // try retrieving block from memory, and if unavailable subsequently from the fork - let maybe_result = reader - .blocks - .get(&block_hash) - .map(|block| U256::from(block.transactions.len())) - .or_else(|| { - reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage") - .fork - .as_ref() - .and_then(|fork| { - fork.fork_source - .get_block_transaction_count_by_hash(block_hash) - .ok() - .flatten() - }) - }); + let result = self + .blockchain + .inspect_block_by_id(block_id, |block| block.transactions.len()) + .await; + let result = match result { + Some(result) => Some(U256::from(result)), + None => self + .inner + .read() + .await + .fork_storage + .inner + .read() + .expect("failed reading fork storage") + .fork + .as_ref() + .and_then(|fork| { + fork.fork_source + .get_block_transaction_count_by_id(block_id) + .ok() + .flatten() + }), + }; - match maybe_result { + // TODO: Is this right? What is the purpose of having `Option` here then? + match result { Some(value) => Ok(Some(value)), None => Err(Web3Error::NoBlock), } @@ -644,175 +580,49 @@ impl InMemoryNode { &self, address: Address, idx: U256, - block: Option, + block: Option, ) -> Result { - let writer = self.write_inner()?; - - let storage_key = StorageKey::new(AccountTreeId::new(address), u256_to_h256(idx)); + self.inner + .read() + .await + .get_storage_at_block(address, idx, block) + .await + } - let block_number = block - .map(|block| match block { - BlockIdVariant::BlockNumber(block_number) => Ok(utils::to_real_block_number( - block_number, - U64::from(writer.current_miniblock), - )), - BlockIdVariant::BlockNumberObject(o) => Ok(utils::to_real_block_number( - o.block_number, - U64::from(writer.current_miniblock), - )), - BlockIdVariant::BlockHashObject(o) => writer - .blocks - .get(&o.block_hash) - .map(|block| block.number) - .ok_or_else(|| { - tracing::error!("unable to map block number to hash #{:#x}", o.block_hash); - Web3Error::InternalError(anyhow::Error::msg( - "Failed to map block number to hash.", - )) - }), + pub async fn get_transaction_by_block_and_index_impl( + &self, + block_id: api::BlockId, + index: web3::Index, + ) -> anyhow::Result> { + let tx = self + .blockchain + .inspect_block_by_id(block_id, |b| { + b.transactions.get(index.as_usize()).map(|tv| match tv { + TransactionVariant::Full(tx) => tx.clone(), + TransactionVariant::Hash(_) => unreachable!("we only store full txs in blocks"), + }) }) - .unwrap_or_else(|| Ok(U64::from(writer.current_miniblock)))?; - - if block_number.as_u64() == writer.current_miniblock { - match writer.fork_storage.read_value_internal(&storage_key) { - Ok(value) => Ok(H256(value.0)), - Err(error) => Err(Web3Error::InternalError(anyhow::anyhow!( - "failed to read storage: {}", - error - ))), - } - } else if writer.block_hashes.contains_key(&block_number.as_u64()) { - let value = writer - .block_hashes - .get(&block_number.as_u64()) - .and_then(|block_hash| writer.previous_states.get(block_hash)) - .and_then(|state| state.get(&storage_key)) - .cloned() - .unwrap_or_default(); - - if value.is_zero() { - match writer.fork_storage.read_value_internal(&storage_key) { - Ok(value) => Ok(H256(value.0)), - Err(error) => Err(Web3Error::InternalError(anyhow::anyhow!( - "failed to read storage: {}", - error - ))), - } - } else { - Ok(value) - } - } else { - writer + .await + .flatten(); + let maybe_tx = match tx { + Some(tx) => Some(tx), + None => self + .inner + .read() + .await .fork_storage .inner .read() .expect("failed reading fork storage") .fork .as_ref() - .and_then(|fork| fork.fork_source.get_storage_at(address, idx, block).ok()) - .ok_or_else(|| { - tracing::error!( - "unable to get storage at address {:?}, index {:?} for block {:?}", - address, - idx, - block - ); - Web3Error::InternalError(anyhow::Error::msg("Failed to get storage.")) + .and_then(|fork| { + fork.fork_source + .get_transaction_by_block_id_and_index(block_id, index) + .ok() }) - } - } - - pub async fn get_transaction_by_block_hash_and_index_impl( - &self, - block_hash: H256, - index: web3::Index, - ) -> anyhow::Result> { - let reader = self.read_inner()?; - - let maybe_tx = reader - .blocks - .get(&block_hash) - .and_then(|block| block.transactions.get(index.as_usize())) - .and_then(|tx| match tx { - TransactionVariant::Full(tx) => Some(tx.clone()), - TransactionVariant::Hash(tx_hash) => reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage") - .fork - .as_ref() - .and_then(|fork| { - fork.fork_source - .get_transaction_by_hash(*tx_hash) - .ok() - .flatten() - }), - }) - .or_else(|| { - reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage") - .fork - .as_ref() - .and_then(|fork| { - fork.fork_source - .get_transaction_by_block_hash_and_index(block_hash, index) - .ok() - }) - .flatten() - }); - - Ok(maybe_tx) - } - - pub async fn get_transaction_by_block_number_and_index_impl( - &self, - block_number: BlockNumber, - index: web3::Index, - ) -> anyhow::Result> { - let reader = self.read_inner()?; - - let real_block_number = - utils::to_real_block_number(block_number, U64::from(reader.current_miniblock)); - let maybe_tx = reader - .block_hashes - .get(&real_block_number.as_u64()) - .and_then(|block_hash| reader.blocks.get(block_hash)) - .and_then(|block| block.transactions.get(index.as_usize())) - .and_then(|tx| match tx { - TransactionVariant::Full(tx) => Some(tx.clone()), - TransactionVariant::Hash(tx_hash) => reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage") - .fork - .as_ref() - .and_then(|fork| { - fork.fork_source - .get_transaction_by_hash(*tx_hash) - .ok() - .flatten() - }), - }) - .or_else(|| { - reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage") - .fork - .as_ref() - .and_then(|fork| { - fork.fork_source - .get_transaction_by_block_number_and_index(block_number, index) - .ok() - }) - .flatten() - }); + .flatten(), + }; Ok(maybe_tx) } @@ -827,7 +637,9 @@ impl InMemoryNode { pub async fn accounts_impl(&self) -> anyhow::Result> { Ok(self - .read_inner()? + .inner + .read() + .await .rich_accounts .clone() .into_iter() @@ -841,17 +653,17 @@ impl InMemoryNode { _newest_block: BlockNumber, reward_percentiles: Option>, ) -> anyhow::Result { - let reader = self.read_inner()?; + let current_block = self.blockchain.current_block_number().await; - let block_count = block_count + let block_count = (block_count as usize) .min(1024) // Can't be more than the total number of blocks - .clamp(1, reader.current_miniblock + 1); + .clamp(1, current_block.0 as usize + 1); let mut base_fee_per_gas = - vec![U256::from(reader.fee_input_provider.gas_price()); block_count as usize]; + vec![U256::from(self.inner.read().await.fee_input_provider.gas_price()); block_count]; - let oldest_block = reader.current_miniblock + 1 - base_fee_per_gas.len() as u64; + let oldest_block = current_block + 1 - base_fee_per_gas.len() as u32; // We do not store gas used ratio for blocks, returns array of zeroes as a placeholder. let gas_used_ratio = vec![0.0; base_fee_per_gas.len()]; // Effective priority gas price is currently 0. @@ -868,7 +680,7 @@ impl InMemoryNode { Ok(zksync_types::api::FeeHistory { inner: FeeHistory { - oldest_block: web3::BlockNumber::Number(oldest_block.into()), + oldest_block: web3::BlockNumber::Number(oldest_block.0.into()), base_fee_per_gas, gas_used_ratio, reward, @@ -883,9 +695,9 @@ impl InMemoryNode { #[cfg(test)] mod tests { use super::*; + use crate::node::fork::ForkDetails; use crate::{ - fork::ForkDetails, - node::{compute_hash, InMemoryNode, Snapshot}, + node::{compute_hash, InMemoryNode}, testing::{ self, default_tx_debug_info, ForkBlockConfig, LogBuilder, MockServer, TransactionResponseBuilder, @@ -903,13 +715,13 @@ mod tests { api, api::{BlockHashObject, BlockNumber, BlockNumberObject, TransactionReceipt}, utils::deployed_address_create, - Bloom, K256PrivateKey, EMPTY_UNCLES_HASH, + Bloom, K256PrivateKey, L2BlockNumber, StorageKey, EMPTY_UNCLES_HASH, }; - use zksync_types::{web3, Nonce}; + use zksync_types::{u256_to_h256, web3, Nonce}; use zksync_web3_decl::types::{SyncState, ValueOrArray}; async fn test_node(url: &str) -> InMemoryNode { - InMemoryNode::default_fork(Some( + InMemoryNode::test(Some( ForkDetails::from_network(url, None, &CacheConfig::None) .await .unwrap(), @@ -918,14 +730,14 @@ mod tests { #[tokio::test] async fn test_eth_syncing() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let syncing = node.syncing_impl(); assert!(matches!(syncing, SyncState::NotSyncing)); } #[tokio::test] async fn test_get_fee_history_with_1_block() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let fee_history = node .fee_history_impl(1, BlockNumber::Latest, Some(vec![25.0, 50.0, 75.0])) @@ -947,7 +759,7 @@ mod tests { #[tokio::test] async fn test_get_fee_history_with_no_reward_percentiles() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let fee_history = node .fee_history_impl(1, BlockNumber::Latest, Some(vec![])) @@ -970,8 +782,8 @@ mod tests { #[tokio::test] async fn test_get_fee_history_with_multiple_blocks() { // Arrange - let node = InMemoryNode::default(); - testing::apply_tx(&node, H256::repeat_byte(0x01)); + let node = InMemoryNode::test(None); + testing::apply_tx(&node, H256::repeat_byte(0x01)).await; // Act let latest_block = node @@ -1001,10 +813,10 @@ mod tests { #[tokio::test] async fn test_get_block_by_hash_returns_none_for_non_existing_block() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let result = node - .get_block_by_hash_impl(H256::repeat_byte(0x01), false) + .get_block_impl(api::BlockId::Hash(H256::repeat_byte(0x01)), false) .await .expect("failed fetching block by hash"); @@ -1013,10 +825,10 @@ mod tests { #[tokio::test] async fn test_node_has_genesis_block() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let block = node - .get_block_by_number_impl(BlockNumber::Latest, false) + .get_block_impl(api::BlockId::Number(BlockNumber::Latest), false) .await .expect("failed fetching block by number") .expect("no block"); @@ -1027,10 +839,10 @@ mod tests { #[tokio::test] async fn test_node_creates_genesis_block_with_hash_and_zero_parent_hash() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let block = node - .get_block_by_hash_impl(compute_hash(0, []), false) + .get_block_impl(api::BlockId::Hash(compute_hash(0, [])), false) .await .expect("failed fetching block by hash") .expect("no block"); @@ -1040,21 +852,21 @@ mod tests { #[tokio::test] async fn test_node_produces_blocks_with_parent_hash_links() { - let node = InMemoryNode::default(); - testing::apply_tx(&node, H256::repeat_byte(0x01)); + let node = InMemoryNode::test(None); + testing::apply_tx(&node, H256::repeat_byte(0x01)).await; let genesis_block = node - .get_block_by_number_impl(BlockNumber::from(0), false) + .get_block_impl(api::BlockId::Number(BlockNumber::from(0)), false) .await .expect("failed fetching block by number") .expect("no block"); let first_block = node - .get_block_by_number_impl(BlockNumber::from(1), false) + .get_block_impl(api::BlockId::Number(BlockNumber::from(1)), false) .await .expect("failed fetching block by number") .expect("no block"); let second_block = node - .get_block_by_number_impl(BlockNumber::from(2), false) + .get_block_impl(api::BlockId::Number(BlockNumber::from(2)), false) .await .expect("failed fetching block by number") .expect("no block"); @@ -1065,17 +877,17 @@ mod tests { #[tokio::test] async fn test_get_block_by_hash_for_produced_block() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let tx_hash = H256::repeat_byte(0x01); - let (expected_block_hash, _, _) = testing::apply_tx(&node, tx_hash); + let (expected_block_hash, _, _) = testing::apply_tx(&node, tx_hash).await; let genesis_block = node - .get_block_by_number_impl(BlockNumber::from(0), false) + .get_block_impl(api::BlockId::Number(BlockNumber::from(0)), false) .await .expect("failed fetching block by number") .expect("no block"); let actual_block = node - .get_block_by_hash_impl(expected_block_hash, false) + .get_block_impl(api::BlockId::Hash(expected_block_hash), false) .await .expect("failed fetching block by hash") .expect("no block"); @@ -1127,17 +939,13 @@ mod tests { }); let node = test_node(&mock_server.url()).await; - - let inner = node.get_inner(); - let inner = inner.read().unwrap(); assert!( - inner.blocks.contains_key(&input_block_hash), + node.blockchain + .get_block_by_hash(&input_block_hash) + .await + .is_some(), "block wasn't cached" ); - assert!( - inner.block_hashes.contains_key(&input_block_number), - "block number wasn't cached" - ); } #[tokio::test] @@ -1169,7 +977,7 @@ mod tests { let node = test_node(&mock_server.url()).await; let actual_block = node - .get_block_by_hash_impl(input_block_hash, false) + .get_block_impl(api::BlockId::Hash(input_block_hash), false) .await .expect("failed fetching block by hash") .expect("no block"); @@ -1181,10 +989,13 @@ mod tests { #[tokio::test] async fn test_get_block_by_number_returns_none_for_non_existing_block() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let result = node - .get_block_by_number_impl(BlockNumber::Number(U64::from(42)), false) + .get_block_impl( + api::BlockId::Number(BlockNumber::Number(U64::from(42))), + false, + ) .await .expect("failed fetching block by number"); @@ -1193,18 +1004,21 @@ mod tests { #[tokio::test] async fn test_get_block_by_number_for_produced_block() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let tx_hash = H256::repeat_byte(0x01); - let (expected_block_hash, _, _) = testing::apply_tx(&node, tx_hash); + let (expected_block_hash, _, _) = testing::apply_tx(&node, tx_hash).await; let expected_block_number = 1; let genesis_block = node - .get_block_by_number_impl(BlockNumber::from(0), false) + .get_block_impl(api::BlockId::Number(BlockNumber::from(0)), false) .await .expect("failed fetching block by number") .expect("no block"); let actual_block = node - .get_block_by_number_impl(BlockNumber::Number(U64::from(expected_block_number)), false) + .get_block_impl( + api::BlockId::Number(BlockNumber::Number(U64::from(expected_block_number))), + false, + ) .await .expect("failed fetching block by number") .expect("no block"); @@ -1247,13 +1061,16 @@ mod tests { #[tokio::test] async fn test_get_block_by_number_for_produced_block_full_txs() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let tx_hash = H256::repeat_byte(0x01); - let (block_hash, _, tx) = testing::apply_tx(&node, tx_hash); + let (block_hash, _, tx) = testing::apply_tx(&node, tx_hash).await; let expected_block_number = 1; let mut actual_block = node - .get_block_by_number_impl(BlockNumber::Number(U64::from(expected_block_number)), true) + .get_block_impl( + api::BlockId::Number(BlockNumber::Number(U64::from(expected_block_number))), + true, + ) .await .expect("failed fetching block by number") .expect("no block"); @@ -1319,7 +1136,10 @@ mod tests { let node = test_node(&mock_server.url()).await; let actual_block = node - .get_block_by_number_impl(BlockNumber::Number(U64::from(8)), false) + .get_block_impl( + api::BlockId::Number(BlockNumber::Number(U64::from(8))), + false, + ) .await .expect("failed fetching block by hash") .expect("no block"); @@ -1328,12 +1148,12 @@ mod tests { #[tokio::test] async fn test_get_block_by_number_for_latest_block_produced_locally() { - let node = InMemoryNode::default(); - testing::apply_tx(&node, H256::repeat_byte(0x01)); + let node = InMemoryNode::test(None); + testing::apply_tx(&node, H256::repeat_byte(0x01)).await; // The latest block, will be the 'virtual' one with 0 transactions (block 2). let virtual_block = node - .get_block_by_number_impl(BlockNumber::Latest, true) + .get_block_impl(api::BlockId::Number(BlockNumber::Latest), true) .await .expect("failed fetching block by hash") .expect("no block"); @@ -1342,7 +1162,10 @@ mod tests { assert_eq!(0, virtual_block.transactions.len()); let actual_block = node - .get_block_by_number_impl(BlockNumber::Number(U64::from(1)), true) + .get_block_impl( + api::BlockId::Number(BlockNumber::Number(U64::from(1))), + true, + ) .await .expect("failed fetching block by hash") .expect("no block"); @@ -1363,7 +1186,7 @@ mod tests { let node = test_node(&mock_server.url()).await; let actual_block = node - .get_block_by_number_impl(BlockNumber::Latest, false) + .get_block_impl(api::BlockId::Number(BlockNumber::Latest), false) .await .expect("failed fetching block by hash") .expect("no block"); @@ -1395,7 +1218,7 @@ mod tests { let node = test_node(&mock_server.url()).await; let actual_block = node - .get_block_by_number_impl(BlockNumber::Earliest, false) + .get_block_impl(api::BlockId::Number(BlockNumber::Earliest), false) .await .expect("failed fetching block by hash") .expect("no block"); @@ -1418,7 +1241,7 @@ mod tests { let node = test_node(&mock_server.url()).await; let actual_block = node - .get_block_by_number_impl(block_number, false) + .get_block_impl(api::BlockId::Number(block_number), false) .await .expect("failed fetching block by hash") .expect("no block"); @@ -1433,11 +1256,11 @@ mod tests { #[tokio::test] async fn test_get_block_transaction_count_by_hash_for_produced_block() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); - let (expected_block_hash, _, _) = testing::apply_tx(&node, H256::repeat_byte(0x01)); + let (expected_block_hash, _, _) = testing::apply_tx(&node, H256::repeat_byte(0x01)).await; let actual_transaction_count = node - .get_block_transaction_count_by_hash_impl(expected_block_hash) + .get_block_transaction_count_impl(api::BlockId::Hash(expected_block_hash)) .await .expect("failed fetching block by hash") .expect("no result"); @@ -1472,7 +1295,7 @@ mod tests { let node = test_node(&mock_server.url()).await; let actual_transaction_count = node - .get_block_transaction_count_by_hash_impl(input_block_hash) + .get_block_transaction_count_impl(api::BlockId::Hash(input_block_hash)) .await .expect("failed fetching block by hash") .expect("no result"); @@ -1485,11 +1308,13 @@ mod tests { #[tokio::test] async fn test_get_block_transaction_count_by_number_for_produced_block() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); - testing::apply_tx(&node, H256::repeat_byte(0x01)); + testing::apply_tx(&node, H256::repeat_byte(0x01)).await; let actual_transaction_count = node - .get_block_transaction_count_by_number_impl(BlockNumber::Number(U64::from(1))) + .get_block_transaction_count_impl(api::BlockId::Number(api::BlockNumber::Number( + U64::from(1), + ))) .await .expect("failed fetching block by hash") .expect("no result"); @@ -1525,7 +1350,9 @@ mod tests { let node = test_node(&mock_server.url()).await; let actual_transaction_count = node - .get_block_transaction_count_by_number_impl(BlockNumber::Number(U64::from(1))) + .get_block_transaction_count_impl(api::BlockId::Number(BlockNumber::Number(U64::from( + 1, + )))) .await .expect("failed fetching block by hash") .expect("no result"); @@ -1563,7 +1390,7 @@ mod tests { let node = test_node(&mock_server.url()).await; let actual_transaction_count = node - .get_block_transaction_count_by_number_impl(BlockNumber::Earliest) + .get_block_transaction_count_impl(api::BlockId::Number(BlockNumber::Earliest)) .await .expect("failed fetching block by hash") .expect("no result"); @@ -1592,7 +1419,7 @@ mod tests { let node = test_node(&mock_server.url()).await; let actual_transaction_count = node - .get_block_transaction_count_by_number_impl(block_number) + .get_block_transaction_count_impl(api::BlockId::Number(block_number)) .await .expect("failed fetching block by hash") .expect("no result"); @@ -1608,9 +1435,9 @@ mod tests { #[tokio::test] async fn test_get_transaction_receipt_uses_produced_block_hash() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let tx_hash = H256::repeat_byte(0x01); - let (expected_block_hash, _, _) = testing::apply_tx(&node, tx_hash); + let (expected_block_hash, _, _) = testing::apply_tx(&node, tx_hash).await; let actual_tx_receipt = node .get_transaction_receipt_impl(tx_hash) @@ -1623,7 +1450,7 @@ mod tests { #[tokio::test] async fn test_new_block_filter_returns_filter_id() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let actual_filter_id = node .new_block_filter_impl() @@ -1635,7 +1462,7 @@ mod tests { #[tokio::test] async fn test_new_filter_returns_filter_id() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let actual_filter_id = node .new_filter_impl(Filter::default()) @@ -1647,7 +1474,7 @@ mod tests { #[tokio::test] async fn test_new_pending_transaction_filter_returns_filter_id() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let actual_filter_id = node .new_pending_transaction_filter_impl() @@ -1659,7 +1486,7 @@ mod tests { #[tokio::test] async fn test_uninstall_filter_returns_true_if_filter_exists() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let filter_id = node .new_block_filter_impl() .await @@ -1675,7 +1502,7 @@ mod tests { #[tokio::test] async fn test_uninstall_filter_returns_false_if_filter_does_not_exist() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let actual_result = node .uninstall_filter_impl(U256::from(100)) @@ -1687,12 +1514,12 @@ mod tests { #[tokio::test] async fn test_get_filter_changes_returns_block_hash_updates_only_once() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let filter_id = node .new_block_filter_impl() .await .expect("failed creating filter"); - let (block_hash, _, _) = testing::apply_tx(&node, H256::repeat_byte(0x1)); + let (block_hash, _, _) = testing::apply_tx(&node, H256::repeat_byte(0x1)).await; match node .get_filter_changes_impl(filter_id) @@ -1719,7 +1546,7 @@ mod tests { #[tokio::test] async fn test_get_filter_changes_returns_log_updates_only_once() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let filter_id = node .new_filter_impl(Filter { from_block: None, @@ -1730,7 +1557,7 @@ mod tests { }) .await .expect("failed creating filter"); - testing::apply_tx(&node, H256::repeat_byte(0x1)); + testing::apply_tx(&node, H256::repeat_byte(0x1)).await; match node .get_filter_changes_impl(filter_id) @@ -1753,12 +1580,12 @@ mod tests { #[tokio::test] async fn test_get_filter_changes_returns_pending_transaction_updates_only_once() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let filter_id = node .new_pending_transaction_filter_impl() .await .expect("failed creating filter"); - testing::apply_tx(&node, H256::repeat_byte(0x1)); + testing::apply_tx(&node, H256::repeat_byte(0x1)).await; match node .get_filter_changes_impl(filter_id) @@ -1781,37 +1608,31 @@ mod tests { #[tokio::test] async fn test_produced_block_archives_previous_blocks() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let input_storage_key = StorageKey::new( AccountTreeId::new(H160::repeat_byte(0x1)), u256_to_h256(U256::zero()), ); let input_storage_value = H256::repeat_byte(0xcd); - node.get_inner() + node.inner .write() - .unwrap() + .await .fork_storage .set_value(input_storage_key, input_storage_value); - let initial_miniblock = node.get_inner().read().unwrap().current_miniblock; - - testing::apply_tx(&node, H256::repeat_byte(0x1)); - let current_miniblock = node.get_inner().read().unwrap().current_miniblock; - - let inner = node.get_inner(); - let reader = inner.read().unwrap(); - for miniblock in initial_miniblock..current_miniblock { - let actual_cached_value = reader - .block_hashes - .get(&miniblock) - .map(|hash| { - reader - .previous_states - .get(hash) - .unwrap_or_else(|| panic!("state was not cached for block {}", miniblock)) - }) - .and_then(|state| state.get(&input_storage_key)) - .copied(); + let initial_miniblock = node.blockchain.current_block_number().await; + + testing::apply_tx(&node, H256::repeat_byte(0x1)).await; + let current_miniblock = node.blockchain.current_block_number().await; + + for miniblock in initial_miniblock.0..current_miniblock.0 { + let hash = node + .blockchain + .get_block_hash_by_number(L2BlockNumber(miniblock)) + .await + .unwrap(); + let previous_state = node.inner.read().await.get_previous_state(hash).unwrap(); + let actual_cached_value = previous_state.get(&input_storage_key).copied(); assert_eq!( Some(input_storage_value), @@ -1824,7 +1645,7 @@ mod tests { #[tokio::test] async fn test_get_storage_fetches_zero_value_for_non_existent_key() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let value = node .get_storage_impl(H160::repeat_byte(0xf1), U256::from(1024), None) @@ -1886,28 +1707,28 @@ mod tests { ); let input_storage_value = H256::repeat_byte(0xcd); - let node = InMemoryNode::default(); - node.get_inner() - .write() - .map(|mut writer| { - let historical_block = Block:: { - hash: H256::repeat_byte(0x2), - number: U64::from(2), - ..Default::default() - }; - writer.block_hashes.insert(2, historical_block.hash); - - writer.previous_states.insert( - historical_block.hash, - hashmap! { - input_storage_key => input_storage_value, - }, - ); - writer - .blocks - .insert(historical_block.hash, historical_block); - }) - .expect("failed setting storage for historical block"); + let node = InMemoryNode::test(None); + { + let mut writer = node.inner.write().await; + let historical_block = Block:: { + hash: H256::repeat_byte(0x2), + number: U64::from(2), + ..Default::default() + }; + writer + .insert_block_hash(L2BlockNumber(2), historical_block.hash) + .await; + + writer.insert_previous_state( + historical_block.hash, + hashmap! { + input_storage_key => input_storage_value, + }, + ); + writer + .insert_block(historical_block.hash, historical_block) + .await; + } let actual_value = node .get_storage_impl( @@ -1952,23 +1773,21 @@ mod tests { ); let node = test_node(&mock_server.url()).await; - node.get_inner() - .write() - .map(|mut writer| { - let historical_block = Block:: { - hash: H256::repeat_byte(0x2), - number: U64::from(2), - ..Default::default() - }; - writer.block_hashes.insert(2, historical_block.hash); - writer - .previous_states - .insert(historical_block.hash, Default::default()); - writer - .blocks - .insert(historical_block.hash, historical_block); - }) - .expect("failed setting storage for historical block"); + { + let mut writer = node.inner.write().await; + let historical_block = Block:: { + hash: H256::repeat_byte(0x2), + number: U64::from(2), + ..Default::default() + }; + writer + .insert_block_hash(L2BlockNumber(2), historical_block.hash) + .await; + writer.insert_previous_state(historical_block.hash, Default::default()); + writer + .insert_block(historical_block.hash, historical_block) + .await; + }; let actual_value = node .get_storage_impl( @@ -1987,11 +1806,12 @@ mod tests { #[tokio::test] async fn test_get_storage_fetches_state_for_deployed_smart_contract_in_current_block() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let private_key = K256PrivateKey::from_bytes(H256::repeat_byte(0xef)).unwrap(); let from_account = private_key.address(); - node.set_rich_account(from_account, U256::from(DEFAULT_ACCOUNT_BALANCE)); + node.set_rich_account(from_account, U256::from(DEFAULT_ACCOUNT_BALANCE)) + .await; let deployed_address = deployed_address_create(from_account, U256::zero()); @@ -2002,7 +1822,8 @@ mod tests { hex::decode(testing::STORAGE_CONTRACT_BYTECODE).unwrap(), None, Nonce(0), - ); + ) + .await; let number1 = node .get_storage_impl(deployed_address, U256::from(0), None) @@ -2019,11 +1840,12 @@ mod tests { #[tokio::test] async fn test_get_storage_fetches_state_for_deployed_smart_contract_in_old_block() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let private_key = K256PrivateKey::from_bytes(H256::repeat_byte(0xef)).unwrap(); let from_account = private_key.address(); - node.set_rich_account(from_account, U256::from(DEFAULT_ACCOUNT_BALANCE)); + node.set_rich_account(from_account, U256::from(DEFAULT_ACCOUNT_BALANCE)) + .await; let deployed_address = deployed_address_create(from_account, U256::zero()); @@ -2034,17 +1856,18 @@ mod tests { hex::decode(testing::STORAGE_CONTRACT_BYTECODE).unwrap(), None, Nonce(0), - ); + ) + .await; // simulate a tx modifying the storage - testing::apply_tx(&node, H256::repeat_byte(0x2)); + testing::apply_tx(&node, H256::repeat_byte(0x2)).await; let key = StorageKey::new( AccountTreeId::new(deployed_address), u256_to_h256(U256::from(0)), ); - node.get_inner() + node.inner .write() - .unwrap() + .await .fork_storage .inner .write() @@ -2063,11 +1886,9 @@ mod tests { .get_storage_impl( deployed_address, U256::from(0), - Some(zksync_types::api::BlockIdVariant::BlockHashObject( - BlockHashObject { - block_hash: initial_block_hash, - }, - )), + Some(api::BlockIdVariant::BlockHashObject(BlockHashObject { + block_hash: initial_block_hash, + })), ) .await .expect("failed retrieving storage at slot 0"); @@ -2076,43 +1897,46 @@ mod tests { #[tokio::test] async fn test_get_filter_logs_returns_matching_logs_for_valid_id() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); // populate tx receipts with 2 tx each having logs { - let inner = node.get_inner(); - let mut writer = inner.write().unwrap(); - writer.tx_results.insert( - H256::repeat_byte(0x1), - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: TransactionReceipt { - logs: vec![LogBuilder::new() - .set_address(H160::repeat_byte(0xa1)) - .build()], - ..Default::default() - }, - debug: default_tx_debug_info(), - }, - ); - writer.tx_results.insert( - H256::repeat_byte(0x2), - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: TransactionReceipt { - logs: vec![ - LogBuilder::new() + let mut writer = node.inner.write().await; + writer + .insert_tx_result( + H256::repeat_byte(0x1), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: TransactionReceipt { + logs: vec![LogBuilder::new() .set_address(H160::repeat_byte(0xa1)) - .build(), - LogBuilder::new() - .set_address(H160::repeat_byte(0xa2)) - .build(), - ], - ..Default::default() + .build()], + ..Default::default() + }, + debug: default_tx_debug_info(), }, - debug: default_tx_debug_info(), - }, - ); + ) + .await; + writer + .insert_tx_result( + H256::repeat_byte(0x2), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: TransactionReceipt { + logs: vec![ + LogBuilder::new() + .set_address(H160::repeat_byte(0xa1)) + .build(), + LogBuilder::new() + .set_address(H160::repeat_byte(0xa2)) + .build(), + ], + ..Default::default() + }, + debug: default_tx_debug_info(), + }, + ) + .await; } let filter_id = node @@ -2135,25 +1959,26 @@ mod tests { #[tokio::test] async fn test_get_filter_logs_returns_error_for_invalid_id() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); // populate tx receipts with 2 tx each having logs { - let inner = node.get_inner(); - let mut writer = inner.write().unwrap(); - writer.tx_results.insert( - H256::repeat_byte(0x1), - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: TransactionReceipt { - logs: vec![LogBuilder::new() - .set_address(H160::repeat_byte(0xa1)) - .build()], - ..Default::default() + let mut writer = node.inner.write().await; + writer + .insert_tx_result( + H256::repeat_byte(0x1), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: TransactionReceipt { + logs: vec![LogBuilder::new() + .set_address(H160::repeat_byte(0xa1)) + .build()], + ..Default::default() + }, + debug: default_tx_debug_info(), }, - debug: default_tx_debug_info(), - }, - ); + ) + .await; } let invalid_filter_id = U256::from(100); @@ -2164,43 +1989,46 @@ mod tests { #[tokio::test] async fn test_get_logs_returns_matching_logs() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); // populate tx receipts with 2 tx each having logs { - let inner = node.get_inner(); - let mut writer = inner.write().unwrap(); - writer.tx_results.insert( - H256::repeat_byte(0x1), - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: TransactionReceipt { - logs: vec![LogBuilder::new() - .set_address(H160::repeat_byte(0xa1)) - .build()], - ..Default::default() - }, - debug: testing::default_tx_debug_info(), - }, - ); - writer.tx_results.insert( - H256::repeat_byte(0x2), - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: TransactionReceipt { - logs: vec![ - LogBuilder::new() + let mut writer = node.inner.write().await; + writer + .insert_tx_result( + H256::repeat_byte(0x1), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: TransactionReceipt { + logs: vec![LogBuilder::new() .set_address(H160::repeat_byte(0xa1)) - .build(), - LogBuilder::new() - .set_address(H160::repeat_byte(0xa2)) - .build(), - ], - ..Default::default() + .build()], + ..Default::default() + }, + debug: testing::default_tx_debug_info(), }, - debug: testing::default_tx_debug_info(), - }, - ); + ) + .await; + writer + .insert_tx_result( + H256::repeat_byte(0x2), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: TransactionReceipt { + logs: vec![ + LogBuilder::new() + .set_address(H160::repeat_byte(0xa1)) + .build(), + LogBuilder::new() + .set_address(H160::repeat_byte(0xa2)) + .build(), + ], + ..Default::default() + }, + debug: testing::default_tx_debug_info(), + }, + ) + .await; } let result = node @@ -2233,11 +2061,12 @@ mod tests { #[tokio::test] async fn test_accounts_impl() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let private_key = H256::repeat_byte(0x01); let from_account = K256PrivateKey::from_bytes(private_key).unwrap().address(); - node.set_rich_account(from_account, U256::from(DEFAULT_ACCOUNT_BALANCE)); + node.set_rich_account(from_account, U256::from(DEFAULT_ACCOUNT_BALANCE)) + .await; let account_result = node.accounts_impl().await; let expected_accounts: Vec = vec![from_account]; @@ -2252,243 +2081,19 @@ mod tests { } } - #[tokio::test] - async fn test_snapshot() { - let node = InMemoryNode::default(); - let inner = node.get_inner(); - let mut inner = inner.write().unwrap(); - - inner - .blocks - .insert(H256::repeat_byte(0x1), Default::default()); - inner.block_hashes.insert(1, H256::repeat_byte(0x1)); - inner.tx_results.insert( - H256::repeat_byte(0x1), - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: Default::default(), - debug: testing::default_tx_debug_info(), - }, - ); - inner.current_batch = 1; - inner.current_miniblock = 1; - inner.current_miniblock_hash = H256::repeat_byte(0x1); - node.time.set_current_timestamp_unchecked(1); - inner - .filters - .add_block_filter() - .expect("failed adding block filter"); - inner.impersonation.impersonate(H160::repeat_byte(0x1)); - inner.rich_accounts.insert(H160::repeat_byte(0x1)); - inner - .previous_states - .insert(H256::repeat_byte(0x1), Default::default()); - inner.fork_storage.set_value( - StorageKey::new(AccountTreeId::new(H160::repeat_byte(0x1)), H256::zero()), - H256::repeat_byte(0x1), - ); - - let storage = inner.fork_storage.inner.read().unwrap(); - let expected_snapshot = Snapshot { - current_batch: inner.current_batch, - current_miniblock: inner.current_miniblock, - current_miniblock_hash: inner.current_miniblock_hash, - fee_input_provider: inner.fee_input_provider.clone(), - tx_results: inner.tx_results.clone(), - blocks: inner.blocks.clone(), - block_hashes: inner.block_hashes.clone(), - filters: inner.filters.clone(), - impersonation_state: inner.impersonation.state(), - rich_accounts: inner.rich_accounts.clone(), - previous_states: inner.previous_states.clone(), - raw_storage: storage.raw_storage.clone(), - value_read_cache: storage.value_read_cache.clone(), - factory_dep_cache: storage.factory_dep_cache.clone(), - }; - let actual_snapshot = inner.snapshot().expect("failed taking snapshot"); - - assert_eq!( - expected_snapshot.current_batch, - actual_snapshot.current_batch - ); - assert_eq!( - expected_snapshot.current_miniblock, - actual_snapshot.current_miniblock - ); - assert_eq!( - expected_snapshot.current_miniblock_hash, - actual_snapshot.current_miniblock_hash - ); - assert_eq!( - expected_snapshot.fee_input_provider, - actual_snapshot.fee_input_provider - ); - assert_eq!( - expected_snapshot.tx_results.keys().collect_vec(), - actual_snapshot.tx_results.keys().collect_vec() - ); - assert_eq!(expected_snapshot.blocks, actual_snapshot.blocks); - assert_eq!(expected_snapshot.block_hashes, actual_snapshot.block_hashes); - assert_eq!(expected_snapshot.filters, actual_snapshot.filters); - assert_eq!( - expected_snapshot.impersonation_state, - actual_snapshot.impersonation_state - ); - assert_eq!( - expected_snapshot.rich_accounts, - actual_snapshot.rich_accounts - ); - assert_eq!( - expected_snapshot.previous_states, - actual_snapshot.previous_states - ); - assert_eq!(expected_snapshot.raw_storage, actual_snapshot.raw_storage); - assert_eq!( - expected_snapshot.value_read_cache, - actual_snapshot.value_read_cache - ); - assert_eq!( - expected_snapshot.factory_dep_cache, - actual_snapshot.factory_dep_cache - ); - } - - #[tokio::test] - async fn test_snapshot_restore() { - let node = InMemoryNode::default(); - let inner = node.get_inner(); - let mut inner = inner.write().unwrap(); - - inner - .blocks - .insert(H256::repeat_byte(0x1), Default::default()); - inner.block_hashes.insert(1, H256::repeat_byte(0x1)); - inner.tx_results.insert( - H256::repeat_byte(0x1), - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: Default::default(), - debug: testing::default_tx_debug_info(), - }, - ); - inner.current_batch = 1; - inner.current_miniblock = 1; - inner.current_miniblock_hash = H256::repeat_byte(0x1); - node.time.set_current_timestamp_unchecked(1); - inner - .filters - .add_block_filter() - .expect("failed adding block filter"); - inner.impersonation.impersonate(H160::repeat_byte(0x1)); - inner.rich_accounts.insert(H160::repeat_byte(0x1)); - inner - .previous_states - .insert(H256::repeat_byte(0x1), Default::default()); - inner.fork_storage.set_value( - StorageKey::new(AccountTreeId::new(H160::repeat_byte(0x1)), H256::zero()), - H256::repeat_byte(0x1), - ); - - let expected_snapshot = { - let storage = inner.fork_storage.inner.read().unwrap(); - Snapshot { - current_batch: inner.current_batch, - current_miniblock: inner.current_miniblock, - current_miniblock_hash: inner.current_miniblock_hash, - fee_input_provider: inner.fee_input_provider.clone(), - tx_results: inner.tx_results.clone(), - blocks: inner.blocks.clone(), - block_hashes: inner.block_hashes.clone(), - filters: inner.filters.clone(), - impersonation_state: inner.impersonation.state(), - rich_accounts: inner.rich_accounts.clone(), - previous_states: inner.previous_states.clone(), - raw_storage: storage.raw_storage.clone(), - value_read_cache: storage.value_read_cache.clone(), - factory_dep_cache: storage.factory_dep_cache.clone(), - } - }; - - // snapshot and modify node state - let snapshot = inner.snapshot().expect("failed taking snapshot"); - inner - .blocks - .insert(H256::repeat_byte(0x2), Default::default()); - inner.block_hashes.insert(2, H256::repeat_byte(0x2)); - inner.tx_results.insert( - H256::repeat_byte(0x2), - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: Default::default(), - debug: default_tx_debug_info(), - }, - ); - inner.current_batch = 2; - inner.current_miniblock = 2; - inner.current_miniblock_hash = H256::repeat_byte(0x2); - node.time.set_current_timestamp_unchecked(2); - inner - .filters - .add_pending_transaction_filter() - .expect("failed adding pending transaction filter"); - inner.impersonation.impersonate(H160::repeat_byte(0x2)); - inner.rich_accounts.insert(H160::repeat_byte(0x2)); - inner - .previous_states - .insert(H256::repeat_byte(0x2), Default::default()); - inner.fork_storage.set_value( - StorageKey::new(AccountTreeId::new(H160::repeat_byte(0x2)), H256::zero()), - H256::repeat_byte(0x2), - ); - - // restore - inner - .restore_snapshot(snapshot) - .expect("failed restoring snapshot"); - - let storage = inner.fork_storage.inner.read().unwrap(); - assert_eq!(expected_snapshot.current_batch, inner.current_batch); - assert_eq!(expected_snapshot.current_miniblock, inner.current_miniblock); - assert_eq!( - expected_snapshot.current_miniblock_hash, - inner.current_miniblock_hash - ); - - assert_eq!( - expected_snapshot.fee_input_provider, - inner.fee_input_provider - ); - assert_eq!( - expected_snapshot.tx_results.keys().collect_vec(), - inner.tx_results.keys().collect_vec() - ); - assert_eq!(expected_snapshot.blocks, inner.blocks); - assert_eq!(expected_snapshot.block_hashes, inner.block_hashes); - assert_eq!(expected_snapshot.filters, inner.filters); - assert_eq!( - expected_snapshot.impersonation_state, - inner.impersonation.state() - ); - assert_eq!(expected_snapshot.rich_accounts, inner.rich_accounts); - assert_eq!(expected_snapshot.previous_states, inner.previous_states); - assert_eq!(expected_snapshot.raw_storage, storage.raw_storage); - assert_eq!(expected_snapshot.value_read_cache, storage.value_read_cache); - assert_eq!( - expected_snapshot.factory_dep_cache, - storage.factory_dep_cache - ); - } - #[tokio::test] async fn test_get_transaction_by_block_hash_and_index_returns_none_for_invalid_block_hash() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let input_tx_hash = H256::repeat_byte(0x01); - let (input_block_hash, _, _) = testing::apply_tx(&node, input_tx_hash); + let (input_block_hash, _, _) = testing::apply_tx(&node, input_tx_hash).await; let invalid_block_hash = H256::repeat_byte(0xab); assert_ne!(input_block_hash, invalid_block_hash); let result = node - .get_transaction_by_block_hash_and_index_impl(invalid_block_hash, U64::from(0)) + .get_transaction_by_block_and_index_impl( + api::BlockId::Hash(invalid_block_hash), + U64::from(0), + ) .await .expect("failed fetching transaction"); @@ -2497,12 +2102,15 @@ mod tests { #[tokio::test] async fn test_get_transaction_by_block_hash_and_index_returns_none_for_invalid_index() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let input_tx_hash = H256::repeat_byte(0x01); - let (input_block_hash, _, _) = testing::apply_tx(&node, input_tx_hash); + let (input_block_hash, _, _) = testing::apply_tx(&node, input_tx_hash).await; let result = node - .get_transaction_by_block_hash_and_index_impl(input_block_hash, U64::from(10)) + .get_transaction_by_block_and_index_impl( + api::BlockId::Hash(input_block_hash), + U64::from(10), + ) .await .expect("failed fetching transaction"); @@ -2511,68 +2119,20 @@ mod tests { #[tokio::test] async fn test_get_transaction_by_block_hash_and_index_returns_transaction_for_valid_input() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let input_tx_hash = H256::repeat_byte(0x01); - let (input_block_hash, _, _) = testing::apply_tx(&node, input_tx_hash); - - let actual_tx = node - .get_transaction_by_block_hash_and_index_impl(input_block_hash, U64::from(0)) - .await - .expect("failed fetching transaction") - .expect("no transaction"); - - assert_eq!(input_tx_hash, actual_tx.hash); - } - - #[tokio::test] - async fn test_get_transaction_by_block_hash_and_index_fetches_full_transaction_for_hash_from_fork( - ) { - let mock_server = MockServer::run_with_config(ForkBlockConfig { - number: 10, - transaction_count: 0, - hash: H256::repeat_byte(0xab), - }); - let input_block_hash = H256::repeat_byte(0x01); - let input_tx_hash = H256::repeat_byte(0x02); - mock_server.expect( - serde_json::json!({ - "jsonrpc": "2.0", - "id": 0, - "method": "eth_getTransactionByHash", - "params": [ - format!("{:#x}", input_tx_hash), - ], - }), - TransactionResponseBuilder::new() - .set_hash(input_tx_hash) - .set_block_hash(input_block_hash) - .set_block_number(U64::from(1)) - .build(), - ); - - let node = test_node(&mock_server.url()).await; - - // store the block info with just the tx hash invariant - { - let inner = node.get_inner(); - let mut writer = inner.write().unwrap(); - writer.blocks.insert( - input_block_hash, - Block { - transactions: vec![TransactionVariant::Hash(input_tx_hash)], - ..Default::default() - }, - ); - } + let (input_block_hash, _, _) = testing::apply_tx(&node, input_tx_hash).await; let actual_tx = node - .get_transaction_by_block_hash_and_index_impl(input_block_hash, U64::from(0)) + .get_transaction_by_block_and_index_impl( + api::BlockId::Hash(input_block_hash), + U64::from(0), + ) .await .expect("failed fetching transaction") .expect("no transaction"); assert_eq!(input_tx_hash, actual_tx.hash); - assert_eq!(Some(U64::from(1)), actual_tx.block_number); } #[tokio::test] @@ -2604,7 +2164,10 @@ mod tests { let node = test_node(&mock_server.url()).await; let actual_tx = node - .get_transaction_by_block_hash_and_index_impl(input_block_hash, U64::from(1)) + .get_transaction_by_block_and_index_impl( + api::BlockId::Hash(input_block_hash), + U64::from(1), + ) .await .expect("failed fetching transaction") .expect("no transaction"); @@ -2616,15 +2179,15 @@ mod tests { #[tokio::test] async fn test_get_transaction_by_block_number_and_index_returns_none_for_invalid_block_number() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let input_tx_hash = H256::repeat_byte(0x01); - let (input_block_hash, _, _) = testing::apply_tx(&node, input_tx_hash); + let (input_block_hash, _, _) = testing::apply_tx(&node, input_tx_hash).await; let invalid_block_hash = H256::repeat_byte(0xab); assert_ne!(input_block_hash, invalid_block_hash); let result = node - .get_transaction_by_block_number_and_index_impl( - BlockNumber::Number(U64::from(100)), + .get_transaction_by_block_and_index_impl( + api::BlockId::Number(BlockNumber::Number(U64::from(100))), U64::from(0), ) .await @@ -2635,12 +2198,15 @@ mod tests { #[tokio::test] async fn test_get_transaction_by_block_number_and_index_returns_none_for_invalid_index() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let input_tx_hash = H256::repeat_byte(0x01); - testing::apply_tx(&node, input_tx_hash); + testing::apply_tx(&node, input_tx_hash).await; let result = node - .get_transaction_by_block_number_and_index_impl(BlockNumber::Latest, U64::from(10)) + .get_transaction_by_block_and_index_impl( + api::BlockId::Number(BlockNumber::Latest), + U64::from(10), + ) .await .expect("failed fetching transaction"); @@ -2649,70 +2215,13 @@ mod tests { #[tokio::test] async fn test_get_transaction_by_block_number_and_index_returns_transaction_for_valid_input() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let input_tx_hash = H256::repeat_byte(0x01); - let (_, input_block_number, _) = testing::apply_tx(&node, input_tx_hash); - - let actual_tx = node - .get_transaction_by_block_number_and_index_impl( - BlockNumber::Number(input_block_number), - U64::from(0), - ) - .await - .expect("failed fetching transaction") - .expect("no transaction"); - - assert_eq!(input_tx_hash, actual_tx.hash); - } - - #[tokio::test] - async fn test_get_transaction_by_block_number_and_index_fetches_full_transaction_for_hash_from_fork( - ) { - let mock_server = MockServer::run_with_config(ForkBlockConfig { - number: 10, - transaction_count: 0, - hash: H256::repeat_byte(0xab), - }); - let input_block_hash = H256::repeat_byte(0x01); - let input_block_number = U64::from(100); - let input_tx_hash = H256::repeat_byte(0x02); - mock_server.expect( - serde_json::json!({ - "jsonrpc": "2.0", - "id": 0, - "method": "eth_getTransactionByHash", - "params": [ - format!("{:#x}", input_tx_hash), - ], - }), - TransactionResponseBuilder::new() - .set_hash(input_tx_hash) - .set_block_hash(input_block_hash) - .set_block_number(input_block_number) - .build(), - ); - - let node = test_node(&mock_server.url()).await; - - // store the block info with just the tx hash invariant - { - let inner = node.get_inner(); - let mut writer = inner.write().unwrap(); - writer - .block_hashes - .insert(input_block_number.as_u64(), input_block_hash); - writer.blocks.insert( - input_block_hash, - Block { - transactions: vec![TransactionVariant::Hash(input_tx_hash)], - ..Default::default() - }, - ); - } + let (_, input_block_number, _) = testing::apply_tx(&node, input_tx_hash).await; let actual_tx = node - .get_transaction_by_block_number_and_index_impl( - BlockNumber::Number(input_block_number), + .get_transaction_by_block_and_index_impl( + api::BlockId::Number(BlockNumber::Number(input_block_number)), U64::from(0), ) .await @@ -2720,7 +2229,6 @@ mod tests { .expect("no transaction"); assert_eq!(input_tx_hash, actual_tx.hash); - assert_eq!(Some(input_block_number), actual_tx.block_number); } #[tokio::test] @@ -2753,8 +2261,8 @@ mod tests { let node = test_node(&mock_server.url()).await; let actual_tx = node - .get_transaction_by_block_number_and_index_impl( - BlockNumber::Number(input_block_number), + .get_transaction_by_block_and_index_impl( + api::BlockId::Number(BlockNumber::Number(input_block_number)), U64::from(1), ) .await @@ -2767,7 +2275,7 @@ mod tests { #[tokio::test] async fn test_protocol_version_returns_currently_supported_version() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let expected_version = String::from(PROTOCOL_VERSION); let actual_version = node.protocol_version_impl(); diff --git a/crates/core/src/node/fee_model.rs b/crates/core/src/node/fee_model.rs index 33b7fcd5..97dab926 100644 --- a/crates/core/src/node/fee_model.rs +++ b/crates/core/src/node/fee_model.rs @@ -4,10 +4,12 @@ use zksync_types::fee_model::{ BaseTokenConversionRatio, BatchFeeInput, FeeModelConfigV2, FeeParams, FeeParamsV2, }; +use super::inner::fork::ForkDetails; use anvil_zksync_config::constants::{ DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR, DEFAULT_ESTIMATE_GAS_SCALE_FACTOR, DEFAULT_FAIR_PUBDATA_PRICE, DEFAULT_L1_GAS_PRICE, DEFAULT_L2_GAS_PRICE, }; + #[derive(Debug, Clone)] pub struct TestNodeFeeInputProvider { /// L1 Gas Price Scale Factor for gas estimation. @@ -40,6 +42,25 @@ impl PartialEq for TestNodeFeeInputProvider { } impl TestNodeFeeInputProvider { + pub fn from_fork(fork: Option<&ForkDetails>) -> Self { + if let Some(fork) = fork { + if let Some(params) = fork.fee_params { + TestNodeFeeInputProvider::from_fee_params_and_estimate_scale_factors( + params, + fork.estimate_gas_price_scale_factor, + fork.estimate_gas_scale_factor, + ) + } else { + TestNodeFeeInputProvider::from_estimate_scale_factors( + fork.estimate_gas_price_scale_factor, + fork.estimate_gas_scale_factor, + ) + } + } else { + TestNodeFeeInputProvider::default() + } + } + pub fn from_fee_params_and_estimate_scale_factors( fee_params: FeeParams, estimate_gas_price_scale_factor: f64, diff --git a/crates/core/src/node/in_memory.rs b/crates/core/src/node/in_memory.rs index e01e4961..649b64d8 100644 --- a/crates/core/src/node/in_memory.rs +++ b/crates/core/src/node/in_memory.rs @@ -1,33 +1,27 @@ //! In-memory node, that supports forking other networks. -use crate::fork::SerializableStorage; +use super::inner::fork::ForkDetails; +use super::inner::node_executor::NodeExecutorHandle; +use super::inner::InMemoryNodeInner; +use crate::deps::{storage_view::StorageView, InMemoryStorage}; +use crate::filters::EthFilters; +use crate::formatter; +use crate::node::call_error_tracer::CallErrorTracer; use crate::node::error::LoadStateError; +use crate::node::fee_model::TestNodeFeeInputProvider; use crate::node::impersonate::{ImpersonationManager, ImpersonationState}; -use crate::node::state::{StateV1, VersionedState}; -use crate::node::time::{AdvanceTime, ReadTime, TimestampManager}; -use crate::node::{BlockSealer, BlockSealerMode, TxPool}; -use crate::{ - bootloader_debug::{BootloaderDebug, BootloaderDebugTracer}, - console_log::ConsoleLogHandler, - deps::{storage_view::StorageView, InMemoryStorage}, - filters::EthFilters, - fork::{ForkDetails, ForkStorage}, - formatter, - node::{ - call_error_tracer::CallErrorTracer, fee_model::TestNodeFeeInputProvider, - storage_logs::print_storage_logs_details, - }, - observability::Observability, - system_contracts::SystemContracts, - utils::{bytecode_to_factory_dep, create_debug_output}, -}; +use crate::node::inner::blockchain::BlockchainReader; +use crate::node::inner::time::TimeReader; +use crate::node::sealer::BlockSealerState; +use crate::node::state::VersionedState; +use crate::node::{BlockSealer, BlockSealerMode, NodeExecutor, TxPool}; +use crate::observability::Observability; +use crate::system_contracts::SystemContracts; use anvil_zksync_config::constants::{ LEGACY_RICH_WALLETS, NON_FORK_FIRST_BLOCK_TIMESTAMP, RICH_WALLETS, TEST_NODE_NETWORK_ID, }; -use anvil_zksync_config::types::{CacheConfig, Genesis, SystemContractsOptions}; +use anvil_zksync_config::types::Genesis; use anvil_zksync_config::TestNodeConfig; -use anvil_zksync_types::{ - LogLevel, ShowCalls, ShowGasDetails, ShowStorageLogs, ShowVMDetails, TransactionOrder, -}; +use anvil_zksync_types::{LogLevel, ShowCalls, ShowGasDetails, ShowStorageLogs, ShowVMDetails}; use colored::Colorize; use flate2::read::GzDecoder; use flate2::write::GzEncoder; @@ -35,53 +29,35 @@ use flate2::Compression; use indexmap::IndexMap; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; use std::io::{Read, Write}; -use std::sync::{RwLockReadGuard, RwLockWriteGuard}; -use std::{ - collections::{HashMap, HashSet}, - convert::TryInto, - str::FromStr, - sync::{Arc, RwLock}, -}; +use std::str::FromStr; +use std::sync::Arc; +use tokio::sync::RwLock; use zksync_contracts::BaseSystemContracts; -use zksync_multivm::vm_latest::HistoryEnabled; -use zksync_multivm::{ - interface::{ - storage::{ReadStorage, StoragePtr, WriteStorage}, - Call, ExecutionResult, InspectExecutionMode, L1BatchEnv, L2Block, L2BlockEnv, SystemEnv, - TxExecutionMode, VmExecutionResultAndLogs, VmFactory, VmInterface, VmInterfaceExt, - VmInterfaceHistoryEnabled, - }, - tracers::CallTracer, - utils::{ - adjust_pubdata_price_for_tx, derive_base_fee_and_gas_per_pubdata, derive_overhead, - get_batch_base_fee, get_max_batch_gas_limit, get_max_gas_per_pubdata_byte, - }, - vm_latest::{ - constants::{BATCH_COMPUTATIONAL_GAS_LIMIT, BATCH_GAS_LIMIT, MAX_VM_PUBDATA_PER_BATCH}, - utils::l2_blocks::load_last_l2_block, - HistoryDisabled, ToTracerPointer, Vm, - }, - HistoryMode, VmVersion, +use zksync_multivm::interface::storage::{ReadStorage, StoragePtr}; +use zksync_multivm::interface::{ + ExecutionResult, InspectExecutionMode, L1BatchEnv, L2BlockEnv, TxExecutionMode, VmFactory, + VmInterface, }; +use zksync_multivm::tracers::CallTracer; +use zksync_multivm::utils::{get_batch_base_fee, get_max_batch_gas_limit}; +use zksync_multivm::vm_latest::{HistoryDisabled, ToTracerPointer, Vm}; +use zksync_multivm::VmVersion; +use zksync_types::api::{Block, DebugCall, TransactionReceipt, TransactionVariant}; +use zksync_types::block::unpack_block_info; use zksync_types::bytecode::BytecodeHash; -use zksync_types::transaction_request::CallRequest; +use zksync_types::fee_model::BatchFeeInput; +use zksync_types::l2::L2Tx; +use zksync_types::storage::{ + EMPTY_UNCLES_HASH, SYSTEM_CONTEXT_ADDRESS, SYSTEM_CONTEXT_BLOCK_INFO_POSITION, +}; +use zksync_types::web3::{keccak256, Bytes}; +use zksync_types::{get_code_key, h256_to_u256}; use zksync_types::{ - api::{Block, DebugCall, Log, TransactionReceipt, TransactionVariant}, - block::{build_bloom, unpack_block_info, L2BlockHasher}, - fee::Fee, - fee_model::{BatchFeeInput, PubdataIndependentBatchFeeModelInput}, - get_code_key, get_nonce_key, - l2::{L2Tx, TransactionType}, - utils::{decompose_full_nonce, nonces_to_full_nonce, storage_key_for_eth_balance}, - web3::{keccak256, Bytes, Index}, - AccountTreeId, Address, Bloom, BloomInput, L1BatchNumber, L2BlockNumber, PackedEthSignature, - StorageKey, StorageValue, Transaction, ACCOUNT_CODE_STORAGE_ADDRESS, EMPTY_UNCLES_HASH, H160, - H256, H64, MAX_L2_TX_GAS_LIMIT, SYSTEM_CONTEXT_ADDRESS, SYSTEM_CONTEXT_BLOCK_INFO_POSITION, - U256, U64, + AccountTreeId, Address, Bloom, L1BatchNumber, L2BlockNumber, PackedEthSignature, StorageKey, + StorageValue, Transaction, H160, H256, H64, U256, U64, }; -use zksync_types::{h256_to_address, h256_to_u256, u256_to_h256}; -use zksync_web3_decl::error::Web3Error; /// Max possible size of an ABI encoded tx (in bytes). pub const MAX_TX_SIZE: usize = 1_000_000; @@ -92,6 +68,7 @@ pub const MAX_PREVIOUS_STATES: u16 = 128; /// The zks protocol version. pub const PROTOCOL_VERSION: &str = "zks/1"; +// TODO: Use L2BlockNumber pub fn compute_hash<'a>(block_number: u64, tx_hashes: impl IntoIterator) -> H256 { let tx_bytes = tx_hashes .into_iter() @@ -167,7 +144,7 @@ pub fn create_genesis(timestamp: Option) -> Block { } #[allow(clippy::too_many_arguments)] -fn create_block( +pub fn create_block( batch_env: &L1BatchEnv, hash: H256, parent_hash: H256, @@ -237,831 +214,19 @@ impl TransactionResult { } } -/// Helper struct for InMemoryNode. -/// S - is the Source of the Fork. -pub struct InMemoryNodeInner { - /// The latest batch number that was already generated. - /// Next block will be current_batch + 1 - pub current_batch: u32, - /// The latest miniblock number that was already generated. - /// Next transaction will go to the block current_miniblock + 1 - pub current_miniblock: u64, - /// The latest miniblock hash. - pub current_miniblock_hash: H256, - /// The fee input provider. - pub fee_input_provider: TestNodeFeeInputProvider, - // Map from transaction to details about the exeuction - pub tx_results: HashMap, - // Map from block hash to information about the block. - pub blocks: HashMap>, - // Map from block number to a block hash. - pub block_hashes: HashMap, - // Map from filter_id to the eth filter - pub filters: EthFilters, - // Underlying storage - pub fork_storage: ForkStorage, - // Configuration. - pub config: TestNodeConfig, - pub console_log_handler: ConsoleLogHandler, - pub system_contracts: SystemContracts, - pub impersonation: ImpersonationManager, - pub rich_accounts: HashSet, - /// Keeps track of historical states indexed via block hash. Limited to [MAX_PREVIOUS_STATES]. - pub previous_states: IndexMap>, -} - -#[derive(Debug)] -pub struct TxExecutionOutput { - result: VmExecutionResultAndLogs, - call_traces: Vec, - bytecodes: HashMap>, -} - -impl InMemoryNodeInner { - /// Create the state to be used implementing [InMemoryNode]. - pub fn new( - fork: Option, - config: &TestNodeConfig, - time: &TimestampManager, - impersonation: ImpersonationManager, - system_contracts: SystemContracts, - ) -> Self { - let updated_config = config.clone(); - if config.enable_auto_impersonate { - // Enable auto impersonation if configured - impersonation.set_auto_impersonation(true); - } - - 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 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, - ) - }; - time.set_current_timestamp_unchecked(f.block_timestamp); - - InMemoryNodeInner { - 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, - &updated_config.system_contracts_options, - updated_config.use_evm_emulator, - updated_config.chain_id, - ), - config: updated_config.clone(), - console_log_handler: ConsoleLogHandler::default(), - system_contracts, - impersonation, - rich_accounts: HashSet::new(), - previous_states: Default::default(), - } - } else { - let mut block_hashes = HashMap::::new(); - let block_hash = compute_hash(0, []); - block_hashes.insert(0, block_hash); - let mut blocks = HashMap::>::new(); - let genesis_block: Block = if let Some(ref genesis) = config.genesis - { - create_genesis_from_json(genesis, config.genesis_timestamp) - } else { - create_genesis(config.genesis_timestamp) - }; - - blocks.insert(block_hash, genesis_block); - let fee_input_provider = TestNodeFeeInputProvider::default(); - time.set_current_timestamp_unchecked(NON_FORK_FIRST_BLOCK_TIMESTAMP); - - InMemoryNodeInner { - 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, - config.use_evm_emulator, - updated_config.chain_id, - ), - config: config.clone(), - console_log_handler: ConsoleLogHandler::default(), - system_contracts, - impersonation, - rich_accounts: HashSet::new(), - previous_states: Default::default(), - } - } - } - - /// Create [L1BatchEnv] to be used in the VM. - /// - /// We compute l1/l2 block details from storage to support fork testing, where the storage - /// can be updated mid execution and no longer matches with the initial node's state. - /// The L1 & L2 timestamps are also compared with node's timestamp to ensure it always increases monotonically. - pub fn create_l1_batch_env( - &self, - time: &T, - storage: StoragePtr, - ) -> (L1BatchEnv, BlockContext) { - tracing::debug!("Creating l1 batch env..."); - - let last_l1_block_num = load_last_l1_batch(storage.clone()) - .map(|(num, _)| num as u32) - .unwrap_or(self.current_batch); - let last_l2_block = load_last_l2_block(&storage).unwrap_or_else(|| L2Block { - number: self.current_miniblock as u32, - hash: L2BlockHasher::legacy_hash(L2BlockNumber(self.current_miniblock as u32)), - timestamp: time.current_timestamp(), - }); - - let block_ctx = BlockContext { - hash: H256::zero(), - batch: last_l1_block_num.saturating_add(1), - miniblock: (last_l2_block.number as u64).saturating_add(1), - timestamp: time.peek_next_timestamp(), - }; - - let fee_input = if let Some(fork) = &self - .fork_storage - .inner - .read() - .expect("fork_storage lock is already held by the current thread") - .fork - { - BatchFeeInput::PubdataIndependent(PubdataIndependentBatchFeeModelInput { - l1_gas_price: fork.l1_gas_price, - fair_l2_gas_price: fork.l2_fair_gas_price, - fair_pubdata_price: fork.fair_pubdata_price, - }) - } else { - self.fee_input_provider.get_batch_fee_input() - }; - - let batch_env = L1BatchEnv { - // TODO: set the previous batch hash properly (take from fork, when forking, and from local storage, when this is not the first block). - previous_batch_hash: None, - number: L1BatchNumber::from(block_ctx.batch), - timestamp: block_ctx.timestamp, - fee_input, - fee_account: H160::zero(), - enforced_base_fee: None, - first_l2_block: L2BlockEnv { - // the 'current_miniblock' contains the block that was already produced. - // So the next one should be one higher. - number: block_ctx.miniblock as u32, - timestamp: block_ctx.timestamp, - prev_block_hash: last_l2_block.hash, - // This is only used during zksyncEra block timestamp/number transition. - // In case of starting a new network, it doesn't matter. - // In theory , when forking mainnet, we should match this value - // to the value that was set in the node at that time - but AFAIK - // we don't have any API for this - so this might result in slightly - // incorrect replays of transacions during the migration period, that - // depend on block number or timestamp. - max_virtual_blocks_to_create: 1, - }, - }; - - (batch_env, block_ctx) - } - - pub fn create_system_env( - &self, - base_system_contracts: BaseSystemContracts, - execution_mode: TxExecutionMode, - ) -> SystemEnv { - SystemEnv { - zk_porter_available: false, - // TODO: when forking, we could consider taking the protocol version id from the fork itself. - version: zksync_types::ProtocolVersionId::latest(), - base_system_smart_contracts: base_system_contracts, - bootloader_gas_limit: BATCH_COMPUTATIONAL_GAS_LIMIT, - execution_mode, - default_validation_computational_gas_limit: BATCH_COMPUTATIONAL_GAS_LIMIT, - chain_id: self.fork_storage.chain_id, - } - } - - /// Estimates the gas required for a given call request. - /// - /// # Arguments - /// - /// * `req` - A `CallRequest` struct representing the call request to estimate gas for. - /// - /// # Returns - /// - /// A `Result` with a `Fee` representing the estimated gas related data. - pub fn estimate_gas_impl( - &self, - time: &T, - req: CallRequest, - ) -> Result { - let mut request_with_gas_per_pubdata_overridden = req; - - if let Some(ref mut eip712_meta) = request_with_gas_per_pubdata_overridden.eip712_meta { - if eip712_meta.gas_per_pubdata == U256::zero() { - eip712_meta.gas_per_pubdata = - get_max_gas_per_pubdata_byte(VmVersion::latest()).into(); - } - } - - let is_eip712 = request_with_gas_per_pubdata_overridden - .eip712_meta - .is_some(); - let initiator_address = request_with_gas_per_pubdata_overridden - .from - .unwrap_or_default(); - let impersonating = self.impersonation.is_impersonating(&initiator_address); - let system_contracts = self - .system_contracts - .contracts_for_fee_estimate(impersonating) - .clone(); - let allow_no_target = system_contracts.evm_emulator.is_some(); - - let mut l2_tx = L2Tx::from_request( - request_with_gas_per_pubdata_overridden.into(), - MAX_TX_SIZE, - allow_no_target, - ) - .map_err(Web3Error::SerializationError)?; - - let tx: Transaction = l2_tx.clone().into(); - - let fee_input = { - let fee_input = self.fee_input_provider.get_batch_fee_input_scaled(); - // In order for execution to pass smoothly, we need to ensure that block's required gasPerPubdata will be - // <= to the one in the transaction itself. - adjust_pubdata_price_for_tx( - fee_input, - tx.gas_per_pubdata_byte_limit(), - None, - VmVersion::latest(), - ) - }; - - let (base_fee, gas_per_pubdata_byte) = - derive_base_fee_and_gas_per_pubdata(fee_input, VmVersion::latest()); - - // Properly format signature - if l2_tx.common_data.signature.is_empty() { - l2_tx.common_data.signature = vec![0u8; 65]; - l2_tx.common_data.signature[64] = 27; - } - - // The user may not include the proper transaction type during the estimation of - // the gas fee. However, it is needed for the bootloader checks to pass properly. - if is_eip712 { - l2_tx.common_data.transaction_type = TransactionType::EIP712Transaction; - } - - l2_tx.common_data.fee.gas_per_pubdata_limit = - get_max_gas_per_pubdata_byte(VmVersion::latest()).into(); - l2_tx.common_data.fee.max_fee_per_gas = base_fee.into(); - l2_tx.common_data.fee.max_priority_fee_per_gas = base_fee.into(); - - let storage_view = StorageView::new(&self.fork_storage); - let storage = storage_view.into_rc_ptr(); - - let execution_mode = TxExecutionMode::EstimateFee; - let (mut batch_env, _) = self.create_l1_batch_env(time, storage.clone()); - batch_env.fee_input = fee_input; - - let system_env = self.create_system_env(system_contracts, execution_mode); - - // When the pubdata cost grows very high, the total gas limit required may become very high as well. If - // we do binary search over any possible gas limit naively, we may end up with a very high number of iterations, - // which affects performance. - // - // To optimize for this case, we first calculate the amount of gas needed to cover for the pubdata. After that, we - // need to do a smaller binary search that is focused on computational gas limit only. - let additional_gas_for_pubdata = if tx.is_l1() { - // For L1 transactions the pubdata priced in such a way that the maximal computational - // gas limit should be enough to cover for the pubdata as well, so no additional gas is provided there. - 0u64 - } else { - // For L2 transactions, we estimate the amount of gas needed to cover for the pubdata by creating a transaction with infinite gas limit. - // And getting how much pubdata it used. - - // In theory, if the transaction has failed with such large gas limit, we could have returned an API error here right away, - // but doing it later on keeps the code more lean. - let result = InMemoryNodeInner::estimate_gas_step( - l2_tx.clone(), - gas_per_pubdata_byte, - BATCH_GAS_LIMIT, - batch_env.clone(), - system_env.clone(), - &self.fork_storage, - ); - - if result.statistics.pubdata_published > MAX_VM_PUBDATA_PER_BATCH.try_into().unwrap() { - return Err(Web3Error::SubmitTransactionError( - "exceeds limit for published pubdata".into(), - Default::default(), - )); - } - - // It is assumed that there is no overflow here - (result.statistics.pubdata_published as u64) * gas_per_pubdata_byte - }; - - // We are using binary search to find the minimal values of gas_limit under which the transaction succeeds - let mut lower_bound = 0u64; - let mut upper_bound = MAX_L2_TX_GAS_LIMIT; - let mut attempt_count = 1; - - tracing::trace!("Starting gas estimation loop"); - while lower_bound + ESTIMATE_GAS_ACCEPTABLE_OVERESTIMATION < upper_bound { - let mid = (lower_bound + upper_bound) / 2; - tracing::trace!( - "Attempt {} (lower_bound: {}, upper_bound: {}, mid: {})", - attempt_count, - lower_bound, - upper_bound, - mid - ); - let try_gas_limit = additional_gas_for_pubdata + mid; - - let estimate_gas_result = InMemoryNodeInner::estimate_gas_step( - l2_tx.clone(), - gas_per_pubdata_byte, - try_gas_limit, - batch_env.clone(), - system_env.clone(), - &self.fork_storage, - ); - - if estimate_gas_result.result.is_failed() { - tracing::trace!("Attempt {} FAILED", attempt_count); - lower_bound = mid + 1; - } else { - tracing::trace!("Attempt {} SUCCEEDED", attempt_count); - upper_bound = mid; - } - attempt_count += 1; - } - - tracing::trace!("Gas Estimation Values:"); - tracing::trace!(" Final upper_bound: {}", upper_bound); - tracing::trace!( - " ESTIMATE_GAS_SCALE_FACTOR: {}", - self.fee_input_provider.estimate_gas_scale_factor - ); - tracing::trace!(" MAX_L2_TX_GAS_LIMIT: {}", MAX_L2_TX_GAS_LIMIT); - let tx_body_gas_limit = upper_bound; - let suggested_gas_limit = ((upper_bound + additional_gas_for_pubdata) as f32 - * self.fee_input_provider.estimate_gas_scale_factor) - as u64; - - let estimate_gas_result = InMemoryNodeInner::estimate_gas_step( - l2_tx.clone(), - gas_per_pubdata_byte, - suggested_gas_limit, - batch_env, - system_env, - &self.fork_storage, - ); - - let overhead = derive_overhead( - suggested_gas_limit, - gas_per_pubdata_byte as u32, - tx.encoding_len(), - l2_tx.common_data.transaction_type as u8, - VmVersion::latest(), - ) as u64; - - match estimate_gas_result.result { - ExecutionResult::Revert { output } => { - tracing::info!("{}", format!("Unable to estimate gas for the request with our suggested gas limit of {}. The transaction is most likely unexecutable. Breakdown of estimation:", suggested_gas_limit + overhead).red()); - tracing::info!( - "{}", - format!( - "\tEstimated transaction body gas cost: {}", - tx_body_gas_limit - ) - .red() - ); - tracing::info!( - "{}", - format!("\tGas for pubdata: {}", additional_gas_for_pubdata).red() - ); - tracing::info!("{}", format!("\tOverhead: {}", overhead).red()); - let message = output.to_string(); - let pretty_message = format!( - "execution reverted{}{}", - if message.is_empty() { "" } else { ": " }, - message - ); - let data = output.encoded_data(); - tracing::info!("{}", pretty_message.on_red()); - Err(Web3Error::SubmitTransactionError(pretty_message, data)) - } - ExecutionResult::Halt { reason } => { - tracing::info!("{}", format!("Unable to estimate gas for the request with our suggested gas limit of {}. The transaction is most likely unexecutable. Breakdown of estimation:", suggested_gas_limit + overhead).red()); - tracing::info!( - "{}", - format!( - "\tEstimated transaction body gas cost: {}", - tx_body_gas_limit - ) - .red() - ); - tracing::info!( - "{}", - format!("\tGas for pubdata: {}", additional_gas_for_pubdata).red() - ); - tracing::info!("{}", format!("\tOverhead: {}", overhead).red()); - let message = reason.to_string(); - let pretty_message = format!( - "execution reverted{}{}", - if message.is_empty() { "" } else { ": " }, - message - ); - - tracing::info!("{}", pretty_message.on_red()); - Err(Web3Error::SubmitTransactionError(pretty_message, vec![])) - } - ExecutionResult::Success { .. } => { - let full_gas_limit = match suggested_gas_limit.overflowing_add(overhead) { - (value, false) => value, - (_, true) => { - tracing::info!("{}", "Overflow when calculating gas estimation. We've exceeded the block gas limit by summing the following values:".red()); - tracing::info!( - "{}", - format!( - "\tEstimated transaction body gas cost: {}", - tx_body_gas_limit - ) - .red() - ); - tracing::info!( - "{}", - format!("\tGas for pubdata: {}", additional_gas_for_pubdata).red() - ); - tracing::info!("{}", format!("\tOverhead: {}", overhead).red()); - return Err(Web3Error::SubmitTransactionError( - "exceeds block gas limit".into(), - Default::default(), - )); - } - }; - - tracing::trace!("Gas Estimation Results"); - tracing::trace!(" tx_body_gas_limit: {}", tx_body_gas_limit); - tracing::trace!( - " additional_gas_for_pubdata: {}", - additional_gas_for_pubdata - ); - tracing::trace!(" overhead: {}", overhead); - tracing::trace!(" full_gas_limit: {}", full_gas_limit); - let fee = Fee { - max_fee_per_gas: base_fee.into(), - max_priority_fee_per_gas: 0u32.into(), - gas_limit: full_gas_limit.into(), - gas_per_pubdata_limit: gas_per_pubdata_byte.into(), - }; - Ok(fee) - } - } - } - - /// Runs fee estimation against a sandbox vm with the given gas_limit. - #[allow(clippy::too_many_arguments)] - fn estimate_gas_step( - mut l2_tx: L2Tx, - gas_per_pubdata_byte: u64, - tx_gas_limit: u64, - batch_env: L1BatchEnv, - system_env: SystemEnv, - fork_storage: &ForkStorage, - ) -> VmExecutionResultAndLogs { - let tx: Transaction = l2_tx.clone().into(); - - // Set gas_limit for transaction - let gas_limit_with_overhead = tx_gas_limit - + derive_overhead( - tx_gas_limit, - gas_per_pubdata_byte as u32, - tx.encoding_len(), - l2_tx.common_data.transaction_type as u8, - VmVersion::latest(), - ) as u64; - l2_tx.common_data.fee.gas_limit = gas_limit_with_overhead.into(); - - let storage = StorageView::new(fork_storage).into_rc_ptr(); - - // The nonce needs to be updated - let nonce = l2_tx.nonce(); - let nonce_key = get_nonce_key(&l2_tx.initiator_account()); - let full_nonce = storage.borrow_mut().read_value(&nonce_key); - let (_, deployment_nonce) = decompose_full_nonce(h256_to_u256(full_nonce)); - let enforced_full_nonce = nonces_to_full_nonce(U256::from(nonce.0), deployment_nonce); - storage - .borrow_mut() - .set_value(nonce_key, u256_to_h256(enforced_full_nonce)); - - // We need to explicitly put enough balance into the account of the users - let payer = l2_tx.payer(); - let balance_key = storage_key_for_eth_balance(&payer); - let mut current_balance = h256_to_u256(storage.borrow_mut().read_value(&balance_key)); - let added_balance = l2_tx.common_data.fee.gas_limit * l2_tx.common_data.fee.max_fee_per_gas; - current_balance += added_balance; - storage - .borrow_mut() - .set_value(balance_key, u256_to_h256(current_balance)); - - let mut vm: Vm<_, HistoryDisabled> = Vm::new(batch_env, system_env, storage.clone()); - - let tx: Transaction = l2_tx.into(); - vm.push_transaction(tx); - - vm.execute(InspectExecutionMode::OneTx) - } - - /// Archives the current state for later queries. - pub fn archive_state(&mut self) -> Result<(), String> { - if self.previous_states.len() > MAX_PREVIOUS_STATES as usize { - if let Some(entry) = self.previous_states.shift_remove_index(0) { - tracing::debug!("removing archived state for previous block {:#x}", entry.0); - } - } - tracing::debug!( - "archiving state for {:#x} #{}", - self.current_miniblock_hash, - self.current_miniblock - ); - self.previous_states.insert( - self.current_miniblock_hash, - self.fork_storage - .inner - .read() - .map_err(|err| err.to_string())? - .raw_storage - .state - .clone(), - ); - - Ok(()) - } - - /// Creates a [Snapshot] of the current state of the node. - pub fn snapshot(&self) -> Result { - let storage = self - .fork_storage - .inner - .read() - .map_err(|err| format!("failed acquiring read lock on storage: {:?}", err))?; - - Ok(Snapshot { - current_batch: self.current_batch, - current_miniblock: self.current_miniblock, - current_miniblock_hash: self.current_miniblock_hash, - fee_input_provider: self.fee_input_provider.clone(), - tx_results: self.tx_results.clone(), - blocks: self.blocks.clone(), - block_hashes: self.block_hashes.clone(), - filters: self.filters.clone(), - impersonation_state: self.impersonation.state(), - rich_accounts: self.rich_accounts.clone(), - previous_states: self.previous_states.clone(), - raw_storage: storage.raw_storage.clone(), - value_read_cache: storage.value_read_cache.clone(), - factory_dep_cache: storage.factory_dep_cache.clone(), - }) - } - - /// Restores a previously created [Snapshot] of the node. - pub fn restore_snapshot(&mut self, snapshot: Snapshot) -> Result<(), String> { - let mut storage = self - .fork_storage - .inner - .write() - .map_err(|err| format!("failed acquiring write lock on storage: {:?}", err))?; - - self.current_batch = snapshot.current_batch; - self.current_miniblock = snapshot.current_miniblock; - self.current_miniblock_hash = snapshot.current_miniblock_hash; - self.fee_input_provider = snapshot.fee_input_provider; - self.tx_results = snapshot.tx_results; - self.blocks = snapshot.blocks; - self.block_hashes = snapshot.block_hashes; - self.filters = snapshot.filters; - self.impersonation.set_state(snapshot.impersonation_state); - self.rich_accounts = snapshot.rich_accounts; - self.previous_states = snapshot.previous_states; - storage.raw_storage = snapshot.raw_storage; - storage.value_read_cache = snapshot.value_read_cache; - storage.factory_dep_cache = snapshot.factory_dep_cache; - - Ok(()) - } - - fn dump_state(&self, preserve_historical_states: bool) -> anyhow::Result { - let fork_storage = self.fork_storage.dump_state(); - let historical_states = if preserve_historical_states { - self.previous_states - .iter() - .map(|(k, v)| (*k, SerializableStorage(v.clone().into_iter().collect()))) - .collect() - } else { - Vec::new() - }; - - Ok(VersionedState::v1(StateV1 { - blocks: self.blocks.values().cloned().collect(), - transactions: self.tx_results.values().cloned().collect(), - fork_storage, - historical_states, - })) - } - - fn load_blocks(&mut self, mut time: T, blocks: Vec>) { - tracing::trace!( - blocks = blocks.len(), - "loading new blocks from supplied state" - ); - for block in blocks { - let number = block.number.as_u64(); - tracing::trace!( - number, - hash = %block.hash, - "loading new block from supplied state" - ); - - self.block_hashes.insert(number, block.hash); - self.blocks.insert(block.hash, block); - } - - // Safe unwrap as there was at least one block in the loaded state - let latest_block = self.blocks.values().max_by_key(|b| b.number).unwrap(); - let latest_number = latest_block.number.as_u64(); - let latest_hash = latest_block.hash; - let Some(latest_batch_number) = latest_block.l1_batch_number.map(|n| n.as_u32()) else { - panic!("encountered a block with no batch; this is not supposed to happen") - }; - let latest_timestamp = latest_block.timestamp.as_u64(); - tracing::info!( - number = latest_number, - hash = %latest_hash, - batch_number = latest_batch_number, - timestamp = latest_timestamp, - "latest block after loading state" - ); - self.current_miniblock = latest_number; - self.current_miniblock_hash = latest_hash; - self.current_batch = latest_batch_number; - time.reset_to(latest_timestamp); - } - - fn load_transactions(&mut self, transactions: Vec) { - tracing::trace!( - transactions = transactions.len(), - "loading new transactions from supplied state" - ); - for transaction in transactions { - tracing::trace!( - hash = %transaction.receipt.transaction_hash, - "loading new transaction from supplied state" - ); - self.tx_results - .insert(transaction.receipt.transaction_hash, transaction); - } - } - - fn load_state( - &mut self, - time: T, - state: VersionedState, - ) -> Result { - if self.blocks.len() > 1 { - tracing::debug!( - blocks = self.blocks.len(), - "node has existing state; refusing to load new state" - ); - return Err(LoadStateError::HasExistingState); - } - let state = match state { - VersionedState::V1 { state, .. } => state, - VersionedState::Unknown { version } => { - return Err(LoadStateError::UnknownStateVersion(version)) - } - }; - if state.blocks.is_empty() { - tracing::debug!("new state has no blocks; refusing to load"); - return Err(LoadStateError::EmptyState); - } - - self.load_blocks(time, state.blocks); - self.load_transactions(state.transactions); - self.fork_storage.load_state(state.fork_storage); - - tracing::trace!( - states = state.historical_states.len(), - "loading historical states from supplied state" - ); - self.previous_states.extend( - state - .historical_states - .into_iter() - .map(|(k, v)| (k, v.0.into_iter().collect())), - ); - - Ok(true) - } - - fn apply_block( - &mut self, - time: &mut T, - block: Block, - index: u32, - ) { - // archive current state before we produce new batch/blocks - if let Err(err) = self.archive_state() { - tracing::error!( - "failed archiving state for block {}: {}", - self.current_miniblock, - err - ); - } - - self.current_miniblock = self.current_miniblock.saturating_add(1); - let expected_timestamp = time.advance_timestamp(); - - let actual_l1_batch_number = block - .l1_batch_number - .expect("block must have a l1_batch_number"); - if actual_l1_batch_number.as_u32() != self.current_batch { - panic!( - "expected next block to have batch_number {}, got {}", - self.current_batch, - actual_l1_batch_number.as_u32() - ); - } - - if block.number.as_u64() != self.current_miniblock { - panic!( - "expected next block to have miniblock {}, got {} | {index}", - self.current_miniblock, - block.number.as_u64() - ); - } - - if block.timestamp.as_u64() != expected_timestamp { - panic!( - "expected next block to have timestamp {}, got {} | {index}", - expected_timestamp, - block.timestamp.as_u64() - ); - } - - let block_hash = block.hash; - self.current_miniblock_hash = block_hash; - self.block_hashes.insert(block.number.as_u64(), block.hash); - self.blocks.insert(block.hash, block); - self.filters.notify_new_block(block_hash); - } - - fn get_block(&self, block_number: L2BlockNumber) -> Option<&Block> { - self.block_hashes - .get(&(block_number.0 as u64)) - .and_then(|hash| self.blocks.get(hash)) - } -} - /// Creates a restorable snapshot for the [InMemoryNodeInner]. The snapshot contains all the necessary /// data required to restore the [InMemoryNodeInner] state to a previous point in time. #[derive(Debug, Clone, Default)] pub struct Snapshot { - pub(crate) current_batch: u32, - pub(crate) current_miniblock: u64, - pub(crate) current_miniblock_hash: H256, + pub(crate) current_batch: L1BatchNumber, + pub(crate) current_block: L2BlockNumber, + pub(crate) current_block_hash: H256, // Currently, the fee is static and the fee input provider is immutable during the test node life cycle, // but in the future, it may contain some mutable state. pub(crate) fee_input_provider: TestNodeFeeInputProvider, pub(crate) tx_results: HashMap, pub(crate) blocks: HashMap>, - pub(crate) block_hashes: HashMap, + pub(crate) hashes: HashMap, pub(crate) filters: EthFilters, pub(crate) impersonation_state: ImpersonationState, pub(crate) rich_accounts: HashSet, @@ -1078,176 +243,65 @@ pub struct Snapshot { pub struct InMemoryNode { /// A thread safe reference to the [InMemoryNodeInner]. pub(crate) inner: Arc>, + pub(crate) blockchain: BlockchainReader, + pub(crate) node_handle: NodeExecutorHandle, /// List of snapshots of the [InMemoryNodeInner]. This is bounded at runtime by [MAX_SNAPSHOTS]. pub(crate) snapshots: Arc>>, - /// Configuration option that survives reset. - #[allow(dead_code)] - pub(crate) system_contracts_options: SystemContractsOptions, - pub(crate) time: TimestampManager, + pub(crate) time: TimeReader, pub(crate) impersonation: ImpersonationManager, /// An optional handle to the observability stack pub(crate) observability: Option, pub(crate) pool: TxPool, - pub(crate) sealer: BlockSealer, + pub(crate) sealer_state: BlockSealerState, pub(crate) system_contracts: SystemContracts, } -fn contract_address_from_tx_result(execution_result: &VmExecutionResultAndLogs) -> Option { - for query in execution_result.logs.storage_logs.iter().rev() { - if query.log.is_write() && query.log.key.address() == &ACCOUNT_CODE_STORAGE_ADDRESS { - return Some(h256_to_address(query.log.key.key())); - } - } - None -} - -impl Default for InMemoryNode { - fn default() -> Self { - let impersonation = ImpersonationManager::default(); - let pool = TxPool::new(impersonation.clone(), TransactionOrder::Fifo); - let tx_listener = pool.add_tx_listener(); - InMemoryNode::new( - None, - None, - &TestNodeConfig::default(), - TimestampManager::default(), - impersonation, - pool, - BlockSealer::new(BlockSealerMode::immediate(1000, tx_listener)), - ) - } -} - impl InMemoryNode { + #[allow(clippy::too_many_arguments)] pub fn new( - fork: Option, + inner: Arc>, + blockchain: BlockchainReader, + node_handle: NodeExecutorHandle, observability: Option, - config: &TestNodeConfig, - time: TimestampManager, + time: TimeReader, impersonation: ImpersonationManager, pool: TxPool, - sealer: BlockSealer, + sealer_state: BlockSealerState, + system_contracts: SystemContracts, ) -> Self { - let system_contracts_options = config.system_contracts_options; - let system_contracts = SystemContracts::from_options( - &config.system_contracts_options, - config.use_evm_emulator, - ); - let inner = InMemoryNodeInner::new( - fork, - config, - &time, - impersonation.clone(), - system_contracts.clone(), - ); InMemoryNode { - inner: Arc::new(RwLock::new(inner)), + inner, + blockchain, + node_handle, snapshots: Default::default(), - system_contracts_options, time, impersonation, observability, pool, - sealer, + sealer_state, system_contracts, } } - // Common pattern in tests - // TODO: Refactor InMemoryNode with a builder pattern - pub fn default_fork(fork: Option) -> Self { - let impersonation = ImpersonationManager::default(); - let pool = TxPool::new(impersonation.clone(), TransactionOrder::Fifo); - let tx_listener = pool.add_tx_listener(); - Self::new( - fork, - None, - &Default::default(), - TimestampManager::default(), - impersonation, - pool, - BlockSealer::new(BlockSealerMode::immediate(1000, tx_listener)), - ) - } - - pub fn get_inner(&self) -> Arc> { - self.inner.clone() - } - - pub fn read_inner(&self) -> anyhow::Result> { - self.inner - .read() - .map_err(|e| anyhow::anyhow!("InMemoryNode lock is poisoned: {}", e)) - } - - pub fn write_inner(&self) -> anyhow::Result> { - self.inner - .write() - .map_err(|e| anyhow::anyhow!("InMemoryNode lock is poisoned: {}", e)) - } - - 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) -> Result { - let inner = self - .inner - .read() - .map_err(|e| format!("Failed to acquire read lock: {}", e))?; - - Ok(inner.config.clone()) - } - - pub fn reset(&self, fork: Option) -> Result<(), String> { - let config = self.get_config()?; - let inner = InMemoryNodeInner::new( - fork, - &config, - &self.time, - self.impersonation.clone(), - self.system_contracts.clone(), - ); - - 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; - } + pub async fn reset(&self, fork: Option) -> Result<(), String> { + self.inner.write().await.reset(fork).await; + self.snapshots.write().await.clear(); for wallet in LEGACY_RICH_WALLETS.iter() { let address = wallet.0; self.set_rich_account( H160::from_str(address).unwrap(), U256::from(100u128 * 10u128.pow(18)), - ); + ) + .await; } for wallet in RICH_WALLETS.iter() { let address = wallet.0; self.set_rich_account( H160::from_str(address).unwrap(), U256::from(100u128 * 10u128.pow(18)), - ); + ) + .await; } Ok(()) } @@ -1255,36 +309,31 @@ impl InMemoryNode { /// Applies multiple transactions across multiple blocks. All transactions are expected to be /// executable. Note that on error this method may leave node in partially applied state (i.e. /// some txs have been applied while others have not). - pub fn apply_txs(&self, txs: Vec, max_transactions: usize) -> anyhow::Result<()> { + pub async fn apply_txs(&self, txs: Vec, max_transactions: usize) -> anyhow::Result<()> { tracing::debug!(count = txs.len(), "applying transactions"); // Create a temporary tx pool (i.e. state is not shared with the node mempool). let pool = TxPool::new( self.impersonation.clone(), - self.read_inner()?.config.transaction_order, + self.inner.read().await.config.transaction_order, ); pool.add_txs(txs); - // Lock time so that the produced blocks are guaranteed to be sequential in time. - let mut time = self.time.lock(); while let Some(tx_batch) = pool.take_uniform(max_transactions) { // Getting contracts is reasonably cheap, so we don't cache them. We may need differing contracts // depending on whether impersonation should be enabled for a block. - let system_contracts = self - .system_contracts - .contracts(TxExecutionMode::VerifyExecute, tx_batch.impersonating) - .clone(); let expected_tx_hashes = tx_batch .txs .iter() .map(|tx| tx.hash()) .collect::>(); - let block_numer = self.seal_block(&mut time, tx_batch.txs, system_contracts)?; + let block_numer = self.node_handle.seal_block_sync(tx_batch).await?; // Fetch the block that was just sealed - let inner = self.read_inner()?; - let block = inner - .get_block(block_numer) + let block = self + .blockchain + .get_block_by_number(block_numer) + .await .expect("freshly sealed block could not be found in storage"); // Calculate tx hash set from that block @@ -1312,67 +361,27 @@ impl InMemoryNode { } /// Adds a lot of tokens to a given account with a specified balance. - pub fn set_rich_account(&self, address: H160, balance: U256) { - let key = storage_key_for_eth_balance(&address); - - let mut inner = match self.inner.write() { - Ok(guard) => guard, - Err(e) => { - tracing::info!("Failed to acquire write lock: {}", e); - return; - } - }; - - let keys = { - let mut storage_view = StorageView::new(&inner.fork_storage); - // Set balance to the specified amount - storage_view.set_value(key, u256_to_h256(balance)); - storage_view.modified_storage_keys().clone() - }; - - for (key, value) in keys.iter() { - inner.fork_storage.set_value(*key, *value); - } - inner.rich_accounts.insert(address); - } - - pub fn system_contracts_for_tx( - &self, - tx_initiator: Address, - ) -> anyhow::Result { - Ok(if self.impersonation.is_impersonating(&tx_initiator) { - tracing::info!("🕵️ Executing tx from impersonated account {tx_initiator:?}"); - self.system_contracts - .contracts(TxExecutionMode::VerifyExecute, true) - .clone() - } else { - self.system_contracts - .contracts(TxExecutionMode::VerifyExecute, false) - .clone() - }) + pub async fn set_rich_account(&self, address: H160, balance: U256) { + self.inner.write().await.set_rich_account(address, balance) } /// Runs L2 'eth call' method - that doesn't commit to a block. - pub fn run_l2_call( + pub async fn run_l2_call( &self, mut l2_tx: L2Tx, base_contracts: BaseSystemContracts, ) -> anyhow::Result { let execution_mode = TxExecutionMode::EthCall; - let inner = self - .inner - .read() - .map_err(|_| anyhow::anyhow!("Failed to acquire write lock"))?; - - let storage = StorageView::new(&inner.fork_storage).into_rc_ptr(); + let inner = self.inner.read().await; // init vm - let (batch_env, _) = inner.create_l1_batch_env(&self.time, storage.clone()); + let (batch_env, _) = inner.create_l1_batch_env().await; let system_env = inner.create_system_env(base_contracts, execution_mode); - let mut vm: Vm<_, HistoryDisabled> = Vm::new(batch_env, system_env, storage.clone()); + let storage = StorageView::new(&inner.fork_storage).into_rc_ptr(); + let mut vm: Vm<_, HistoryDisabled> = Vm::new(batch_env, system_env, storage); // We must inject *some* signature (otherwise bootloader code fails to generate hash). if l2_tx.common_data.signature.is_empty() { @@ -1433,7 +442,7 @@ impl InMemoryNode { tx.execute.contract_address, call, is_last_sibling, - &inner.config.show_calls, + inner.config.show_calls, inner.config.show_outputs, inner.config.resolve_hashes, ); @@ -1443,499 +452,13 @@ impl InMemoryNode { Ok(tx_result.result) } - // Prints the gas details of the transaction for debugging purposes. - fn display_detailed_gas_info( - &self, - bootloader_debug_result: Option<&eyre::Result>, - spent_on_pubdata: u64, - ) -> eyre::Result<(), String> { - if let Some(bootloader_result) = bootloader_debug_result { - let bootloader_debug = bootloader_result.clone()?; - - let gas_details = formatter::compute_gas_details(&bootloader_debug, spent_on_pubdata); - let mut formatter = formatter::Formatter::new(); - - let fee_model_config = self - .inner - .read() - .unwrap() - .fee_input_provider - .get_fee_model_config(); - - formatter.print_gas_details(&gas_details, &fee_model_config); - - Ok(()) - } else { - Err("Bootloader tracer didn't finish.".to_owned()) - } - } - - /// Validates L2 transaction - fn validate_tx(&self, tx: &L2Tx) -> anyhow::Result<()> { - let max_gas = U256::from(u64::MAX); - if tx.common_data.fee.gas_limit > max_gas - || tx.common_data.fee.gas_per_pubdata_limit > max_gas - { - anyhow::bail!("exceeds block gas limit"); - } - - let l2_gas_price = self - .inner - .read() - .expect("failed acquiring reader") - .fee_input_provider - .gas_price(); - if tx.common_data.fee.max_fee_per_gas < l2_gas_price.into() { - tracing::info!( - "Submitted Tx is Unexecutable {:?} because of MaxFeePerGasTooLow {}", - tx.hash(), - tx.common_data.fee.max_fee_per_gas - ); - anyhow::bail!("block base fee higher than max fee per gas"); - } - - if tx.common_data.fee.max_fee_per_gas < tx.common_data.fee.max_priority_fee_per_gas { - tracing::info!( - "Submitted Tx is Unexecutable {:?} because of MaxPriorityFeeGreaterThanMaxFee {}", - tx.hash(), - tx.common_data.fee.max_fee_per_gas - ); - anyhow::bail!("max priority fee per gas higher than max fee per gas"); - } - Ok(()) - } - - /// Executes the given L2 transaction and returns all the VM logs. - /// The bootloader can be omitted via specifying the `execute_bootloader` boolean. - /// This causes the VM to produce 1 L2 block per L1 block, instead of the usual 2 blocks per L1 block. - /// - /// **NOTE** - /// - /// This function must only rely on data populated initially via [ForkDetails]: - /// * [InMemoryNodeInner::current_timestamp] - /// * [InMemoryNodeInner::current_batch] - /// * [InMemoryNodeInner::current_miniblock] - /// * [InMemoryNodeInner::current_miniblock_hash] - /// * [InMemoryNodeInner::fee_input_provider] - /// - /// And must _NEVER_ rely on data updated in [InMemoryNodeInner] during previous runs: - /// (if used, they must never panic and/or have meaningful defaults) - /// * [InMemoryNodeInner::block_hashes] - /// * [InMemoryNodeInner::blocks] - /// * [InMemoryNodeInner::tx_results] - /// - /// This is because external users of the library may call this function to perform an isolated - /// VM operation (optionally without bootloader execution) with an external storage and get the results back. - /// So any data populated in [Self::run_l2_tx] will not be available for the next invocation. - pub fn run_l2_tx_raw( - &self, - l2_tx: L2Tx, - vm: &mut Vm, - ) -> anyhow::Result { - let inner = self - .inner - .read() - .map_err(|_| anyhow::anyhow!("Failed to acquire read lock"))?; - - let tx: Transaction = l2_tx.into(); - - let call_tracer_result = Arc::new(OnceCell::default()); - let bootloader_debug_result = Arc::new(OnceCell::default()); - - let tracers = vec![ - CallErrorTracer::new().into_tracer_pointer(), - CallTracer::new(call_tracer_result.clone()).into_tracer_pointer(), - BootloaderDebugTracer { - result: bootloader_debug_result.clone(), - } - .into_tracer_pointer(), - ]; - let compressed_bytecodes = vm - .push_transaction(tx.clone()) - .compressed_bytecodes - .into_owned(); - let tx_result = vm.inspect(&mut tracers.into(), InspectExecutionMode::OneTx); - - let call_traces = call_tracer_result.get().unwrap(); - - let spent_on_pubdata = - tx_result.statistics.gas_used - tx_result.statistics.computational_gas_used as u64; - - let status = match &tx_result.result { - ExecutionResult::Success { .. } => "SUCCESS", - ExecutionResult::Revert { .. } => "FAILED", - ExecutionResult::Halt { .. } => "HALTED", - }; - - // Print transaction summary - if inner.config.show_tx_summary { - tracing::info!(""); - formatter::print_transaction_summary( - inner.config.get_l2_gas_price(), - &tx, - &tx_result, - status, - ); - tracing::info!(""); - } - // Print gas details if enabled - if inner.config.show_gas_details != ShowGasDetails::None { - self.display_detailed_gas_info(bootloader_debug_result.get(), spent_on_pubdata) - .unwrap_or_else(|err| { - tracing::error!("{}", format!("Cannot display gas details: {err}").on_red()); - }); - } - // Print storage logs if enabled - if inner.config.show_storage_logs != ShowStorageLogs::None { - print_storage_logs_details(&inner.config.show_storage_logs, &tx_result); - } - // Print VM details if enabled - if inner.config.show_vm_details != ShowVMDetails::None { - let mut formatter = formatter::Formatter::new(); - formatter.print_vm_details(&tx_result); - } - - if !inner.config.disable_console_log { - inner - .console_log_handler - .handle_calls_recursive(call_traces); - } - - if inner.config.show_calls != ShowCalls::None { - tracing::info!(""); - tracing::info!( - "[Transaction Execution] ({} calls)", - call_traces[0].calls.len() - ); - let num_calls = call_traces.len(); - for (i, call) in call_traces.iter().enumerate() { - let is_last_sibling = i == num_calls - 1; - let mut formatter = formatter::Formatter::new(); - formatter.print_call( - tx.initiator_account(), - tx.execute.contract_address, - call, - is_last_sibling, - &inner.config.show_calls, - inner.config.show_outputs, - inner.config.resolve_hashes, - ); - } - } - // Print event logs if enabled - if inner.config.show_event_logs { - tracing::info!(""); - tracing::info!("[Events] ({} events)", tx_result.logs.events.len()); - for (i, event) in tx_result.logs.events.iter().enumerate() { - let is_last = i == tx_result.logs.events.len() - 1; - let mut formatter = formatter::Formatter::new(); - formatter.print_event(event, inner.config.resolve_hashes, is_last); - } - tracing::info!(""); - } - - let mut bytecodes = HashMap::new(); - for b in &*compressed_bytecodes { - let (hash, bytecode) = bytecode_to_factory_dep(b.original.clone()).map_err(|err| { - tracing::error!("{}", format!("cannot convert bytecode: {err}").on_red()); - err - })?; - bytecodes.insert(hash, bytecode); - } - - Ok(TxExecutionOutput { - result: tx_result, - call_traces: call_traces.clone(), - bytecodes, - }) - } - - /// Runs L2 transaction and commits it to a new block. - pub fn run_l2_tx( - &self, - l2_tx: L2Tx, - l2_tx_index: U64, - block_ctx: &BlockContext, - batch_env: &L1BatchEnv, - vm: &mut Vm, - ) -> anyhow::Result<()> { - let tx_hash = l2_tx.hash(); - let transaction_type = l2_tx.common_data.transaction_type; - - let show_tx_summary = self - .inner - .read() - .map_err(|_| anyhow::anyhow!("Failed to acquire read lock"))? - .config - .show_tx_summary; - - if show_tx_summary { - tracing::info!(""); - tracing::info!("Validating {}", format!("{:?}", tx_hash).bold()); - } - - self.validate_tx(&l2_tx)?; - - if show_tx_summary { - tracing::info!("Executing {}", format!("{:?}", tx_hash).bold()); - } - - self.inner - .write() - .map_err(|_| anyhow::anyhow!("Failed to acquire write lock"))? - .filters - .notify_new_pending_transaction(tx_hash); - - let TxExecutionOutput { - result, - bytecodes, - call_traces, - } = self.run_l2_tx_raw(l2_tx.clone(), vm)?; - - if let ExecutionResult::Halt { reason } = result.result { - // Halt means that something went really bad with the transaction execution (in most cases invalid signature, - // but it could also be bootloader panic etc). - // In such case, we should not persist the VM data, and we should pretend that transaction never existed. - anyhow::bail!("Transaction HALT: {reason}"); - } - - // Write all the factory deps. - let mut inner = self - .inner - .write() - .map_err(|_| anyhow::anyhow!("Failed to acquire write lock"))?; - for (hash, code) in bytecodes.iter() { - inner.fork_storage.store_factory_dep( - u256_to_h256(*hash), - code.iter() - .flat_map(|entry| { - let mut bytes = vec![0u8; 32]; - entry.to_big_endian(&mut bytes); - bytes.to_vec() - }) - .collect(), - ) - } - - let logs = result - .logs - .events - .iter() - .enumerate() - .map(|(log_idx, log)| Log { - address: log.address, - topics: log.indexed_topics.clone(), - data: Bytes(log.value.clone()), - block_hash: Some(block_ctx.hash), - block_number: Some(block_ctx.miniblock.into()), - l1_batch_number: Some(U64::from(batch_env.number.0)), - transaction_hash: Some(tx_hash), - transaction_index: Some(l2_tx_index), - log_index: Some(U256::from(log_idx)), - transaction_log_index: Some(U256::from(log_idx)), - log_type: None, - removed: Some(false), - block_timestamp: Some(block_ctx.timestamp.into()), - }) - .collect(); - for log in &logs { - inner - .filters - .notify_new_log(log, block_ctx.miniblock.into()); - } - let tx_receipt = TransactionReceipt { - transaction_hash: tx_hash, - transaction_index: l2_tx_index, - block_hash: block_ctx.hash, - block_number: block_ctx.miniblock.into(), - l1_batch_tx_index: None, - l1_batch_number: Some(U64::from(batch_env.number.0)), - from: l2_tx.initiator_account(), - to: l2_tx.recipient_account(), - cumulative_gas_used: Default::default(), - gas_used: Some(l2_tx.common_data.fee.gas_limit - result.refunds.gas_refunded), - contract_address: contract_address_from_tx_result(&result), - logs, - l2_to_l1_logs: vec![], - status: if result.result.is_failed() { - U64::from(0) - } else { - U64::from(1) - }, - effective_gas_price: Some(inner.fee_input_provider.gas_price().into()), - transaction_type: Some((transaction_type as u32).into()), - logs_bloom: Default::default(), - }; - let debug = create_debug_output(&l2_tx, &result, call_traces).expect("create debug output"); // OK to unwrap here as Halt is handled above - inner.tx_results.insert( - tx_hash, - TransactionResult { - info: TxExecutionInfo { - tx: l2_tx, - batch_number: batch_env.number.0, - miniblock_number: block_ctx.miniblock, - }, - receipt: tx_receipt, - debug, - }, - ); - - Ok(()) - } - - // Requirement for `TimeExclusive` ensures that we have exclusive writeable access to time - // manager. Meaning we can construct blocks and apply them without worrying about TOCTOU with - // timestamps. - pub fn seal_block( - &self, - time: &mut T, - txs: Vec, - system_contracts: BaseSystemContracts, - ) -> anyhow::Result { - // Prepare a new block context and a new batch env - let inner = self - .inner - .read() - .map_err(|_| anyhow::anyhow!("Failed to acquire read lock"))?; - let storage = StorageView::new(inner.fork_storage.clone()).into_rc_ptr(); - let system_env = inner.create_system_env(system_contracts, TxExecutionMode::VerifyExecute); - let (batch_env, mut block_ctx) = inner.create_l1_batch_env(time, storage.clone()); - drop(inner); - - let mut vm: Vm<_, HistoryEnabled> = Vm::new(batch_env.clone(), system_env, storage.clone()); - - // Compute block hash. Note that the computed block hash here will be different than that in production. - let tx_hashes = txs.iter().map(|t| t.hash()).collect::>(); - let hash = compute_hash(block_ctx.miniblock, &tx_hashes); - block_ctx.hash = hash; - - // Execute transactions and bootloader - let mut executed_tx_hashes = Vec::with_capacity(tx_hashes.len()); - let mut tx_index = U64::from(0); - for tx in txs { - // Executing a next transaction means that a previous transaction was either rolled back (in which case its snapshot - // was already removed), or that we build on top of it (in which case, it can be removed now). - vm.pop_snapshot_no_rollback(); - // Save pre-execution VM snapshot. - vm.make_snapshot(); - let hash = tx.hash(); - if let Err(e) = self.run_l2_tx(tx, tx_index, &block_ctx, &batch_env, &mut vm) { - tracing::error!("Error while executing transaction: {e}"); - vm.rollback_to_the_latest_snapshot(); - } else { - executed_tx_hashes.push(hash); - tx_index += U64::from(1); - } - } - vm.execute(InspectExecutionMode::Bootloader); - - // Write all the mutated keys (storage slots). - let mut inner = self - .inner - .write() - .map_err(|_| anyhow::anyhow!("Failed to acquire write lock"))?; - for (key, value) in storage.borrow().modified_storage_keys() { - inner.fork_storage.set_value(*key, *value); - } - - let mut transactions = Vec::new(); - let mut tx_receipts = Vec::new(); - let mut debug_calls = Vec::new(); - for (index, tx_hash) in executed_tx_hashes.iter().enumerate() { - let Some(tx_result) = inner.tx_results.get(tx_hash) else { - // Skipping halted transaction - continue; - }; - tx_receipts.push(&tx_result.receipt); - debug_calls.push(&tx_result.debug); - - let mut transaction = zksync_types::api::Transaction::from(tx_result.info.tx.clone()); - transaction.block_hash = Some(block_ctx.hash); - transaction.block_number = Some(U64::from(block_ctx.miniblock)); - transaction.transaction_index = Some(index.into()); - transaction.l1_batch_number = Some(U64::from(batch_env.number.0)); - transaction.l1_batch_tx_index = Some(Index::zero()); - if transaction.transaction_type == Some(U64::zero()) - || transaction.transaction_type.is_none() - { - transaction.v = transaction - .v - .map(|v| v + 35 + inner.fork_storage.chain_id.as_u64() * 2); - } - transactions.push(TransactionVariant::Full(transaction)); - } - - // Build bloom hash - let iter = tx_receipts - .iter() - .flat_map(|r| r.logs.iter()) - .flat_map(|event| { - event - .topics - .iter() - .map(|topic| BloomInput::Raw(topic.as_bytes())) - .chain([BloomInput::Raw(event.address.as_bytes())]) - }); - let logs_bloom = build_bloom(iter); - - // Calculate how much gas was used across all txs - let gas_used = debug_calls - .iter() - .map(|r| r.gas_used) - .fold(U256::zero(), |acc, x| acc + x); - - // Construct the block - let parent_block_hash = inner - .block_hashes - .get(&(block_ctx.miniblock - 1)) - .cloned() - .unwrap_or_default(); - let block = create_block( - &batch_env, - hash, - parent_block_hash, - block_ctx.miniblock, - block_ctx.timestamp, - transactions, - gas_used, - logs_bloom, - ); - inner.current_batch = inner.current_batch.saturating_add(1); - inner.apply_block(time, block, 0); - - // Hack to ensure we don't mine two empty blocks in the same batch. Otherwise this creates - // weird side effect on the VM side wrt virtual block logic. - // TODO: Remove once we separate batch sealing from block sealing - if !executed_tx_hashes.is_empty() { - // With the introduction of 'l2 blocks' (and virtual blocks), - // we are adding one l2 block at the end of each batch (to handle things like remaining events etc). - // You can look at insert_fictive_l2_block function in VM to see how this fake block is inserted. - let parent_block_hash = block_ctx.hash; - let block_ctx = block_ctx.new_block(time); - let hash = compute_hash(block_ctx.miniblock, []); - - let virtual_block = create_block( - &batch_env, - hash, - parent_block_hash, - block_ctx.miniblock, - block_ctx.timestamp, - vec![], - U256::zero(), - Bloom::zero(), - ); - inner.apply_block(time, virtual_block, 1); - } - - Ok(L2BlockNumber(block_ctx.miniblock as u32)) - } - // Forcefully stores the given bytecode at a given account. - pub fn override_bytecode(&self, address: &Address, bytecode: &[u8]) -> Result<(), String> { - let mut inner = self - .inner - .write() - .map_err(|e| format!("Failed to acquire write lock: {}", e))?; + pub async fn override_bytecode( + &self, + address: &Address, + bytecode: &[u8], + ) -> Result<(), String> { + let inner = self.inner.write().await; let code_key = get_code_key(address); @@ -1950,18 +473,19 @@ impl InMemoryNode { Ok(()) } - pub fn dump_state(&self, preserve_historical_states: bool) -> anyhow::Result { + pub async fn dump_state(&self, preserve_historical_states: bool) -> anyhow::Result { let state = self .inner .read() - .map_err(|_| anyhow::anyhow!("Failed to acquire read lock"))? - .dump_state(preserve_historical_states)?; + .await + .dump_state(preserve_historical_states) + .await?; let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); encoder.write_all(&serde_json::to_vec(&state)?)?; Ok(encoder.finish()?.into()) } - pub fn load_state(&self, buf: Bytes) -> Result { + pub async fn load_state(&self, buf: Bytes) -> Result { let orig_buf = &buf.0[..]; let mut decoder = GzDecoder::new(orig_buf); let mut decoded_data = Vec::new(); @@ -1980,83 +504,87 @@ impl InMemoryNode { let state: VersionedState = serde_json::from_slice(decoded).map_err(LoadStateError::FailedDeserialize)?; - let time = self.time.lock(); - self.inner - .write() - .map_err(|_| anyhow::anyhow!("Failed to acquire write lock"))? - .load_state(time, state) + self.inner.write().await.load_state(state).await } - pub fn get_chain_id(&self) -> anyhow::Result { + pub async fn get_chain_id(&self) -> anyhow::Result { Ok(self - .read_inner()? + .inner + .read() + .await .config .chain_id .unwrap_or(TEST_NODE_NETWORK_ID)) } - pub fn get_show_calls(&self) -> anyhow::Result { - Ok(self.read_inner()?.config.show_calls.to_string()) + pub async fn get_show_calls(&self) -> anyhow::Result { + Ok(self.inner.read().await.config.show_calls.to_string()) } - pub fn get_show_outputs(&self) -> anyhow::Result { - Ok(self.read_inner()?.config.show_outputs) + pub async fn get_show_outputs(&self) -> anyhow::Result { + Ok(self.inner.read().await.config.show_outputs) } pub fn get_current_timestamp(&self) -> anyhow::Result { Ok(self.time.current_timestamp()) } - pub fn set_show_calls(&self, show_calls: ShowCalls) -> anyhow::Result { - self.write_inner()?.config.show_calls = show_calls; + pub async fn set_show_calls(&self, show_calls: ShowCalls) -> anyhow::Result { + self.inner.write().await.config.show_calls = show_calls; Ok(show_calls.to_string()) } - pub fn set_show_outputs(&self, value: bool) -> anyhow::Result { - self.write_inner()?.config.show_outputs = value; + pub async fn set_show_outputs(&self, value: bool) -> anyhow::Result { + self.inner.write().await.config.show_outputs = value; Ok(value) } - pub fn set_show_storage_logs( + pub async fn set_show_storage_logs( &self, show_storage_logs: ShowStorageLogs, ) -> anyhow::Result { - self.write_inner()?.config.show_storage_logs = show_storage_logs; + self.inner.write().await.config.show_storage_logs = show_storage_logs; Ok(show_storage_logs.to_string()) } - pub fn set_show_vm_details(&self, show_vm_details: ShowVMDetails) -> anyhow::Result { - self.write_inner()?.config.show_vm_details = show_vm_details; + pub async fn set_show_vm_details( + &self, + show_vm_details: ShowVMDetails, + ) -> anyhow::Result { + self.inner.write().await.config.show_vm_details = show_vm_details; Ok(show_vm_details.to_string()) } - pub fn set_show_gas_details(&self, show_gas_details: ShowGasDetails) -> anyhow::Result { - self.write_inner()?.config.show_gas_details = show_gas_details; + pub async fn set_show_gas_details( + &self, + show_gas_details: ShowGasDetails, + ) -> anyhow::Result { + self.inner.write().await.config.show_gas_details = show_gas_details; Ok(show_gas_details.to_string()) } - pub fn set_resolve_hashes(&self, value: bool) -> anyhow::Result { - self.write_inner()?.config.resolve_hashes = value; + pub async fn set_resolve_hashes(&self, value: bool) -> anyhow::Result { + self.inner.write().await.config.resolve_hashes = value; Ok(value) } - pub fn set_show_node_config(&self, value: bool) -> anyhow::Result { - self.write_inner()?.config.show_node_config = value; + pub async fn set_show_node_config(&self, value: bool) -> anyhow::Result { + self.inner.write().await.config.show_node_config = value; Ok(value) } - pub fn set_show_tx_summary(&self, value: bool) -> anyhow::Result { - self.write_inner()?.config.show_tx_summary = value; + pub async fn set_show_tx_summary(&self, value: bool) -> anyhow::Result { + self.inner.write().await.config.show_tx_summary = value; Ok(value) } - pub fn set_show_event_logs(&self, value: bool) -> anyhow::Result { - self.write_inner()?.config.show_event_logs = value; + pub async fn set_show_event_logs(&self, value: bool) -> anyhow::Result { + self.inner.write().await.config.show_event_logs = value; Ok(value) } - pub fn set_disable_console_log(&self, value: bool) -> anyhow::Result { - self.write_inner()?.config.disable_console_log = value; + pub async fn set_disable_console_log(&self, value: bool) -> anyhow::Result { + self.inner.write().await.config.disable_console_log = value; Ok(value) } @@ -2079,28 +607,6 @@ impl InMemoryNode { } } -/// Keeps track of a block's batch number, miniblock number and timestamp. -/// Useful for keeping track of the current context when creating multiple blocks. -#[derive(Debug, Clone, Default)] -pub struct BlockContext { - pub hash: H256, - pub batch: u32, - pub miniblock: u64, - pub timestamp: u64, -} - -impl BlockContext { - /// Create the next batch instance that uses the same batch number, and has all other parameters incremented by `1`. - pub fn new_block(&self, time: &T) -> BlockContext { - Self { - hash: H256::zero(), - batch: self.batch, - miniblock: self.miniblock.saturating_add(1), - timestamp: time.peek_next_timestamp(), - } - } -} - pub fn load_last_l1_batch(storage: StoragePtr) -> Option<(u64, u64)> { // Get block number and timestamp let current_l1_batch_info_key = StorageKey::new( @@ -2118,241 +624,55 @@ pub fn load_last_l1_batch(storage: StoragePtr) -> Option<(u64 Some((batch_number, batch_timestamp)) } -#[cfg(test)] -mod tests { - use anvil_zksync_config::constants::{ - DEFAULT_ACCOUNT_BALANCE, DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR, - DEFAULT_ESTIMATE_GAS_SCALE_FACTOR, DEFAULT_FAIR_PUBDATA_PRICE, DEFAULT_L2_GAS_PRICE, - TEST_NODE_NETWORK_ID, - }; - use anvil_zksync_config::types::SystemContractsOptions; - use anvil_zksync_config::TestNodeConfig; - use anvil_zksync_types::TransactionOrder; - use ethabi::{Token, Uint}; - use zksync_types::{utils::deployed_address_create, K256PrivateKey, Nonce}; - - use super::*; - use crate::{node::InMemoryNode, testing}; - - fn test_vm( - node: &InMemoryNode, - system_contracts: BaseSystemContracts, - ) -> ( - BlockContext, - L1BatchEnv, - Vm, HistoryDisabled>, - ) { - let inner = node.inner.read().unwrap(); - let storage = StorageView::new(inner.fork_storage.clone()).into_rc_ptr(); - let system_env = inner.create_system_env(system_contracts, TxExecutionMode::VerifyExecute); - let (batch_env, block_ctx) = inner.create_l1_batch_env(&node.time, storage.clone()); - let vm: Vm<_, HistoryDisabled> = Vm::new(batch_env.clone(), system_env, storage); - - (block_ctx, batch_env, vm) - } - - #[tokio::test] - async fn test_run_l2_tx_validates_tx_gas_limit_too_high() { - let node = InMemoryNode::default(); - let tx = testing::TransactionBuilder::new() - .set_gas_limit(U256::from(u64::MAX) + 1) - .build(); - node.set_rich_account( - tx.common_data.initiator_address, - U256::from(100u128 * 10u128.pow(18)), - ); - - let system_contracts = node - .system_contracts_for_tx(tx.initiator_account()) - .unwrap(); - let (block_ctx, batch_env, mut vm) = test_vm(&node, system_contracts.clone()); - let err = node - .run_l2_tx(tx, U64::from(0), &block_ctx, &batch_env, &mut vm) - .unwrap_err(); - assert_eq!(err.to_string(), "exceeds block gas limit"); - } - - #[tokio::test] - async fn test_run_l2_tx_validates_tx_max_fee_per_gas_too_low() { - let node = InMemoryNode::default(); - let tx = testing::TransactionBuilder::new() - .set_max_fee_per_gas(U256::from(DEFAULT_L2_GAS_PRICE - 1)) - .build(); - node.set_rich_account( - tx.common_data.initiator_address, - U256::from(100u128 * 10u128.pow(18)), - ); - - let system_contracts = node - .system_contracts_for_tx(tx.initiator_account()) - .unwrap(); - let (block_ctx, batch_env, mut vm) = test_vm(&node, system_contracts.clone()); - let err = node - .run_l2_tx(tx, U64::from(0), &block_ctx, &batch_env, &mut vm) - .unwrap_err(); - - assert_eq!( - err.to_string(), - "block base fee higher than max fee per gas" +// Test utils +// TODO: Consider builder pattern with sensible defaults +// #[cfg(test)] +// TODO: Mark with #[cfg(test)] once it is not used in other modules +impl InMemoryNode { + pub fn test_config(fork: Option, config: TestNodeConfig) -> Self { + let fee_provider = TestNodeFeeInputProvider::from_fork(fork.as_ref()); + let impersonation = ImpersonationManager::default(); + let system_contracts = SystemContracts::from_options( + &config.system_contracts_options, + config.use_evm_emulator, ); - } - - #[tokio::test] - async fn test_run_l2_tx_validates_tx_max_priority_fee_per_gas_higher_than_max_fee_per_gas() { - let node = InMemoryNode::default(); - let tx = testing::TransactionBuilder::new() - .set_max_priority_fee_per_gas(U256::from(250_000_000 + 1)) - .build(); - node.set_rich_account( - tx.common_data.initiator_address, - U256::from(100u128 * 10u128.pow(18)), + let (inner, _, blockchain, time) = InMemoryNodeInner::init( + fork, + fee_provider, + Arc::new(RwLock::new(Default::default())), + config, + impersonation.clone(), + system_contracts.clone(), ); - - let system_contracts = node - .system_contracts_for_tx(tx.initiator_account()) - .unwrap(); - let (block_ctx, batch_env, mut vm) = test_vm(&node, system_contracts.clone()); - let err = node - .run_l2_tx(tx, U64::from(0), &block_ctx, &batch_env, &mut vm) - .unwrap_err(); - - assert_eq!( - err.to_string(), - "max priority fee per gas higher than max fee per gas" + let (node_executor, node_handle) = + NodeExecutor::new(inner.clone(), system_contracts.clone()); + let pool = TxPool::new( + impersonation.clone(), + anvil_zksync_types::TransactionOrder::Fifo, ); - } - - #[tokio::test] - async fn test_create_genesis_creates_block_with_hash_and_zero_parent_hash() { - let first_block = create_genesis::(Some(1000)); - - assert_eq!(first_block.hash, compute_hash(0, [])); - assert_eq!(first_block.parent_hash, H256::zero()); - } - - #[tokio::test] - async fn test_run_l2_tx_raw_does_not_panic_on_external_storage_call() { - // Perform a transaction to get storage to an intermediate state - let node = InMemoryNode::default(); - let tx = testing::TransactionBuilder::new().build(); - node.set_rich_account( - tx.common_data.initiator_address, - U256::from(100u128 * 10u128.pow(18)), + let tx_listener = pool.add_tx_listener(); + let (block_sealer, block_sealer_state) = BlockSealer::new( + BlockSealerMode::immediate(1000, tx_listener), + pool.clone(), + node_handle.clone(), ); - let system_contracts = node - .system_contracts_for_tx(tx.initiator_account()) - .unwrap(); - node.seal_block(&mut node.time.lock(), vec![tx], system_contracts) - .unwrap(); - let external_storage = node.inner.read().unwrap().fork_storage.clone(); - - // Execute next transaction using a fresh in-memory node and the external fork storage - let mock_db = testing::ExternalStorage { - raw_storage: external_storage.inner.read().unwrap().raw_storage.clone(), - }; - let impersonation = ImpersonationManager::default(); - let pool = TxPool::new(impersonation.clone(), TransactionOrder::Fifo); - let sealer = BlockSealer::new(BlockSealerMode::immediate(1000, pool.add_tx_listener())); - let node = InMemoryNode::new( - Some(ForkDetails { - fork_source: Box::new(mock_db), - chain_id: TEST_NODE_NETWORK_ID.into(), - l1_block: L1BatchNumber(1), - l2_block: Block::default(), - l2_miniblock: 2, - l2_miniblock_hash: Default::default(), - block_timestamp: 1002, - overwrite_chain_id: None, - l1_gas_price: 1000, - l2_fair_gas_price: DEFAULT_L2_GAS_PRICE, - fair_pubdata_price: DEFAULT_FAIR_PUBDATA_PRICE, - 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(), - }), + tokio::spawn(node_executor); + tokio::spawn(block_sealer); + Self::new( + inner, + blockchain, + node_handle, None, - &Default::default(), - TimestampManager::default(), + time, impersonation, pool, - sealer, - ); - - let tx = testing::TransactionBuilder::new().build(); - let system_contracts = node - .system_contracts_for_tx(tx.initiator_account()) - .unwrap(); - let (_, _, mut vm) = test_vm(&node, system_contracts); - node.run_l2_tx_raw(tx, &mut vm) - .expect("transaction must pass with external storage"); + block_sealer_state, + system_contracts, + ) } - #[tokio::test] - async fn test_transact_returns_data_in_built_in_without_security_mode() { - let impersonation = ImpersonationManager::default(); - let pool = TxPool::new(impersonation.clone(), TransactionOrder::Fifo); - let sealer = BlockSealer::new(BlockSealerMode::immediate(1000, pool.add_tx_listener())); - let node = InMemoryNode::new( - None, - None, - &TestNodeConfig { - system_contracts_options: SystemContractsOptions::BuiltInWithoutSecurity, - ..Default::default() - }, - TimestampManager::default(), - impersonation, - pool, - sealer, - ); - - let private_key = K256PrivateKey::from_bytes(H256::repeat_byte(0xef)).unwrap(); - let from_account = private_key.address(); - node.set_rich_account(from_account, U256::from(DEFAULT_ACCOUNT_BALANCE)); - - let deployed_address = deployed_address_create(from_account, U256::zero()); - testing::deploy_contract( - &node, - H256::repeat_byte(0x1), - &private_key, - hex::decode(testing::STORAGE_CONTRACT_BYTECODE).unwrap(), - None, - Nonce(0), - ); - - let mut tx = L2Tx::new_signed( - Some(deployed_address), - hex::decode("bbf55335").unwrap(), // keccak selector for "transact_retrieve1()" - Nonce(1), - Fee { - gas_limit: U256::from(4_000_000), - max_fee_per_gas: U256::from(250_000_000), - max_priority_fee_per_gas: U256::from(250_000_000), - gas_per_pubdata_limit: U256::from(50000), - }, - U256::from(0), - zksync_types::L2ChainId::from(260), - &private_key, - vec![], - Default::default(), - ) - .expect("failed signing tx"); - tx.common_data.transaction_type = TransactionType::LegacyTransaction; - tx.set_input(vec![], H256::repeat_byte(0x2)); - - let system_contracts = node - .system_contracts_for_tx(tx.initiator_account()) - .unwrap(); - let (_, _, mut vm) = test_vm(&node, system_contracts); - let TxExecutionOutput { result, .. } = node.run_l2_tx_raw(tx, &mut vm).expect("failed tx"); - - match result.result { - ExecutionResult::Success { output } => { - let actual = testing::decode_tx_result(&output, ethabi::ParamType::Uint(256)); - let expected = Token::Uint(Uint::from(1024u64)); - assert_eq!(expected, actual, "invalid result"); - } - _ => panic!("invalid result {:?}", result.result), - } + pub fn test(fork: Option) -> Self { + let config = TestNodeConfig::default(); + Self::test_config(fork, config) } } diff --git a/crates/core/src/node/in_memory_ext.rs b/crates/core/src/node/in_memory_ext.rs index 2cd59dea..bd5cdd78 100644 --- a/crates/core/src/node/in_memory_ext.rs +++ b/crates/core/src/node/in_memory_ext.rs @@ -1,10 +1,12 @@ -use crate::node::pool::TxBatch; -use crate::node::sealer::BlockSealerMode; -use crate::{fork::ForkDetails, node::InMemoryNode, utils::bytecode_to_factory_dep}; +use super::inner::fork::ForkDetails; +use super::pool::TxBatch; +use super::sealer::BlockSealerMode; +use super::InMemoryNode; +use super::TransactionResult; +use crate::utils::bytecode_to_factory_dep; use anvil_zksync_types::api::{DetailedTransaction, ResetRequest}; use anyhow::anyhow; use std::time::Duration; -use zksync_multivm::interface::TxExecutionMode; use zksync_types::api::{Block, TransactionVariant}; use zksync_types::u256_to_h256; use zksync_types::{ @@ -28,20 +30,25 @@ impl InMemoryNode { /// /// # Returns /// The applied time delta to `current_timestamp` value for the InMemoryNodeInner. - pub fn increase_time(&self, time_delta_seconds: u64) -> Result { - self.time.increase_time(time_delta_seconds); + pub async fn increase_time(&self, time_delta_seconds: u64) -> Result { + self.node_handle + .increase_time_sync(time_delta_seconds) + .await?; Ok(time_delta_seconds) } - /// Set the current timestamp for the node. The timestamp must be in future. + /// Set the current timestamp for the node. The timestamp must be in the future. /// /// # Parameters /// - `timestamp`: The timestamp to set the time to /// /// # Returns /// The new timestamp value for the InMemoryNodeInner. - pub fn set_next_block_timestamp(&self, timestamp: u64) -> Result<()> { - self.time.enforce_next_timestamp(timestamp) + pub async fn set_next_block_timestamp(&self, timestamp: u64) -> Result<()> { + self.node_handle + .enforce_next_timestamp_sync(timestamp) + .await?; + Ok(()) } /// Set the current timestamp for the node. @@ -53,8 +60,8 @@ impl InMemoryNode { /// /// # Returns /// The difference between the `current_timestamp` and the new timestamp for the InMemoryNodeInner. - pub fn set_time(&self, timestamp: u64) -> Result { - Ok(self.time.set_current_timestamp_unchecked(timestamp)) + pub async fn set_time(&self, timestamp: u64) -> Result { + self.node_handle.set_current_timestamp_sync(timestamp).await } /// Force a single block to be mined. @@ -63,54 +70,51 @@ impl InMemoryNode { /// /// # Returns /// The string "0x0". - pub fn mine_block(&self) -> Result { + pub async fn mine_block(&self) -> Result { // TODO: Remove locking once `TestNodeConfig` is refactored into mutable/immutable components - let max_transactions = self.read_inner()?.config.max_transactions; - let TxBatch { impersonating, txs } = - self.pool.take_uniform(max_transactions).unwrap_or(TxBatch { - impersonating: false, - txs: Vec::new(), - }); - let base_system_contracts = self - .system_contracts - .contracts(TxExecutionMode::VerifyExecute, impersonating) - .clone(); + let max_transactions = self.inner.read().await.config.max_transactions; + let tx_batch = self.pool.take_uniform(max_transactions).unwrap_or(TxBatch { + impersonating: false, + txs: Vec::new(), + }); - let block_number = self.seal_block(&mut self.time.lock(), txs, base_system_contracts)?; + let block_number = self.node_handle.seal_block_sync(tx_batch).await?; tracing::info!("👷 Mined block #{}", block_number); Ok(block_number) } - pub fn mine_detailed(&self) -> Result> { - let block_number = self.mine_block()?; - let inner = self.read_inner()?; - let mut block = inner - .block_hashes - .get(&(block_number.0 as u64)) - .and_then(|hash| inner.blocks.get(hash)) - .expect("freshly mined block is missing from storage") - .clone(); - let detailed_txs = std::mem::take(&mut block.transactions) - .into_iter() - .map(|tx| match tx { - TransactionVariant::Full(tx) => { - let tx_result = inner - .tx_results - .get(&tx.hash) - .expect("freshly executed tx is missing from storage"); - let output = Some(tx_result.debug.output.clone()); - let revert_reason = tx_result.debug.revert_reason.clone(); - DetailedTransaction { - inner: tx, - output, - revert_reason, - } - } + pub async fn mine_detailed(&self) -> Result> { + let block_number = self.mine_block().await?; + let mut block = self + .blockchain + .get_block_by_number(block_number) + .await + .expect("freshly mined block is missing from storage"); + let mut detailed_txs = Vec::with_capacity(block.transactions.len()); + for tx in std::mem::take(&mut block.transactions) { + let detailed_tx = match tx { + TransactionVariant::Full(tx) => self + .blockchain + .inspect_tx( + &tx.hash.clone(), + |TransactionResult { ref debug, .. }| { + let output = Some(debug.output.clone()); + let revert_reason = debug.revert_reason.clone(); + DetailedTransaction { + inner: tx, + output, + revert_reason, + } + }, + ) + .await + .expect("freshly executed tx is missing from storage"), TransactionVariant::Hash(_) => { - unreachable!() + unreachable!("we only store full txs in blocks") } - }) - .collect(); + }; + detailed_txs.push(detailed_tx); + } Ok(block.with_transactions(detailed_txs)) } @@ -121,36 +125,24 @@ impl InMemoryNode { /// /// # Returns /// The `U64` identifier for this snapshot. - pub fn snapshot(&self) -> Result { + pub async fn snapshot(&self) -> Result { let snapshots = self.snapshots.clone(); - self.read_inner().and_then(|writer| { - // validate max snapshots - snapshots - .read() - .map_err(|err| anyhow!("failed acquiring read lock for snapshot: {:?}", err)) - .and_then(|snapshots| { - if snapshots.len() >= MAX_SNAPSHOTS as usize { - return Err(anyhow!( - "maximum number of '{}' snapshots exceeded", - MAX_SNAPSHOTS - )); - } + let reader = self.inner.read().await; + // FIXME: TOCTOU with below + // validate max snapshots + if snapshots.read().await.len() >= MAX_SNAPSHOTS as usize { + return Err(anyhow!( + "maximum number of '{}' snapshots exceeded", + MAX_SNAPSHOTS + )); + }; - Ok(()) - })?; - - // snapshot the node - let snapshot = writer.snapshot().map_err(|err| anyhow!("{}", err))?; - snapshots - .write() - .map(|mut snapshots| { - snapshots.push(snapshot); - tracing::info!("Created snapshot '{}'", snapshots.len()); - snapshots.len() - }) - .map_err(|err| anyhow!("failed storing snapshot: {:?}", err)) - .map(U64::from) - }) + // snapshot the node + let snapshot = reader.snapshot().await.map_err(|err| anyhow!("{}", err))?; + let mut snapshots = snapshots.write().await; + snapshots.push(snapshot); + tracing::info!("Created snapshot '{}'", snapshots.len()); + Ok(U64::from(snapshots.len())) } /// Revert the state of the blockchain to a previous snapshot. Takes a single parameter, @@ -162,66 +154,62 @@ impl InMemoryNode { /// /// # Returns /// `true` if a snapshot was reverted, otherwise `false`. - pub fn revert_snapshot(&self, snapshot_id: U64) -> Result { + pub async fn revert_snapshot(&self, snapshot_id: U64) -> Result { let snapshots = self.snapshots.clone(); - self.write_inner().and_then(|mut writer| { - let mut snapshots = snapshots - .write() - .map_err(|err| anyhow!("failed acquiring read lock for snapshots: {:?}", err))?; - let snapshot_id_index = snapshot_id.as_usize().saturating_sub(1); - if snapshot_id_index >= snapshots.len() { - return Err(anyhow!("no snapshot exists for the id '{}'", snapshot_id)); - } + let mut writer = self.inner.write().await; + let mut snapshots = snapshots.write().await; + let snapshot_id_index = snapshot_id.as_usize().saturating_sub(1); + if snapshot_id_index >= snapshots.len() { + return Err(anyhow!("no snapshot exists for the id '{}'", snapshot_id)); + } - // remove all snapshots following the index and use the first snapshot for restore - let selected_snapshot = snapshots - .drain(snapshot_id_index..) - .next() - .expect("unexpected failure, value must exist"); - - tracing::info!("Reverting node to snapshot '{snapshot_id:?}'"); - writer - .restore_snapshot(selected_snapshot) - .map(|_| { - tracing::info!("Reverting node to snapshot '{snapshot_id:?}'"); - true - }) - .map_err(|err| anyhow!("{}", err)) - }) + // remove all snapshots following the index and use the first snapshot for restore + let selected_snapshot = snapshots + .drain(snapshot_id_index..) + .next() + .expect("unexpected failure, value must exist"); + + tracing::info!("Reverting node to snapshot '{snapshot_id:?}'"); + writer + .restore_snapshot(selected_snapshot) + .await + .map(|_| { + tracing::info!("Reverting node to snapshot '{snapshot_id:?}'"); + true + }) + .map_err(|err| anyhow!("{}", err)) } - pub fn set_balance(&self, address: Address, balance: U256) -> Result { - self.write_inner().map(|mut writer| { - let balance_key = storage_key_for_eth_balance(&address); - writer - .fork_storage - .set_value(balance_key, u256_to_h256(balance)); - tracing::info!( - "👷 Balance for address {:?} has been manually set to {} Wei", - address, - balance - ); - true - }) + pub async fn set_balance(&self, address: Address, balance: U256) -> bool { + let writer = self.inner.write().await; + let balance_key = storage_key_for_eth_balance(&address); + writer + .fork_storage + .set_value(balance_key, u256_to_h256(balance)); + tracing::info!( + "👷 Balance for address {:?} has been manually set to {} Wei", + address, + balance + ); + true } - pub fn set_nonce(&self, address: Address, nonce: U256) -> Result { - self.write_inner().map(|mut writer| { - let nonce_key = get_nonce_key(&address); - let enforced_full_nonce = nonces_to_full_nonce(nonce, nonce); - tracing::info!( - "👷 Nonces for address {:?} have been set to {}", - address, - nonce - ); - writer - .fork_storage - .set_value(nonce_key, u256_to_h256(enforced_full_nonce)); - true - }) + pub async fn set_nonce(&self, address: Address, nonce: U256) -> bool { + let writer = self.inner.write().await; + let nonce_key = get_nonce_key(&address); + let enforced_full_nonce = nonces_to_full_nonce(nonce, nonce); + tracing::info!( + "👷 Nonces for address {:?} have been set to {}", + address, + nonce + ); + writer + .fork_storage + .set_value(nonce_key, u256_to_h256(enforced_full_nonce)); + true } - pub fn mine_blocks(&self, num_blocks: Option, interval: Option) -> Result<()> { + pub async fn mine_blocks(&self, num_blocks: Option, interval: Option) -> Result<()> { let num_blocks = num_blocks.map_or(1, |x| x.as_u64()); let interval_sec = interval.map_or(1, |x| x.as_u64()); @@ -233,22 +221,18 @@ impl InMemoryNode { } // TODO: Remove locking once `TestNodeConfig` is refactored into mutable/immutable components - let max_transactions = self.read_inner()?.config.max_transactions; - let mut time = self - .time - .lock_with_offsets((0..num_blocks).map(|i| i * interval_sec)); + let max_transactions = self.inner.read().await.config.max_transactions; + let mut tx_batches = Vec::with_capacity(num_blocks as usize); for _ in 0..num_blocks { - let TxBatch { impersonating, txs } = - self.pool.take_uniform(max_transactions).unwrap_or(TxBatch { - impersonating: false, - txs: Vec::new(), - }); - let base_system_contracts = self - .system_contracts - .contracts(TxExecutionMode::VerifyExecute, impersonating) - .clone(); - self.seal_block(&mut time, txs, base_system_contracts)?; + let tx_batch = self.pool.take_uniform(max_transactions).unwrap_or(TxBatch { + impersonating: false, + txs: Vec::new(), + }); + tx_batches.push(tx_batch); } + self.node_handle + .seal_blocks_sync(tx_batches, interval_sec) + .await?; tracing::info!("👷 Mined {} blocks", num_blocks); Ok(()) @@ -263,7 +247,7 @@ impl InMemoryNode { Ok(true) } - pub fn reset_network(&self, reset_spec: Option) -> Result { + pub async 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() { @@ -271,7 +255,7 @@ impl InMemoryNode { "Only one of 'to' and 'forking' attributes can be specified" )); } - let url = match self.get_fork_url() { + let url = match self.inner.read().await.fork_storage.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`."); @@ -290,7 +274,13 @@ impl InMemoryNode { }; let fork_details = if let Some(url) = opt_url { - let cache_config = self.get_cache_config().map_err(|err| anyhow!(err))?; + let cache_config = self + .inner + .read() + .await + .fork_storage + .get_cache_config() + .map_err(|err| anyhow!(err))?; match ForkDetails::from_url(url, block_number, cache_config) { Ok(fd) => Some(fd), Err(error) => { @@ -301,7 +291,7 @@ impl InMemoryNode { None }; - match self.reset(fork_details) { + match self.reset(fork_details).await { Ok(()) => { tracing::info!("👷 Network reset"); Ok(true) @@ -337,37 +327,35 @@ impl InMemoryNode { } } - pub fn set_code(&self, address: Address, code: String) -> Result<()> { - self.write_inner().and_then(|mut writer| { - let code_key = get_code_key(&address); - tracing::info!("set code for address {address:#x}"); - let code_slice = code - .strip_prefix("0x") - .ok_or_else(|| anyhow!("code must be 0x-prefixed"))?; - let code_bytes = hex::decode(code_slice)?; - let hashcode = bytecode_to_factory_dep(code_bytes)?; - let hash = u256_to_h256(hashcode.0); - let code = hashcode - .1 - .iter() - .flat_map(|entry| { - let mut bytes = vec![0u8; 32]; - entry.to_big_endian(&mut bytes); - bytes.to_vec() - }) - .collect(); - writer.fork_storage.store_factory_dep(hash, code); - writer.fork_storage.set_value(code_key, hash); - Ok(()) - }) + pub async fn set_code(&self, address: Address, code: String) -> Result<()> { + let writer = self.inner.write().await; + let code_key = get_code_key(&address); + tracing::info!("set code for address {address:#x}"); + let code_slice = code + .strip_prefix("0x") + .ok_or_else(|| anyhow!("code must be 0x-prefixed"))?; + let code_bytes = hex::decode(code_slice)?; + let hashcode = bytecode_to_factory_dep(code_bytes)?; + let hash = u256_to_h256(hashcode.0); + let code = hashcode + .1 + .iter() + .flat_map(|entry| { + let mut bytes = vec![0u8; 32]; + entry.to_big_endian(&mut bytes); + bytes.to_vec() + }) + .collect(); + writer.fork_storage.store_factory_dep(hash, code); + writer.fork_storage.set_value(code_key, hash); + Ok(()) } - pub fn set_storage_at(&self, address: Address, slot: U256, value: U256) -> Result { - self.write_inner().map(|mut writer| { - let key = StorageKey::new(AccountTreeId::new(address), u256_to_h256(slot)); - writer.fork_storage.set_value(key, u256_to_h256(value)); - true - }) + pub async fn set_storage_at(&self, address: Address, slot: U256, value: U256) -> bool { + let writer = self.inner.write().await; + let key = StorageKey::new(AccountTreeId::new(address), u256_to_h256(slot)); + writer.fork_storage.set_value(key, u256_to_h256(value)); + true } pub fn set_logging_enabled(&self, enable: bool) -> Result<()> { @@ -382,51 +370,44 @@ impl InMemoryNode { } pub fn get_immediate_sealing(&self) -> Result { - Ok(self.sealer.is_immediate()) + Ok(self.sealer_state.is_immediate()) } - pub fn set_block_timestamp_interval(&self, seconds: u64) -> Result<()> { - self.time.set_block_timestamp_interval(seconds); + pub async fn set_block_timestamp_interval(&self, seconds: u64) -> Result<()> { + self.node_handle + .set_block_timestamp_interval(seconds) + .await?; Ok(()) } - pub fn remove_block_timestamp_interval(&self) -> Result { - Ok(self.time.remove_block_timestamp_interval()) + pub async fn remove_block_timestamp_interval(&self) -> Result { + self.node_handle + .remove_block_timestamp_interval_sync() + .await } - pub fn set_immediate_sealing(&self, enable: bool) -> Result<()> { + pub async fn set_immediate_sealing(&self, enable: bool) -> Result<()> { if enable { let listener = self.pool.add_tx_listener(); - self.sealer.set_mode(BlockSealerMode::immediate( - self.inner - .read() - .map_err(|err| anyhow!("failed acquiring lock: {:?}", err))? - .config - .max_transactions, + self.sealer_state.set_mode(BlockSealerMode::immediate( + self.inner.read().await.config.max_transactions, listener, )) } else { - self.sealer.set_mode(BlockSealerMode::Noop) + self.sealer_state.set_mode(BlockSealerMode::Noop) } Ok(()) } - pub fn set_interval_sealing(&self, seconds: u64) -> Result<()> { + pub async fn set_interval_sealing(&self, seconds: u64) -> Result<()> { let sealing_mode = if seconds == 0 { BlockSealerMode::noop() } else { let block_time = Duration::from_secs(seconds); - BlockSealerMode::fixed_time( - self.inner - .read() - .map_err(|err| anyhow!("failed acquiring lock: {:?}", err))? - .config - .max_transactions, - block_time, - ) + BlockSealerMode::fixed_time(self.inner.read().await.config.max_transactions, block_time) }; - self.sealer.set_mode(sealing_mode); + self.sealer_state.set_mode(sealing_mode); Ok(()) } @@ -445,20 +426,17 @@ impl InMemoryNode { Ok(()) } - pub fn set_next_block_base_fee_per_gas(&self, base_fee: U256) -> Result<()> { + pub async fn set_next_block_base_fee_per_gas(&self, base_fee: U256) -> Result<()> { self.inner .write() - .expect("") + .await .fee_input_provider .set_base_fee(base_fee.as_u64()); Ok(()) } - pub fn set_rpc_url(&self, url: String) -> Result<()> { - let inner = self - .inner - .read() - .map_err(|err| anyhow!("failed acquiring lock: {:?}", err))?; + pub async fn set_rpc_url(&self, url: String) -> Result<()> { + let inner = self.inner.read().await; let mut fork_storage = inner .fork_storage .inner @@ -479,11 +457,8 @@ impl InMemoryNode { Ok(()) } - pub fn set_chain_id(&self, id: u32) -> Result<()> { - let mut inner = self - .inner - .write() - .map_err(|_| anyhow::anyhow!("Failed to acquire write lock"))?; + pub async fn set_chain_id(&self, id: u32) -> Result<()> { + let mut inner = self.inner.write().await; inner.config.update_chain_id(Some(id)); inner.fork_storage.set_chain_id(id.into()); @@ -494,25 +469,20 @@ impl InMemoryNode { #[cfg(test)] mod tests { use super::*; - use crate::fork::ForkStorage; - use crate::node::time::{ReadTime, TimestampManager}; use crate::node::InMemoryNode; - use crate::node::{BlockSealer, ImpersonationManager, InMemoryNodeInner, Snapshot, TxPool}; - use anvil_zksync_types::TransactionOrder; use std::str::FromStr; - use std::sync::{Arc, RwLock}; use zksync_multivm::interface::storage::ReadStorage; - use zksync_types::{api::BlockNumber, fee::Fee, l2::L2Tx, PackedEthSignature}; + use zksync_types::{api, fee::Fee, l2::L2Tx, L1BatchNumber, PackedEthSignature}; use zksync_types::{h256_to_u256, L2ChainId, Nonce, H256}; #[tokio::test] async fn test_set_balance() { let address = Address::from_str("0x36615Cf349d7F6344891B1e7CA7C72883F5dc049").unwrap(); - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let balance_before = node.get_balance_impl(address, None).await.unwrap(); - let result = node.set_balance(address, U256::from(1337)).unwrap(); + let result = node.set_balance(address, U256::from(1337)).await; assert!(result); let balance_after = node.get_balance_impl(address, None).await.unwrap(); @@ -523,14 +493,14 @@ mod tests { #[tokio::test] async fn test_set_nonce() { let address = Address::from_str("0x36615Cf349d7F6344891B1e7CA7C72883F5dc049").unwrap(); - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let nonce_before = node .get_transaction_count_impl(address, None) .await .unwrap(); - let result = node.set_nonce(address, U256::from(1337)).unwrap(); + let result = node.set_nonce(address, U256::from(1337)).await; assert!(result); let nonce_after = node @@ -540,7 +510,7 @@ mod tests { assert_eq!(nonce_after, U256::from(1337)); assert_ne!(nonce_before, nonce_after); - let result = node.set_nonce(address, U256::from(1336)).unwrap(); + let result = node.set_nonce(address, U256::from(1336)).await; assert!(result); let nonce_after = node @@ -552,29 +522,29 @@ mod tests { #[tokio::test] async fn test_mine_blocks_default() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let start_block = node - .get_block_by_number_impl(zksync_types::api::BlockNumber::Latest, false) + .get_block_impl(api::BlockId::Number(api::BlockNumber::Latest), false) .await .unwrap() .expect("block exists"); // test with defaults - node.mine_blocks(None, None).expect("mine_blocks"); + node.mine_blocks(None, None).await.expect("mine_blocks"); let current_block = node - .get_block_by_number_impl(zksync_types::api::BlockNumber::Latest, false) + .get_block_impl(api::BlockId::Number(api::BlockNumber::Latest), false) .await .unwrap() .expect("block exists"); assert_eq!(start_block.number + 1, current_block.number); assert_eq!(start_block.timestamp + 1, current_block.timestamp); - node.mine_blocks(None, None).expect("mine_blocks"); + node.mine_blocks(None, None).await.expect("mine_blocks"); let current_block = node - .get_block_by_number_impl(zksync_types::api::BlockNumber::Latest, false) + .get_block_impl(api::BlockId::Number(api::BlockNumber::Latest), false) .await .unwrap() .expect("block exists"); @@ -585,10 +555,10 @@ mod tests { #[tokio::test] async fn test_mine_blocks() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let start_block = node - .get_block_by_number_impl(zksync_types::api::BlockNumber::Latest, false) + .get_block_impl(api::BlockId::Number(api::BlockNumber::Latest), false) .await .unwrap() .expect("block exists"); @@ -598,11 +568,15 @@ mod tests { let start_timestamp = start_block.timestamp + 1; node.mine_blocks(Some(U64::from(num_blocks)), Some(U64::from(interval))) + .await .expect("mine blocks"); for i in 0..num_blocks { let current_block = node - .get_block_by_number_impl(BlockNumber::Number(start_block.number + i + 1), false) + .get_block_impl( + api::BlockId::Number(api::BlockNumber::Number(start_block.number + i + 1)), + false, + ) .await .unwrap() .expect("block exists"); @@ -613,53 +587,26 @@ mod tests { #[tokio::test] async fn test_reset() { - let old_snapshots = Arc::new(RwLock::new(vec![Snapshot::default()])); - let old_system_contracts_options = Default::default(); - let time = TimestampManager::new(123); - let impersonation = ImpersonationManager::default(); - let old_inner = InMemoryNodeInner { - current_batch: 100, - current_miniblock: 300, - current_miniblock_hash: H256::random(), - fee_input_provider: Default::default(), - tx_results: Default::default(), - blocks: Default::default(), - block_hashes: Default::default(), - filters: Default::default(), - fork_storage: ForkStorage::new(None, &old_system_contracts_options, false, None), - config: Default::default(), - console_log_handler: Default::default(), - system_contracts: Default::default(), - impersonation: impersonation.clone(), - rich_accounts: Default::default(), - previous_states: Default::default(), - }; - let pool = TxPool::new(impersonation.clone(), TransactionOrder::Fifo); - let sealer = BlockSealer::new(BlockSealerMode::immediate(1000, pool.add_tx_listener())); - - let node = InMemoryNode { - inner: Arc::new(RwLock::new(old_inner)), - snapshots: old_snapshots, - system_contracts_options: old_system_contracts_options, - time, - impersonation, - observability: None, - pool, - sealer, - system_contracts: Default::default(), - }; + let node = InMemoryNode::test(None); + // Seal a few blocks to create non-trivial local state + for _ in 0..10 { + node.node_handle + .seal_block_sync(TxBatch { + impersonating: false, + txs: vec![], + }) + .await + .unwrap(); + } let address = Address::from_str("0x36615Cf349d7F6344891B1e7CA7C72883F5dc049").unwrap(); let nonce_before = node .get_transaction_count_impl(address, None) .await .unwrap(); + assert!(node.set_nonce(address, U256::from(1337)).await); - 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); + assert!(node.reset_network(None).await.unwrap()); let nonce_after = node .get_transaction_count_impl(address, None) @@ -667,23 +614,24 @@ mod tests { .unwrap(); assert_eq!(nonce_before, nonce_after); - assert_eq!(node.snapshots.read().unwrap().len(), 0); - - let inner = node.inner.read().unwrap(); + assert_eq!(node.snapshots.read().await.len(), 0); assert_eq!(node.time.current_timestamp(), 1000); - assert_eq!(inner.current_batch, 0); - assert_eq!(inner.current_miniblock, 0); - assert_ne!(inner.current_miniblock_hash, H256::random()); + assert_eq!(node.blockchain.current_batch().await, L1BatchNumber(0)); + assert_eq!( + node.blockchain.current_block_number().await, + L2BlockNumber(0) + ); + assert_ne!(node.blockchain.current_block_hash().await, H256::random()); } #[tokio::test] async fn test_impersonate_account() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let to_impersonate = Address::from_str("0xd8da6bf26964af9d7eed9e03e53415d37aa96045").unwrap(); // give impersonated account some balance - let result = node.set_balance(to_impersonate, U256::exp10(18)).unwrap(); + let result = node.set_balance(to_impersonate, U256::exp10(18)).await; assert!(result); // construct a tx @@ -708,7 +656,7 @@ mod tests { } // try to execute the tx- should fail without signature - assert!(node.apply_txs(vec![tx.clone()], 1).is_err()); + assert!(node.apply_txs(vec![tx.clone()], 1).await.is_err()); // impersonate the account let result = node @@ -725,7 +673,7 @@ mod tests { assert!(!result); // execution should now succeed - assert!(node.apply_txs(vec![tx.clone()], 1).is_ok()); + assert!(node.apply_txs(vec![tx.clone()], 1).await.is_ok()); // stop impersonating the account let result = node @@ -742,13 +690,13 @@ mod tests { assert!(!result); // execution should now fail again - assert!(node.apply_txs(vec![tx], 1).is_err()); + assert!(node.apply_txs(vec![tx], 1).await.is_err()); } #[tokio::test] async fn test_set_code() { let address = Address::repeat_byte(0x1); - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let new_code = vec![0x1u8; 32]; let code_before = node @@ -759,6 +707,7 @@ mod tests { assert_eq!(Vec::::default(), code_before); node.set_code(address, format!("0x{}", hex::encode(new_code.clone()))) + .await .expect("failed setting code"); let code_after = node @@ -771,27 +720,25 @@ mod tests { #[tokio::test] async fn test_set_storage_at() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let address = Address::repeat_byte(0x1); let slot = U256::from(37); let value = U256::from(42); let key = StorageKey::new(AccountTreeId::new(address), u256_to_h256(slot)); - let value_before = node.write_inner().unwrap().fork_storage.read_value(&key); + let value_before = node.inner.write().await.fork_storage.read_value(&key); assert_eq!(H256::default(), value_before); - let result = node - .set_storage_at(address, slot, value) - .expect("failed setting value"); + let result = node.set_storage_at(address, slot, value).await; assert!(result); - let value_after = node.write_inner().unwrap().fork_storage.read_value(&key); + let value_after = node.inner.write().await.fork_storage.read_value(&key); assert_eq!(value, h256_to_u256(value_after)); } #[tokio::test] async fn test_increase_time_zero_value() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let increase_value_seconds = 0u64; let timestamp_before = node.time.current_timestamp(); @@ -799,6 +746,7 @@ mod tests { let actual_response = node .increase_time(increase_value_seconds) + .await .expect("failed increasing timestamp"); let timestamp_after = node.time.current_timestamp(); @@ -812,7 +760,7 @@ mod tests { #[tokio::test] async fn test_increase_time_max_value() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let increase_value_seconds = u64::MAX; let timestamp_before = node.time.current_timestamp(); @@ -821,6 +769,7 @@ mod tests { let actual_response = node .increase_time(increase_value_seconds) + .await .expect("failed increasing timestamp"); let timestamp_after = node.time.current_timestamp(); @@ -832,9 +781,9 @@ mod tests { ); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread", worker_threads = 3)] async fn test_increase_time() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let increase_value_seconds = 100u64; let timestamp_before = node.time.current_timestamp(); @@ -842,6 +791,7 @@ mod tests { let actual_response = node .increase_time(increase_value_seconds) + .await .expect("failed increasing timestamp"); let timestamp_after = node.time.current_timestamp(); @@ -855,7 +805,7 @@ mod tests { #[tokio::test] async fn test_set_next_block_timestamp_future() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let new_timestamp = 10_000u64; let timestamp_before = node.time.current_timestamp(); @@ -865,8 +815,9 @@ mod tests { ); node.set_next_block_timestamp(new_timestamp) + .await .expect("failed setting timestamp"); - node.mine_block().expect("failed to mine a block"); + node.mine_block().await.expect("failed to mine a block"); let timestamp_after = node.time.current_timestamp(); assert_eq!( @@ -877,30 +828,31 @@ mod tests { #[tokio::test] async fn test_set_next_block_timestamp_past_fails() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let timestamp_before = node.time.current_timestamp(); let new_timestamp = timestamp_before + 500; node.set_next_block_timestamp(new_timestamp) + .await .expect("failed setting timestamp"); - node.mine_block().expect("failed to mine a block"); + node.mine_block().await.expect("failed to mine a block"); - let result = node.set_next_block_timestamp(timestamp_before); + let result = node.set_next_block_timestamp(timestamp_before).await; assert!(result.is_err(), "expected an error for timestamp in past"); } #[tokio::test] async fn test_set_next_block_timestamp_same_value() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let new_timestamp = 1000u64; let timestamp_before = node.time.current_timestamp(); assert_eq!(timestamp_before, new_timestamp, "timestamps must be same"); - let response = node.set_next_block_timestamp(new_timestamp); + let response = node.set_next_block_timestamp(new_timestamp).await; assert!(response.is_err()); let timestamp_after = node.time.current_timestamp(); @@ -912,14 +864,17 @@ mod tests { #[tokio::test] async fn test_set_time_future() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let new_time = 10_000u64; let timestamp_before = node.time.current_timestamp(); assert_ne!(timestamp_before, new_time, "timestamps must be different"); let expected_response = 9000; - let actual_response = node.set_time(new_time).expect("failed setting timestamp"); + let actual_response = node + .set_time(new_time) + .await + .expect("failed setting timestamp"); let timestamp_after = node.time.current_timestamp(); assert_eq!(expected_response, actual_response, "erroneous response"); @@ -928,14 +883,17 @@ mod tests { #[tokio::test] async fn test_set_time_past() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let new_time = 10u64; let timestamp_before = node.time.current_timestamp(); assert_ne!(timestamp_before, new_time, "timestamps must be different"); let expected_response = -990; - let actual_response = node.set_time(new_time).expect("failed setting timestamp"); + let actual_response = node + .set_time(new_time) + .await + .expect("failed setting timestamp"); let timestamp_after = node.time.current_timestamp(); assert_eq!(expected_response, actual_response, "erroneous response"); @@ -944,14 +902,17 @@ mod tests { #[tokio::test] async fn test_set_time_same_value() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let new_time = 1000u64; let timestamp_before = node.time.current_timestamp(); assert_eq!(timestamp_before, new_time, "timestamps must be same"); let expected_response = 0; - let actual_response = node.set_time(new_time).expect("failed setting timestamp"); + let actual_response = node + .set_time(new_time) + .await + .expect("failed setting timestamp"); let timestamp_after = node.time.current_timestamp(); assert_eq!(expected_response, actual_response, "erroneous response"); @@ -963,7 +924,7 @@ mod tests { #[tokio::test] async fn test_set_time_edges() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); for new_time in [0, u64::MAX] { let timestamp_before = node.time.current_timestamp(); @@ -973,7 +934,10 @@ mod tests { ); let expected_response = (new_time as i128).saturating_sub(timestamp_before as i128); - let actual_response = node.set_time(new_time).expect("failed setting timestamp"); + let actual_response = node + .set_time(new_time) + .await + .expect("failed setting timestamp"); let timestamp_after = node.time.current_timestamp(); assert_eq!( @@ -989,18 +953,18 @@ mod tests { #[tokio::test] async fn test_mine_block() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let start_block = node - .get_block_by_number_impl(zksync_types::api::BlockNumber::Latest, false) + .get_block_impl(api::BlockId::Number(api::BlockNumber::Latest), false) .await .unwrap() .expect("block exists"); - let result = node.mine_block().expect("mine_block"); + let result = node.mine_block().await.expect("mine_block"); assert_eq!(result, L2BlockNumber(1)); let current_block = node - .get_block_by_number_impl(zksync_types::api::BlockNumber::Latest, false) + .get_block_impl(api::BlockId::Number(api::BlockNumber::Latest), false) .await .unwrap() .expect("block exists"); @@ -1008,11 +972,11 @@ mod tests { assert_eq!(start_block.number + 1, current_block.number); assert_eq!(start_block.timestamp + 1, current_block.timestamp); - let result = node.mine_block().expect("mine_block"); + let result = node.mine_block().await.expect("mine_block"); assert_eq!(result, L2BlockNumber(start_block.number.as_u32() + 2)); let current_block = node - .get_block_by_number_impl(BlockNumber::Latest, false) + .get_block_impl(api::BlockId::Number(api::BlockNumber::Latest), false) .await .unwrap() .expect("block exists"); @@ -1023,10 +987,10 @@ mod tests { #[tokio::test] async fn test_evm_snapshot_creates_incrementing_ids() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); - let snapshot_id_1 = node.snapshot().expect("failed creating snapshot 1"); - let snapshot_id_2 = node.snapshot().expect("failed creating snapshot 2"); + let snapshot_id_1 = node.snapshot().await.expect("failed creating snapshot 1"); + let snapshot_id_2 = node.snapshot().await.expect("failed creating snapshot 2"); assert_eq!(snapshot_id_1, U64::from(1)); assert_eq!(snapshot_id_2, U64::from(2)); @@ -1034,14 +998,14 @@ mod tests { #[tokio::test] async fn test_evm_revert_snapshot_restores_state() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let initial_block = node .get_block_number_impl() .await .expect("failed fetching block number"); - let snapshot_id = node.snapshot().expect("failed creating snapshot"); - node.mine_block().expect("mine_block"); + let snapshot_id = node.snapshot().await.expect("failed creating snapshot"); + node.mine_block().await.expect("mine_block"); let current_block = node .get_block_number_impl() .await @@ -1050,6 +1014,7 @@ mod tests { let reverted = node .revert_snapshot(snapshot_id) + .await .expect("failed reverting snapshot"); assert!(reverted); @@ -1062,37 +1027,38 @@ mod tests { #[tokio::test] async fn test_evm_revert_snapshot_removes_all_snapshots_following_the_reverted_one() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); - let _snapshot_id_1 = node.snapshot().expect("failed creating snapshot"); - let snapshot_id_2 = node.snapshot().expect("failed creating snapshot"); - let _snapshot_id_3 = node.snapshot().expect("failed creating snapshot"); - assert_eq!(3, node.snapshots.read().unwrap().len()); + let _snapshot_id_1 = node.snapshot().await.expect("failed creating snapshot"); + let snapshot_id_2 = node.snapshot().await.expect("failed creating snapshot"); + let _snapshot_id_3 = node.snapshot().await.expect("failed creating snapshot"); + assert_eq!(3, node.snapshots.read().await.len()); let reverted = node .revert_snapshot(snapshot_id_2) + .await .expect("failed reverting snapshot"); assert!(reverted); - assert_eq!(1, node.snapshots.read().unwrap().len()); + assert_eq!(1, node.snapshots.read().await.len()); } #[tokio::test] async fn test_evm_revert_snapshot_fails_for_invalid_snapshot_id() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); - let result = node.revert_snapshot(U64::from(100)); + let result = node.revert_snapshot(U64::from(100)).await; assert!(result.is_err()); } #[tokio::test] async fn test_node_set_chain_id() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let new_chain_id = 261; - let _ = node.set_chain_id(new_chain_id); + let _ = node.set_chain_id(new_chain_id).await; - let node_inner = node.inner.read().unwrap(); + let node_inner = node.inner.read().await; assert_eq!(new_chain_id, node_inner.config.chain_id.unwrap()); assert_eq!( L2ChainId::from(new_chain_id), diff --git a/crates/core/src/node/inner/blockchain.rs b/crates/core/src/node/inner/blockchain.rs new file mode 100644 index 00000000..397cb675 --- /dev/null +++ b/crates/core/src/node/inner/blockchain.rs @@ -0,0 +1,369 @@ +use super::fork::ForkDetails; +use crate::node::time::TimeWriter; +use crate::node::{compute_hash, create_genesis, create_genesis_from_json, TransactionResult}; +use crate::utils::ArcRLock; +use anvil_zksync_config::types::Genesis; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use zksync_multivm::interface::storage::{ReadStorage, StoragePtr}; +use zksync_multivm::interface::L2Block; +use zksync_multivm::vm_latest::utils::l2_blocks::load_last_l2_block; +use zksync_types::block::{unpack_block_info, L2BlockHasher}; +use zksync_types::{ + api, h256_to_u256, AccountTreeId, L1BatchNumber, L2BlockNumber, StorageKey, H256, + SYSTEM_CONTEXT_ADDRESS, SYSTEM_CONTEXT_BLOCK_INFO_POSITION, +}; + +/// A read-only blockchain representation. All clones agree on the internal state. +#[derive(Clone)] +pub struct BlockchainReader { + /// Underlying read-only storage. + inner: ArcRLock, +} + +impl BlockchainReader { + pub(super) fn new( + fork: Option<&ForkDetails>, + genesis: Option<&Genesis>, + genesis_timestamp: Option, + ) -> (Self, BlockchainWriter) { + let storage = if let Some(fork) = fork { + Blockchain { + current_batch: fork.l1_block, + current_block: L2BlockNumber(fork.l2_miniblock as u32), + current_block_hash: fork.l2_miniblock_hash, + tx_results: Default::default(), + blocks: HashMap::from_iter([(fork.l2_block.hash, fork.l2_block.clone())]), + hashes: HashMap::from_iter([( + fork.l2_block.number.as_u32().into(), + fork.l2_block.hash, + )]), + } + } else { + let block_hash = compute_hash(0, []); + let genesis_block: api::Block = if let Some(genesis) = genesis + { + create_genesis_from_json(genesis, genesis_timestamp) + } else { + create_genesis(genesis_timestamp) + }; + + Blockchain { + current_batch: L1BatchNumber(0), + current_block: L2BlockNumber(0), + current_block_hash: block_hash, + tx_results: Default::default(), + blocks: HashMap::from_iter([(block_hash, genesis_block)]), + hashes: HashMap::from_iter([(L2BlockNumber(0), block_hash)]), + } + }; + let inner = Arc::new(RwLock::new(storage)); + ( + Self { + inner: ArcRLock::wrap(inner.clone()), + }, + BlockchainWriter { inner }, + ) + } + + pub async fn current_batch(&self) -> L1BatchNumber { + self.inner.read().await.current_batch + } + + pub async fn current_block_number(&self) -> L2BlockNumber { + self.inner.read().await.current_block + } + + pub async fn current_block_hash(&self) -> H256 { + self.inner.read().await.current_block_hash + } + + pub async fn get_block_by_hash( + &self, + hash: &H256, + ) -> Option> { + self.inspect_block_by_hash(hash, |block| block.clone()) + .await + } + + pub async fn get_block_by_number( + &self, + number: L2BlockNumber, + ) -> Option> { + self.inspect_block_by_number(number, |block| block.clone()) + .await + } + + pub async fn get_block_by_id( + &self, + block_id: api::BlockId, + ) -> Option> { + self.inspect_block_by_id(block_id, |block| block.clone()) + .await + } + + pub async fn get_block_hash_by_number(&self, number: L2BlockNumber) -> Option { + self.inspect_block_by_number(number, |block| block.hash) + .await + } + + pub async fn get_block_hash_by_id(&self, block_id: api::BlockId) -> Option { + self.inspect_block_by_id(block_id, |block| block.hash).await + } + + pub async fn get_block_number_by_hash(&self, hash: &H256) -> Option { + self.inspect_block_by_hash(hash, |block| L2BlockNumber(block.number.as_u32())) + .await + } + + pub async fn get_block_number_by_id(&self, block_id: api::BlockId) -> Option { + self.inspect_block_by_id(block_id, |block| L2BlockNumber(block.number.as_u32())) + .await + } + + pub async fn inspect_block_by_hash( + &self, + hash: &H256, + f: impl FnOnce(&api::Block) -> T, + ) -> Option { + Some(f(self.inner.read().await.blocks.get(hash)?)) + } + + pub async fn inspect_block_by_number( + &self, + number: L2BlockNumber, + f: impl FnOnce(&api::Block) -> T, + ) -> Option { + let storage = self.inner.read().await; + let hash = storage.get_block_hash_by_number(number)?; + Some(f(storage.blocks.get(&hash)?)) + } + + pub async fn inspect_block_by_id( + &self, + block_id: api::BlockId, + f: impl FnOnce(&api::Block) -> T, + ) -> Option { + let storage = self.inner.read().await; + let hash = storage.get_block_hash_by_id(block_id)?; + Some(f(storage.blocks.get(&hash)?)) + } + + pub async fn get_tx_receipt(&self, tx_hash: &H256) -> Option { + self.inspect_tx(tx_hash, |tx| tx.receipt.clone()).await + } + + pub async fn get_tx_debug_info( + &self, + tx_hash: &H256, + only_top: bool, + ) -> Option { + self.inspect_tx(tx_hash, |tx| tx.debug_info(only_top)).await + } + + pub async fn inspect_tx( + &self, + tx_hash: &H256, + f: impl FnOnce(&TransactionResult) -> T, + ) -> Option { + Some(f(self.inner.read().await.tx_results.get(tx_hash)?)) + } + + // TODO: Seems like a strange pattern to allow this + pub async fn inspect_all_txs( + &self, + f: impl FnOnce(&HashMap) -> T, + ) -> T { + f(&self.inner.read().await.tx_results) + } +} + +/// A single-instance writer to blockchain state that is only available to [`super::InMemoryNodeInner`]. +pub(super) struct BlockchainWriter { + pub(super) inner: Arc>, +} + +impl BlockchainWriter { + pub(super) async fn read(&self) -> RwLockReadGuard { + self.inner.read().await + } + + pub(super) async fn write(&self) -> RwLockWriteGuard { + self.inner.write().await + } +} + +/// Stores the blockchain data (blocks, transactions) +#[derive(Clone)] +pub(super) struct Blockchain { + /// The latest batch number that was already generated. + /// Next block will go to the batch `current_batch + 1`. + pub(super) current_batch: L1BatchNumber, + /// The latest block number that was already generated. + /// Next transaction will go to the block `current_block + 1`. + pub(super) current_block: L2BlockNumber, + /// The latest block hash. + pub(super) current_block_hash: H256, + /// Map from transaction to details about the execution. + pub(super) tx_results: HashMap, + /// Map from block hash to information about the block. + pub(super) blocks: HashMap>, + /// Map from block number to a block hash. + pub(super) hashes: HashMap, +} + +impl Blockchain { + pub(super) fn get_block_hash_by_number(&self, number: L2BlockNumber) -> Option { + self.hashes.get(&number).copied() + } + + pub(super) fn get_block_hash_by_id(&self, block_id: api::BlockId) -> Option { + match block_id { + api::BlockId::Number(number) => { + let number = match number { + api::BlockNumber::Finalized + | api::BlockNumber::Pending + | api::BlockNumber::Committed + | api::BlockNumber::L1Committed + | api::BlockNumber::Latest => self.current_block, + api::BlockNumber::Earliest => L2BlockNumber(0), + api::BlockNumber::Number(n) => L2BlockNumber(n.as_u32()), + }; + self.hashes.get(&number).copied() + } + api::BlockId::Hash(hash) => Some(hash), + } + } + + pub(super) fn last_env( + &self, + storage: &StoragePtr, + time_writer: &TimeWriter, + ) -> (L1BatchNumber, L2Block) { + // TODO: This whole logic seems off to me, reconsider if we need it at all. + // Specifically it is weird that we might not have our latest block in the storage. + // Likely has to do with genesis but let's make it clear if that is actually the case. + let last_l1_batch_number = load_last_l1_batch(storage) + .map(|(num, _)| L1BatchNumber(num as u32)) + .unwrap_or(self.current_batch); + let last_l2_block = load_last_l2_block(storage).unwrap_or_else(|| L2Block { + number: self.current_block.0, + hash: L2BlockHasher::legacy_hash(self.current_block), + timestamp: time_writer.current_timestamp(), + }); + (last_l1_batch_number, last_l2_block) + } + + pub(super) fn apply_block(&mut self, block: api::Block, index: u32) { + let latest_block = self.blocks.get(&self.current_block_hash).unwrap(); + self.current_block += 1; + + let actual_l1_batch_number = block + .l1_batch_number + .expect("block must have a l1_batch_number"); + if L1BatchNumber(actual_l1_batch_number.as_u32()) != self.current_batch { + panic!( + "expected next block to have batch_number {}, got {}", + self.current_batch, + actual_l1_batch_number.as_u32() + ); + } + + if L2BlockNumber(block.number.as_u32()) != self.current_block { + panic!( + "expected next block to have miniblock {}, got {} | {index}", + self.current_block, + block.number.as_u64() + ); + } + + if block.timestamp.as_u64() <= latest_block.timestamp.as_u64() { + panic!( + "expected next block to have timestamp bigger than {}, got {} | {index}", + latest_block.timestamp.as_u64(), + block.timestamp.as_u64() + ); + } + + let block_hash = block.hash; + self.current_block_hash = block_hash; + self.hashes + .insert(L2BlockNumber(block.number.as_u32()), block.hash); + self.blocks.insert(block.hash, block); + } + + pub(super) fn load_blocks( + &mut self, + time_writer: &mut TimeWriter, + blocks: Vec>, + ) { + tracing::trace!( + blocks = blocks.len(), + "loading new blocks from supplied state" + ); + for block in blocks { + let number = block.number.as_u64(); + tracing::trace!( + number, + hash = %block.hash, + "loading new block from supplied state" + ); + + self.hashes.insert(L2BlockNumber(number as u32), block.hash); + self.blocks.insert(block.hash, block); + } + + // Safe unwrap as there was at least one block in the loaded state + let latest_block = self.blocks.values().max_by_key(|b| b.number).unwrap(); + let latest_number = latest_block.number.as_u64(); + let latest_hash = latest_block.hash; + let Some(latest_batch_number) = latest_block.l1_batch_number.map(|n| n.as_u32()) else { + panic!("encountered a block with no batch; this is not supposed to happen") + }; + let latest_timestamp = latest_block.timestamp.as_u64(); + tracing::info!( + number = latest_number, + hash = %latest_hash, + batch_number = latest_batch_number, + timestamp = latest_timestamp, + "latest block after loading state" + ); + self.current_block = L2BlockNumber(latest_number as u32); + self.current_block_hash = latest_hash; + self.current_batch = L1BatchNumber(latest_batch_number); + time_writer.reset_to(latest_timestamp); + } + + pub(super) fn load_transactions(&mut self, transactions: Vec) { + tracing::trace!( + transactions = transactions.len(), + "loading new transactions from supplied state" + ); + for transaction in transactions { + tracing::trace!( + hash = %transaction.receipt.transaction_hash, + "loading new transaction from supplied state" + ); + self.tx_results + .insert(transaction.receipt.transaction_hash, transaction); + } + } +} + +fn load_last_l1_batch(storage: &StoragePtr) -> Option<(u64, u64)> { + // Get block number and timestamp + let current_l1_batch_info_key = StorageKey::new( + AccountTreeId::new(SYSTEM_CONTEXT_ADDRESS), + SYSTEM_CONTEXT_BLOCK_INFO_POSITION, + ); + let mut storage_ptr = storage.borrow_mut(); + let current_l1_batch_info = storage_ptr.read_value(¤t_l1_batch_info_key); + let (batch_number, batch_timestamp) = unpack_block_info(h256_to_u256(current_l1_batch_info)); + let block_number = batch_number as u32; + if block_number == 0 { + // The block does not exist yet + return None; + } + Some((batch_number, batch_timestamp)) +} diff --git a/crates/core/src/fork.rs b/crates/core/src/node/inner/fork.rs similarity index 95% rename from crates/core/src/fork.rs rename to crates/core/src/node/inner/fork.rs index 9f98be2d..acb5f6ed 100644 --- a/crates/core/src/fork.rs +++ b/crates/core/src/node/inner/fork.rs @@ -3,6 +3,7 @@ //! There is ForkStorage (that is a wrapper over InMemoryStorage) //! And ForkDetails - that parses network address and fork height from arguments. +use crate::utils::block_on; use crate::{deps::InMemoryStorage, http_fork_source::HttpForkSource}; use anvil_zksync_config::constants::{ DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR, DEFAULT_ESTIMATE_GAS_SCALE_FACTOR, @@ -17,14 +18,14 @@ use std::{ collections::HashMap, convert::{TryFrom, TryInto}, fmt, - future::Future, str::FromStr, sync::{Arc, RwLock}, }; -use tokio::runtime::Builder; use zksync_multivm::interface::storage::ReadStorage; +use zksync_types::api::BlockId; use zksync_types::web3::Bytes; use zksync_types::{ + api, api::{ Block, BlockDetails, BlockIdVariant, BlockNumber, BridgeAddresses, Transaction, TransactionDetails, TransactionVariant, @@ -45,21 +46,6 @@ use zksync_web3_decl::{ }; use zksync_web3_decl::{namespaces::EthNamespaceClient, types::Index}; -pub fn block_on(future: F) -> F::Output -where - F::Output: Send, -{ - std::thread::spawn(move || { - let runtime = Builder::new_current_thread() - .enable_all() - .build() - .expect("tokio runtime creation failed"); - runtime.block_on(future) - }) - .join() - .unwrap() -} - /// The possible networks to fork from. #[derive(Debug, Clone)] pub enum ForkNetwork { @@ -103,21 +89,22 @@ pub struct ForkStorage { pub chain_id: L2ChainId, } +// TODO: Hide mutable state and mark everything with `pub(super)` #[derive(Debug)] pub struct ForkStorageInner { // Underlying local storage pub raw_storage: InMemoryStorage, // Cache of data that was read from remote location. - pub value_read_cache: HashMap, + pub(super) value_read_cache: HashMap, // Cache of factory deps that were read from remote location. - pub factory_dep_cache: HashMap>>, + pub(super) 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>, } impl ForkStorage { - pub fn new( + pub(super) fn new( fork: Option, system_contracts_options: &SystemContractsOptions, use_evm_emulator: bool, @@ -337,11 +324,11 @@ impl ReadStorage for &ForkStorage { } impl ForkStorage { - pub fn set_value(&mut self, key: StorageKey, value: zksync_types::StorageValue) { + pub fn set_value(&self, key: StorageKey, value: zksync_types::StorageValue) { let mut mutator = self.inner.write().unwrap(); mutator.raw_storage.set_value(key, value) } - pub fn store_factory_dep(&mut self, hash: H256, bytecode: Vec) { + pub fn store_factory_dep(&self, hash: H256, bytecode: Vec) { let mut mutator = self.inner.write().unwrap(); mutator.raw_storage.store_factory_dep(hash, bytecode) } @@ -402,6 +389,17 @@ pub trait ForkSource { full_transactions: bool, ) -> eyre::Result>>; + fn get_block_by_id( + &self, + block_id: zksync_types::api::BlockId, + full_transactions: bool, + ) -> eyre::Result>> { + match block_id { + BlockId::Hash(hash) => self.get_block_by_hash(hash, full_transactions), + BlockId::Number(number) => self.get_block_by_number(number, full_transactions), + } + } + /// Returns the block details for a given miniblock number. fn get_block_details(&self, miniblock: L2BlockNumber) -> eyre::Result>; @@ -417,6 +415,16 @@ pub trait ForkSource { block_number: zksync_types::api::BlockNumber, ) -> eyre::Result>; + fn get_block_transaction_count_by_id( + &self, + block_id: api::BlockId, + ) -> eyre::Result> { + match block_id { + BlockId::Hash(hash) => self.get_block_transaction_count_by_hash(hash), + BlockId::Number(number) => self.get_block_transaction_count_by_number(number), + } + } + /// Returns information about a transaction by block hash and transaction index position. fn get_transaction_by_block_hash_and_index( &self, @@ -431,6 +439,19 @@ pub trait ForkSource { index: Index, ) -> eyre::Result>; + fn get_transaction_by_block_id_and_index( + &self, + block_id: api::BlockId, + index: Index, + ) -> eyre::Result> { + match block_id { + BlockId::Hash(hash) => self.get_transaction_by_block_hash_and_index(hash, index), + BlockId::Number(number) => { + self.get_transaction_by_block_number_and_index(number, index) + } + } + } + /// Returns addresses of the default bridge contracts. fn get_bridge_contracts(&self) -> eyre::Result; @@ -810,7 +831,7 @@ pub struct SerializableForkStorage { pub struct SerializableStorage(pub BTreeMap); mod serde_from { - use crate::fork::SerializableStorage; + use super::SerializableStorage; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::convert::TryFrom; diff --git a/crates/core/src/node/inner/in_memory_inner.rs b/crates/core/src/node/inner/in_memory_inner.rs new file mode 100644 index 00000000..fa932127 --- /dev/null +++ b/crates/core/src/node/inner/in_memory_inner.rs @@ -0,0 +1,1992 @@ +use super::blockchain::{BlockchainReader, BlockchainWriter}; +use super::fork::{ForkDetails, ForkStorage, SerializableStorage}; +use super::time::TimeWriter; +use crate::bootloader_debug::{BootloaderDebug, BootloaderDebugTracer}; +use crate::console_log::ConsoleLogHandler; +use crate::deps::storage_view::StorageView; +use crate::filters::EthFilters; +use crate::node::call_error_tracer::CallErrorTracer; +use crate::node::error::LoadStateError; +use crate::node::state::StateV1; +use crate::node::storage_logs::print_storage_logs_details; +use crate::node::{ + compute_hash, create_block, ImpersonationManager, Snapshot, TestNodeFeeInputProvider, + TransactionResult, TxExecutionInfo, VersionedState, ESTIMATE_GAS_ACCEPTABLE_OVERESTIMATION, + MAX_PREVIOUS_STATES, MAX_TX_SIZE, +}; +use crate::system_contracts::SystemContracts; +use crate::utils::{bytecode_to_factory_dep, create_debug_output}; +use crate::{formatter, utils}; +use anvil_zksync_config::constants::NON_FORK_FIRST_BLOCK_TIMESTAMP; +use anvil_zksync_config::TestNodeConfig; +use anvil_zksync_types::{ShowCalls, ShowGasDetails, ShowStorageLogs, ShowVMDetails}; +use colored::Colorize; +use indexmap::IndexMap; +use once_cell::sync::OnceCell; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use tokio::sync::RwLock; +use zksync_contracts::BaseSystemContracts; +use zksync_multivm::interface::storage::{ReadStorage, WriteStorage}; +use zksync_multivm::interface::{ + Call, ExecutionResult, InspectExecutionMode, L1BatchEnv, L2BlockEnv, SystemEnv, + TxExecutionMode, VmExecutionResultAndLogs, VmFactory, VmInterface, VmInterfaceExt, + VmInterfaceHistoryEnabled, +}; +use zksync_multivm::tracers::CallTracer; +use zksync_multivm::utils::{ + adjust_pubdata_price_for_tx, derive_base_fee_and_gas_per_pubdata, derive_overhead, + get_max_gas_per_pubdata_byte, +}; +use zksync_multivm::vm_latest::constants::{ + BATCH_COMPUTATIONAL_GAS_LIMIT, BATCH_GAS_LIMIT, MAX_VM_PUBDATA_PER_BATCH, +}; +use zksync_multivm::vm_latest::{HistoryDisabled, HistoryEnabled, ToTracerPointer, Vm}; +use zksync_multivm::{HistoryMode, VmVersion}; +use zksync_types::api::{BlockIdVariant, TransactionVariant}; +use zksync_types::block::build_bloom; +use zksync_types::fee::Fee; +use zksync_types::fee_model::{BatchFeeInput, PubdataIndependentBatchFeeModelInput}; +use zksync_types::l2::{L2Tx, TransactionType}; +use zksync_types::transaction_request::CallRequest; +use zksync_types::utils::{ + decompose_full_nonce, nonces_to_full_nonce, storage_key_for_eth_balance, +}; +use zksync_types::web3::{Bytes, Index}; +use zksync_types::{ + api, get_nonce_key, h256_to_address, h256_to_u256, u256_to_h256, AccountTreeId, Address, Bloom, + BloomInput, L1BatchNumber, L2BlockNumber, StorageKey, StorageValue, Transaction, + ACCOUNT_CODE_STORAGE_ADDRESS, H160, H256, MAX_L2_TX_GAS_LIMIT, U256, U64, +}; +use zksync_web3_decl::error::Web3Error; + +// TODO: Rename `InMemoryNodeInner` to something more sensible +/// Helper struct for InMemoryNode. +pub struct InMemoryNodeInner { + // Special right to write into blockchain as opposed to [`Blockchain`] + blockchain_writer: BlockchainWriter, + pub(super) time_writer: TimeWriter, + /// The fee input provider. + pub fee_input_provider: TestNodeFeeInputProvider, + // Map from filter_id to the eth filter + pub filters: Arc>, + // TODO: Make private + // Underlying storage + pub fork_storage: ForkStorage, + // Configuration. + pub config: TestNodeConfig, + pub console_log_handler: ConsoleLogHandler, + system_contracts: SystemContracts, + impersonation: ImpersonationManager, + pub rich_accounts: HashSet, + /// Keeps track of historical states indexed via block hash. Limited to [MAX_PREVIOUS_STATES]. + previous_states: IndexMap>, +} + +impl InMemoryNodeInner { + /// Create the state to be used implementing [InMemoryNode]. + #[allow(clippy::too_many_arguments)] + pub(super) fn new( + blockchain_writer: BlockchainWriter, + time_writer: TimeWriter, + fork_storage: ForkStorage, + fee_input_provider: TestNodeFeeInputProvider, + filters: Arc>, + config: TestNodeConfig, + impersonation: ImpersonationManager, + system_contracts: SystemContracts, + ) -> Self { + InMemoryNodeInner { + blockchain_writer, + time_writer, + fee_input_provider, + filters, + fork_storage, + config, + console_log_handler: ConsoleLogHandler::default(), + system_contracts, + impersonation, + rich_accounts: HashSet::new(), + previous_states: Default::default(), + } + } + + pub fn create_system_env( + &self, + base_system_contracts: BaseSystemContracts, + execution_mode: TxExecutionMode, + ) -> SystemEnv { + SystemEnv { + zk_porter_available: false, + // TODO: when forking, we could consider taking the protocol version id from the fork itself. + version: zksync_types::ProtocolVersionId::latest(), + base_system_smart_contracts: base_system_contracts, + bootloader_gas_limit: BATCH_COMPUTATIONAL_GAS_LIMIT, + execution_mode, + default_validation_computational_gas_limit: BATCH_COMPUTATIONAL_GAS_LIMIT, + chain_id: self.fork_storage.chain_id, + } + } + + /// Create [L1BatchEnv] to be used in the VM. + /// + /// We compute l1/l2 block details from storage to support fork testing, where the storage + /// can be updated mid execution and no longer matches with the initial node's state. + /// The L1 & L2 timestamps are also compared with node's timestamp to ensure it always increases monotonically. + pub async fn create_l1_batch_env(&self) -> (L1BatchEnv, BlockContext) { + tracing::debug!("creating L1 batch env"); + + let (last_l1_batch_number, last_l2_block) = self.blockchain_writer.read().await.last_env( + &StorageView::new(&self.fork_storage).into_rc_ptr(), + &self.time_writer, + ); + + let block_ctx = BlockContext { + hash: H256::zero(), + batch: (last_l1_batch_number + 1).0, + miniblock: last_l2_block.number as u64 + 1, + timestamp: self.time_writer.peek_next_timestamp(), + }; + + let fee_input = if let Some(fork) = &self + .fork_storage + .inner + .read() + .expect("fork_storage lock is already held by the current thread") + .fork + { + BatchFeeInput::PubdataIndependent(PubdataIndependentBatchFeeModelInput { + l1_gas_price: fork.l1_gas_price, + fair_l2_gas_price: fork.l2_fair_gas_price, + fair_pubdata_price: fork.fair_pubdata_price, + }) + } else { + self.fee_input_provider.get_batch_fee_input() + }; + + let batch_env = L1BatchEnv { + // TODO: set the previous batch hash properly (take from fork, when forking, and from local storage, when this is not the first block). + previous_batch_hash: None, + number: L1BatchNumber::from(block_ctx.batch), + timestamp: block_ctx.timestamp, + fee_input, + fee_account: H160::zero(), + enforced_base_fee: None, + first_l2_block: L2BlockEnv { + // the 'current_miniblock' contains the block that was already produced. + // So the next one should be one higher. + number: block_ctx.miniblock as u32, + timestamp: block_ctx.timestamp, + prev_block_hash: last_l2_block.hash, + // This is only used during zksyncEra block timestamp/number transition. + // In case of starting a new network, it doesn't matter. + // In theory , when forking mainnet, we should match this value + // to the value that was set in the node at that time - but AFAIK + // we don't have any API for this - so this might result in slightly + // incorrect replays of transacions during the migration period, that + // depend on block number or timestamp. + max_virtual_blocks_to_create: 1, + }, + }; + + (batch_env, block_ctx) + } + + async fn apply_batch( + &mut self, + blocks: impl IntoIterator>, + tx_results: impl IntoIterator, + ) { + // TODO: Move this to a dedicated `PreviousStates` struct once we have one + /// Archives the current state for later queries. + fn archive_state( + previous_states: &mut IndexMap>, + state: HashMap, + block_number: L2BlockNumber, + block_hash: H256, + ) { + if previous_states.len() > MAX_PREVIOUS_STATES as usize { + if let Some(entry) = previous_states.shift_remove_index(0) { + tracing::debug!("removing archived state for previous block {:#x}", entry.0); + } + } + tracing::debug!("archiving state for {:#x} #{}", block_hash, block_number); + previous_states.insert(block_hash, state); + } + + let mut storage = self.blockchain_writer.write().await; + storage.current_batch += 1; + storage.tx_results.extend( + tx_results + .into_iter() + .map(|r| (r.receipt.transaction_hash, r)), + ); + for (index, block) in blocks.into_iter().enumerate() { + // archive current state before we produce new batch/blocks + archive_state( + &mut self.previous_states, + self.fork_storage + .inner + .read() + .unwrap() + .raw_storage + .state + .clone(), + storage.current_block, + storage.current_block_hash, + ); + storage.apply_block(block, index as u32); + } + } + + // Prints the gas details of the transaction for debugging purposes. + fn display_detailed_gas_info( + &self, + bootloader_debug_result: Option<&eyre::Result>, + spent_on_pubdata: u64, + ) -> eyre::Result<(), String> { + if let Some(bootloader_result) = bootloader_debug_result { + let bootloader_debug = bootloader_result.clone()?; + + let gas_details = formatter::compute_gas_details(&bootloader_debug, spent_on_pubdata); + let mut formatter = formatter::Formatter::new(); + + let fee_model_config = self.fee_input_provider.get_fee_model_config(); + + formatter.print_gas_details(&gas_details, &fee_model_config); + + Ok(()) + } else { + Err("Bootloader tracer didn't finish.".to_owned()) + } + } + + /// Validates L2 transaction + fn validate_tx(&self, tx: &L2Tx) -> anyhow::Result<()> { + let max_gas = U256::from(u64::MAX); + if tx.common_data.fee.gas_limit > max_gas + || tx.common_data.fee.gas_per_pubdata_limit > max_gas + { + anyhow::bail!("exceeds block gas limit"); + } + + let l2_gas_price = self.fee_input_provider.gas_price(); + if tx.common_data.fee.max_fee_per_gas < l2_gas_price.into() { + tracing::info!( + "Submitted Tx is Unexecutable {:?} because of MaxFeePerGasTooLow {}", + tx.hash(), + tx.common_data.fee.max_fee_per_gas + ); + anyhow::bail!("block base fee higher than max fee per gas"); + } + + if tx.common_data.fee.max_fee_per_gas < tx.common_data.fee.max_priority_fee_per_gas { + tracing::info!( + "Submitted Tx is Unexecutable {:?} because of MaxPriorityFeeGreaterThanMaxFee {}", + tx.hash(), + tx.common_data.fee.max_fee_per_gas + ); + anyhow::bail!("max priority fee per gas higher than max fee per gas"); + } + Ok(()) + } + + /// Executes the given L2 transaction and returns all the VM logs. + /// The bootloader can be omitted via specifying the `execute_bootloader` boolean. + /// This causes the VM to produce 1 L2 block per L1 block, instead of the usual 2 blocks per L1 block. + /// + /// **NOTE** + /// + /// This function must only rely on data populated initially via [ForkDetails]: + /// * [InMemoryNodeInner::current_timestamp] + /// * [InMemoryNodeInner::current_batch] + /// * [InMemoryNodeInner::current_miniblock] + /// * [InMemoryNodeInner::current_miniblock_hash] + /// * [InMemoryNodeInner::fee_input_provider] + /// + /// And must _NEVER_ rely on data updated in [InMemoryNodeInner] during previous runs: + /// (if used, they must never panic and/or have meaningful defaults) + /// * [InMemoryNodeInner::block_hashes] + /// * [InMemoryNodeInner::blocks] + /// * [InMemoryNodeInner::tx_results] + /// + /// This is because external users of the library may call this function to perform an isolated + /// VM operation (optionally without bootloader execution) with an external storage and get the results back. + /// So any data populated in [Self::run_l2_tx] will not be available for the next invocation. + fn run_l2_tx_raw( + &self, + l2_tx: L2Tx, + vm: &mut Vm, + ) -> anyhow::Result { + let tx: Transaction = l2_tx.into(); + + let call_tracer_result = Arc::new(OnceCell::default()); + let bootloader_debug_result = Arc::new(OnceCell::default()); + + let tracers = vec![ + CallErrorTracer::new().into_tracer_pointer(), + CallTracer::new(call_tracer_result.clone()).into_tracer_pointer(), + BootloaderDebugTracer { + result: bootloader_debug_result.clone(), + } + .into_tracer_pointer(), + ]; + let compressed_bytecodes = vm + .push_transaction(tx.clone()) + .compressed_bytecodes + .into_owned(); + let tx_result = vm.inspect(&mut tracers.into(), InspectExecutionMode::OneTx); + + let call_traces = call_tracer_result.get().unwrap(); + + let spent_on_pubdata = + tx_result.statistics.gas_used - tx_result.statistics.computational_gas_used as u64; + + let status = match &tx_result.result { + ExecutionResult::Success { .. } => "SUCCESS", + ExecutionResult::Revert { .. } => "FAILED", + ExecutionResult::Halt { .. } => "HALTED", + }; + + // Print transaction summary + if self.config.show_tx_summary { + tracing::info!(""); + formatter::print_transaction_summary( + self.config.get_l2_gas_price(), + &tx, + &tx_result, + status, + ); + tracing::info!(""); + } + // Print gas details if enabled + if self.config.show_gas_details != ShowGasDetails::None { + self.display_detailed_gas_info(bootloader_debug_result.get(), spent_on_pubdata) + .unwrap_or_else(|err| { + tracing::error!("{}", format!("Cannot display gas details: {err}").on_red()); + }); + } + // Print storage logs if enabled + if self.config.show_storage_logs != ShowStorageLogs::None { + print_storage_logs_details(self.config.show_storage_logs, &tx_result); + } + // Print VM details if enabled + if self.config.show_vm_details != ShowVMDetails::None { + let mut formatter = formatter::Formatter::new(); + formatter.print_vm_details(&tx_result); + } + + if !self.config.disable_console_log { + self.console_log_handler.handle_calls_recursive(call_traces); + } + + if self.config.show_calls != ShowCalls::None { + tracing::info!(""); + tracing::info!( + "[Transaction Execution] ({} calls)", + call_traces[0].calls.len() + ); + let num_calls = call_traces.len(); + for (i, call) in call_traces.iter().enumerate() { + let is_last_sibling = i == num_calls - 1; + let mut formatter = formatter::Formatter::new(); + formatter.print_call( + tx.initiator_account(), + tx.execute.contract_address, + call, + is_last_sibling, + self.config.show_calls, + self.config.show_outputs, + self.config.resolve_hashes, + ); + } + } + // Print event logs if enabled + if self.config.show_event_logs { + tracing::info!(""); + tracing::info!("[Events] ({} events)", tx_result.logs.events.len()); + for (i, event) in tx_result.logs.events.iter().enumerate() { + let is_last = i == tx_result.logs.events.len() - 1; + let mut formatter = formatter::Formatter::new(); + formatter.print_event(event, self.config.resolve_hashes, is_last); + } + tracing::info!(""); + } + + let mut bytecodes = HashMap::new(); + for b in &*compressed_bytecodes { + let (hash, bytecode) = bytecode_to_factory_dep(b.original.clone()).map_err(|err| { + tracing::error!("{}", format!("cannot convert bytecode: {err}").on_red()); + err + })?; + bytecodes.insert(hash, bytecode); + } + + Ok(TxExecutionOutput { + result: tx_result, + call_traces: call_traces.clone(), + bytecodes, + }) + } + + /// Runs L2 transaction and commits it to a new block. + fn run_l2_tx( + &mut self, + l2_tx: L2Tx, + l2_tx_index: u64, + block_ctx: &BlockContext, + batch_env: &L1BatchEnv, + vm: &mut Vm, + ) -> anyhow::Result { + let tx_hash = l2_tx.hash(); + let transaction_type = l2_tx.common_data.transaction_type; + + if self.config.show_tx_summary { + tracing::info!(""); + tracing::info!("Validating {}", format!("{:?}", tx_hash).bold()); + } + + self.validate_tx(&l2_tx)?; + + if self.config.show_tx_summary { + tracing::info!("Executing {}", format!("{:?}", tx_hash).bold()); + } + + let TxExecutionOutput { + result, + bytecodes, + call_traces, + } = self.run_l2_tx_raw(l2_tx.clone(), vm)?; + + if let ExecutionResult::Halt { reason } = result.result { + // Halt means that something went really bad with the transaction execution (in most cases invalid signature, + // but it could also be bootloader panic etc). + // In such case, we should not persist the VM data, and we should pretend that transaction never existed. + anyhow::bail!("Transaction HALT: {reason}"); + } + + // Write all the factory deps. + for (hash, code) in bytecodes.iter() { + self.fork_storage.store_factory_dep( + u256_to_h256(*hash), + code.iter() + .flat_map(|entry| { + let mut bytes = vec![0u8; 32]; + entry.to_big_endian(&mut bytes); + bytes.to_vec() + }) + .collect(), + ) + } + + let logs = result + .logs + .events + .iter() + .enumerate() + .map(|(log_idx, log)| api::Log { + address: log.address, + topics: log.indexed_topics.clone(), + data: Bytes(log.value.clone()), + block_hash: Some(block_ctx.hash), + block_number: Some(block_ctx.miniblock.into()), + l1_batch_number: Some(U64::from(batch_env.number.0)), + transaction_hash: Some(tx_hash), + transaction_index: Some(U64::from(l2_tx_index)), + log_index: Some(U256::from(log_idx)), + transaction_log_index: Some(U256::from(log_idx)), + log_type: None, + removed: Some(false), + block_timestamp: Some(block_ctx.timestamp.into()), + }) + .collect(); + let tx_receipt = api::TransactionReceipt { + transaction_hash: tx_hash, + transaction_index: U64::from(l2_tx_index), + block_hash: block_ctx.hash, + block_number: block_ctx.miniblock.into(), + l1_batch_tx_index: None, + l1_batch_number: Some(U64::from(batch_env.number.0)), + from: l2_tx.initiator_account(), + to: l2_tx.recipient_account(), + cumulative_gas_used: Default::default(), + gas_used: Some(l2_tx.common_data.fee.gas_limit - result.refunds.gas_refunded), + contract_address: contract_address_from_tx_result(&result), + logs, + l2_to_l1_logs: vec![], + status: if result.result.is_failed() { + U64::from(0) + } else { + U64::from(1) + }, + effective_gas_price: Some(self.fee_input_provider.gas_price().into()), + transaction_type: Some((transaction_type as u32).into()), + logs_bloom: Default::default(), + }; + let debug = create_debug_output(&l2_tx, &result, call_traces).expect("create debug output"); // OK to unwrap here as Halt is handled above + + Ok(TransactionResult { + info: TxExecutionInfo { + tx: l2_tx, + batch_number: batch_env.number.0, + miniblock_number: block_ctx.miniblock, + }, + receipt: tx_receipt, + debug, + }) + } + + fn run_l2_txs( + &mut self, + txs: Vec, + batch_env: L1BatchEnv, + system_env: SystemEnv, + block_ctx: &mut BlockContext, + ) -> Vec { + let storage = StorageView::new(self.fork_storage.clone()).into_rc_ptr(); + let mut vm: Vm<_, HistoryEnabled> = Vm::new(batch_env.clone(), system_env, storage.clone()); + + // Compute block hash. Note that the computed block hash here will be different than that in production. + let tx_hashes = txs.iter().map(|t| t.hash()).collect::>(); + let hash = compute_hash(block_ctx.miniblock, &tx_hashes); + block_ctx.hash = hash; + + // Execute transactions and bootloader + let mut tx_results = Vec::with_capacity(tx_hashes.len()); + let mut tx_index = 0; + for tx in txs { + // Executing a next transaction means that a previous transaction was either rolled back (in which case its snapshot + // was already removed), or that we build on top of it (in which case, it can be removed now). + vm.pop_snapshot_no_rollback(); + // Save pre-execution VM snapshot. + vm.make_snapshot(); + match self.run_l2_tx(tx, tx_index, block_ctx, &batch_env, &mut vm) { + Ok(tx_result) => { + tx_results.push(tx_result); + tx_index += 1; + } + Err(e) => { + tracing::error!("Error while executing transaction: {e}"); + vm.rollback_to_the_latest_snapshot(); + } + } + } + vm.execute(InspectExecutionMode::Bootloader); + + // Write all the mutated keys (storage slots). + for (key, value) in storage.borrow().modified_storage_keys() { + self.fork_storage.set_value(*key, *value); + } + tx_results + } + + pub(super) async fn seal_block( + &mut self, + txs: Vec, + system_contracts: BaseSystemContracts, + ) -> anyhow::Result { + // Prepare a new block context and a new batch env + let system_env = self.create_system_env(system_contracts, TxExecutionMode::VerifyExecute); + let (batch_env, mut block_ctx) = self.create_l1_batch_env().await; + // Advance clock as we are consuming next timestamp for this block + anyhow::ensure!( + self.time_writer.advance_timestamp() == block_ctx.timestamp, + "advancing clock produced different timestamp than expected" + ); + + let tx_results = self.run_l2_txs(txs, batch_env.clone(), system_env, &mut block_ctx); + + let mut filters = self.filters.write().await; + for tx_result in &tx_results { + // TODO: Is this the right place to notify about new pending txs? + filters.notify_new_pending_transaction(tx_result.receipt.transaction_hash); + for log in &tx_result.receipt.logs { + filters.notify_new_log(log, block_ctx.miniblock.into()); + } + } + drop(filters); + + let mut transactions = Vec::new(); + for (index, tx_result) in tx_results.iter().enumerate() { + let mut transaction = zksync_types::api::Transaction::from(tx_result.info.tx.clone()); + transaction.block_hash = Some(block_ctx.hash); + transaction.block_number = Some(U64::from(block_ctx.miniblock)); + transaction.transaction_index = Some(index.into()); + transaction.l1_batch_number = Some(U64::from(batch_env.number.0)); + transaction.l1_batch_tx_index = Some(Index::zero()); + if transaction.transaction_type == Some(U64::zero()) + || transaction.transaction_type.is_none() + { + transaction.v = transaction + .v + .map(|v| v + 35 + self.fork_storage.chain_id.as_u64() * 2); + } + transactions.push(TransactionVariant::Full(transaction)); + } + + // Build bloom hash + let iter = tx_results + .iter() + .flat_map(|r| r.receipt.logs.iter()) + .flat_map(|event| { + event + .topics + .iter() + .map(|topic| BloomInput::Raw(topic.as_bytes())) + .chain([BloomInput::Raw(event.address.as_bytes())]) + }); + let logs_bloom = build_bloom(iter); + + // Calculate how much gas was used across all txs + let gas_used = tx_results + .iter() + .map(|r| r.debug.gas_used) + .fold(U256::zero(), |acc, x| acc + x); + + // Construct the block + let parent_block_hash = self + .blockchain_writer + .read() + .await + .get_block_hash_by_number(L2BlockNumber(block_ctx.miniblock as u32 - 1)) + .unwrap_or_default(); + let mut blocks = vec![create_block( + &batch_env, + block_ctx.hash, + parent_block_hash, + block_ctx.miniblock, + block_ctx.timestamp, + transactions, + gas_used, + logs_bloom, + )]; + + // Hack to ensure we don't mine two empty blocks in the same batch. Otherwise this creates + // weird side effect on the VM side wrt virtual block logic. + // TODO: Remove once we separate batch sealing from block sealing + if !tx_results.is_empty() { + // With the introduction of 'l2 blocks' (and virtual blocks), + // we are adding one l2 block at the end of each batch (to handle things like remaining events etc). + // You can look at insert_fictive_l2_block function in VM to see how this fake block is inserted. + let parent_block_hash = block_ctx.hash; + let block_ctx = block_ctx.new_block(&mut self.time_writer); + let hash = compute_hash(block_ctx.miniblock, []); + + let virtual_block = create_block( + &batch_env, + hash, + parent_block_hash, + block_ctx.miniblock, + block_ctx.timestamp, + vec![], + U256::zero(), + Bloom::zero(), + ); + blocks.push(virtual_block); + } + let block_hashes = blocks.iter().map(|b| b.hash).collect::>(); + self.apply_batch(blocks, tx_results).await; + + let mut filters = self.filters.write().await; + for block_hash in block_hashes { + filters.notify_new_block(block_hash); + } + drop(filters); + + Ok(L2BlockNumber(block_ctx.miniblock as u32)) + } + + /// Estimates the gas required for a given call request. + /// + /// # Arguments + /// + /// * `req` - A `CallRequest` struct representing the call request to estimate gas for. + /// + /// # Returns + /// + /// A `Result` with a `Fee` representing the estimated gas related data. + pub async fn estimate_gas_impl(&self, req: CallRequest) -> Result { + let mut request_with_gas_per_pubdata_overridden = req; + + if let Some(ref mut eip712_meta) = request_with_gas_per_pubdata_overridden.eip712_meta { + if eip712_meta.gas_per_pubdata == U256::zero() { + eip712_meta.gas_per_pubdata = + get_max_gas_per_pubdata_byte(VmVersion::latest()).into(); + } + } + + let is_eip712 = request_with_gas_per_pubdata_overridden + .eip712_meta + .is_some(); + let initiator_address = request_with_gas_per_pubdata_overridden + .from + .unwrap_or_default(); + let impersonating = self.impersonation.is_impersonating(&initiator_address); + let system_contracts = self + .system_contracts + .contracts_for_fee_estimate(impersonating) + .clone(); + let allow_no_target = system_contracts.evm_emulator.is_some(); + + let mut l2_tx = L2Tx::from_request( + request_with_gas_per_pubdata_overridden.into(), + MAX_TX_SIZE, + allow_no_target, + ) + .map_err(Web3Error::SerializationError)?; + + let tx: Transaction = l2_tx.clone().into(); + + let fee_input = { + let fee_input = self.fee_input_provider.get_batch_fee_input_scaled(); + // In order for execution to pass smoothly, we need to ensure that block's required gasPerPubdata will be + // <= to the one in the transaction itself. + adjust_pubdata_price_for_tx( + fee_input, + tx.gas_per_pubdata_byte_limit(), + None, + VmVersion::latest(), + ) + }; + + let (base_fee, gas_per_pubdata_byte) = + derive_base_fee_and_gas_per_pubdata(fee_input, VmVersion::latest()); + + // Properly format signature + if l2_tx.common_data.signature.is_empty() { + l2_tx.common_data.signature = vec![0u8; 65]; + l2_tx.common_data.signature[64] = 27; + } + + // The user may not include the proper transaction type during the estimation of + // the gas fee. However, it is needed for the bootloader checks to pass properly. + if is_eip712 { + l2_tx.common_data.transaction_type = TransactionType::EIP712Transaction; + } + + l2_tx.common_data.fee.gas_per_pubdata_limit = + get_max_gas_per_pubdata_byte(VmVersion::latest()).into(); + l2_tx.common_data.fee.max_fee_per_gas = base_fee.into(); + l2_tx.common_data.fee.max_priority_fee_per_gas = base_fee.into(); + + let execution_mode = TxExecutionMode::EstimateFee; + let (mut batch_env, _) = self.create_l1_batch_env().await; + batch_env.fee_input = fee_input; + + let system_env = self.create_system_env(system_contracts, execution_mode); + + // When the pubdata cost grows very high, the total gas limit required may become very high as well. If + // we do binary search over any possible gas limit naively, we may end up with a very high number of iterations, + // which affects performance. + // + // To optimize for this case, we first calculate the amount of gas needed to cover for the pubdata. After that, we + // need to do a smaller binary search that is focused on computational gas limit only. + let additional_gas_for_pubdata = if tx.is_l1() { + // For L1 transactions the pubdata priced in such a way that the maximal computational + // gas limit should be enough to cover for the pubdata as well, so no additional gas is provided there. + 0u64 + } else { + // For L2 transactions, we estimate the amount of gas needed to cover for the pubdata by creating a transaction with infinite gas limit. + // And getting how much pubdata it used. + + // In theory, if the transaction has failed with such large gas limit, we could have returned an API error here right away, + // but doing it later on keeps the code more lean. + let result = InMemoryNodeInner::estimate_gas_step( + l2_tx.clone(), + gas_per_pubdata_byte, + BATCH_GAS_LIMIT, + batch_env.clone(), + system_env.clone(), + &self.fork_storage, + ); + + if result.statistics.pubdata_published > MAX_VM_PUBDATA_PER_BATCH.try_into().unwrap() { + return Err(Web3Error::SubmitTransactionError( + "exceeds limit for published pubdata".into(), + Default::default(), + )); + } + + // It is assumed that there is no overflow here + (result.statistics.pubdata_published as u64) * gas_per_pubdata_byte + }; + + // We are using binary search to find the minimal values of gas_limit under which the transaction succeeds + let mut lower_bound = 0u64; + let mut upper_bound = MAX_L2_TX_GAS_LIMIT; + let mut attempt_count = 1; + + tracing::trace!("Starting gas estimation loop"); + while lower_bound + ESTIMATE_GAS_ACCEPTABLE_OVERESTIMATION < upper_bound { + let mid = (lower_bound + upper_bound) / 2; + tracing::trace!( + "Attempt {} (lower_bound: {}, upper_bound: {}, mid: {})", + attempt_count, + lower_bound, + upper_bound, + mid + ); + let try_gas_limit = additional_gas_for_pubdata + mid; + + let estimate_gas_result = InMemoryNodeInner::estimate_gas_step( + l2_tx.clone(), + gas_per_pubdata_byte, + try_gas_limit, + batch_env.clone(), + system_env.clone(), + &self.fork_storage, + ); + + if estimate_gas_result.result.is_failed() { + tracing::trace!("Attempt {} FAILED", attempt_count); + lower_bound = mid + 1; + } else { + tracing::trace!("Attempt {} SUCCEEDED", attempt_count); + upper_bound = mid; + } + attempt_count += 1; + } + + tracing::trace!("Gas Estimation Values:"); + tracing::trace!(" Final upper_bound: {}", upper_bound); + tracing::trace!( + " ESTIMATE_GAS_SCALE_FACTOR: {}", + self.fee_input_provider.estimate_gas_scale_factor + ); + tracing::trace!(" MAX_L2_TX_GAS_LIMIT: {}", MAX_L2_TX_GAS_LIMIT); + let tx_body_gas_limit = upper_bound; + let suggested_gas_limit = ((upper_bound + additional_gas_for_pubdata) as f32 + * self.fee_input_provider.estimate_gas_scale_factor) + as u64; + + let estimate_gas_result = InMemoryNodeInner::estimate_gas_step( + l2_tx.clone(), + gas_per_pubdata_byte, + suggested_gas_limit, + batch_env, + system_env, + &self.fork_storage, + ); + + let overhead = derive_overhead( + suggested_gas_limit, + gas_per_pubdata_byte as u32, + tx.encoding_len(), + l2_tx.common_data.transaction_type as u8, + VmVersion::latest(), + ) as u64; + + match estimate_gas_result.result { + ExecutionResult::Revert { output } => { + tracing::info!("{}", format!("Unable to estimate gas for the request with our suggested gas limit of {}. The transaction is most likely unexecutable. Breakdown of estimation:", suggested_gas_limit + overhead).red()); + tracing::info!( + "{}", + format!( + "\tEstimated transaction body gas cost: {}", + tx_body_gas_limit + ) + .red() + ); + tracing::info!( + "{}", + format!("\tGas for pubdata: {}", additional_gas_for_pubdata).red() + ); + tracing::info!("{}", format!("\tOverhead: {}", overhead).red()); + let message = output.to_string(); + let pretty_message = format!( + "execution reverted{}{}", + if message.is_empty() { "" } else { ": " }, + message + ); + let data = output.encoded_data(); + tracing::info!("{}", pretty_message.on_red()); + Err(Web3Error::SubmitTransactionError(pretty_message, data)) + } + ExecutionResult::Halt { reason } => { + tracing::info!("{}", format!("Unable to estimate gas for the request with our suggested gas limit of {}. The transaction is most likely unexecutable. Breakdown of estimation:", suggested_gas_limit + overhead).red()); + tracing::info!( + "{}", + format!( + "\tEstimated transaction body gas cost: {}", + tx_body_gas_limit + ) + .red() + ); + tracing::info!( + "{}", + format!("\tGas for pubdata: {}", additional_gas_for_pubdata).red() + ); + tracing::info!("{}", format!("\tOverhead: {}", overhead).red()); + let message = reason.to_string(); + let pretty_message = format!( + "execution reverted{}{}", + if message.is_empty() { "" } else { ": " }, + message + ); + + tracing::info!("{}", pretty_message.on_red()); + Err(Web3Error::SubmitTransactionError(pretty_message, vec![])) + } + ExecutionResult::Success { .. } => { + let full_gas_limit = match suggested_gas_limit.overflowing_add(overhead) { + (value, false) => value, + (_, true) => { + tracing::info!("{}", "Overflow when calculating gas estimation. We've exceeded the block gas limit by summing the following values:".red()); + tracing::info!( + "{}", + format!( + "\tEstimated transaction body gas cost: {}", + tx_body_gas_limit + ) + .red() + ); + tracing::info!( + "{}", + format!("\tGas for pubdata: {}", additional_gas_for_pubdata).red() + ); + tracing::info!("{}", format!("\tOverhead: {}", overhead).red()); + return Err(Web3Error::SubmitTransactionError( + "exceeds block gas limit".into(), + Default::default(), + )); + } + }; + + tracing::trace!("Gas Estimation Results"); + tracing::trace!(" tx_body_gas_limit: {}", tx_body_gas_limit); + tracing::trace!( + " additional_gas_for_pubdata: {}", + additional_gas_for_pubdata + ); + tracing::trace!(" overhead: {}", overhead); + tracing::trace!(" full_gas_limit: {}", full_gas_limit); + let fee = Fee { + max_fee_per_gas: base_fee.into(), + max_priority_fee_per_gas: 0u32.into(), + gas_limit: full_gas_limit.into(), + gas_per_pubdata_limit: gas_per_pubdata_byte.into(), + }; + Ok(fee) + } + } + } + + /// Runs fee estimation against a sandbox vm with the given gas_limit. + #[allow(clippy::too_many_arguments)] + fn estimate_gas_step( + mut l2_tx: L2Tx, + gas_per_pubdata_byte: u64, + tx_gas_limit: u64, + batch_env: L1BatchEnv, + system_env: SystemEnv, + fork_storage: &ForkStorage, + ) -> VmExecutionResultAndLogs { + let tx: Transaction = l2_tx.clone().into(); + + // Set gas_limit for transaction + let gas_limit_with_overhead = tx_gas_limit + + derive_overhead( + tx_gas_limit, + gas_per_pubdata_byte as u32, + tx.encoding_len(), + l2_tx.common_data.transaction_type as u8, + VmVersion::latest(), + ) as u64; + l2_tx.common_data.fee.gas_limit = gas_limit_with_overhead.into(); + + let storage = StorageView::new(fork_storage).into_rc_ptr(); + + // The nonce needs to be updated + let nonce = l2_tx.nonce(); + let nonce_key = get_nonce_key(&l2_tx.initiator_account()); + let full_nonce = storage.borrow_mut().read_value(&nonce_key); + let (_, deployment_nonce) = decompose_full_nonce(h256_to_u256(full_nonce)); + let enforced_full_nonce = nonces_to_full_nonce(U256::from(nonce.0), deployment_nonce); + storage + .borrow_mut() + .set_value(nonce_key, u256_to_h256(enforced_full_nonce)); + + // We need to explicitly put enough balance into the account of the users + let payer = l2_tx.payer(); + let balance_key = storage_key_for_eth_balance(&payer); + let mut current_balance = h256_to_u256(storage.borrow_mut().read_value(&balance_key)); + let added_balance = l2_tx.common_data.fee.gas_limit * l2_tx.common_data.fee.max_fee_per_gas; + current_balance += added_balance; + storage + .borrow_mut() + .set_value(balance_key, u256_to_h256(current_balance)); + + let mut vm: Vm<_, HistoryDisabled> = Vm::new(batch_env, system_env, storage.clone()); + + let tx: Transaction = l2_tx.into(); + vm.push_transaction(tx); + + vm.execute(InspectExecutionMode::OneTx) + } + + /// Creates a [Snapshot] of the current state of the node. + pub async fn snapshot(&self) -> Result { + let blockchain = self.blockchain_writer.read().await; + let filters = self.filters.read().await.clone(); + let storage = self + .fork_storage + .inner + .read() + .map_err(|err| format!("failed acquiring read lock on storage: {:?}", err))?; + + Ok(Snapshot { + current_batch: blockchain.current_batch, + current_block: blockchain.current_block, + current_block_hash: blockchain.current_block_hash, + fee_input_provider: self.fee_input_provider.clone(), + tx_results: blockchain.tx_results.clone(), + blocks: blockchain.blocks.clone(), + hashes: blockchain.hashes.clone(), + filters, + impersonation_state: self.impersonation.state(), + rich_accounts: self.rich_accounts.clone(), + previous_states: self.previous_states.clone(), + raw_storage: storage.raw_storage.clone(), + value_read_cache: storage.value_read_cache.clone(), + factory_dep_cache: storage.factory_dep_cache.clone(), + }) + } + + /// Restores a previously created [Snapshot] of the node. + pub async fn restore_snapshot(&mut self, snapshot: Snapshot) -> Result<(), String> { + let mut blockchain = self.blockchain_writer.write().await; + let mut storage = self + .fork_storage + .inner + .write() + .map_err(|err| format!("failed acquiring write lock on storage: {:?}", err))?; + + blockchain.current_batch = snapshot.current_batch; + blockchain.current_block = snapshot.current_block; + blockchain.current_block_hash = snapshot.current_block_hash; + self.fee_input_provider = snapshot.fee_input_provider; + blockchain.tx_results = snapshot.tx_results; + blockchain.blocks = snapshot.blocks; + blockchain.hashes = snapshot.hashes; + // FIXME: This logic is incorrect but it doesn't matter as filters should not be a part of + // snapshots anyway + self.filters = Arc::new(RwLock::new(snapshot.filters)); + self.impersonation.set_state(snapshot.impersonation_state); + self.rich_accounts = snapshot.rich_accounts; + self.previous_states = snapshot.previous_states; + storage.raw_storage = snapshot.raw_storage; + storage.value_read_cache = snapshot.value_read_cache; + storage.factory_dep_cache = snapshot.factory_dep_cache; + + Ok(()) + } + + pub async fn dump_state( + &self, + preserve_historical_states: bool, + ) -> anyhow::Result { + let blockchain = self.blockchain_writer.read().await; + let blocks = blockchain.blocks.values().cloned().collect(); + let transactions = blockchain.tx_results.values().cloned().collect(); + drop(blockchain); + let fork_storage = self.fork_storage.dump_state(); + let historical_states = if preserve_historical_states { + self.previous_states + .iter() + .map(|(k, v)| (*k, SerializableStorage(v.clone().into_iter().collect()))) + .collect() + } else { + Vec::new() + }; + + Ok(VersionedState::v1(StateV1 { + blocks, + transactions, + fork_storage, + historical_states, + })) + } + + pub async fn load_state(&mut self, state: VersionedState) -> Result { + let mut storage = self.blockchain_writer.write().await; + if storage.blocks.len() > 1 { + tracing::debug!( + blocks = storage.blocks.len(), + "node has existing state; refusing to load new state" + ); + return Err(LoadStateError::HasExistingState); + } + let state = match state { + VersionedState::V1 { state, .. } => state, + VersionedState::Unknown { version } => { + return Err(LoadStateError::UnknownStateVersion(version)) + } + }; + if state.blocks.is_empty() { + tracing::debug!("new state has no blocks; refusing to load"); + return Err(LoadStateError::EmptyState); + } + + storage.load_blocks(&mut self.time_writer, state.blocks); + storage.load_transactions(state.transactions); + self.fork_storage.load_state(state.fork_storage); + + tracing::trace!( + states = state.historical_states.len(), + "loading historical states from supplied state" + ); + self.previous_states.extend( + state + .historical_states + .into_iter() + .map(|(k, v)| (k, v.0.into_iter().collect())), + ); + + Ok(true) + } + + pub async fn get_storage_at_block( + &self, + address: Address, + idx: U256, + block: Option, + ) -> Result { + let storage_key = StorageKey::new(AccountTreeId::new(address), u256_to_h256(idx)); + let storage = self.blockchain_writer.read().await; + + let block_number = block + .map(|block| match block { + BlockIdVariant::BlockNumber(block_number) => Ok(utils::to_real_block_number( + block_number, + U64::from(storage.current_block.0), + )), + BlockIdVariant::BlockNumberObject(o) => Ok(utils::to_real_block_number( + o.block_number, + U64::from(storage.current_block.0), + )), + BlockIdVariant::BlockHashObject(o) => storage + .blocks + .get(&o.block_hash) + .map(|block| block.number) + .ok_or_else(|| { + tracing::error!("unable to map block number to hash #{:#x}", o.block_hash); + Web3Error::InternalError(anyhow::Error::msg( + "Failed to map block number to hash.", + )) + }), + }) + .unwrap_or_else(|| Ok(U64::from(storage.current_block.0)))?; + // FIXME: Conversion mess above + let block_number = L2BlockNumber(block_number.as_u32()); + + if block_number == storage.current_block { + match self.fork_storage.read_value_internal(&storage_key) { + Ok(value) => Ok(H256(value.0)), + Err(error) => Err(Web3Error::InternalError(anyhow::anyhow!( + "failed to read storage: {}", + error + ))), + } + } else if storage.hashes.contains_key(&block_number) { + let value = storage + .hashes + .get(&block_number) + .and_then(|block_hash| self.previous_states.get(block_hash)) + .and_then(|state| state.get(&storage_key)) + .cloned() + .unwrap_or_default(); + + if value.is_zero() { + match self.fork_storage.read_value_internal(&storage_key) { + Ok(value) => Ok(H256(value.0)), + Err(error) => Err(Web3Error::InternalError(anyhow::anyhow!( + "failed to read storage: {}", + error + ))), + } + } else { + Ok(value) + } + } else { + self.fork_storage + .inner + .read() + .expect("failed reading fork storage") + .fork + .as_ref() + .and_then(|fork| fork.fork_source.get_storage_at(address, idx, block).ok()) + .ok_or_else(|| { + tracing::error!( + "unable to get storage at address {:?}, index {:?} for block {:?}", + address, + idx, + block + ); + Web3Error::InternalError(anyhow::Error::msg("Failed to get storage.")) + }) + } + } + + pub async fn reset(&mut self, fork: Option) { + let (_, blockchain_writer) = BlockchainReader::new( + fork.as_ref(), + self.config.genesis.as_ref(), + self.config.genesis_timestamp, + ); + let blockchain_storage = blockchain_writer.inner.read().await.clone(); + drop(std::mem::replace( + &mut *self.blockchain_writer.write().await, + blockchain_storage, + )); + + self.time_writer.set_current_timestamp_unchecked( + fork.as_ref() + .map(|f| f.block_timestamp) + .unwrap_or(NON_FORK_FIRST_BLOCK_TIMESTAMP), + ); + + drop(std::mem::take(&mut *self.filters.write().await)); + + let fork_storage = ForkStorage::new( + fork, + &self.config.system_contracts_options, + self.config.use_evm_emulator, + self.config.chain_id, + ); + let mut old_storage = self.fork_storage.inner.write().unwrap(); + let mut new_storage = fork_storage.inner.write().unwrap(); + old_storage.raw_storage = std::mem::take(&mut new_storage.raw_storage); + old_storage.value_read_cache = std::mem::take(&mut new_storage.value_read_cache); + old_storage.factory_dep_cache = std::mem::take(&mut new_storage.factory_dep_cache); + old_storage.fork = std::mem::take(&mut new_storage.fork); + self.fork_storage.chain_id = fork_storage.chain_id; + drop(old_storage); + drop(new_storage); + + self.rich_accounts.clear(); + self.previous_states.clear(); + } + + /// Adds a lot of tokens to a given account with a specified balance. + pub fn set_rich_account(&mut self, address: H160, balance: U256) { + let key = storage_key_for_eth_balance(&address); + + let keys = { + let mut storage_view = StorageView::new(&self.fork_storage); + // Set balance to the specified amount + storage_view.set_value(key, u256_to_h256(balance)); + storage_view.modified_storage_keys().clone() + }; + + for (key, value) in keys.iter() { + self.fork_storage.set_value(*key, *value); + } + self.rich_accounts.insert(address); + } +} + +#[derive(Debug)] +pub struct TxExecutionOutput { + result: VmExecutionResultAndLogs, + call_traces: Vec, + bytecodes: HashMap>, +} + +/// Keeps track of a block's batch number, miniblock number and timestamp. +/// Useful for keeping track of the current context when creating multiple blocks. +#[derive(Debug, Clone, Default)] +pub struct BlockContext { + pub hash: H256, + pub batch: u32, + pub miniblock: u64, + pub timestamp: u64, +} + +impl BlockContext { + /// Create the next batch instance that uses the same batch number, and has all other parameters incremented by `1`. + fn new_block(&self, time: &mut TimeWriter) -> BlockContext { + Self { + hash: H256::zero(), + batch: self.batch, + miniblock: self.miniblock.saturating_add(1), + timestamp: time.advance_timestamp(), + } + } +} + +fn contract_address_from_tx_result(execution_result: &VmExecutionResultAndLogs) -> Option { + for query in execution_result.logs.storage_logs.iter().rev() { + if query.log.is_write() && query.log.key.address() == &ACCOUNT_CODE_STORAGE_ADDRESS { + return Some(h256_to_address(query.log.key.key())); + } + } + None +} + +// Test utils +#[cfg(test)] +impl InMemoryNodeInner { + pub fn test_config(config: TestNodeConfig) -> Arc> { + let fee_provider = TestNodeFeeInputProvider::default(); + let impersonation = ImpersonationManager::default(); + let system_contracts = SystemContracts::from_options( + &config.system_contracts_options, + config.use_evm_emulator, + ); + let (inner, _, _, _) = InMemoryNodeInner::init( + None, + fee_provider, + Arc::new(RwLock::new(Default::default())), + config, + impersonation.clone(), + system_contracts.clone(), + ); + inner + } + + pub fn test() -> Arc> { + Self::test_config(TestNodeConfig::default()) + } + + /// Deploys a contract with the given bytecode. + pub async fn deploy_contract( + &mut self, + tx_hash: H256, + private_key: &zksync_types::K256PrivateKey, + bytecode: Vec, + calldata: Option>, + nonce: zksync_types::Nonce, + ) -> H256 { + use ethers::abi::Function; + use ethers::types::Bytes; + use zksync_web3_rs::eip712; + + let salt = [0u8; 32]; + let bytecode_hash = eip712::hash_bytecode(&bytecode).expect("invalid bytecode"); + let call_data: Bytes = calldata.unwrap_or_default().into(); + let create: Function = serde_json::from_str( + r#"{ + "inputs": [ + { + "internalType": "bytes32", + "name": "_salt", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_bytecodeHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_input", + "type": "bytes" + } + ], + "name": "create", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "function" + }"#, + ) + .unwrap(); + + let data = + ethers::contract::encode_function_data(&create, (salt, bytecode_hash, call_data)) + .expect("failed encoding function data"); + + let mut tx = L2Tx::new_signed( + Some(zksync_types::CONTRACT_DEPLOYER_ADDRESS), + data.to_vec(), + nonce, + Fee { + gas_limit: U256::from(400_000_000), + max_fee_per_gas: U256::from(50_000_000), + max_priority_fee_per_gas: U256::from(50_000_000), + gas_per_pubdata_limit: U256::from(50000), + }, + U256::from(0), + zksync_types::L2ChainId::from(260), + private_key, + vec![bytecode], + Default::default(), + ) + .expect("failed signing tx"); + tx.set_input(vec![], tx_hash); + + let system_contracts = self + .system_contracts + .system_contracts_for_initiator(&self.impersonation, &tx.initiator_account()); + let block_number = self + .seal_block(vec![tx], system_contracts) + .await + .expect("failed deploying contract"); + + self.blockchain_writer + .read() + .await + .get_block_hash_by_number(block_number) + .unwrap() + } + + // TODO: Return L2BlockNumber + /// Applies a transaction with a given hash to the node and returns the block hash. + pub async fn apply_tx(&mut self, tx_hash: H256) -> (H256, U64, L2Tx) { + let tx = crate::testing::TransactionBuilder::new() + .set_hash(tx_hash) + .build(); + + self.set_rich_account( + tx.common_data.initiator_address, + U256::from(100u128 * 10u128.pow(18)), + ); + let system_contracts = self + .system_contracts + .system_contracts_for_initiator(&self.impersonation, &tx.initiator_account()); + let block_number = self + .seal_block(vec![tx.clone()], system_contracts) + .await + .expect("failed applying tx"); + + let block_hash = self + .blockchain_writer + .read() + .await + .get_block_hash_by_number(block_number) + .unwrap(); + + (block_hash, U64::from(block_number.0), tx) + } + + pub async fn insert_block(&mut self, hash: H256, block: api::Block) { + self.blockchain_writer + .write() + .await + .blocks + .insert(hash, block); + } + + pub async fn insert_block_hash(&mut self, number: L2BlockNumber, hash: H256) { + self.blockchain_writer + .write() + .await + .hashes + .insert(number, hash); + } + + pub async fn insert_tx_result(&mut self, hash: H256, tx_result: TransactionResult) { + self.blockchain_writer + .write() + .await + .tx_results + .insert(hash, tx_result); + } + + pub fn insert_previous_state(&mut self, hash: H256, state: HashMap) { + self.previous_states.insert(hash, state); + } + + pub fn get_previous_state(&self, hash: H256) -> Option> { + self.previous_states.get(&hash).cloned() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::node::create_genesis; + use crate::node::fork::ForkStorage; + use crate::testing; + use crate::testing::{ExternalStorage, TransactionBuilder, STORAGE_CONTRACT_BYTECODE}; + use anvil_zksync_config::constants::{ + DEFAULT_ACCOUNT_BALANCE, DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR, + DEFAULT_ESTIMATE_GAS_SCALE_FACTOR, DEFAULT_FAIR_PUBDATA_PRICE, DEFAULT_L2_GAS_PRICE, + TEST_NODE_NETWORK_ID, + }; + use anvil_zksync_config::types::{CacheConfig, SystemContractsOptions}; + use anvil_zksync_config::TestNodeConfig; + use ethabi::{ParamType, Token, Uint}; + use itertools::Itertools; + use zksync_types::{utils::deployed_address_create, K256PrivateKey, Nonce}; + + async fn test_vm( + node: &mut InMemoryNodeInner, + system_contracts: BaseSystemContracts, + ) -> ( + BlockContext, + L1BatchEnv, + Vm, HistoryDisabled>, + ) { + let storage = StorageView::new(node.fork_storage.clone()).into_rc_ptr(); + let system_env = node.create_system_env(system_contracts, TxExecutionMode::VerifyExecute); + let (batch_env, block_ctx) = node.create_l1_batch_env().await; + let vm: Vm<_, HistoryDisabled> = Vm::new(batch_env.clone(), system_env, storage); + + (block_ctx, batch_env, vm) + } + + /// Decodes a `bytes` tx result to its concrete parameter type. + fn decode_tx_result(output: &[u8], param_type: ParamType) -> Token { + let result = ethabi::decode(&[ParamType::Bytes], output).expect("failed decoding output"); + if result.is_empty() { + panic!("result was empty"); + } + + let result_bytes = result[0] + .clone() + .into_bytes() + .expect("failed converting result to bytes"); + let result = + ethabi::decode(&[param_type], &result_bytes).expect("failed converting output"); + if result.is_empty() { + panic!("decoded result was empty"); + } + + result[0].clone() + } + + #[tokio::test] + async fn test_run_l2_tx_validates_tx_gas_limit_too_high() { + let inner = InMemoryNodeInner::test(); + let mut node = inner.write().await; + let tx = TransactionBuilder::new() + .set_gas_limit(U256::from(u64::MAX) + 1) + .build(); + node.set_rich_account(tx.initiator_account(), U256::from(100u128 * 10u128.pow(18))); + + let system_contracts = node + .system_contracts + .system_contracts_for_initiator(&node.impersonation, &tx.initiator_account()); + let (block_ctx, batch_env, mut vm) = test_vm(&mut *node, system_contracts).await; + let err = node + .run_l2_tx(tx, 0, &block_ctx, &batch_env, &mut vm) + .unwrap_err(); + assert_eq!(err.to_string(), "exceeds block gas limit"); + } + + #[tokio::test] + async fn test_run_l2_tx_validates_tx_max_fee_per_gas_too_low() { + let inner = InMemoryNodeInner::test(); + let mut node = inner.write().await; + let tx = TransactionBuilder::new() + .set_max_fee_per_gas(U256::from(DEFAULT_L2_GAS_PRICE - 1)) + .build(); + node.set_rich_account(tx.initiator_account(), U256::from(100u128 * 10u128.pow(18))); + + let system_contracts = node + .system_contracts + .system_contracts_for_initiator(&node.impersonation, &tx.initiator_account()); + let (block_ctx, batch_env, mut vm) = test_vm(&mut *node, system_contracts).await; + let err = node + .run_l2_tx(tx, 0, &block_ctx, &batch_env, &mut vm) + .unwrap_err(); + + assert_eq!( + err.to_string(), + "block base fee higher than max fee per gas" + ); + } + + #[tokio::test] + async fn test_run_l2_tx_validates_tx_max_priority_fee_per_gas_higher_than_max_fee_per_gas() { + let inner = InMemoryNodeInner::test(); + let mut node = inner.write().await; + let tx = TransactionBuilder::new() + .set_max_priority_fee_per_gas(U256::from(250_000_000 + 1)) + .build(); + node.set_rich_account(tx.initiator_account(), U256::from(100u128 * 10u128.pow(18))); + + let system_contracts = node + .system_contracts + .system_contracts_for_initiator(&node.impersonation, &tx.initiator_account()); + let (block_ctx, batch_env, mut vm) = test_vm(&mut *node, system_contracts).await; + let err = node + .run_l2_tx(tx, 0, &block_ctx, &batch_env, &mut vm) + .unwrap_err(); + + assert_eq!( + err.to_string(), + "max priority fee per gas higher than max fee per gas" + ); + } + + #[tokio::test] + async fn test_create_genesis_creates_block_with_hash_and_zero_parent_hash() { + let first_block = create_genesis::(Some(1000)); + + assert_eq!(first_block.hash, compute_hash(0, [])); + assert_eq!(first_block.parent_hash, H256::zero()); + } + + #[tokio::test] + async fn test_run_l2_tx_raw_does_not_panic_on_external_storage_call() { + // Perform a transaction to get storage to an intermediate state + let inner = InMemoryNodeInner::test(); + let mut node = inner.write().await; + let tx = TransactionBuilder::new().build(); + node.set_rich_account(tx.initiator_account(), U256::from(100u128 * 10u128.pow(18))); + + let system_contracts = node + .system_contracts + .system_contracts_for_initiator(&node.impersonation, &tx.initiator_account()); + node.seal_block(vec![tx], system_contracts).await.unwrap(); + let external_storage = node.fork_storage.clone(); + + // Execute next transaction using a fresh in-memory node and the external fork storage + let mock_db = ExternalStorage { + raw_storage: external_storage.inner.read().unwrap().raw_storage.clone(), + }; + let impersonation = ImpersonationManager::default(); + let (node, _, _, _) = InMemoryNodeInner::init( + Some(ForkDetails { + fork_source: Box::new(mock_db), + chain_id: TEST_NODE_NETWORK_ID.into(), + l1_block: L1BatchNumber(1), + l2_block: api::Block::default(), + l2_miniblock: 2, + l2_miniblock_hash: Default::default(), + block_timestamp: 1002, + overwrite_chain_id: None, + l1_gas_price: 1000, + l2_fair_gas_price: DEFAULT_L2_GAS_PRICE, + fair_pubdata_price: DEFAULT_FAIR_PUBDATA_PRICE, + 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(), + }), + TestNodeFeeInputProvider::default(), + Arc::new(RwLock::new(Default::default())), + TestNodeConfig::default(), + impersonation, + node.system_contracts.clone(), + ); + let mut node = node.write().await; + + let tx = TransactionBuilder::new().build(); + + let system_contracts = node + .system_contracts + .system_contracts_for_initiator(&node.impersonation, &tx.initiator_account()); + let (_, _, mut vm) = test_vm(&mut *node, system_contracts).await; + node.run_l2_tx_raw(tx, &mut vm) + .expect("transaction must pass with external storage"); + } + + #[tokio::test] + async fn test_transact_returns_data_in_built_in_without_security_mode() { + let inner = InMemoryNodeInner::test_config(TestNodeConfig { + system_contracts_options: SystemContractsOptions::BuiltInWithoutSecurity, + ..Default::default() + }); + let mut node = inner.write().await; + + let private_key = K256PrivateKey::from_bytes(H256::repeat_byte(0xef)).unwrap(); + let from_account = private_key.address(); + node.set_rich_account(from_account, U256::from(DEFAULT_ACCOUNT_BALANCE)); + + let deployed_address = deployed_address_create(from_account, U256::zero()); + node.deploy_contract( + H256::repeat_byte(0x1), + &private_key, + hex::decode(STORAGE_CONTRACT_BYTECODE).unwrap(), + None, + Nonce(0), + ) + .await; + + let mut tx = L2Tx::new_signed( + Some(deployed_address), + hex::decode("bbf55335").unwrap(), // keccak selector for "transact_retrieve1()" + Nonce(1), + Fee { + gas_limit: U256::from(4_000_000), + max_fee_per_gas: U256::from(250_000_000), + max_priority_fee_per_gas: U256::from(250_000_000), + gas_per_pubdata_limit: U256::from(50000), + }, + U256::from(0), + zksync_types::L2ChainId::from(260), + &private_key, + vec![], + Default::default(), + ) + .expect("failed signing tx"); + tx.common_data.transaction_type = TransactionType::LegacyTransaction; + tx.set_input(vec![], H256::repeat_byte(0x2)); + + let system_contracts = node + .system_contracts + .system_contracts_for_initiator(&node.impersonation, &tx.initiator_account()); + let (_, _, mut vm) = test_vm(&mut *node, system_contracts).await; + let TxExecutionOutput { result, .. } = node.run_l2_tx_raw(tx, &mut vm).expect("failed tx"); + + match result.result { + ExecutionResult::Success { output } => { + let actual = decode_tx_result(&output, ethabi::ParamType::Uint(256)); + let expected = Token::Uint(Uint::from(1024u64)); + assert_eq!(expected, actual, "invalid result"); + } + _ => panic!("invalid result {:?}", result.result), + } + } + + #[tokio::test] + async fn test_snapshot() { + let node = InMemoryNodeInner::test(); + let mut writer = node.write().await; + + { + let mut blockchain = writer.blockchain_writer.write().await; + blockchain + .blocks + .insert(H256::repeat_byte(0x1), Default::default()); + blockchain + .hashes + .insert(L2BlockNumber(1), H256::repeat_byte(0x1)); + blockchain.tx_results.insert( + H256::repeat_byte(0x1), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: Default::default(), + debug: testing::default_tx_debug_info(), + }, + ); + blockchain.current_batch = L1BatchNumber(1); + blockchain.current_block = L2BlockNumber(1); + blockchain.current_block_hash = H256::repeat_byte(0x1); + } + writer.time_writer.set_current_timestamp_unchecked(1); + writer + .filters + .write() + .await + .add_block_filter() + .expect("failed adding block filter"); + writer.impersonation.impersonate(H160::repeat_byte(0x1)); + writer.rich_accounts.insert(H160::repeat_byte(0x1)); + writer + .previous_states + .insert(H256::repeat_byte(0x1), Default::default()); + writer.fork_storage.set_value( + StorageKey::new(AccountTreeId::new(H160::repeat_byte(0x1)), H256::zero()), + H256::repeat_byte(0x1), + ); + + let storage = writer.fork_storage.inner.read().unwrap(); + let blockchain = writer.blockchain_writer.read().await; + let expected_snapshot = Snapshot { + current_batch: blockchain.current_batch, + current_block: blockchain.current_block, + current_block_hash: blockchain.current_block_hash, + fee_input_provider: writer.fee_input_provider.clone(), + tx_results: blockchain.tx_results.clone(), + blocks: blockchain.blocks.clone(), + hashes: blockchain.hashes.clone(), + filters: writer.filters.read().await.clone(), + impersonation_state: writer.impersonation.state(), + rich_accounts: writer.rich_accounts.clone(), + previous_states: writer.previous_states.clone(), + raw_storage: storage.raw_storage.clone(), + value_read_cache: storage.value_read_cache.clone(), + factory_dep_cache: storage.factory_dep_cache.clone(), + }; + drop(blockchain); + let actual_snapshot = writer.snapshot().await.expect("failed taking snapshot"); + + assert_eq!( + expected_snapshot.current_batch, + actual_snapshot.current_batch + ); + assert_eq!( + expected_snapshot.current_block, + actual_snapshot.current_block + ); + assert_eq!( + expected_snapshot.current_block_hash, + actual_snapshot.current_block_hash + ); + assert_eq!( + expected_snapshot.fee_input_provider, + actual_snapshot.fee_input_provider + ); + assert_eq!( + expected_snapshot.tx_results.keys().collect_vec(), + actual_snapshot.tx_results.keys().collect_vec() + ); + assert_eq!(expected_snapshot.blocks, actual_snapshot.blocks); + assert_eq!(expected_snapshot.hashes, actual_snapshot.hashes); + assert_eq!(expected_snapshot.filters, actual_snapshot.filters); + assert_eq!( + expected_snapshot.impersonation_state, + actual_snapshot.impersonation_state + ); + assert_eq!( + expected_snapshot.rich_accounts, + actual_snapshot.rich_accounts + ); + assert_eq!( + expected_snapshot.previous_states, + actual_snapshot.previous_states + ); + assert_eq!(expected_snapshot.raw_storage, actual_snapshot.raw_storage); + assert_eq!( + expected_snapshot.value_read_cache, + actual_snapshot.value_read_cache + ); + assert_eq!( + expected_snapshot.factory_dep_cache, + actual_snapshot.factory_dep_cache + ); + } + + #[tokio::test] + async fn test_snapshot_restore() { + let node = InMemoryNodeInner::test(); + let mut writer = node.write().await; + + { + let mut blockchain = writer.blockchain_writer.write().await; + blockchain + .blocks + .insert(H256::repeat_byte(0x1), Default::default()); + blockchain + .hashes + .insert(L2BlockNumber(1), H256::repeat_byte(0x1)); + blockchain.tx_results.insert( + H256::repeat_byte(0x1), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: Default::default(), + debug: testing::default_tx_debug_info(), + }, + ); + blockchain.current_batch = L1BatchNumber(1); + blockchain.current_block = L2BlockNumber(1); + blockchain.current_block_hash = H256::repeat_byte(0x1); + } + writer.time_writer.set_current_timestamp_unchecked(1); + writer + .filters + .write() + .await + .add_block_filter() + .expect("failed adding block filter"); + writer.impersonation.impersonate(H160::repeat_byte(0x1)); + writer.rich_accounts.insert(H160::repeat_byte(0x1)); + writer + .previous_states + .insert(H256::repeat_byte(0x1), Default::default()); + writer.fork_storage.set_value( + StorageKey::new(AccountTreeId::new(H160::repeat_byte(0x1)), H256::zero()), + H256::repeat_byte(0x1), + ); + + let blockchain = writer.blockchain_writer.read().await; + let expected_snapshot = { + let storage = writer.fork_storage.inner.read().unwrap(); + Snapshot { + current_batch: blockchain.current_batch, + current_block: blockchain.current_block, + current_block_hash: blockchain.current_block_hash, + fee_input_provider: writer.fee_input_provider.clone(), + tx_results: blockchain.tx_results.clone(), + blocks: blockchain.blocks.clone(), + hashes: blockchain.hashes.clone(), + filters: writer.filters.read().await.clone(), + impersonation_state: writer.impersonation.state(), + rich_accounts: writer.rich_accounts.clone(), + previous_states: writer.previous_states.clone(), + raw_storage: storage.raw_storage.clone(), + value_read_cache: storage.value_read_cache.clone(), + factory_dep_cache: storage.factory_dep_cache.clone(), + } + }; + drop(blockchain); + + // snapshot and modify node state + let snapshot = writer.snapshot().await.expect("failed taking snapshot"); + + { + let mut blockchain = writer.blockchain_writer.write().await; + blockchain + .blocks + .insert(H256::repeat_byte(0x2), Default::default()); + blockchain + .hashes + .insert(L2BlockNumber(2), H256::repeat_byte(0x2)); + blockchain.tx_results.insert( + H256::repeat_byte(0x2), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: Default::default(), + debug: testing::default_tx_debug_info(), + }, + ); + blockchain.current_batch = L1BatchNumber(2); + blockchain.current_block = L2BlockNumber(2); + blockchain.current_block_hash = H256::repeat_byte(0x2); + } + writer.time_writer.set_current_timestamp_unchecked(2); + writer + .filters + .write() + .await + .add_pending_transaction_filter() + .expect("failed adding pending transaction filter"); + writer.impersonation.impersonate(H160::repeat_byte(0x2)); + writer.rich_accounts.insert(H160::repeat_byte(0x2)); + writer + .previous_states + .insert(H256::repeat_byte(0x2), Default::default()); + writer.fork_storage.set_value( + StorageKey::new(AccountTreeId::new(H160::repeat_byte(0x2)), H256::zero()), + H256::repeat_byte(0x2), + ); + + // restore + writer + .restore_snapshot(snapshot) + .await + .expect("failed restoring snapshot"); + + let storage = writer.fork_storage.inner.read().unwrap(); + let blockchain = writer.blockchain_writer.read().await; + assert_eq!(expected_snapshot.current_batch, blockchain.current_batch); + assert_eq!(expected_snapshot.current_block, blockchain.current_block); + assert_eq!( + expected_snapshot.current_block_hash, + blockchain.current_block_hash + ); + + assert_eq!( + expected_snapshot.fee_input_provider, + writer.fee_input_provider + ); + assert_eq!( + expected_snapshot.tx_results.keys().collect_vec(), + blockchain.tx_results.keys().collect_vec() + ); + assert_eq!(expected_snapshot.blocks, blockchain.blocks); + assert_eq!(expected_snapshot.hashes, blockchain.hashes); + assert_eq!(expected_snapshot.filters, *writer.filters.read().await); + assert_eq!( + expected_snapshot.impersonation_state, + writer.impersonation.state() + ); + assert_eq!(expected_snapshot.rich_accounts, writer.rich_accounts); + assert_eq!(expected_snapshot.previous_states, writer.previous_states); + assert_eq!(expected_snapshot.raw_storage, storage.raw_storage); + assert_eq!(expected_snapshot.value_read_cache, storage.value_read_cache); + assert_eq!( + expected_snapshot.factory_dep_cache, + storage.factory_dep_cache + ); + } +} diff --git a/crates/core/src/node/inner/mod.rs b/crates/core/src/node/inner/mod.rs new file mode 100644 index 00000000..d9bbc723 --- /dev/null +++ b/crates/core/src/node/inner/mod.rs @@ -0,0 +1,77 @@ +//! This module encapsulates mutable parts of the system and provides read-only views on various +//! components of the system's state (e.g. time, storage, blocks). It is still possible to mutate +//! the state outside of this module but only through [`InMemoryNodeInner`]'s public high-level +//! methods. +//! +//! The idea behind this is being able to read current time to answer API requests while a lock on +//! [`InMemoryNodeInner`] is being held for block production. At the same time it is impossible to +//! advance the time without holding a lock to [`InMemoryNodeInner`]. +//! +//! FIXME: The above is not 100% true yet (there are some internal parts of InMemoryNodeInner that +//! are available outside of this module) +pub mod blockchain; +pub mod fork; +mod in_memory_inner; +pub mod node_executor; +pub mod time; + +pub use in_memory_inner::{InMemoryNodeInner, TxExecutionOutput}; + +use crate::filters::EthFilters; +use crate::node::{ImpersonationManager, TestNodeFeeInputProvider}; +use crate::system_contracts::SystemContracts; +use anvil_zksync_config::constants::NON_FORK_FIRST_BLOCK_TIMESTAMP; +use anvil_zksync_config::TestNodeConfig; +use blockchain::BlockchainReader; +use fork::{ForkDetails, ForkStorage}; +use std::sync::Arc; +use time::TimeReader; +use tokio::sync::RwLock; + +impl InMemoryNodeInner { + // TODO: Bake in Arc> into the struct itself + pub fn init( + fork: Option, + fee_input_provider: TestNodeFeeInputProvider, + filters: Arc>, + config: TestNodeConfig, + impersonation: ImpersonationManager, + system_contracts: SystemContracts, + ) -> (Arc>, ForkStorage, BlockchainReader, TimeReader) { + let (time, time_writer) = TimeReader::new( + fork.as_ref() + .map(|f| f.block_timestamp) + .unwrap_or(NON_FORK_FIRST_BLOCK_TIMESTAMP), + ); + let (blockchain, blockchain_storage) = BlockchainReader::new( + fork.as_ref(), + config.genesis.as_ref(), + config.genesis_timestamp, + ); + // TODO: Create read-only/mutable versions of `ForkStorage` like `blockchain` and `time` above + let fork_storage = ForkStorage::new( + fork, + &config.system_contracts_options, + config.use_evm_emulator, + config.chain_id, + ); + + let node_inner = InMemoryNodeInner::new( + blockchain_storage, + time_writer, + fork_storage.clone(), + fee_input_provider.clone(), + filters, + config.clone(), + impersonation.clone(), + system_contracts.clone(), + ); + + ( + Arc::new(RwLock::new(node_inner)), + fork_storage, + blockchain, + time, + ) + } +} diff --git a/crates/core/src/node/inner/node_executor.rs b/crates/core/src/node/inner/node_executor.rs new file mode 100644 index 00000000..4baa748b --- /dev/null +++ b/crates/core/src/node/inner/node_executor.rs @@ -0,0 +1,449 @@ +use super::InMemoryNodeInner; +use crate::node::pool::TxBatch; +use crate::system_contracts::SystemContracts; +use futures::future::BoxFuture; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use tokio::sync::{mpsc, oneshot, RwLock}; +use zksync_contracts::BaseSystemContracts; +use zksync_multivm::interface::TxExecutionMode; +use zksync_types::l2::L2Tx; +use zksync_types::L2BlockNumber; + +#[pin_project::pin_project] +pub struct NodeExecutor { + node_inner: Arc>, + system_contracts: SystemContracts, + command_receiver: mpsc::Receiver, + /// Future that is processing the next command + #[pin] + future: Option>, +} + +impl NodeExecutor { + pub fn new( + node_inner: Arc>, + system_contracts: SystemContracts, + ) -> (Self, NodeExecutorHandle) { + let (command_sender, command_receiver) = mpsc::channel(128); + let this = Self { + node_inner, + system_contracts, + command_receiver, + future: None, + }; + let handle = NodeExecutorHandle { command_sender }; + (this, handle) + } +} + +impl NodeExecutor { + async fn seal_block( + node_inner: Arc>, + txs: Vec, + base_system_contracts: BaseSystemContracts, + reply: Option>>, + ) { + let result = node_inner + .write() + .await + .seal_block(txs, base_system_contracts) + .await; + // Reply to sender if we can, otherwise hold result for further processing + let result = if let Some(reply) = reply { + if let Err(result) = reply.send(result) { + tracing::info!("failed to reply as receiver has been dropped"); + result + } else { + return; + } + } else { + result + }; + // Not much we can do with an error at this level so we just print it + if let Err(err) = result { + tracing::error!("failed to seal a block: {:#?}", err); + } + } + + async fn seal_blocks( + node_inner: Arc>, + tx_batches: Vec, + interval: u64, + system_contracts: SystemContracts, + reply: oneshot::Sender>>, + ) { + let mut node_inner = node_inner.write().await; + + // Save old interval to restore later: it might get replaced with `interval` below + let old_interval = node_inner.time_writer.get_block_timestamp_interval(); + let result = async { + let mut block_numbers = Vec::with_capacity(tx_batches.len()); + // Processing the entire vector is essentially atomic here because `NodeExecutor` is + // the only component that seals blocks. + for (i, TxBatch { txs, impersonating }) in tx_batches.into_iter().enumerate() { + // Enforce provided interval starting from the second block (i.e. first block should + // use the existing interval). + if i == 1 { + node_inner + .time_writer + .set_block_timestamp_interval(Some(interval)); + } + let base_system_contracts = system_contracts + .contracts(TxExecutionMode::VerifyExecute, impersonating) + .clone(); + let number = node_inner.seal_block(txs, base_system_contracts).await?; + block_numbers.push(number); + } + anyhow::Ok(block_numbers) + } + .await; + // Restore old interval + node_inner + .time_writer + .set_block_timestamp_interval(old_interval); + + // Reply to sender if we can, otherwise hold result for further processing + let result = if let Err(result) = reply.send(result) { + tracing::info!("failed to reply as receiver has been dropped"); + result + } else { + return; + }; + // Not much we can do with an error at this level so we just print it + if let Err(err) = result { + tracing::error!("failed to seal blocks: {:#?}", err); + } + } + + async fn increase_time( + node_inner: Arc>, + delta: u64, + reply: oneshot::Sender<()>, + ) { + node_inner.write().await.time_writer.increase_time(delta); + // Reply to sender if we can + if reply.send(()).is_err() { + tracing::info!("failed to reply as receiver has been dropped"); + } + } + + async fn enforce_next_timestamp( + node_inner: Arc>, + timestamp: u64, + reply: oneshot::Sender>, + ) { + let result = node_inner + .write() + .await + .time_writer + .enforce_next_timestamp(timestamp); + // Reply to sender if we can, otherwise hold result for further processing + let result = if let Err(result) = reply.send(result) { + tracing::info!("failed to reply as receiver has been dropped"); + result + } else { + return; + }; + // Not much we can do with an error at this level so we just print it + if let Err(err) = result { + tracing::error!("failed to enforce next timestamp: {:#?}", err); + } + } + + async fn set_current_timestamp( + node_inner: Arc>, + timestamp: u64, + reply: oneshot::Sender, + ) { + let result = node_inner + .write() + .await + .time_writer + .set_current_timestamp_unchecked(timestamp); + // Reply to sender if we can + if reply.send(result).is_err() { + tracing::info!("failed to reply as receiver has been dropped"); + } + } + + async fn set_timestamp_interval(node_inner: Arc>, delta: u64) { + node_inner + .write() + .await + .time_writer + .set_block_timestamp_interval(Some(delta)); + } + + async fn remove_timestamp_interval( + node_inner: Arc>, + reply: oneshot::Sender, + ) { + let result = node_inner + .write() + .await + .time_writer + .remove_block_timestamp_interval(); + // Reply to sender if we can + if reply.send(result).is_err() { + tracing::info!("failed to reply as receiver has been dropped"); + } + } +} + +impl Future for NodeExecutor { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + + if this.future.is_none() { + let command_opt = futures::ready!(this.command_receiver.poll_recv(cx)); + let Some(command) = command_opt else { + tracing::trace!("channel has been closed; stopping node executor"); + return Poll::Ready(()); + }; + match command { + Command::SealBlock(tx_batch, reply) => { + let TxBatch { impersonating, txs } = tx_batch; + + let base_system_contracts = this + .system_contracts + .contracts(TxExecutionMode::VerifyExecute, impersonating) + .clone(); + let node_inner = this.node_inner.clone(); + *this.future = Some(Box::pin(Self::seal_block( + node_inner, + txs, + base_system_contracts, + reply, + ))); + } + Command::SealBlocks(tx_batches, interval, reply) => { + let node_inner = this.node_inner.clone(); + let system_contracts = this.system_contracts.clone(); + *this.future = Some(Box::pin(Self::seal_blocks( + node_inner, + tx_batches, + interval, + system_contracts, + reply, + ))); + } + Command::IncreaseTime(delta, reply) => { + let node_inner = this.node_inner.clone(); + *this.future = Some(Box::pin(Self::increase_time(node_inner, delta, reply))); + } + Command::EnforceNextTimestamp(timestamp, reply) => { + let node_inner = this.node_inner.clone(); + *this.future = Some(Box::pin(Self::enforce_next_timestamp( + node_inner, timestamp, reply, + ))); + } + Command::SetCurrentTimestamp(timestamp, reply) => { + let node_inner = this.node_inner.clone(); + *this.future = Some(Box::pin(Self::set_current_timestamp( + node_inner, timestamp, reply, + ))); + } + Command::SetTimestampInterval(seconds) => { + let node_inner = this.node_inner.clone(); + *this.future = + Some(Box::pin(Self::set_timestamp_interval(node_inner, seconds))); + } + Command::RemoveTimestampInterval(reply) => { + let node_inner = this.node_inner.clone(); + *this.future = + Some(Box::pin(Self::remove_timestamp_interval(node_inner, reply))); + } + } + } + + if let Some(future) = this.future.as_mut().as_pin_mut() { + // Clear pending future if it completed + if let Poll::Ready(()) = future.poll(cx) { + *this.future = None; + // Wake yourself up as we might have some unprocessed commands left + cx.waker().wake_by_ref(); + } + Poll::Pending + } else { + Poll::Pending + } + } +} + +#[derive(Clone, Debug)] +pub struct NodeExecutorHandle { + command_sender: mpsc::Sender, +} + +impl NodeExecutorHandle { + /// Request [`NodeExecutor`] to seal a new block from the provided transaction batch. Does not + /// wait for the block to actually be produced. + /// + /// It is sender's responsibility to make sure [`TxBatch`] is constructed correctly (see its + /// docs). + pub async fn seal_block( + &self, + tx_batch: TxBatch, + ) -> Result<(), mpsc::error::SendError> { + self.command_sender + .send(Command::SealBlock(tx_batch, None)) + .await + } + + /// Request [`NodeExecutor`] to seal a new block from the provided transaction batch. Waits for + /// the block to be produced and returns its number. + /// + /// It is sender's responsibility to make sure [`TxBatch`] is constructed correctly (see its + /// docs). + pub async fn seal_block_sync(&self, tx_batch: TxBatch) -> anyhow::Result { + let (response_sender, response_receiver) = oneshot::channel(); + self.command_sender + .send(Command::SealBlock(tx_batch, Some(response_sender))) + .await + .map_err(|_| anyhow::anyhow!("failed to seal a block as node executor is dropped"))?; + + match response_receiver.await { + Ok(result) => result, + Err(_) => anyhow::bail!("failed to seal a block as node executor is dropped"), + } + } + + /// Request [`NodeExecutor`] to seal multiple blocks from the provided transaction batches with + /// `interval` seconds in-between of two consecutive blocks. + /// Waits for the blocks to be produced and returns their numbers. + /// + /// Guarantees that the resulting block numbers will be sequential (i.e. no other blocks can + /// be produced in-between). + /// + /// It is sender's responsibility to make sure [`TxBatch`]es are constructed correctly (see + /// docs). + pub async fn seal_blocks_sync( + &self, + tx_batches: Vec, + interval: u64, + ) -> anyhow::Result> { + let (response_sender, response_receiver) = oneshot::channel(); + self.command_sender + .send(Command::SealBlocks(tx_batches, interval, response_sender)) + .await + .map_err(|_| anyhow::anyhow!("failed to seal a block as node executor is dropped"))?; + + match response_receiver.await { + Ok(result) => result, + Err(_) => anyhow::bail!("failed to seal a block as node executor is dropped"), + } + } + + /// Request [`NodeExecutor`] to increase time by the given delta (in seconds). Waits for the + /// change to take place. + pub async fn increase_time_sync(&self, delta: u64) -> anyhow::Result<()> { + let (response_sender, response_receiver) = oneshot::channel(); + self.command_sender + .send(Command::IncreaseTime(delta, response_sender)) + .await + .map_err(|_| anyhow::anyhow!("failed to increase time as node executor is dropped"))?; + match response_receiver.await { + Ok(()) => Ok(()), + Err(_) => { + anyhow::bail!("failed to increase time as node executor is dropped") + } + } + } + + /// Request [`NodeExecutor`] to enforce next block's timestamp (in seconds). Waits for the + /// timestamp validity to be confirmed. Block might still not be produced by then. + pub async fn enforce_next_timestamp_sync(&self, timestamp: u64) -> anyhow::Result<()> { + let (response_sender, response_receiver) = oneshot::channel(); + self.command_sender + .send(Command::EnforceNextTimestamp(timestamp, response_sender)) + .await + .map_err(|_| { + anyhow::anyhow!("failed to enforce next timestamp as node executor is dropped") + })?; + match response_receiver.await { + Ok(result) => result, + Err(_) => { + anyhow::bail!("failed to enforce next timestamp as node executor is dropped") + } + } + } + + /// Request [`NodeExecutor`] to set current timestamp (in seconds). Waits for the + /// change to take place. + pub async fn set_current_timestamp_sync(&self, timestamp: u64) -> anyhow::Result { + let (response_sender, response_receiver) = oneshot::channel(); + self.command_sender + .send(Command::SetCurrentTimestamp(timestamp, response_sender)) + .await + .map_err(|_| { + anyhow::anyhow!("failed to set current timestamp as node executor is dropped") + })?; + + match response_receiver.await { + Ok(result) => Ok(result), + Err(_) => anyhow::bail!("failed to set current timestamp as node executor is dropped"), + } + } + + /// Request [`NodeExecutor`] to set block timestamp interval (in seconds). Does not wait for the + /// change to take place. + pub async fn set_block_timestamp_interval( + &self, + seconds: u64, + ) -> Result<(), mpsc::error::SendError> { + self.command_sender + .send(Command::SetTimestampInterval(seconds)) + .await + } + + /// Request [`NodeExecutor`] to remove block timestamp interval. Waits for the change to take + /// place. Returns `true` if an existing interval was removed, `false` otherwise. + pub async fn remove_block_timestamp_interval_sync(&self) -> anyhow::Result { + let (response_sender, response_receiver) = oneshot::channel(); + self.command_sender + .send(Command::RemoveTimestampInterval(response_sender)) + .await + .map_err(|_| { + anyhow::anyhow!("failed to remove block interval as node executor is dropped") + })?; + + match response_receiver.await { + Ok(result) => Ok(result), + Err(_) => anyhow::bail!("failed to remove block interval as node executor is dropped"), + } + } +} + +#[cfg(test)] +impl NodeExecutorHandle { + pub fn test() -> (Self, mpsc::Receiver) { + let (command_sender, command_receiver) = mpsc::channel(128); + (NodeExecutorHandle { command_sender }, command_receiver) + } +} + +#[derive(Debug)] +pub enum Command { + // Block sealing commands + SealBlock( + TxBatch, + Option>>, + ), + SealBlocks( + Vec, + u64, + oneshot::Sender>>, + ), + // Time manipulation commands. Caveat: reply-able commands can hold user connections alive for + // a long time (until the command is processed). + IncreaseTime(u64, oneshot::Sender<()>), + EnforceNextTimestamp(u64, oneshot::Sender>), + SetCurrentTimestamp(u64, oneshot::Sender), + SetTimestampInterval(u64), + RemoveTimestampInterval(oneshot::Sender), +} diff --git a/crates/core/src/node/inner/time.rs b/crates/core/src/node/inner/time.rs new file mode 100644 index 00000000..5777e239 --- /dev/null +++ b/crates/core/src/node/inner/time.rs @@ -0,0 +1,167 @@ +use anyhow::anyhow; +use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; + +/// Read-only view on system's time (in seconds). Clones always agree on the underlying time. +#[derive(Clone, Debug, Default)] +pub struct TimeReader { + internal: Arc>, +} + +impl TimeReader { + pub(super) fn new(current_timestamp: u64) -> (Self, TimeWriter) { + let internal = Arc::new(RwLock::new(TimestampManagerInternal { + current_timestamp, + next_timestamp: None, + interval: None, + })); + ( + Self { + internal: internal.clone(), + }, + TimeWriter { internal }, + ) + } + + fn get(&self) -> RwLockReadGuard { + self.internal + .read() + .expect("TimestampManager lock is poisoned") + } + + /// Returns timestamp (in seconds) that the clock is currently on. + pub fn current_timestamp(&self) -> u64 { + self.get().current_timestamp + } +} + +/// Exclusive access to mutable time (only supposed to be owned by [`super::InMemoryNodeInner`]). +#[derive(Debug)] +pub(super) struct TimeWriter { + internal: Arc>, +} + +impl TimeWriter { + fn get(&self) -> RwLockReadGuard { + self.internal + .read() + .expect("TimestampWriter lock is poisoned") + } + + fn get_mut(&self) -> RwLockWriteGuard { + self.internal + .write() + .expect("TimestampWriter lock is poisoned") + } + + /// Sets last used timestamp (in seconds) to the provided value and returns the difference + /// between new value and old value (represented as a signed number of seconds). + pub(super) fn set_current_timestamp_unchecked(&self, timestamp: u64) -> i128 { + let mut this = self.get_mut(); + let diff = (timestamp as i128).saturating_sub(this.current_timestamp as i128); + this.next_timestamp.take(); + this.current_timestamp = timestamp; + diff + } + + /// Forces clock to return provided value as the next timestamp. Time skip will not be performed + /// before the next invocation of `advance_timestamp`. + /// + /// Expects provided timestamp to be in the future, returns error otherwise. + pub(super) fn enforce_next_timestamp(&self, timestamp: u64) -> anyhow::Result<()> { + let mut this = self.get_mut(); + if timestamp <= this.current_timestamp { + Err(anyhow!( + "timestamp ({}) must be greater than the last used timestamp ({})", + timestamp, + this.current_timestamp + )) + } else { + this.next_timestamp.replace(timestamp); + Ok(()) + } + } + + /// Fast-forwards time by the given amount of seconds. + pub(super) fn increase_time(&self, seconds: u64) -> u64 { + let mut this = self.get_mut(); + let next = this.current_timestamp.saturating_add(seconds); + this.next_timestamp.take(); + this.current_timestamp = next; + next + } + + pub(super) fn get_block_timestamp_interval(&self) -> Option { + self.get().interval + } + + /// Sets an interval to use when computing the next timestamp + /// + /// If an interval already exists, this will update the interval, otherwise a new interval will + /// be set starting with the current timestamp. + pub(super) fn set_block_timestamp_interval(&self, seconds: Option) { + self.get_mut().interval = seconds; + } + + /// Removes the interval. Returns true if it existed before being removed, false otherwise. + pub(super) fn remove_block_timestamp_interval(&self) -> bool { + self.get_mut().interval.take().is_some() + } + + /// Returns timestamp (in seconds) that the clock is currently on. + pub(super) fn current_timestamp(&self) -> u64 { + self.get().current_timestamp + } + + /// Peek at what the next call to `advance_timestamp` will return. + pub(super) fn peek_next_timestamp(&self) -> u64 { + let internal = self.get(); + internal.next_timestamp.unwrap_or_else(|| { + internal + .current_timestamp + .saturating_add(internal.interval()) + }) + } + + /// Advances clock to the next timestamp and returns that timestamp in seconds. + /// + /// Subsequent calls to this method return monotonically increasing values. Time difference + /// between calls is implementation-specific. + pub(super) fn advance_timestamp(&self) -> u64 { + let mut internal = self.get_mut(); + let next_timestamp = match internal.next_timestamp.take() { + Some(next_timestamp) => next_timestamp, + None => internal + .current_timestamp + .saturating_add(internal.interval()), + }; + + internal.current_timestamp = next_timestamp; + next_timestamp + } + + /// Reset current timestamp to the provided value. WARNING: Moving clock to the past can cause + /// unexpected behavior. + pub(super) fn reset_to(&self, timestamp: u64) { + let mut internal = self.get_mut(); + internal.next_timestamp.take(); + internal.current_timestamp = timestamp; + } +} + +#[derive(Debug, Default)] +struct TimestampManagerInternal { + /// The current timestamp (in seconds). This timestamp is considered to be used already: there + /// might be a logical event that already happened on that timestamp (e.g. a block was sealed + /// with this timestamp). + current_timestamp: u64, + /// The next timestamp (in seconds) that the clock will be forced to advance to. + next_timestamp: Option, + /// The interval to use when determining the next timestamp to advance to. + interval: Option, +} + +impl TimestampManagerInternal { + fn interval(&self) -> u64 { + self.interval.unwrap_or(1) + } +} diff --git a/crates/core/src/node/mod.rs b/crates/core/src/node/mod.rs index 08bcc01e..5201158f 100644 --- a/crates/core/src/node/mod.rs +++ b/crates/core/src/node/mod.rs @@ -1,6 +1,5 @@ //! anvil-zksync, that supports forking other networks. -mod block_producer; mod call_error_tracer; mod debug; pub mod error; @@ -9,16 +8,18 @@ mod fee_model; mod impersonate; mod in_memory; mod in_memory_ext; +mod inner; mod pool; mod sealer; mod state; mod storage_logs; -mod time; mod zks; pub use self::{ - block_producer::BlockProducer, impersonate::ImpersonationManager, pool::TxPool, - sealer::BlockSealer, sealer::BlockSealerMode, time::TimestampManager, + fee_model::TestNodeFeeInputProvider, impersonate::ImpersonationManager, + node_executor::NodeExecutor, pool::TxPool, sealer::BlockSealer, sealer::BlockSealerMode, + state::VersionedState, }; pub use in_memory::*; -pub use state::VersionedState; +pub use inner::{blockchain, fork, node_executor, time}; +pub use inner::{InMemoryNodeInner, TxExecutionOutput}; diff --git a/crates/core/src/node/pool.rs b/crates/core/src/node/pool.rs index c7ca7f26..626c5f6c 100644 --- a/crates/core/src/node/pool.rs +++ b/crates/core/src/node/pool.rs @@ -199,7 +199,18 @@ impl TxPool { } } -/// A batch of transactions sharing the same impersonation status. +/// A batch of transactions meant to be sealed as a block. All transactions in the batch share the +/// same impersonation status on the moment of the batch's creation. +/// +/// A block produced from this batch is guaranteed to: +/// * Not contain any transactions outside of this transaction batch +/// * Use contracts matching `impersonating` mode of this transaction batch. +/// +/// Potential caveats: +/// * The impersonation status of transactions' initiators (as defined by [`ImpersonationManager`]) +/// is not guaranteed to be the same by the time the batch gets executed +/// * The resulting block is not guaranteed to contain all transactions as some of them could be +/// non-executable. #[derive(PartialEq, Debug)] pub struct TxBatch { pub impersonating: bool, diff --git a/crates/core/src/node/sealer.rs b/crates/core/src/node/sealer.rs index 589c5ede..b7ed7615 100644 --- a/crates/core/src/node/sealer.rs +++ b/crates/core/src/node/sealer.rs @@ -1,31 +1,115 @@ -use crate::node::pool::{TxBatch, TxPool}; +use super::inner::node_executor::{Command, NodeExecutorHandle}; +use super::pool::{TxBatch, TxPool}; use futures::channel::mpsc::Receiver; +use futures::future::BoxFuture; use futures::stream::{Fuse, StreamExt}; use futures::task::AtomicWaker; use futures::Stream; +use std::future::Future; use std::pin::Pin; use std::sync::{Arc, RwLock}; use std::task::{Context, Poll}; use std::time::Duration; +use tokio::sync::mpsc; use tokio::time::{Interval, MissedTickBehavior}; use zksync_types::H256; -#[derive(Clone, Debug)] +// TODO: `BlockSealer` is probably a bad name as this doesn't actually seal blocks, just decides +// that certain tx batch needs to be sealed. The actual sealing is handled in `NodeExecutor`. +// Consider renaming. +#[pin_project::pin_project] pub struct BlockSealer { - /// The mode this sealer currently operates in - mode: Arc>, - /// Used for task wake up when the sealing mode was forcefully changed - waker: Arc, + /// Block sealer state (externally mutable). + state: BlockSealerState, + /// Pool where block sealer is sourcing transactions from. + pool: TxPool, + /// Node handle to be used when a block needs to be sealed. + node_handle: NodeExecutorHandle, + /// Future that is sending the next seal command to [`super::NodeExecutor`] + #[pin] + future: Option>>>, } impl BlockSealer { - pub fn new(mode: BlockSealerMode) -> Self { - Self { + pub fn new( + mode: BlockSealerMode, + pool: TxPool, + node_handle: NodeExecutorHandle, + ) -> (Self, BlockSealerState) { + let state = BlockSealerState { mode: Arc::new(RwLock::new(mode)), waker: Arc::new(AtomicWaker::new()), + }; + ( + Self { + state: state.clone(), + pool, + node_handle, + future: None, + }, + state, + ) + } +} + +impl Future for BlockSealer { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + this.state.waker.register(cx.waker()); + if this.future.is_none() { + tracing::debug!("no pending messages to node executor, polling for a new tx batch"); + let mut mode = this + .state + .mode + .write() + .expect("BlockSealer lock is poisoned"); + let tx_batch = futures::ready!(match &mut *mode { + BlockSealerMode::Noop => Poll::Pending, + BlockSealerMode::Immediate(immediate) => immediate.poll(this.pool, cx), + BlockSealerMode::FixedTime(fixed) => fixed.poll(this.pool, cx), + }); + tracing::debug!( + impersonating = tx_batch.impersonating, + txs = tx_batch.txs.len(), + "new tx batch found" + ); + let handle = this.node_handle.clone(); + *this.future = Some(Box::pin(async move { handle.seal_block(tx_batch).await })); + } + + if let Some(future) = this.future.as_mut().as_pin_mut() { + match futures::ready!(future.poll(cx)) { + Ok(()) => { + // Clear pending future if it completed successfully + *this.future = None; + // Wake yourself up as we might have some unprocessed txs in the pool left + cx.waker().wake_by_ref(); + Poll::Pending + } + Err(_) => { + tracing::error!( + "failed to seal a block as node executor is dropped; shutting down" + ); + Poll::Ready(()) + } + } + } else { + Poll::Pending } } +} +#[derive(Clone, Debug)] +pub struct BlockSealerState { + /// The mode this sealer currently operates in + mode: Arc>, + /// Used for task wake up when the sealing mode was forcefully changed + waker: Arc, +} + +impl BlockSealerState { pub fn is_immediate(&self) -> bool { matches!( *self.mode.read().expect("BlockSealer lock is poisoned"), @@ -38,16 +122,6 @@ impl BlockSealer { // Notify last used waker that the mode might have changed self.waker.wake(); } - - pub fn poll(&mut self, pool: &TxPool, cx: &mut Context<'_>) -> Poll { - self.waker.register(cx.waker()); - let mut mode = self.mode.write().expect("BlockSealer lock is poisoned"); - match &mut *mode { - BlockSealerMode::Noop => Poll::Pending, - BlockSealerMode::Immediate(immediate) => immediate.poll(pool, cx), - BlockSealerMode::FixedTime(fixed) => fixed.poll(pool, cx), - } - } } /// Represents different modes of block sealing available on the node @@ -151,108 +225,163 @@ impl FixedTimeBlockSealer { #[cfg(test)] mod tests { + use crate::node::node_executor::{Command, NodeExecutorHandle}; use crate::node::pool::TxBatch; use crate::node::sealer::BlockSealerMode; use crate::node::{BlockSealer, ImpersonationManager, TxPool}; use anvil_zksync_types::TransactionOrder; - use std::ptr; - use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + use backon::Retryable; + use backon::{ConstantBuilder, ExponentialBuilder}; + use std::sync::Arc; use std::time::Duration; + use tokio::sync::mpsc::error::TryRecvError; + use tokio::sync::{mpsc, RwLock}; + use tokio::task::JoinHandle; - const NOOP: RawWaker = { - const VTABLE: RawWakerVTable = RawWakerVTable::new( - // Cloning just returns a new no-op raw waker - |_| NOOP, - // `wake` does nothing - |_| {}, - // `wake_by_ref` does nothing - |_| {}, - // Dropping does nothing as we don't allocate anything - |_| {}, - ); - RawWaker::new(ptr::null(), &VTABLE) - }; - const WAKER_NOOP: Waker = unsafe { Waker::from_raw(NOOP) }; - - #[test] - fn immediate_empty() { - let pool = TxPool::new(ImpersonationManager::default(), TransactionOrder::Fifo); - let mut block_sealer = - BlockSealer::new(BlockSealerMode::immediate(1000, pool.add_tx_listener())); - let waker = &WAKER_NOOP; - let mut cx = Context::from_waker(waker); - - assert_eq!(block_sealer.poll(&pool, &mut cx), Poll::Pending); + struct Tester { + _handle: JoinHandle<()>, + receiver: Arc>>, } - #[test] - fn immediate_one_tx() { - let pool = TxPool::new(ImpersonationManager::default(), TransactionOrder::Fifo); - let mut block_sealer = - BlockSealer::new(BlockSealerMode::immediate(1000, pool.add_tx_listener())); - let waker = &WAKER_NOOP; - let mut cx = Context::from_waker(waker); + impl Tester { + fn new(sealer_mode_fn: impl FnOnce(&TxPool) -> BlockSealerMode) -> (Self, TxPool) { + let (node_handle, receiver) = NodeExecutorHandle::test(); + let pool = TxPool::new(ImpersonationManager::default(), TransactionOrder::Fifo); + let (block_sealer, _) = + BlockSealer::new(sealer_mode_fn(&pool), pool.clone(), node_handle); + let _handle = tokio::spawn(block_sealer); + let receiver = Arc::new(RwLock::new(receiver)); - let [tx] = pool.populate::<1>(); + (Self { _handle, receiver }, pool) + } - assert_eq!( - block_sealer.poll(&pool, &mut cx), - Poll::Ready(TxBatch { + async fn recv(&self) -> anyhow::Result { + let mut receiver = self.receiver.write().await; + tokio::time::timeout(Duration::from_millis(100), receiver.recv()) + .await + .map_err(|_| anyhow::anyhow!("no command received")) + .and_then(|res| res.ok_or(anyhow::anyhow!("disconnected"))) + } + + async fn expect_tx_batch(&self, expected_tx_batch: TxBatch) -> anyhow::Result<()> { + let command = (|| self.recv()) + .retry(ExponentialBuilder::default()) + .await?; + match command { + Command::SealBlock(actual_tx_batch, _) if actual_tx_batch == expected_tx_batch => { + Ok(()) + } + _ => anyhow::bail!("unexpected command: {:?}", command), + } + } + + /// Assert that the next command is sealing provided tx batch. Unlike `expect_tx_batch` + /// this method does not retry. + async fn expect_immediate_tx_batch( + &self, + expected_tx_batch: TxBatch, + ) -> anyhow::Result<()> { + let result = self.receiver.write().await.try_recv(); + match result { + Ok(Command::SealBlock(actual_tx_batch, _)) + if actual_tx_batch == expected_tx_batch => + { + Ok(()) + } + Ok(command) => anyhow::bail!("unexpected command: {:?}", command), + Err(TryRecvError::Empty) => anyhow::bail!("no command received"), + Err(TryRecvError::Disconnected) => anyhow::bail!("disconnected"), + } + } + + async fn expect_no_tx_batch(&self) -> anyhow::Result<()> { + let result = (|| self.recv()) + .retry( + ConstantBuilder::default() + .with_delay(Duration::from_millis(100)) + .with_max_times(3), + ) + .await; + match result { + Ok(command) => { + anyhow::bail!("unexpected command: {:?}", command) + } + Err(err) if err.to_string().contains("no command received") => Ok(()), + Err(err) => anyhow::bail!("unexpected error: {:?}", err), + } + } + + /// Assert that there are no command currently in receiver queue. Unlike `expect_no_tx_batch` + /// this method does not retry. + async fn expect_no_immediate_tx_batch(&self) -> anyhow::Result<()> { + let result = self.receiver.write().await.try_recv(); + match result { + Ok(command) => { + anyhow::bail!("unexpected command: {:?}", command) + } + Err(TryRecvError::Empty) => Ok(()), + Err(TryRecvError::Disconnected) => anyhow::bail!("disconnected"), + } + } + } + + #[tokio::test] + async fn immediate_empty() -> anyhow::Result<()> { + let (tester, _pool) = + Tester::new(|pool| BlockSealerMode::immediate(1000, pool.add_tx_listener())); + + tester.expect_no_tx_batch().await + } + + #[tokio::test] + async fn immediate_one_tx() -> anyhow::Result<()> { + let (tester, pool) = + Tester::new(|pool| BlockSealerMode::immediate(1000, pool.add_tx_listener())); + + let [tx] = pool.populate::<1>(); + tester + .expect_tx_batch(TxBatch { impersonating: false, - txs: vec![tx] + txs: vec![tx], }) - ); - assert_eq!(block_sealer.poll(&pool, &mut cx), Poll::Pending); + .await } - #[test] - fn immediate_several_txs() { - let pool = TxPool::new(ImpersonationManager::default(), TransactionOrder::Fifo); - let mut block_sealer = - BlockSealer::new(BlockSealerMode::immediate(1000, pool.add_tx_listener())); - let waker = &WAKER_NOOP; - let mut cx = Context::from_waker(waker); + #[tokio::test] + async fn immediate_several_txs() -> anyhow::Result<()> { + let (tester, pool) = + Tester::new(|pool| BlockSealerMode::immediate(1000, pool.add_tx_listener())); let txs = pool.populate::<10>(); - - assert_eq!( - block_sealer.poll(&pool, &mut cx), - Poll::Ready(TxBatch { + tester + .expect_tx_batch(TxBatch { impersonating: false, - txs: txs.to_vec() + txs: txs.to_vec(), }) - ); - assert_eq!(block_sealer.poll(&pool, &mut cx), Poll::Pending); + .await } - #[test] - fn immediate_respect_max_txs() { - let pool = TxPool::new(ImpersonationManager::default(), TransactionOrder::Fifo); - let mut block_sealer = - BlockSealer::new(BlockSealerMode::immediate(3, pool.add_tx_listener())); - let waker = &WAKER_NOOP; - let mut cx = Context::from_waker(waker); + #[tokio::test] + async fn immediate_respect_max_txs() -> anyhow::Result<()> { + let (tester, pool) = + Tester::new(|pool| BlockSealerMode::immediate(3, pool.add_tx_listener())); let txs = pool.populate::<10>(); - for txs in txs.chunks(3) { - assert_eq!( - block_sealer.poll(&pool, &mut cx), - Poll::Ready(TxBatch { + tester + .expect_tx_batch(TxBatch { impersonating: false, - txs: txs.to_vec() + txs: txs.to_vec(), }) - ); + .await?; } + Ok(()) } - #[test] - fn immediate_gradual_txs() { - let pool = TxPool::new(ImpersonationManager::default(), TransactionOrder::Fifo); - let mut block_sealer = - BlockSealer::new(BlockSealerMode::immediate(1000, pool.add_tx_listener())); - let waker = &WAKER_NOOP; - let mut cx = Context::from_waker(waker); + #[tokio::test] + async fn immediate_gradual_txs() -> anyhow::Result<()> { + let (tester, pool) = + Tester::new(|pool| BlockSealerMode::immediate(1000, pool.add_tx_listener())); // Txs are added to the pool in small chunks let txs0 = pool.populate::<3>(); @@ -263,108 +392,82 @@ mod tests { txs.extend(txs1); txs.extend(txs2); - assert_eq!( - block_sealer.poll(&pool, &mut cx), - Poll::Ready(TxBatch { + tester + .expect_tx_batch(TxBatch { impersonating: false, txs, }) - ); - assert_eq!(block_sealer.poll(&pool, &mut cx), Poll::Pending); + .await?; // Txs added after the first poll should be available for sealing let txs = pool.populate::<10>().to_vec(); - assert_eq!( - block_sealer.poll(&pool, &mut cx), - Poll::Ready(TxBatch { + tester + .expect_tx_batch(TxBatch { impersonating: false, txs, }) - ); - assert_eq!(block_sealer.poll(&pool, &mut cx), Poll::Pending); + .await } #[tokio::test] - async fn fixed_time_very_long() { - let pool = TxPool::new(ImpersonationManager::default(), TransactionOrder::Fifo); - let mut block_sealer = BlockSealer::new(BlockSealerMode::fixed_time( - 1000, - Duration::from_secs(10000), - )); - let waker = &WAKER_NOOP; - let mut cx = Context::from_waker(waker); - - assert_eq!(block_sealer.poll(&pool, &mut cx), Poll::Pending); + async fn fixed_time_very_long() -> anyhow::Result<()> { + let (tester, _pool) = + Tester::new(|_| BlockSealerMode::fixed_time(1000, Duration::from_secs(10000))); + + tester.expect_no_tx_batch().await } - #[tokio::test] - async fn fixed_time_seal_empty() { - let pool = TxPool::new(ImpersonationManager::default(), TransactionOrder::Fifo); - let mut block_sealer = BlockSealer::new(BlockSealerMode::fixed_time( - 1000, - Duration::from_millis(100), - )); - let waker = &WAKER_NOOP; - let mut cx = Context::from_waker(waker); - - // Sleep enough time to (theoretically) produce at least 2 blocks - tokio::time::sleep(Duration::from_millis(250)).await; - - // Sealer should seal one empty block when polled and then refuse to seal another one - // shortly after as it ensures enough time passes in-between of blocks. - assert_eq!( - block_sealer.poll(&pool, &mut cx), - Poll::Ready(TxBatch { + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn fixed_time_seal_empty() -> anyhow::Result<()> { + let (tester, _pool) = + Tester::new(|_| BlockSealerMode::fixed_time(1000, Duration::from_millis(100))); + + // Sleep enough time to produce exactly 1 block + tokio::time::sleep(Duration::from_millis(150)).await; + + // Sealer should have sealed exactly one empty block by now + tester + .expect_immediate_tx_batch(TxBatch { impersonating: false, - txs: vec![] + txs: vec![], }) - ); - assert_eq!(block_sealer.poll(&pool, &mut cx), Poll::Pending); + .await?; + tester.expect_no_immediate_tx_batch().await?; - // Sleep enough time to produce one block + // Sleep enough time to produce one more block tokio::time::sleep(Duration::from_millis(150)).await; // Next block should be sealable - assert_eq!( - block_sealer.poll(&pool, &mut cx), - Poll::Ready(TxBatch { + tester + .expect_immediate_tx_batch(TxBatch { impersonating: false, - txs: vec![] + txs: vec![], }) - ); + .await } #[tokio::test] - async fn fixed_time_seal_with_txs() { - let pool = TxPool::new(ImpersonationManager::default(), TransactionOrder::Fifo); - let mut block_sealer = BlockSealer::new(BlockSealerMode::fixed_time( - 1000, - Duration::from_millis(100), - )); - let waker = &WAKER_NOOP; - let mut cx = Context::from_waker(waker); + async fn fixed_time_seal_with_txs() -> anyhow::Result<()> { + let (tester, pool) = + Tester::new(|_| BlockSealerMode::fixed_time(1000, Duration::from_millis(100))); let txs = pool.populate::<3>(); // Sleep enough time to produce one block tokio::time::sleep(Duration::from_millis(150)).await; - assert_eq!( - block_sealer.poll(&pool, &mut cx), - Poll::Ready(TxBatch { + tester + .expect_immediate_tx_batch(TxBatch { impersonating: false, - txs: txs.to_vec() + txs: txs.to_vec(), }) - ); + .await } #[tokio::test] - async fn fixed_time_respect_max_txs() { - let pool = TxPool::new(ImpersonationManager::default(), TransactionOrder::Fifo); - let mut block_sealer = - BlockSealer::new(BlockSealerMode::fixed_time(3, Duration::from_millis(100))); - let waker = &WAKER_NOOP; - let mut cx = Context::from_waker(waker); + async fn fixed_time_respect_max_txs() -> anyhow::Result<()> { + let (tester, pool) = + Tester::new(|_| BlockSealerMode::fixed_time(3, Duration::from_millis(100))); let txs = pool.populate::<10>(); @@ -372,13 +475,14 @@ mod tests { // Sleep enough time to produce one block tokio::time::sleep(Duration::from_millis(150)).await; - assert_eq!( - block_sealer.poll(&pool, &mut cx), - Poll::Ready(TxBatch { + tester + .expect_immediate_tx_batch(TxBatch { impersonating: false, - txs: txs.to_vec() + txs: txs.to_vec(), }) - ); + .await?; } + + Ok(()) } } diff --git a/crates/core/src/node/state.rs b/crates/core/src/node/state.rs index a88fa2f7..9cf6f253 100644 --- a/crates/core/src/node/state.rs +++ b/crates/core/src/node/state.rs @@ -1,5 +1,5 @@ -use crate::fork::{SerializableForkStorage, SerializableStorage}; -use crate::node::TransactionResult; +use super::inner::fork::{SerializableForkStorage, SerializableStorage}; +use super::TransactionResult; use serde::{Deserialize, Serialize}; use zksync_types::api::{Block, TransactionVariant}; use zksync_types::H256; diff --git a/crates/core/src/node/storage_logs.rs b/crates/core/src/node/storage_logs.rs index 6e0ecc56..69fe69bf 100644 --- a/crates/core/src/node/storage_logs.rs +++ b/crates/core/src/node/storage_logs.rs @@ -59,7 +59,7 @@ fn compute_and_update_pubdata_cost( } pub fn print_storage_logs_details( - show_storage_logs: &ShowStorageLogs, + show_storage_logs: ShowStorageLogs, result: &VmExecutionResultAndLogs, ) { tracing::info!(""); diff --git a/crates/core/src/node/time.rs b/crates/core/src/node/time.rs deleted file mode 100644 index 0c5bac92..00000000 --- a/crates/core/src/node/time.rs +++ /dev/null @@ -1,235 +0,0 @@ -use anyhow::anyhow; -use std::collections::VecDeque; -use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; - -/// Shared readable view on time. -pub trait ReadTime { - /// Returns timestamp (in seconds) that the clock is currently on. - fn current_timestamp(&self) -> u64; - - /// Peek at what the next call to `advance_timestamp` will return. - fn peek_next_timestamp(&self) -> u64; -} - -/// Writeable view on time management. The owner of this view should be able to treat it as -/// exclusive access to the underlying clock. -pub trait AdvanceTime: ReadTime { - /// Advances clock to the next timestamp and returns that timestamp in seconds. - /// - /// Subsequent calls to this method return monotonically increasing values. Time difference - /// between calls is implementation-specific. - fn advance_timestamp(&mut self) -> u64; - - fn reset_to(&mut self, timestamp: u64); -} - -/// Manages timestamps (in seconds) across the system. -/// -/// Clones always agree on the underlying timestamp and updating one affects all other instances. -#[derive(Clone, Debug, Default)] -pub struct TimestampManager { - internal: Arc>, -} - -impl TimestampManager { - pub fn new(current_timestamp: u64) -> TimestampManager { - TimestampManager { - internal: Arc::new(RwLock::new(TimestampManagerInternal { - current_timestamp, - next_timestamp: None, - interval: None, - })), - } - } - - fn get(&self) -> RwLockReadGuard { - self.internal - .read() - .expect("TimestampManager lock is poisoned") - } - - fn get_mut(&self) -> RwLockWriteGuard { - self.internal - .write() - .expect("TimestampManager lock is poisoned") - } - - /// Sets last used timestamp (in seconds) to the provided value and returns the difference - /// between new value and old value (represented as a signed number of seconds). - pub fn set_current_timestamp_unchecked(&self, timestamp: u64) -> i128 { - let mut this = self.get_mut(); - let diff = (timestamp as i128).saturating_sub(this.current_timestamp as i128); - this.reset_to(timestamp); - diff - } - - /// Forces clock to return provided value as the next timestamp. Time skip will not be performed - /// before the next invocation of `advance_timestamp`. - /// - /// Expects provided timestamp to be in the future, returns error otherwise. - pub fn enforce_next_timestamp(&self, timestamp: u64) -> anyhow::Result<()> { - let mut this = self.get_mut(); - if timestamp <= this.current_timestamp { - Err(anyhow!( - "timestamp ({}) must be greater than the last used timestamp ({})", - timestamp, - this.current_timestamp - )) - } else { - this.next_timestamp.replace(timestamp); - Ok(()) - } - } - - /// Fast-forwards time by the given amount of seconds. - pub fn increase_time(&self, seconds: u64) -> u64 { - let mut this = self.get_mut(); - let next = this.current_timestamp.saturating_add(seconds); - this.reset_to(next); - next - } - - /// Sets an interval to use when computing the next timestamp - /// - /// If an interval already exists, this will update the interval, otherwise a new interval will - /// be set starting with the current timestamp. - pub fn set_block_timestamp_interval(&self, seconds: u64) { - self.get_mut().interval.replace(seconds); - } - - /// Removes the interval. Returns true if it existed before being removed, false otherwise. - pub fn remove_block_timestamp_interval(&self) -> bool { - self.get_mut().interval.take().is_some() - } - - /// Returns an exclusively owned writeable view on this [`TimeManager`] instance. - /// - /// Use this method when you need to ensure that no one else can access [`TimeManager`] during - /// this view's lifetime. - pub fn lock(&self) -> impl AdvanceTime + '_ { - self.lock_with_offsets([]) - } - - /// Returns an exclusively owned writeable view on this [`TimeManager`] instance where first N - /// timestamps will be offset by the provided amount of seconds (where `N` is the size of - /// iterator). - /// - /// Use this method when you need to ensure that no one else can access [`TimeManager`] during - /// this view's lifetime while also pre-setting first `N` returned timestamps. - pub fn lock_with_offsets<'a, I: IntoIterator>( - &'a self, - offsets: I, - ) -> impl AdvanceTime + 'a - where - ::IntoIter: 'a, - { - let guard = self.get_mut(); - TimeLockWithOffsets { - start_timestamp: guard.peek_next_timestamp(), - guard, - offsets: offsets.into_iter().collect::>(), - } - } -} - -impl ReadTime for TimestampManager { - fn current_timestamp(&self) -> u64 { - (*self.get()).current_timestamp() - } - - fn peek_next_timestamp(&self) -> u64 { - (*self.get()).peek_next_timestamp() - } -} - -#[derive(Debug, Default)] -struct TimestampManagerInternal { - /// The current timestamp (in seconds). This timestamp is considered to be used already: there - /// might be a logical event that already happened on that timestamp (e.g. a block was sealed - /// with this timestamp). - current_timestamp: u64, - /// The next timestamp (in seconds) that the clock will be forced to advance to. - next_timestamp: Option, - /// The interval to use when determining the next timestamp to advance to. - interval: Option, -} - -impl TimestampManagerInternal { - fn interval(&self) -> u64 { - self.interval.unwrap_or(1) - } -} - -impl ReadTime for TimestampManagerInternal { - fn current_timestamp(&self) -> u64 { - self.current_timestamp - } - - fn peek_next_timestamp(&self) -> u64 { - self.next_timestamp - .unwrap_or_else(|| self.current_timestamp.saturating_add(self.interval())) - } -} - -impl AdvanceTime for TimestampManagerInternal { - fn advance_timestamp(&mut self) -> u64 { - let next_timestamp = match self.next_timestamp.take() { - Some(next_timestamp) => next_timestamp, - None => self.current_timestamp.saturating_add(self.interval()), - }; - - self.current_timestamp = next_timestamp; - next_timestamp - } - - fn reset_to(&mut self, timestamp: u64) { - self.next_timestamp.take(); - self.current_timestamp = timestamp; - } -} - -struct TimeLockWithOffsets<'a> { - /// The first timestamp that would have been returned without accounting for offsets - start_timestamp: u64, - /// Exclusive writable ownership over the corresponding [`TimestampManager`] - guard: RwLockWriteGuard<'a, TimestampManagerInternal>, - /// A queue of offsets (relative to `start_timestamp`) to be used for next `N` timestamps - offsets: VecDeque, -} - -impl ReadTime for TimeLockWithOffsets<'_> { - fn current_timestamp(&self) -> u64 { - self.guard.current_timestamp() - } - - fn peek_next_timestamp(&self) -> u64 { - match self.offsets.front() { - Some(offset) => self.start_timestamp.saturating_add(*offset), - None => self.guard.peek_next_timestamp(), - } - } -} - -impl AdvanceTime for TimeLockWithOffsets<'_> { - fn advance_timestamp(&mut self) -> u64 { - match self.offsets.pop_front() { - Some(offset) => { - let timestamp = self.start_timestamp.saturating_add(offset); - // Persist last used timestamp in the underlying state as this instance can be - // dropped before we finish iterating all values. - self.guard.reset_to(timestamp); - - timestamp - } - None => self.guard.advance_timestamp(), - } - } - - fn reset_to(&mut self, timestamp: u64) { - // Resetting `start_timestamp` to `timestamp` may look weird here but at the same time there - // is no "expected" behavior in this case. - // Also, this is temporary logic that will become irrelevant after block production refactoring. - self.guard.reset_to(timestamp); - self.start_timestamp = timestamp; - } -} diff --git a/crates/core/src/node/zks.rs b/crates/core/src/node/zks.rs index 42f25238..f69290fa 100644 --- a/crates/core/src/node/zks.rs +++ b/crates/core/src/node/zks.rs @@ -1,10 +1,9 @@ use crate::node::{InMemoryNode, TransactionResult}; use crate::utils::{internal_error, utc_datetime_from_epoch_ms}; +use anyhow::Context; +use itertools::Itertools; use std::collections::HashMap; -use zksync_types::api::{ - BlockDetails, BlockDetailsBase, BlockStatus, BridgeAddresses, TransactionDetails, - TransactionStatus, TransactionVariant, -}; +use zksync_types::api; use zksync_types::fee::Fee; use zksync_types::h256_to_u256; use zksync_types::transaction_request::CallRequest; @@ -17,69 +16,64 @@ use zksync_web3_decl::error::Web3Error; impl InMemoryNode { pub async fn estimate_fee_impl(&self, req: CallRequest) -> Result { - // TODO: Burn with fire - let time = self.time.lock(); - self.read_inner()?.estimate_gas_impl(&time, req) + self.inner.read().await.estimate_gas_impl(req).await } pub async fn get_raw_block_transactions_impl( &self, block_number: L2BlockNumber, ) -> Result, Web3Error> { - let reader = self.read_inner()?; - - let maybe_transactions = reader - .block_hashes - .get(&(block_number.0 as u64)) - .and_then(|hash| reader.blocks.get(hash)) - .map(|block| { + let tx_hashes = self + .blockchain + .inspect_block_by_number(block_number, |block| { block .transactions .iter() .map(|tx| match tx { - TransactionVariant::Full(tx) => &tx.hash, - TransactionVariant::Hash(hash) => hash, + api::TransactionVariant::Full(tx) => tx.hash, + api::TransactionVariant::Hash(hash) => *hash, }) - .flat_map(|tx_hash| { - reader - .tx_results - .get(tx_hash) - .map(|TransactionResult { info, .. }| Transaction { - common_data: ExecuteTransactionCommon::L2( - info.tx.common_data.clone(), - ), - execute: info.tx.execute.clone(), - received_timestamp_ms: info.tx.received_timestamp_ms, - raw_bytes: info.tx.raw_bytes.clone(), - }) + .collect_vec() + }) + .await; + let transactions = if let Some(tx_hashes) = tx_hashes { + let mut transactions = Vec::with_capacity(tx_hashes.len()); + for tx_hash in tx_hashes { + let transaction = self + .blockchain + .inspect_tx(&tx_hash, |TransactionResult { info, .. }| Transaction { + common_data: ExecuteTransactionCommon::L2(info.tx.common_data.clone()), + execute: info.tx.execute.clone(), + received_timestamp_ms: info.tx.received_timestamp_ms, + raw_bytes: info.tx.raw_bytes.clone(), }) - .collect() - }); - - let transactions = match maybe_transactions { - Some(txns) => Ok(txns), - None => { - let fork_storage_read = reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage"); - - match fork_storage_read.fork.as_ref() { - Some(fork) => fork - .fork_source - .get_raw_block_transactions(block_number) - .map_err(|e| internal_error("get_raw_block_transactions", e)), - None => Ok(vec![]), - } + .await + .with_context(|| anyhow::anyhow!("Unexpectedly transaction (hash={tx_hash}) belongs to a block but could not be found"))?; + transactions.push(transaction); + } + transactions + } else { + let reader = self.inner.read().await; + let fork_storage_read = reader + .fork_storage + .inner + .read() + .expect("failed reading fork storage"); + + match fork_storage_read.fork.as_ref() { + Some(fork) => fork + .fork_source + .get_raw_block_transactions(block_number) + .map_err(|e| internal_error("get_raw_block_transactions", e))?, + None => return Err(Web3Error::NoBlock), } - }?; + }; Ok(transactions) } - pub async fn get_bridge_contracts_impl(&self) -> Result { - let reader = self.read_inner()?; + pub async fn get_bridge_contracts_impl(&self) -> Result { + let reader = self.inner.read().await; let result = match reader .fork_storage @@ -96,7 +90,7 @@ impl InMemoryNode { err ))) })?, - None => BridgeAddresses { + None => api::BridgeAddresses { l1_shared_default_bridge: Default::default(), l2_shared_default_bridge: Default::default(), l1_erc20_default_bridge: Default::default(), @@ -115,7 +109,7 @@ impl InMemoryNode { from: u32, limit: u8, ) -> anyhow::Result> { - let reader = self.read_inner()?; + let reader = self.inner.read().await; let fork_storage_read = reader .fork_storage @@ -144,14 +138,10 @@ impl InMemoryNode { &self, address: Address, ) -> Result, Web3Error> { - let inner = self.get_inner().clone(); let tokens = self.get_confirmed_tokens_impl(0, 100).await?; let balances = { - let writer = inner.write().map_err(|_e| { - let error_message = "Failed to acquire lock. Please ensure the lock is not being held by another process or thread.".to_string(); - Web3Error::InternalError(anyhow::Error::msg(error_message)) - })?; + let writer = self.inner.write().await; let mut balances = HashMap::new(); for token in tokens { let balance_key = storage_key_for_standard_token_balance( @@ -180,23 +170,24 @@ impl InMemoryNode { pub async fn get_block_details_impl( &self, block_number: L2BlockNumber, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let base_system_contracts_hashes = self.system_contracts.base_system_contracts_hashes(); - let reader = self.read_inner()?; - - let maybe_block = reader - .block_hashes - .get(&(block_number.0 as u64)) - .and_then(|hash| reader.blocks.get(hash)) - .map(|block| BlockDetails { + let reader = self.inner.read().await; + let l2_fair_gas_price = reader.fee_input_provider.gas_price(); + let fair_pubdata_price = Some(reader.fee_input_provider.fair_pubdata_price()); + drop(reader); + + let block_details = self + .blockchain + .inspect_block_by_number(block_number, |block| api::BlockDetails { number: L2BlockNumber(block.number.as_u32()), l1_batch_number: L1BatchNumber(block.l1_batch_number.unwrap_or_default().as_u32()), - base: BlockDetailsBase { + base: api::BlockDetailsBase { timestamp: block.timestamp.as_u64(), l1_tx_count: 1, l2_tx_count: block.transactions.len(), root_hash: Some(block.hash), - status: BlockStatus::Verified, + status: api::BlockStatus::Verified, commit_tx_hash: None, commit_chain_id: None, committed_at: None, @@ -207,79 +198,85 @@ impl InMemoryNode { execute_chain_id: None, executed_at: None, l1_gas_price: 0, - l2_fair_gas_price: reader.fee_input_provider.gas_price(), - fair_pubdata_price: Some(reader.fee_input_provider.fair_pubdata_price()), + l2_fair_gas_price, + fair_pubdata_price, base_system_contracts_hashes, }, operator_address: Address::zero(), protocol_version: Some(ProtocolVersionId::latest()), }) - .or_else(|| { - reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage") - .fork - .as_ref() - .and_then(|fork| { - fork.fork_source - .get_block_details(block_number) - .ok() - .flatten() - }) - }); + .await; + + let maybe_block_details = match block_details { + Some(block_details) => Some(block_details), + None => self + .inner + .read() + .await + .fork_storage + .inner + .read() + .expect("failed reading fork storage") + .fork + .as_ref() + .and_then(|fork| { + fork.fork_source + .get_block_details(block_number) + .ok() + .flatten() + }), + }; - Ok(maybe_block) + Ok(maybe_block_details) } pub async fn get_transaction_details_impl( &self, hash: H256, - ) -> anyhow::Result> { - let reader = self.read_inner()?; - - let maybe_result = { - reader - .tx_results - .get(&hash) - .map(|TransactionResult { info, receipt, .. }| { - TransactionDetails { - is_l1_originated: false, - status: TransactionStatus::Included, - // if these are not set, fee is effectively 0 - fee: receipt.effective_gas_price.unwrap_or_default() - * receipt.gas_used.unwrap_or_default(), - gas_per_pubdata: info.tx.common_data.fee.gas_per_pubdata_limit, - initiator_address: info.tx.initiator_account(), - received_at: utc_datetime_from_epoch_ms(info.tx.received_timestamp_ms), - eth_commit_tx_hash: None, - eth_prove_tx_hash: None, - eth_execute_tx_hash: None, - } - }) - .or_else(|| { - reader - .fork_storage - .inner - .read() - .expect("failed reading fork storage") - .fork - .as_ref() - .and_then(|fork| { - fork.fork_source - .get_transaction_details(hash) - .ok() - .flatten() - }) - }) + ) -> anyhow::Result> { + let tx_details = self + .blockchain + .inspect_tx(&hash, |TransactionResult { info, receipt, .. }| { + api::TransactionDetails { + is_l1_originated: false, + status: api::TransactionStatus::Included, + // if these are not set, fee is effectively 0 + fee: receipt.effective_gas_price.unwrap_or_default() + * receipt.gas_used.unwrap_or_default(), + gas_per_pubdata: info.tx.common_data.fee.gas_per_pubdata_limit, + initiator_address: info.tx.initiator_account(), + received_at: utc_datetime_from_epoch_ms(info.tx.received_timestamp_ms), + eth_commit_tx_hash: None, + eth_prove_tx_hash: None, + eth_execute_tx_hash: None, + } + }) + .await; + let maybe_tx_details = match tx_details { + Some(tx_details) => Some(tx_details), + None => self + .inner + .read() + .await + .fork_storage + .inner + .read() + .expect("failed reading fork storage") + .fork + .as_ref() + .and_then(|fork| { + fork.fork_source + .get_transaction_details(hash) + .ok() + .flatten() + }), }; - Ok(maybe_result) + Ok(maybe_tx_details) } pub async fn get_bytecode_by_hash_impl(&self, hash: H256) -> anyhow::Result>> { - let writer = self.write_inner()?; + let writer = self.inner.write().await; let maybe_bytecode = match writer.fork_storage.load_factory_dep_internal(hash) { Ok(maybe_bytecode) => maybe_bytecode, @@ -323,15 +320,11 @@ mod tests { use anvil_zksync_config::types::CacheConfig; use zksync_types::u256_to_h256; - use zksync_types::{ - api::{self, Block, TransactionReceipt, TransactionVariant}, - transaction_request::CallRequest, - Address, H160, H256, - }; + use zksync_types::{api, transaction_request::CallRequest, Address, H160, H256}; use super::*; + use crate::node::fork::ForkDetails; use crate::{ - fork::ForkDetails, node::InMemoryNode, testing, testing::{ForkBlockConfig, MockServer}, @@ -339,7 +332,7 @@ mod tests { #[tokio::test] async fn test_estimate_fee() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let mock_request = CallRequest { from: Some( @@ -376,23 +369,24 @@ mod tests { #[tokio::test] async fn test_get_transaction_details_local() { // Arrange - let node = InMemoryNode::default(); - let inner = node.get_inner(); + let node = InMemoryNode::test(None); { - let mut writer = inner.write().unwrap(); - writer.tx_results.insert( - H256::repeat_byte(0x1), - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: TransactionReceipt { - logs: vec![], - gas_used: Some(U256::from(10_000)), - effective_gas_price: Some(U256::from(1_000_000_000)), - ..Default::default() + let mut writer = node.inner.write().await; + writer + .insert_tx_result( + H256::repeat_byte(0x1), + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: api::TransactionReceipt { + logs: vec![], + gas_used: Some(U256::from(10_000)), + effective_gas_price: Some(U256::from(1_000_000_000)), + ..Default::default() + }, + debug: testing::default_tx_debug_info(), }, - debug: testing::default_tx_debug_info(), - }, - ); + ) + .await; } let result = node .get_transaction_details_impl(H256::repeat_byte(0x1)) @@ -401,7 +395,7 @@ mod tests { .expect("transaction details"); // Assert - assert!(matches!(result.status, TransactionStatus::Included)); + assert!(matches!(result.status, api::TransactionStatus::Included)); assert_eq!(result.fee, U256::from(10_000_000_000_000u64)); } @@ -439,7 +433,7 @@ mod tests { }), ); - let node = InMemoryNode::default_fork(Some( + let node = InMemoryNode::test(Some( ForkDetails::from_network(&mock_server.url(), None, &CacheConfig::None) .await .unwrap(), @@ -451,20 +445,21 @@ mod tests { .expect("get transaction details") .expect("transaction details"); - assert!(matches!(result.status, TransactionStatus::Included)); + assert!(matches!(result.status, api::TransactionStatus::Included)); assert_eq!(result.fee, U256::from(127_720_500_000_000u64)); } #[tokio::test] async fn test_get_block_details_local() { // Arrange - let node = InMemoryNode::default(); - let inner = node.get_inner(); + let node = InMemoryNode::test(None); { - let mut writer = inner.write().unwrap(); - let block = Block::::default(); - writer.blocks.insert(H256::repeat_byte(0x1), block); - writer.block_hashes.insert(0, H256::repeat_byte(0x1)); + let mut writer = node.inner.write().await; + let block = api::Block::::default(); + writer.insert_block(H256::repeat_byte(0x1), block).await; + writer + .insert_block_hash(L2BlockNumber(0), H256::repeat_byte(0x1)) + .await; } let result = node .get_block_details_impl(L2BlockNumber(0)) @@ -525,7 +520,7 @@ mod tests { }), ); - let node = InMemoryNode::default_fork(Some( + let node = InMemoryNode::test(Some( ForkDetails::from_network(&mock_server.url(), None, &CacheConfig::None) .await .unwrap(), @@ -546,8 +541,8 @@ mod tests { #[tokio::test] async fn test_get_bridge_contracts_uses_default_values_if_local() { // Arrange - let node = InMemoryNode::default(); - let expected_bridge_addresses = BridgeAddresses { + let node = InMemoryNode::test(None); + let expected_bridge_addresses = api::BridgeAddresses { l1_shared_default_bridge: Default::default(), l2_shared_default_bridge: Default::default(), l1_erc20_default_bridge: Default::default(), @@ -574,7 +569,7 @@ mod tests { transaction_count: 0, hash: H256::repeat_byte(0xab), }); - let input_bridge_addresses = BridgeAddresses { + let input_bridge_addresses = api::BridgeAddresses { l1_shared_default_bridge: Some(H160::repeat_byte(0x1)), l2_shared_default_bridge: Some(H160::repeat_byte(0x2)), l1_erc20_default_bridge: Some(H160::repeat_byte(0x1)), @@ -603,7 +598,7 @@ mod tests { }), ); - let node = InMemoryNode::default_fork(Some( + let node = InMemoryNode::test(Some( ForkDetails::from_network(&mock_server.url(), None, &CacheConfig::None) .await .unwrap(), @@ -621,12 +616,12 @@ mod tests { #[tokio::test] async fn test_get_bytecode_by_hash_returns_local_value_if_available() { // Arrange - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let input_hash = H256::repeat_byte(0x1); let input_bytecode = vec![0x1]; - node.get_inner() + node.inner .write() - .unwrap() + .await .fork_storage .store_factory_dep(input_hash, input_bytecode.clone()); @@ -666,7 +661,7 @@ mod tests { }), ); - let node = InMemoryNode::default_fork(Some( + let node = InMemoryNode::test(Some( ForkDetails::from_network(&mock_server.url(), None, &CacheConfig::None) .await .unwrap(), @@ -685,28 +680,31 @@ mod tests { #[tokio::test] async fn test_get_raw_block_transactions_local() { // Arrange - let node = InMemoryNode::default(); - let inner = node.get_inner(); + let node = InMemoryNode::test(None); { - let mut writer = inner.write().unwrap(); - let mut block = Block::::default(); + let mut writer = node.inner.write().await; + let mut block = api::Block::::default(); let txn = api::Transaction::default(); - writer.tx_results.insert( - txn.hash, - TransactionResult { - info: testing::default_tx_execution_info(), - receipt: TransactionReceipt { - logs: vec![], - gas_used: Some(U256::from(10_000)), - effective_gas_price: Some(U256::from(1_000_000_000)), - ..Default::default() + writer + .insert_tx_result( + txn.hash, + TransactionResult { + info: testing::default_tx_execution_info(), + receipt: api::TransactionReceipt { + logs: vec![], + gas_used: Some(U256::from(10_000)), + effective_gas_price: Some(U256::from(1_000_000_000)), + ..Default::default() + }, + debug: testing::default_tx_debug_info(), }, - debug: testing::default_tx_debug_info(), - }, - ); - block.transactions.push(TransactionVariant::Full(txn)); - writer.blocks.insert(H256::repeat_byte(0x1), block); - writer.block_hashes.insert(0, H256::repeat_byte(0x1)); + ) + .await; + block.transactions.push(api::TransactionVariant::Full(txn)); + writer.insert_block(H256::repeat_byte(0x1), block).await; + writer + .insert_block_hash(L2BlockNumber(0), H256::repeat_byte(0x1)) + .await; } let txns = node @@ -787,7 +785,7 @@ mod tests { }), ); - let node = InMemoryNode::default_fork(Some( + let node = InMemoryNode::test(Some( ForkDetails::from_network(&mock_server.url(), None, &CacheConfig::None) .await .unwrap(), @@ -802,7 +800,7 @@ mod tests { #[tokio::test] async fn test_get_all_account_balances_empty() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let balances = node .get_all_account_balances_impl(Address::zero()) .await @@ -812,7 +810,7 @@ mod tests { #[tokio::test] async fn test_get_confirmed_tokens_eth() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let balances = node .get_confirmed_tokens_impl(0, 100) .await @@ -959,15 +957,14 @@ mod tests { }), ); - let node = InMemoryNode::default_fork(Some( + let node = InMemoryNode::test(Some( ForkDetails::from_network(&mock_server.url(), Some(1), &CacheConfig::None) .await .unwrap(), )); { - let inner = node.get_inner(); - let writer = inner.write().unwrap(); + let writer = node.inner.write().await; let mut fork = writer.fork_storage.inner.write().unwrap(); fork.raw_storage.set_value( storage_key_for_standard_token_balance( @@ -987,7 +984,7 @@ mod tests { #[tokio::test] async fn test_get_base_token_l1_address() { - let node = InMemoryNode::default(); + let node = InMemoryNode::test(None); let token_address = node .get_base_token_l1_address_impl() .await diff --git a/crates/core/src/system_contracts.rs b/crates/core/src/system_contracts.rs index c628cb84..3e6fb6ad 100644 --- a/crates/core/src/system_contracts.rs +++ b/crates/core/src/system_contracts.rs @@ -1,4 +1,5 @@ use crate::deps::system_contracts::bytecode_from_slice; +use crate::node::ImpersonationManager; use anvil_zksync_config::types::SystemContractsOptions; use zksync_contracts::{ read_bootloader_code, read_sys_contract_bytecode, BaseSystemContracts, @@ -6,6 +7,7 @@ use zksync_contracts::{ }; use zksync_multivm::interface::TxExecutionMode; use zksync_types::bytecode::BytecodeHash; +use zksync_types::Address; /// Holds the system contracts (and bootloader) that are used by the in-memory node. #[derive(Debug, Clone)] @@ -76,6 +78,20 @@ impl SystemContracts { pub fn base_system_contracts_hashes(&self) -> BaseSystemContractsHashes { self.baseline_contracts.hashes() } + + pub fn system_contracts_for_initiator( + &self, + impersonation: &ImpersonationManager, + initiator: &Address, + ) -> BaseSystemContracts { + if impersonation.is_impersonating(initiator) { + tracing::info!("🕵️ Executing tx from impersonated account {initiator:?}"); + self.contracts(TxExecutionMode::VerifyExecute, true).clone() + } else { + self.contracts(TxExecutionMode::VerifyExecute, false) + .clone() + } + } } /// Creates BaseSystemContracts object with a specific bootloader. diff --git a/crates/core/src/testing.rs b/crates/core/src/testing.rs index d51875e2..f4614679 100644 --- a/crates/core/src/testing.rs +++ b/crates/core/src/testing.rs @@ -6,11 +6,9 @@ #![cfg(test)] use crate::deps::InMemoryStorage; +use crate::node::fork::ForkSource; use crate::node::{InMemoryNode, TxExecutionInfo}; -use crate::{fork::ForkSource, node::compute_hash}; -use ethabi::{ParamType, Token}; -use ethers::contract; use eyre::eyre; use httptest::{ matchers::{eq, json_decoded, request}, @@ -473,28 +471,12 @@ impl TransactionBuilder { } /// Applies a transaction with a given hash to the node and returns the block hash. -pub fn apply_tx(node: &InMemoryNode, tx_hash: H256) -> (H256, U64, L2Tx) { - let next_miniblock = node - .get_inner() - .read() - .map(|reader| reader.current_miniblock.saturating_add(1)) - .expect("failed getting current batch number"); - let produced_block_hash = compute_hash(next_miniblock, [&tx_hash]); - - let tx = TransactionBuilder::new().set_hash(tx_hash).build(); - - node.set_rich_account( - tx.common_data.initiator_address, - U256::from(100u128 * 10u128.pow(18)), - ); - node.apply_txs(vec![tx.clone()], 1) - .expect("failed applying tx"); - - (produced_block_hash, U64::from(next_miniblock), tx) +pub async fn apply_tx(node: &InMemoryNode, tx_hash: H256) -> (H256, U64, L2Tx) { + node.inner.write().await.apply_tx(tx_hash).await } /// Deploys a contract with the given bytecode. -pub fn deploy_contract( +pub async fn deploy_contract( node: &InMemoryNode, tx_hash: H256, private_key: &K256PrivateKey, @@ -502,78 +484,11 @@ pub fn deploy_contract( calldata: Option>, nonce: Nonce, ) -> H256 { - use ethers::abi::Function; - use ethers::types::Bytes; - use zksync_web3_rs::eip712; - - let next_miniblock = node - .get_inner() - .read() - .map(|reader| reader.current_miniblock.saturating_add(1)) - .expect("failed getting current batch number"); - let produced_block_hash = compute_hash(next_miniblock, [&tx_hash]); - - let salt = [0u8; 32]; - let bytecode_hash = eip712::hash_bytecode(&bytecode).expect("invalid bytecode"); - let call_data: Bytes = calldata.unwrap_or_default().into(); - let create: Function = serde_json::from_str( - r#"{ - "inputs": [ - { - "internalType": "bytes32", - "name": "_salt", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "_bytecodeHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "_input", - "type": "bytes" - } - ], - "name": "create", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "payable", - "type": "function" - }"#, - ) - .unwrap(); - - let data = contract::encode_function_data(&create, (salt, bytecode_hash, call_data)) - .expect("failed encoding function data"); - - let mut tx = L2Tx::new_signed( - Some(zksync_types::CONTRACT_DEPLOYER_ADDRESS), - data.to_vec(), - nonce, - Fee { - gas_limit: U256::from(400_000_000), - max_fee_per_gas: U256::from(50_000_000), - max_priority_fee_per_gas: U256::from(50_000_000), - gas_per_pubdata_limit: U256::from(50000), - }, - U256::from(0), - zksync_types::L2ChainId::from(260), - private_key, - vec![bytecode], - Default::default(), - ) - .expect("failed signing tx"); - tx.set_input(vec![], tx_hash); - node.apply_txs(vec![tx], 1) - .expect("failed deploying contract"); - - produced_block_hash + node.inner + .write() + .await + .deploy_contract(tx_hash, private_key, bytecode, calldata, nonce) + .await } /// Builds transaction logs @@ -697,25 +612,6 @@ pub fn default_tx_debug_info() -> DebugCall { } } -/// Decodes a `bytes` tx result to its concrete parameter type. -pub fn decode_tx_result(output: &[u8], param_type: ParamType) -> Token { - let result = ethabi::decode(&[ParamType::Bytes], output).expect("failed decoding output"); - if result.is_empty() { - panic!("result was empty"); - } - - let result_bytes = result[0] - .clone() - .into_bytes() - .expect("failed converting result to bytes"); - let result = ethabi::decode(&[param_type], &result_bytes).expect("failed converting output"); - if result.is_empty() { - panic!("decoded result was empty"); - } - - result[0].clone() -} - /// Asserts that two instances of [BridgeAddresses] are equal pub fn assert_bridge_addresses_eq( expected_bridge_addresses: &BridgeAddresses, @@ -975,8 +871,9 @@ mod test { #[tokio::test] async fn test_apply_tx() { - let node = InMemoryNode::default(); - let (actual_block_hash, actual_block_number, _) = apply_tx(&node, H256::repeat_byte(0x01)); + let node = InMemoryNode::test(None); + let (actual_block_hash, actual_block_number, _) = + apply_tx(&node, H256::repeat_byte(0x01)).await; assert_eq!( H256::from_str("0xd97ba6a5ab0f2d7fbfc697251321cce20bff3da2b0ddaf12c80f80f0ab270b15") @@ -986,10 +883,10 @@ mod test { assert_eq!(U64::from(1), actual_block_number); assert!( - node.get_inner() - .read() - .map(|inner| inner.blocks.contains_key(&actual_block_hash)) - .unwrap(), + node.blockchain + .get_block_by_hash(&actual_block_hash) + .await + .is_some(), "block was not produced" ); } diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index ba04a5e3..9b3756f9 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,12 +1,16 @@ use anyhow::Context; use chrono::{DateTime, Utc}; use serde::Serialize; +use std::future::Future; +use std::sync::Arc; use std::{convert::TryInto, fmt}; use std::{ fs::File, io::{BufWriter, Write}, path::Path, }; +use tokio::runtime::Builder; +use tokio::sync::{RwLock, RwLockReadGuard}; use zksync_multivm::interface::{Call, CallType, ExecutionResult, VmExecutionResultAndLogs}; use zksync_types::{ api::{BlockNumber, DebugCall, DebugCallType}, @@ -223,6 +227,44 @@ pub fn write_json_file(path: &Path, obj: &T) -> anyhow::Result<()> Ok(()) } +pub fn block_on(future: F) -> F::Output +where + F::Output: Send, +{ + std::thread::spawn(move || { + let runtime = Builder::new_current_thread() + .enable_all() + .build() + .expect("tokio runtime creation failed"); + runtime.block_on(future) + }) + .join() + .unwrap() +} + +/// A special version of `Arc>` that can only be read from. +#[derive(Debug)] +pub struct ArcRLock(Arc>); + +impl Clone for ArcRLock { + fn clone(&self) -> Self { + ArcRLock(self.0.clone()) + } +} + +impl ArcRLock { + /// Wrap writeable `Arc>` into a read-only `ArcRLock`. + pub fn wrap(inner: Arc>) -> Self { + Self(inner) + } + + /// Locks this `ArcRLock` with shared read access, causing the current task + /// to yield until the lock has been acquired. + pub async fn read(&self) -> RwLockReadGuard { + self.0.read().await + } +} + #[cfg(test)] mod tests { use zksync_types::U256; diff --git a/e2e-tests-rust/Cargo.lock b/e2e-tests-rust/Cargo.lock index cdbf102e..dd98e2b5 100644 --- a/e2e-tests-rust/Cargo.lock +++ b/e2e-tests-rust/Cargo.lock @@ -994,6 +994,7 @@ dependencies = [ "itertools 0.13.0", "lazy_static", "once_cell", + "pin-project", "reqwest 0.11.27", "rustc-hash 1.1.0", "serde",