diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md index 7285bc02..2f43b03f 100644 --- a/SUPPORTED_APIS.md +++ b/SUPPORTED_APIS.md @@ -14,6 +14,7 @@ The `status` options are: | Namespace | API |
Status
| Description | | --- | --- | --- | --- | +| `ANVIL` | `anvil_mine_detailed` | `SUPPORTED` | Mines a single block in the same way as `evm_mine` but returns extra fields | | `ANVIL` | `anvil_setRpcUrl` | `SUPPORTED` | Sets the fork RPC url. Assumes the underlying chain is the same as before | | `ANVIL` | `anvil_setNextBlockBaseFeePerGas` | `SUPPORTED` | Sets the base fee of the next block | | `ANVIL` | `anvil_dropTransaction` | `SUPPORTED` | Removes a transaction from the pool | diff --git a/e2e-tests-rust/Cargo.lock b/e2e-tests-rust/Cargo.lock index 0b33af4d..930a0fc1 100644 --- a/e2e-tests-rust/Cargo.lock +++ b/e2e-tests-rust/Cargo.lock @@ -626,8 +626,8 @@ dependencies = [ [[package]] name = "alloy-zksync" -version = "0.6.0" -source = "git+https://github.com/itegulov/alloy-zksync.git?rev=e0e54a5ac9e24c1c32c7a8783bbf3e34dde2e218#e0e54a5ac9e24c1c32c7a8783bbf3e34dde2e218" +version = "0.6.1" +source = "git+https://github.com/itegulov/alloy-zksync.git?rev=c43bba1a6c5e744afb975b261cba6e964d6a58c6#c43bba1a6c5e744afb975b261cba6e964d6a58c6" dependencies = [ "alloy", "async-trait", diff --git a/e2e-tests-rust/Cargo.toml b/e2e-tests-rust/Cargo.toml index 0200265e..02717914 100644 --- a/e2e-tests-rust/Cargo.toml +++ b/e2e-tests-rust/Cargo.toml @@ -10,7 +10,7 @@ categories = ["cryptography"] publish = false [dependencies] -alloy-zksync = { git = "https://github.com/itegulov/alloy-zksync.git", rev = "e0e54a5ac9e24c1c32c7a8783bbf3e34dde2e218" } +alloy-zksync = { git = "https://github.com/itegulov/alloy-zksync.git", rev = "c43bba1a6c5e744afb975b261cba6e964d6a58c6" } alloy = { version = "0.6", features = ["full", "rlp", "serde", "sol-types"] } anyhow = "1.0" fs2 = "0.4.3" diff --git a/e2e-tests-rust/src/lib.rs b/e2e-tests-rust/src/lib.rs index 94fd15ff..0804cbf1 100644 --- a/e2e-tests-rust/src/lib.rs +++ b/e2e-tests-rust/src/lib.rs @@ -1,6 +1,8 @@ +use alloy::network::Network; use alloy::primitives::{Address, TxHash}; use alloy::providers::{Provider, ProviderCall}; use alloy::rpc::client::NoParams; +use alloy::serde::WithOtherFields; use alloy::transports::Transport; use alloy_zksync::network::Zksync; @@ -41,6 +43,29 @@ where .request("anvil_removePoolTransactions", (address,)) .into() } + + fn mine( + &self, + num_blocks: Option, + interval: Option, + ) -> ProviderCall, Option), ()> { + self.client() + .request("anvil_mine", (num_blocks, interval)) + .into() + } + + fn mine_detailed( + &self, + ) -> ProviderCall< + T, + NoParams, + alloy::rpc::types::Block< + WithOtherFields<::TransactionResponse>, + ::HeaderResponse, + >, + > { + self.client().request_noparams("anvil_mine_detailed").into() + } } impl EraTestNodeApiProvider for P diff --git a/e2e-tests-rust/tests/lib.rs b/e2e-tests-rust/tests/lib.rs index dea3e988..294593e6 100644 --- a/e2e-tests-rust/tests/lib.rs +++ b/e2e-tests-rust/tests/lib.rs @@ -128,9 +128,15 @@ async fn no_sealing_timeout() -> anyhow::Result<()> { .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) .with_value(U256::from(100)); let pending_tx = provider.send_transaction(tx).await?.register().await?; + let tx_hash = pending_tx.tx_hash().clone(); let finalization_result = tokio::time::timeout(Duration::from_secs(3), pending_tx).await; assert!(finalization_result.is_err()); + // Mine a block manually and assert that the transaction is finalized now + provider.mine(None, None).await?; + let receipt = provider.get_transaction_receipt(tx_hash).await?.unwrap(); + assert!(receipt.status()); + Ok(()) } @@ -275,3 +281,69 @@ async fn remove_pool_transactions() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn manual_mining_two_txs_in_one_block() -> anyhow::Result<()> { + // Test that we can submit two transaction and then manually mine one block that contains both + // transactions in it. + let provider = init(|node| node.no_mine()).await?; + + let tx0 = TransactionRequest::default() + .with_from(RICH_WALLET0) + .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) + .with_value(U256::from(100)); + let pending_tx0 = provider.send_transaction(tx0).await?.register().await?; + let tx1 = TransactionRequest::default() + .with_from(RICH_WALLET1) + .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) + .with_value(U256::from(100)); + let pending_tx1 = provider.send_transaction(tx1).await?.register().await?; + + // Mine a block manually and assert that both transactions are finalized now + provider.mine(None, None).await?; + let receipt0 = provider + .get_transaction_receipt(pending_tx0.await?) + .await? + .unwrap(); + assert!(receipt0.status()); + let receipt1 = provider + .get_transaction_receipt(pending_tx1.await?) + .await? + .unwrap(); + assert!(receipt1.status()); + + assert_eq!(receipt0.block_hash(), receipt1.block_hash()); + assert_eq!(receipt0.block_number(), receipt1.block_number()); + + Ok(()) +} + +#[tokio::test] +async fn detailed_mining_success() -> anyhow::Result<()> { + // Test that we can detailed mining on a successful transaction and get output from it. + let provider = init(|node| node.no_mine()).await?; + + let tx = TransactionRequest::default() + .with_from(RICH_WALLET0) + .with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")) + .with_value(U256::from(100)); + provider.send_transaction(tx).await?.register().await?; + + // Mine a block manually and assert that it has our transaction with extra fields + let block = provider.mine_detailed().await?; + assert_eq!(block.transactions.len(), 1); + let actual_tx = block + .transactions + .clone() + .into_transactions() + .next() + .unwrap(); + + assert_eq!( + actual_tx.other.get("output").and_then(|x| x.as_str()), + Some("0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000") + ); + assert!(actual_tx.other.get("revertReason").is_none()); + + Ok(()) +} diff --git a/src/deps/mod.rs b/src/deps/mod.rs index 3e886afd..acdcb450 100644 --- a/src/deps/mod.rs +++ b/src/deps/mod.rs @@ -22,10 +22,8 @@ impl InMemoryStorage { system_contracts_options: &crate::system_contracts::Options, use_evm_emulator: bool, ) -> Self { - let contracts = crate::system_contracts::get_deployed_contracts( - system_contracts_options, - use_evm_emulator, - ); + let contracts = + system_contracts::get_deployed_contracts(system_contracts_options, use_evm_emulator); let system_context_init_log = get_system_context_init_logs(chain_id); diff --git a/src/deps/system_contracts.rs b/src/deps/system_contracts.rs index 9d2723b9..65d883f7 100644 --- a/src/deps/system_contracts.rs +++ b/src/deps/system_contracts.rs @@ -1,5 +1,7 @@ +use crate::system_contracts::Options; use once_cell::sync::Lazy; use serde_json::Value; +use zksync_types::system_contracts::get_system_smart_contracts; use zksync_types::{ block::DeployedContract, ACCOUNT_CODE_STORAGE_ADDRESS, BOOTLOADER_ADDRESS, BOOTLOADER_UTILITIES_ADDRESS, CODE_ORACLE_ADDRESS, COMPRESSOR_ADDRESS, @@ -190,3 +192,13 @@ pub static COMPILED_IN_SYSTEM_CONTRACTS: Lazy> = Lazy::new deployed_system_contracts.extend(empty_system_contracts); deployed_system_contracts }); + +pub fn get_deployed_contracts( + options: &Options, + use_evm_emulator: bool, +) -> Vec { + match options { + Options::BuiltIn | Options::BuiltInWithoutSecurity => COMPILED_IN_SYSTEM_CONTRACTS.clone(), + Options::Local => get_system_smart_contracts(use_evm_emulator), + } +} diff --git a/src/namespaces/anvil.rs b/src/namespaces/anvil.rs index deb24f07..b0e9ed25 100644 --- a/src/namespaces/anvil.rs +++ b/src/namespaces/anvil.rs @@ -1,11 +1,21 @@ -use jsonrpc_derive::rpc; -use zksync_types::{Address, H256, U256, U64}; - use super::{ResetRequest, RpcResult}; use crate::utils::Numeric; +use jsonrpc_derive::rpc; +use serde::{Deserialize, Serialize}; +use zksync_types::api::{Block, Transaction}; +use zksync_types::web3::Bytes; +use zksync_types::{Address, H256, U256, U64}; #[rpc] pub trait AnvilNamespaceT { + /// 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. + #[rpc(name = "anvil_mine_detailed")] + fn mine_detailed(&self) -> RpcResult>; + /// Sets the fork RPC url. Assumes the underlying chain is the same as before. /// /// # Arguments @@ -278,3 +288,16 @@ pub trait AnvilNamespaceT { #[rpc(name = "anvil_setStorageAt")] fn set_storage_at(&self, address: Address, slot: U256, value: U256) -> RpcResult; } + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct DetailedTransaction { + #[serde(flatten)] + pub inner: Transaction, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub output: Option, + #[serde(rename = "revertReason")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub revert_reason: Option, +} diff --git a/src/namespaces/mod.rs b/src/namespaces/mod.rs index f1102fb4..ab18100a 100644 --- a/src/namespaces/mod.rs +++ b/src/namespaces/mod.rs @@ -9,7 +9,7 @@ mod net; mod web3; mod zks; -pub use anvil::AnvilNamespaceT; +pub use anvil::{AnvilNamespaceT, DetailedTransaction}; pub use config::ConfigurationApiNamespaceT; pub use debug::DebugNamespaceT; pub use eth::EthNamespaceT; diff --git a/src/node/anvil.rs b/src/node/anvil.rs index f5fdc821..fb7842ab 100644 --- a/src/node/anvil.rs +++ b/src/node/anvil.rs @@ -1,6 +1,8 @@ +use zksync_types::api::Block; use zksync_types::{Address, H256, U256, U64}; use zksync_web3_decl::error::Web3Error; +use crate::namespaces::DetailedTransaction; use crate::utils::Numeric; use crate::{ fork::ForkSource, @@ -12,6 +14,15 @@ use crate::{ impl AnvilNamespaceT for InMemoryNode { + fn mine_detailed(&self) -> RpcResult> { + self.mine_detailed() + .map_err(|err| { + tracing::error!("failed mining with detailed view: {:?}", err); + into_jsrpc_error(Web3Error::InternalError(err)) + }) + .into_boxed_future() + } + fn set_rpc_url(&self, url: String) -> RpcResult<()> { self.set_rpc_url(url) .map_err(|err| { diff --git a/src/node/debug.rs b/src/node/debug.rs index 9dbe6be5..3cd7ca9b 100644 --- a/src/node/debug.rs +++ b/src/node/debug.rs @@ -145,6 +145,7 @@ impl DebugNames let only_top = options.is_some_and(|o| o.tracer_config.only_top_call); let inner = self.get_inner().clone(); let time = self.time.clone(); + let system_contracts = self.system_contracts.contracts_for_l2_call().clone(); Box::pin(async move { if block.is_some() && !matches!(block, Some(BlockId::Number(BlockNumber::Latest))) { return Err(jsonrpc_core::Error::invalid_params( @@ -158,7 +159,6 @@ impl DebugNames ))) })?; - let system_contracts = inner.system_contracts.contracts_for_l2_call(); let allow_no_target = system_contracts.evm_emulator.is_some(); let mut l2_tx = L2Tx::from_request(request.into(), MAX_TX_SIZE, allow_no_target) .map_err(|err| into_jsrpc_error(Web3Error::SerializationError(err)))?; diff --git a/src/node/eth.rs b/src/node/eth.rs index 82f62e16..58fd7a84 100644 --- a/src/node/eth.rs +++ b/src/node/eth.rs @@ -4,7 +4,7 @@ use anyhow::Context as _; use colored::Colorize; use futures::FutureExt; use itertools::Itertools; -use zksync_multivm::interface::ExecutionResult; +use zksync_multivm::interface::{ExecutionResult, TxExecutionMode}; use zksync_multivm::vm_latest::constants::ETH_CALL_GAS_LIMIT; use zksync_types::{ api::{Block, BlockIdVariant, BlockNumber, TransactionVariant}, @@ -41,7 +41,7 @@ impl InMemoryNo &self, req: zksync_types::transaction_request::CallRequest, ) -> Result { - let system_contracts = self.system_contracts_for_l2_call()?; + let system_contracts = self.system_contracts.contracts_for_l2_call().clone(); let allow_no_target = system_contracts.evm_emulator.is_some(); let mut tx = L2Tx::from_request(req.into(), MAX_TX_SIZE, allow_no_target)?; @@ -89,7 +89,11 @@ impl InMemoryNo .chain_id; let (tx_req, hash) = TransactionRequest::from_bytes(&tx_bytes.0, chain_id)?; - let system_contracts = self.system_contracts_for_tx(tx_req.from.unwrap_or_default())?; + // Impersonation does not matter in this context so we assume the tx is not impersonated: + // system contracts here are fetched solely to check for EVM emulator. + let system_contracts = self + .system_contracts + .contracts(TxExecutionMode::VerifyExecute, false); let allow_no_target = system_contracts.evm_emulator.is_some(); let mut l2_tx = L2Tx::from_request(tx_req, MAX_TX_SIZE, allow_no_target)?; @@ -154,7 +158,11 @@ impl InMemoryNo 27, ))?; - let system_contracts = self.system_contracts_for_tx(tx_req.from.unwrap_or_default())?; + // Impersonation does not matter in this context so we assume the tx is not impersonated: + // system contracts here are fetched solely to check for EVM emulator. + let system_contracts = self + .system_contracts + .contracts(TxExecutionMode::VerifyExecute, false); let allow_no_target = system_contracts.evm_emulator.is_some(); let mut l2_tx: L2Tx = L2Tx::from_request(tx_req, MAX_TX_SIZE, allow_no_target)?; diff --git a/src/node/evm.rs b/src/node/evm.rs index 2979714f..e34ba3d5 100644 --- a/src/node/evm.rs +++ b/src/node/evm.rs @@ -36,6 +36,7 @@ impl EvmNamespa tracing::error!("failed mining block: {:?}", err); into_jsrpc_error(Web3Error::InternalError(err)) }) + .map(|_| "0x0".to_string()) .into_boxed_future() } diff --git a/src/node/in_memory.rs b/src/node/in_memory.rs index fee137a7..7492734c 100644 --- a/src/node/in_memory.rs +++ b/src/node/in_memory.rs @@ -1,15 +1,15 @@ //! In-memory node, that supports forking other networks. +use anyhow::{anyhow, Context as _}; +use colored::Colorize; +use indexmap::IndexMap; +use once_cell::sync::OnceCell; +use std::sync::{RwLockReadGuard, RwLockWriteGuard}; use std::{ collections::{HashMap, HashSet}, convert::TryInto, str::FromStr, sync::{Arc, RwLock}, }; - -use anyhow::Context as _; -use colored::Colorize; -use indexmap::IndexMap; -use once_cell::sync::OnceCell; use zksync_contracts::BaseSystemContracts; use zksync_multivm::{ interface::{ @@ -275,6 +275,7 @@ impl InMemoryNodeInner { config: &TestNodeConfig, time: &TimestampManager, impersonation: ImpersonationManager, + system_contracts: SystemContracts, ) -> Self { let updated_config = config.clone(); if config.enable_auto_impersonate { @@ -319,10 +320,7 @@ impl InMemoryNodeInner { ), config: updated_config.clone(), console_log_handler: ConsoleLogHandler::default(), - system_contracts: SystemContracts::from_options( - &updated_config.system_contracts_options, - updated_config.use_evm_emulator, - ), + system_contracts, impersonation, rich_accounts: HashSet::new(), previous_states: Default::default(), @@ -360,10 +358,7 @@ impl InMemoryNodeInner { ), config: config.clone(), console_log_handler: ConsoleLogHandler::default(), - system_contracts: SystemContracts::from_options( - &config.system_contracts_options, - config.use_evm_emulator, - ), + system_contracts, impersonation, rich_accounts: HashSet::new(), previous_states: Default::default(), @@ -972,6 +967,7 @@ pub struct InMemoryNode { pub(crate) observability: Option, pub(crate) pool: TxPool, pub(crate) sealer: BlockSealer, + pub(crate) system_contracts: SystemContracts, } fn contract_address_from_tx_result(execution_result: &VmExecutionResultAndLogs) -> Option { @@ -1009,7 +1005,17 @@ impl InMemoryNode { sealer: BlockSealer, ) -> Self { let system_contracts_options = config.system_contracts_options; - let inner = InMemoryNodeInner::new(fork, config, &time, impersonation.clone()); + 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)), snapshots: Default::default(), @@ -1019,6 +1025,7 @@ impl InMemoryNode { observability, pool, sealer, + system_contracts, } } @@ -1041,6 +1048,18 @@ impl InMemoryNode { self.inner.clone() } + pub fn read_inner(&self) -> anyhow::Result>> { + self.inner + .read() + .map_err(|e| anyhow!("InMemoryNode lock is poisoned: {}", e)) + } + + pub fn write_inner(&self) -> anyhow::Result>> { + self.inner + .write() + .map_err(|e| anyhow!("InMemoryNode lock is poisoned: {}", e)) + } + pub fn get_cache_config(&self) -> Result { let inner = self .inner @@ -1068,7 +1087,13 @@ impl InMemoryNode { pub fn reset(&self, fork: Option) -> Result<(), String> { let config = self.get_config()?; - let inner = InMemoryNodeInner::new(fork, &config, &self.time, self.impersonation.clone()); + let inner = InMemoryNodeInner::new( + fork, + &config, + &self.time, + self.impersonation.clone(), + self.system_contracts.clone(), + ); let mut writer = self .snapshots @@ -1142,31 +1167,17 @@ impl InMemoryNode { inner.rich_accounts.insert(address); } - pub fn system_contracts_for_l2_call(&self) -> anyhow::Result { - let inner = self - .inner - .read() - .map_err(|_| anyhow::anyhow!("Failed to acquire read lock"))?; - Ok(inner.system_contracts.contracts_for_l2_call().clone()) - } - pub fn system_contracts_for_tx( &self, tx_initiator: Address, ) -> anyhow::Result { - let inner = self - .inner - .read() - .map_err(|_| anyhow::anyhow!("Failed to acquire read lock"))?; - Ok(if inner.impersonation.is_impersonating(&tx_initiator) { + Ok(if self.impersonation.is_impersonating(&tx_initiator) { tracing::info!("🕵️ Executing tx from impersonated account {tx_initiator:?}"); - inner - .system_contracts + self.system_contracts .contracts(TxExecutionMode::VerifyExecute, true) .clone() } else { - inner - .system_contracts + self.system_contracts .contracts(TxExecutionMode::VerifyExecute, false) .clone() }) @@ -1808,6 +1819,13 @@ impl InMemoryNode { transaction.transaction_index = Some(Index::zero()); 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)); } diff --git a/src/node/in_memory_ext.rs b/src/node/in_memory_ext.rs index b1b81c61..57f367a2 100644 --- a/src/node/in_memory_ext.rs +++ b/src/node/in_memory_ext.rs @@ -1,3 +1,5 @@ +use crate::namespaces::DetailedTransaction; +use crate::node::pool::TxBatch; use crate::node::sealer::BlockSealerMode; use crate::utils::Numeric; use crate::{ @@ -9,10 +11,12 @@ use crate::{ use anyhow::{anyhow, Context}; use std::convert::TryInto; use std::time::Duration; +use zksync_multivm::interface::{ExecutionResult, TxExecutionMode}; +use zksync_types::api::{Block, TransactionVariant}; use zksync_types::{ get_code_key, get_nonce_key, utils::{nonces_to_full_nonce, storage_key_for_eth_balance}, - StorageKey, + L2BlockNumber, StorageKey, }; use zksync_types::{AccountTreeId, Address, H256, U256, U64}; use zksync_utils::u256_to_h256; @@ -72,16 +76,62 @@ impl InMemoryNo /// /// # Returns /// The string "0x0". - pub fn mine_block(&self) -> Result { - let bootloader_code = self - .get_inner() - .read() - .map_err(|err| anyhow!("failed acquiring lock: {:?}", err)) - .map(|inner| inner.system_contracts.contracts_for_l2_call().clone())?; - let block_number = - self.seal_block(&mut self.time.lock(), vec![], bootloader_code.clone())?; + pub 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 block_number = self.seal_block(&mut self.time.lock(), txs, base_system_contracts)?; tracing::info!("👷 Mined block #{}", block_number); - Ok("0x0".to_string()) + 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, revert_reason) = match &tx_result.info.result.result { + ExecutionResult::Success { output } => (Some(output.clone().into()), None), + ExecutionResult::Revert { output } => ( + Some(output.encoded_data().into()), + Some(output.to_user_friendly_string()), + ), + // Halted transaction should never be a part of a block + ExecutionResult::Halt { .. } => unreachable!(), + }; + DetailedTransaction { + inner: tx, + output, + revert_reason, + } + } + TransactionVariant::Hash(_) => { + unreachable!() + } + }) + .collect(); + Ok(block.with_transactions(detailed_txs)) } /// Snapshot the state of the blockchain at the current block. Takes no parameters. Returns the id of the snapshot @@ -93,37 +143,34 @@ impl InMemoryNo /// The `U64` identifier for this snapshot. pub fn snapshot(&self) -> Result { let snapshots = self.snapshots.clone(); - self.get_inner() - .write() - .map_err(|err| anyhow!("failed acquiring lock: {:?}", err)) - .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 - )); - } - - 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) - }) + 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 + )); + } + + 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) + }) } /// Revert the state of the blockchain to a previous snapshot. Takes a single parameter, @@ -137,70 +184,61 @@ impl InMemoryNo /// `true` if a snapshot was reverted, otherwise `false`. pub fn revert_snapshot(&self, snapshot_id: U64) -> Result { let snapshots = self.snapshots.clone(); - self.get_inner() - .write() - .map_err(|err| anyhow!("failed acquiring lock: {:?}", err)) - .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)); - } + 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)); + } - // 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) + .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.get_inner() - .write() - .map_err(|err| anyhow!("failed acquiring lock: {:?}", err)) - .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 - }) + 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 fn set_nonce(&self, address: Address, nonce: U256) -> Result { - self.get_inner() - .write() - .map_err(|err| anyhow!("failed acquiring lock: {:?}", err)) - .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 - }) + 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 fn mine_blocks(&self, num_blocks: Option, interval: Option) -> Result<()> { @@ -214,16 +252,22 @@ impl InMemoryNo anyhow::bail!("Provided interval is `0`; unable to produce {num_blocks} blocks with the same timestamp"); } - let bootloader_code = self - .get_inner() - .read() - .map_err(|err| anyhow!("failed acquiring lock: {:?}", err)) - .map(|inner| inner.system_contracts.contracts_for_l2_call().clone())?; + // 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)); for _ in 0..num_blocks { - self.seal_block(&mut time, vec![], bootloader_code.clone())?; + 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)?; } tracing::info!("👷 Mined {} blocks", num_blocks); @@ -314,42 +358,36 @@ impl InMemoryNo } pub fn set_code(&self, address: Address, code: String) -> Result<()> { - self.get_inner() - .write() - .map_err(|err| anyhow!("failed acquiring lock: {:?}", err)) - .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(()) - }) + 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 fn set_storage_at(&self, address: Address, slot: U256, value: U256) -> Result { - self.get_inner() - .write() - .map_err(|err| anyhow!("failed acquiring lock: {:?}", err)) - .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 - }) + 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 fn set_logging_enabled(&self, enable: bool) -> Result<()> { @@ -596,6 +634,7 @@ mod tests { observability: None, pool, sealer: BlockSealer::default(), + system_contracts: Default::default(), }; let address = Address::from_str("0x36615Cf349d7F6344891B1e7CA7C72883F5dc049").unwrap(); @@ -720,12 +759,7 @@ mod tests { let value = U256::from(42); let key = StorageKey::new(AccountTreeId::new(address), u256_to_h256(slot)); - let value_before = node - .get_inner() - .write() - .unwrap() - .fork_storage - .read_value(&key); + let value_before = node.write_inner().unwrap().fork_storage.read_value(&key); assert_eq!(H256::default(), value_before); let result = node @@ -733,12 +767,7 @@ mod tests { .expect("failed setting value"); assert!(result); - let value_after = node - .get_inner() - .write() - .unwrap() - .fork_storage - .read_value(&key); + let value_after = node.write_inner().unwrap().fork_storage.read_value(&key); assert_eq!(value, h256_to_u256(value_after)); } @@ -958,7 +987,7 @@ mod tests { .unwrap() .expect("block exists"); let result = node.mine_block().expect("mine_block"); - assert_eq!(&result, "0x0"); + assert_eq!(result, L2BlockNumber(1)); let current_block = node .get_block_by_number(zksync_types::api::BlockNumber::Latest, false) @@ -970,7 +999,7 @@ mod tests { assert_eq!(start_block.timestamp + 1, current_block.timestamp); let result = node.mine_block().expect("mine_block"); - assert_eq!(&result, "0x0"); + assert_eq!(result, L2BlockNumber(start_block.number.as_u32() + 2)); let current_block = node .get_block_by_number(zksync_types::api::BlockNumber::Latest, false) diff --git a/src/node/zks.rs b/src/node/zks.rs index a2ed789a..101d385b 100644 --- a/src/node/zks.rs +++ b/src/node/zks.rs @@ -359,6 +359,7 @@ impl ZksNamespa block_number: zksync_types::L2BlockNumber, ) -> RpcResult> { let inner = self.get_inner().clone(); + let base_system_contracts_hashes = self.system_contracts.base_system_contracts_hashes(); Box::pin(async move { let reader = inner.read().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(); @@ -389,10 +390,7 @@ impl ZksNamespa 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()), - base_system_contracts_hashes: reader - .system_contracts - .baseline_contracts - .hashes(), + base_system_contracts_hashes, }, operator_address: Address::zero(), protocol_version: Some(ProtocolVersionId::latest()), diff --git a/src/system_contracts.rs b/src/system_contracts.rs index 9b8502cb..8d7264d2 100644 --- a/src/system_contracts.rs +++ b/src/system_contracts.rs @@ -1,14 +1,13 @@ use clap::ValueEnum; use serde::Deserialize; use zksync_contracts::{ - read_bootloader_code, read_sys_contract_bytecode, BaseSystemContracts, ContractLanguage, - SystemContractCode, + read_bootloader_code, read_sys_contract_bytecode, BaseSystemContracts, + BaseSystemContractsHashes, ContractLanguage, SystemContractCode, }; use zksync_multivm::interface::TxExecutionMode; -use zksync_types::system_contracts::get_system_smart_contracts; use zksync_utils::{bytecode::hash_bytecode, bytes_to_be_words}; -use crate::deps::system_contracts::{bytecode_from_slice, COMPILED_IN_SYSTEM_CONTRACTS}; +use crate::deps::system_contracts::bytecode_from_slice; #[derive(Deserialize, Default, Debug, Copy, Clone, PartialEq, ValueEnum)] pub enum Options { @@ -24,21 +23,11 @@ pub enum Options { /// Holds the system contracts (and bootloader) that are used by the in-memory node. #[derive(Debug, Clone)] pub struct SystemContracts { - pub baseline_contracts: BaseSystemContracts, - pub playground_contracts: BaseSystemContracts, - pub fee_estimate_contracts: BaseSystemContracts, - pub baseline_impersonating_contracts: BaseSystemContracts, - pub fee_estimate_impersonating_contracts: BaseSystemContracts, -} - -pub fn get_deployed_contracts( - options: &Options, - use_evm_emulator: bool, -) -> Vec { - match options { - Options::BuiltIn | Options::BuiltInWithoutSecurity => COMPILED_IN_SYSTEM_CONTRACTS.clone(), - Options::Local => get_system_smart_contracts(use_evm_emulator), - } + baseline_contracts: BaseSystemContracts, + playground_contracts: BaseSystemContracts, + fee_estimate_contracts: BaseSystemContracts, + baseline_impersonating_contracts: BaseSystemContracts, + fee_estimate_impersonating_contracts: BaseSystemContracts, } impl Default for SystemContracts { @@ -66,6 +55,7 @@ impl SystemContracts { ), } } + pub fn contracts_for_l2_call(&self) -> &BaseSystemContracts { self.contracts(TxExecutionMode::EthCall, false) } @@ -95,6 +85,10 @@ impl SystemContracts { } } } + + pub fn base_system_contracts_hashes(&self) -> BaseSystemContractsHashes { + self.baseline_contracts.hashes() + } } /// Creates BaseSystemContracts object with a specific bootloader. @@ -152,7 +146,7 @@ fn bsc_load_with_bootloader( } /// BaseSystemContracts with playground bootloader - used for handling 'eth_calls'. -pub fn playground(options: &Options, use_evm_emulator: bool) -> BaseSystemContracts { +fn playground(options: &Options, use_evm_emulator: bool) -> BaseSystemContracts { let bootloader_bytecode = match options { Options::BuiltIn | Options::BuiltInWithoutSecurity => { include_bytes!("deps/contracts/playground_batch.yul.zbin").to_vec() @@ -169,7 +163,7 @@ pub fn playground(options: &Options, use_evm_emulator: bool) -> BaseSystemContra /// /// A `BaseSystemContracts` struct containing the system contracts used for handling 'eth_estimateGas'. /// It sets ENSURE_RETURNED_MAGIC to 0 and BOOTLOADER_TYPE to 'playground_block' -pub fn fee_estimate_contracts(options: &Options, use_evm_emulator: bool) -> BaseSystemContracts { +fn fee_estimate_contracts(options: &Options, use_evm_emulator: bool) -> BaseSystemContracts { let bootloader_bytecode = match options { Options::BuiltIn | Options::BuiltInWithoutSecurity => { include_bytes!("deps/contracts/fee_estimate.yul.zbin").to_vec() @@ -180,7 +174,7 @@ pub fn fee_estimate_contracts(options: &Options, use_evm_emulator: bool) -> Base bsc_load_with_bootloader(bootloader_bytecode, options, use_evm_emulator) } -pub fn fee_estimate_impersonating_contracts( +fn fee_estimate_impersonating_contracts( options: &Options, use_evm_emulator: bool, ) -> BaseSystemContracts { @@ -195,7 +189,7 @@ pub fn fee_estimate_impersonating_contracts( bsc_load_with_bootloader(bootloader_bytecode, options, use_evm_emulator) } -pub fn baseline_contracts(options: &Options, use_evm_emulator: bool) -> BaseSystemContracts { +fn baseline_contracts(options: &Options, use_evm_emulator: bool) -> BaseSystemContracts { let bootloader_bytecode = match options { Options::BuiltIn | Options::BuiltInWithoutSecurity => { include_bytes!("deps/contracts/proved_batch.yul.zbin").to_vec() @@ -205,7 +199,7 @@ pub fn baseline_contracts(options: &Options, use_evm_emulator: bool) -> BaseSyst bsc_load_with_bootloader(bootloader_bytecode, options, use_evm_emulator) } -pub fn baseline_impersonating_contracts( +fn baseline_impersonating_contracts( options: &Options, use_evm_emulator: bool, ) -> BaseSystemContracts {