From 3a93e58c3985e2de581607449bd709d493290df9 Mon Sep 17 00:00:00 2001 From: AntonD3 Date: Tue, 26 Sep 2023 12:22:16 +0200 Subject: [PATCH 1/3] Account impersonation using eth call bootloader --- src/node.rs | 59 +++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/node.rs b/src/node.rs index b76027cc..db65a735 100644 --- a/src/node.rs +++ b/src/node.rs @@ -5,7 +5,7 @@ use crate::{ filters::{EthFilters, FilterType, LogFilter}, fork::{ForkDetails, ForkSource, ForkStorage}, formatter, - system_contracts::{self, Options, SystemContracts}, + system_contracts::{self, SystemContracts}, utils::{ self, adjust_l1_gas_price_for_tx, bytecode_to_factory_dep, to_human_size, IntoBoxedFuture, }, @@ -760,7 +760,7 @@ impl InMemoryNode { log::info!("Running {:?} transactions (one per batch)", txs.len()); for tx in txs { - self.run_l2_tx(tx, TxExecutionMode::VerifyExecute)?; + self.run_l2_tx(tx)?; } Ok(()) @@ -1029,24 +1029,7 @@ impl InMemoryNode { let batch_env = inner.create_l1_batch_env(storage.clone()); - // if we are impersonating an account, we need to use non-verifying system contracts - let nonverifying_contracts; - let bootloader_code = { - if inner - .impersonated_accounts - .contains(&l2_tx.common_data.initiator_address) - { - tracing::info!( - "🕵️ Executing tx from impersonated account {:?}", - l2_tx.common_data.initiator_address - ); - nonverifying_contracts = - SystemContracts::from_options(&Options::BuiltInWithoutSecurity); - nonverifying_contracts.contracts(execution_mode) - } else { - inner.system_contracts.contracts(execution_mode) - } - }; + let bootloader_code = inner.system_contracts.contracts(execution_mode); let system_env = inner.create_system_env(bootloader_code.clone(), execution_mode); let mut vm = Vm::new( @@ -1206,7 +1189,7 @@ impl InMemoryNode { } /// Runs L2 transaction and commits it to a new block. - fn run_l2_tx(&self, l2_tx: L2Tx, execution_mode: TxExecutionMode) -> Result<(), String> { + fn run_l2_tx(&self, l2_tx: L2Tx) -> Result<(), String> { let tx_hash = l2_tx.hash(); log::info!(""); log::info!("Executing {}", format!("{:?}", tx_hash).bold()); @@ -1219,6 +1202,24 @@ impl InMemoryNode { inner.filters.notify_new_pending_transaction(tx_hash); } + let execution_mode = if self + .inner + .read() + .map_err(|e| format!("Failed to acquire read lock: {}", e))? + .impersonated_accounts + .contains(&l2_tx.common_data.initiator_address) + { + tracing::info!( + "🕵️ Executing tx from impersonated account {:?}", + l2_tx.common_data.initiator_address + ); + // Using the eth call here to avoid all the account checks + // TODO: think about fictive blocks in case of impersonating via eth call + TxExecutionMode::EthCall + } else { + TxExecutionMode::VerifyExecute + }; + let (keys, result, block, bytecodes) = self.run_l2_tx_inner(l2_tx.clone(), execution_mode)?; @@ -1349,12 +1350,16 @@ impl InMemoryNode { inner.filters.notify_new_block(empty_block_hash); { - // That's why here, we increase the batch by 1, but miniblock (and timestamp) by 2. - // You can look at insert_fictive_l2_block function in VM to see how this fake block is inserted. - inner.current_batch += 1; - inner.current_miniblock += 2; - inner.current_timestamp += 2; + inner.current_miniblock += 1; + inner.current_timestamp += 1; + + // If it was not eth call, we increase the batch by 1, but miniblock (and timestamp) by 2. + // You can look at insert_fictive_l2_block function in VM to see how this fake block is inserted. + if let TxExecutionMode::VerifyExecute = execution_mode { + inner.current_miniblock += 1; + inner.current_timestamp += 1; + } } Ok(()) @@ -1704,7 +1709,7 @@ impl EthNamespaceT for .boxed(); }; - match self.run_l2_tx(l2_tx.clone(), TxExecutionMode::VerifyExecute) { + match self.run_l2_tx(l2_tx.clone()) { Ok(_) => Ok(hash).into_boxed_future(), Err(e) => { let error_message = format!("Execution error: {}", e); From 802b42950f721b69ea24dd810acacac425856f1d Mon Sep 17 00:00:00 2001 From: AntonD3 Date: Wed, 27 Sep 2023 03:15:09 +0200 Subject: [PATCH 2/3] eth_sendTransaction and sync estimateGas with impersonation --- src/eth_test.rs | 13 +++++++ src/lib.rs | 1 + src/main.rs | 5 ++- src/node.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/eth_test.rs diff --git a/src/eth_test.rs b/src/eth_test.rs new file mode 100644 index 00000000..bbdebe1a --- /dev/null +++ b/src/eth_test.rs @@ -0,0 +1,13 @@ +use jsonrpc_core::{BoxFuture, Result}; +use jsonrpc_derive::rpc; +use zksync_basic_types::H256; +use zksync_types::transaction_request::CallRequest; + +/// +/// ETH namespace extension for the test node. +/// +#[rpc] +pub trait EthTestNodeNamespaceT { + #[rpc(name = "eth_sendTransaction")] + fn send_transaction(&self, tx: CallRequest) -> BoxFuture>; +} diff --git a/src/lib.rs b/src/lib.rs index 5c3c410a..1c40b764 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,7 @@ pub mod resolver; pub mod system_contracts; pub mod utils; pub mod zks; +pub mod eth_test; mod cache; mod testing; diff --git a/src/main.rs b/src/main.rs index 5fa55f2f..fa04478e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ mod configuration_api; mod console_log; mod deps; mod evm; +mod eth_test; mod filters; mod fork; mod formatter; @@ -56,6 +57,7 @@ use crate::{configuration_api::ConfigurationApiNamespace, node::TEST_NODE_NETWOR use zksync_core::api_server::web3::backend_jsonrpc::namespaces::{ eth::EthNamespaceT, net::NetNamespaceT, zks::ZksNamespaceT, }; +use crate::eth_test::EthTestNodeNamespaceT; /// List of wallets (address, private key) that we seed with tokens at start. pub const RICH_WALLETS: [(&str, &str); 10] = [ @@ -118,7 +120,8 @@ async fn build_json_http< let io_handler = { let mut io = MetaIoHandler::with_middleware(LoggingMiddleware::new(log_level_filter)); - io.extend_with(node.to_delegate()); + io.extend_with(EthNamespaceT::to_delegate(node.clone())); + io.extend_with(EthTestNodeNamespaceT::to_delegate(node)); io.extend_with(net.to_delegate()); io.extend_with(config_api.to_delegate()); io.extend_with(evm.to_delegate()); diff --git a/src/node.rs b/src/node.rs index db65a735..9c5f973a 100644 --- a/src/node.rs +++ b/src/node.rs @@ -66,6 +66,7 @@ use zksync_web3_decl::{ error::Web3Error, types::{FeeHistory, Filter, FilterChanges}, }; +use crate::eth_test::EthTestNodeNamespaceT; /// Max possible size of an ABI encoded tx (in bytes). pub const MAX_TX_SIZE: usize = 1_000_000; @@ -412,7 +413,14 @@ impl InMemoryNodeInner { let storage = storage_view.to_rc_ptr(); - let execution_mode = TxExecutionMode::EstimateFee; + let execution_mode = if self + .impersonated_accounts + .contains(&l2_tx.common_data.initiator_address) + { + TxExecutionMode::EthCall + } else { + TxExecutionMode::EstimateFee + }; let mut batch_env = self.create_l1_batch_env(storage.clone()); batch_env.l1_gas_price = l1_gas_price; let system_env = self.create_system_env( @@ -653,6 +661,15 @@ pub struct InMemoryNode { inner: Arc>>, } +// Derive doesn't work +impl Clone for InMemoryNode { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone() + } + } +} + fn contract_address_from_tx_result(execution_result: &VmExecutionResultAndLogs) -> Option { for query in execution_result.logs.storage_logs.iter().rev() { if query.log_type == StorageLogQueryType::InitialWrite @@ -2396,6 +2413,86 @@ impl EthNamespaceT for } } +impl EthTestNodeNamespaceT for InMemoryNode { + /// Sends a transaction to the L2 network. Can be used for the impersonated account. + /// + /// # Arguments + /// + /// * `tx` - A `CallRequest` struct representing the transaction. + /// + /// # Returns + /// + /// A future that resolves to the hash of the transaction if successful, or an error if the transaction is invalid or execution fails. + fn send_transaction( + &self, + tx: zksync_types::transaction_request::CallRequest, + ) -> jsonrpc_core::BoxFuture> { + let chain_id = match self.inner.read() { + Ok(reader) => reader.fork_storage.chain_id, + Err(_) => { + return futures::future::err(into_jsrpc_error(Web3Error::InternalError)).boxed() + } + }; + + let mut tx_req = TransactionRequest::from(tx); + // If the sender is impersonated signature will be ignored. + tx_req.r = Some(U256::default()); + tx_req.s = Some(U256::default()); + tx_req.v = Some(U64::from(27)); + + let hash = match tx_req.get_tx_hash(chain_id) { + Ok(result) => result, + Err(e) => { + return futures::future::err(into_jsrpc_error(Web3Error::SerializationError(e))) + .boxed() + } + }; + // v = 27 corresponds to 0 + let bytes = tx_req.get_signed_bytes(&PackedEthSignature::from_rsv(&H256::default(), &H256::default(), 0), chain_id); + let mut l2_tx: L2Tx = match L2Tx::from_request(tx_req, MAX_TX_SIZE) { + Ok(tx) => tx, + Err(e) => { + return futures::future::err(into_jsrpc_error(Web3Error::SerializationError(e))) + .boxed() + } + }; + + l2_tx.set_input(bytes, hash); + if hash != l2_tx.hash() { + return futures::future::err(into_jsrpc_error(Web3Error::InvalidTransactionData( + zksync_types::ethabi::Error::InvalidData, + ))) + .boxed(); + }; + + match self.inner.read() { + Ok(reader) => { + if !reader.impersonated_accounts.contains(&l2_tx.common_data.initiator_address) { + return futures::future::err(into_jsrpc_error(Web3Error::InvalidTransactionData( + zksync_types::ethabi::Error::InvalidData, + ))) + .boxed() + } + }, + Err(_) => { + return futures::future::err(into_jsrpc_error(Web3Error::InternalError)).boxed() + } + } + + match self.run_l2_tx(l2_tx.clone()) { + Ok(_) => Ok(hash).into_boxed_future(), + Err(e) => { + let error_message = format!("Execution error: {}", e); + futures::future::err(into_jsrpc_error(Web3Error::SubmitTransactionError( + error_message, + l2_tx.hash().as_bytes().to_vec(), + ))) + .boxed() + } + } + } +} + #[cfg(test)] mod tests { use crate::{ From 8fce06859a107b16789f0617aa545412f5ee354a Mon Sep 17 00:00:00 2001 From: AntonD3 Date: Wed, 27 Sep 2023 12:54:27 +0200 Subject: [PATCH 3/3] cargo fmt --- src/lib.rs | 2 +- src/main.rs | 4 ++-- src/node.rs | 32 ++++++++++++++++++++------------ 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1c40b764..9f46bde9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,7 @@ pub mod bootloader_debug; pub mod configuration_api; pub mod console_log; pub mod deps; +pub mod eth_test; pub mod filters; pub mod fork; pub mod formatter; @@ -54,7 +55,6 @@ pub mod resolver; pub mod system_contracts; pub mod utils; pub mod zks; -pub mod eth_test; mod cache; mod testing; diff --git a/src/main.rs b/src/main.rs index fa04478e..98418579 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,8 +17,8 @@ mod cache; mod configuration_api; mod console_log; mod deps; -mod evm; mod eth_test; +mod evm; mod filters; mod fork; mod formatter; @@ -53,11 +53,11 @@ use futures::{ use jsonrpc_core::MetaIoHandler; use zksync_basic_types::{L2ChainId, H160, H256}; +use crate::eth_test::EthTestNodeNamespaceT; use crate::{configuration_api::ConfigurationApiNamespace, node::TEST_NODE_NETWORK_ID}; use zksync_core::api_server::web3::backend_jsonrpc::namespaces::{ eth::EthNamespaceT, net::NetNamespaceT, zks::ZksNamespaceT, }; -use crate::eth_test::EthTestNodeNamespaceT; /// List of wallets (address, private key) that we seed with tokens at start. pub const RICH_WALLETS: [(&str, &str); 10] = [ diff --git a/src/node.rs b/src/node.rs index 9c5f973a..511c5478 100644 --- a/src/node.rs +++ b/src/node.rs @@ -24,6 +24,7 @@ use std::{ sync::{Arc, RwLock}, }; +use crate::eth_test::EthTestNodeNamespaceT; use vm::{ constants::{ BLOCK_GAS_LIMIT, BLOCK_OVERHEAD_PUBDATA, ETH_CALL_GAS_LIMIT, MAX_PUBDATA_PER_BLOCK, @@ -66,7 +67,6 @@ use zksync_web3_decl::{ error::Web3Error, types::{FeeHistory, Filter, FilterChanges}, }; -use crate::eth_test::EthTestNodeNamespaceT; /// Max possible size of an ABI encoded tx (in bytes). pub const MAX_TX_SIZE: usize = 1_000_000; @@ -665,7 +665,7 @@ pub struct InMemoryNode { impl Clone for InMemoryNode { fn clone(&self) -> Self { Self { - inner: self.inner.clone() + inner: self.inner.clone(), } } } @@ -2413,7 +2413,9 @@ impl EthNamespaceT for } } -impl EthTestNodeNamespaceT for InMemoryNode { +impl EthTestNodeNamespaceT + for InMemoryNode +{ /// Sends a transaction to the L2 network. Can be used for the impersonated account. /// /// # Arguments @@ -2448,7 +2450,10 @@ impl EthTestNodeNamespa } }; // v = 27 corresponds to 0 - let bytes = tx_req.get_signed_bytes(&PackedEthSignature::from_rsv(&H256::default(), &H256::default(), 0), chain_id); + let bytes = tx_req.get_signed_bytes( + &PackedEthSignature::from_rsv(&H256::default(), &H256::default(), 0), + chain_id, + ); let mut l2_tx: L2Tx = match L2Tx::from_request(tx_req, MAX_TX_SIZE) { Ok(tx) => tx, Err(e) => { @@ -2462,18 +2467,21 @@ impl EthTestNodeNamespa return futures::future::err(into_jsrpc_error(Web3Error::InvalidTransactionData( zksync_types::ethabi::Error::InvalidData, ))) - .boxed(); + .boxed(); }; match self.inner.read() { Ok(reader) => { - if !reader.impersonated_accounts.contains(&l2_tx.common_data.initiator_address) { - return futures::future::err(into_jsrpc_error(Web3Error::InvalidTransactionData( - zksync_types::ethabi::Error::InvalidData, - ))) - .boxed() + if !reader + .impersonated_accounts + .contains(&l2_tx.common_data.initiator_address) + { + return futures::future::err(into_jsrpc_error( + Web3Error::InvalidTransactionData(zksync_types::ethabi::Error::InvalidData), + )) + .boxed(); } - }, + } Err(_) => { return futures::future::err(into_jsrpc_error(Web3Error::InternalError)).boxed() } @@ -2487,7 +2495,7 @@ impl EthTestNodeNamespa error_message, l2_tx.hash().as_bytes().to_vec(), ))) - .boxed() + .boxed() } } }