diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md index 8b5a8de3..f1bc609b 100644 --- a/SUPPORTED_APIS.md +++ b/SUPPORTED_APIS.md @@ -90,7 +90,7 @@ The `status` options are: | `EVM` | `evm_snapshot` | `NOT IMPLEMENTED`
[GitHub Issue #69](https://github.com/matter-labs/era-test-node/issues/69) | Snapshot the state of the blockchain at the current block | | `HARDHAT` | `hardhat_addCompilationResult` | `NOT IMPLEMENTED` | Add information about compiled contracts | | `HARDHAT` | `hardhat_dropTransaction` | `NOT IMPLEMENTED` | Remove a transaction from the mempool | -| `HARDHAT` | `hardhat_impersonateAccount` | `NOT IMPLEMENTED`
[GitHub Issue #73](https://github.com/matter-labs/era-test-node/issues/73) | Impersonate an account | +| [`HARDHAT`](#hardhat-namespace) | [`hardhat_impersonateAccount`](#hardhat_impersonateaccount) | `SUPPORTED` | Impersonate an account | | `HARDHAT` | `hardhat_getAutomine` | `NOT IMPLEMENTED` | Returns `true` if automatic mining is enabled, and `false` otherwise | | `HARDHAT` | `hardhat_metadata` | `NOT IMPLEMENTED` | Returns the metadata of the current network | | [`HARDHAT`](#hardhat-namespace) | [`hardhat_mine`](#hardhat_mine) | Mine any number of blocks at once, in constant time | @@ -104,7 +104,7 @@ The `status` options are: | `HARDHAT` | `hardhat_setPrevRandao` | `NOT IMPLEMENTED` | Sets the PREVRANDAO value of the next block | | [`HARDHAT`](#hardhat-namespace) | [`hardhat_setNonce`](#hardhat_setnonce) | `SUPPORTED` | Sets the nonce of a given account | | `HARDHAT` | `hardhat_setStorageAt` | `NOT IMPLEMENTED` | Sets the storage value at a given key for a given account | -| `HARDHAT` | `hardhat_stopImpersonatingAccount` | `NOT IMPLEMENTED`
[GitHub Issue #74](https://github.com/matter-labs/era-test-node/issues/74) | Stop impersonating an account after having previously used `hardhat_impersonateAccount` | +| [`HARDHAT`](#hardhat-namespace) | [`hardhat_stopImpersonatingAccount`](#hardhat_stopimpersonatingaccount) | `SUPPORTED` | Stop impersonating an account after having previously used `hardhat_impersonateAccount` | | [`NETWORK`](#network-namespace) | [`net_version`](#net_version) | `SUPPORTED` | Returns the current network id
_(default is `260`)_ | | [`NETWORK`](#network-namespace) | [`net_peerCount`](#net_peercount) | `SUPPORTED` | Returns the number of peers currently connected to the client
_(hard-coded to `0`)_ | | [`NETWORK`](#network-namespace) | [`net_listening`](#net_listening) | `SUPPORTED` | Returns `true` if the client is actively listening for network connections
_(hard-coded to `false`)_ | @@ -909,6 +909,60 @@ curl --request POST \ "0x100" ] }' + +``` +### `hardhat_impersonateAccount` + +[source](src/hardhat.rs) + +Begin impersonating account- subsequent transactions sent to the node will be committed as if they were initiated by the supplied address. + +#### Arguments + +- `address: Address` - The address to begin impersonating + +#### Example + +```bash +curl --request POST \ + --url http://localhost:8011/ \ + --header 'content-type: application/json' \ + --data '{ + "jsonrpc": "2.0", + "id": "2", + "method": "hardhat_impersonateAccount", + "params": [ + "0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6" + ] +}' +``` + +### `hardhat_stopImpersonatingAccount` + +[source](src/hardhat.rs) + +Stop impersonating account, should be used after calling `hardhat_impersonateAccount`. +Since we only impersonate one account at a time, the `address` argument is ignored and the current +impersonated account (if any) is cleared. + +#### Arguments + +- `address: Address` - (Optional) Argument accepted for compatibility and will be ignored + +#### Example + +```bash +curl --request POST \ + --url http://localhost:8011/ \ + --header 'content-type: application/json' \ + --data '{ + "jsonrpc": "2.0", + "id": "2", + "method": "hardhat_stopImpersonatingAccount", + "params": [ + "0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6" + ] +}' ``` ## `EVM NAMESPACE` diff --git a/src/hardhat.rs b/src/hardhat.rs index 52063870..0413128e 100644 --- a/src/hardhat.rs +++ b/src/hardhat.rs @@ -71,6 +71,34 @@ pub trait HardhatNamespaceT { num_blocks: Option, interval: Option, ) -> BoxFuture>; + + /// Hardhat Network allows you to send transactions impersonating specific account and contract addresses. + /// To impersonate an account use this method, passing the address to impersonate as its parameter. + /// After calling this method, any transactions with this sender will be executed without verification. + /// Multiple addresses can be impersonated at once. + /// + /// # Arguments + /// + /// * `address` - The address to impersonate + /// + /// # Returns + /// + /// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation. + #[rpc(name = "hardhat_impersonateAccount")] + fn impersonate_account(&self, address: Address) -> BoxFuture>; + + /// Use this method to stop impersonating an account after having previously used `hardhat_impersonateAccount` + /// The method returns `true` if the account was being impersonated and `false` otherwise. + /// + /// # Arguments + /// + /// * `address` - The address to stop impersonating. + /// + /// # Returns + /// + /// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation. + #[rpc(name = "hardhat_stopImpersonatingAccount")] + fn stop_impersonating_account(&self, address: Address) -> BoxFuture>; } impl HardhatNamespaceT @@ -174,6 +202,45 @@ impl HardhatNamespaceT } }) } + + fn impersonate_account(&self, address: Address) -> BoxFuture> { + let inner = Arc::clone(&self.node); + Box::pin(async move { + match inner.write() { + Ok(mut inner) => { + if inner.set_impersonated_account(address) { + log::info!("🕵️ Account {:?} has been impersonated", address); + Ok(true) + } else { + log::info!("🕵️ Account {:?} was already impersonated", address); + Ok(false) + } + } + Err(_) => Err(into_jsrpc_error(Web3Error::InternalError)), + } + }) + } + + fn stop_impersonating_account(&self, address: Address) -> BoxFuture> { + let inner = Arc::clone(&self.node); + Box::pin(async move { + match inner.write() { + Ok(mut inner) => { + if inner.stop_impersonating_account(address) { + log::info!("🕵️ Stopped impersonating account {:?}", address); + Ok(true) + } else { + log::info!( + "🕵️ Account {:?} was not impersonated, nothing to stop", + address + ); + Ok(false) + } + } + Err(_) => Err(into_jsrpc_error(Web3Error::InternalError)), + } + }) + } } #[cfg(test)] @@ -181,8 +248,9 @@ mod tests { use super::*; use crate::{http_fork_source::HttpForkSource, node::InMemoryNode}; use std::str::FromStr; + use zksync_basic_types::{Nonce, H256}; use zksync_core::api_server::web3::backend_jsonrpc::namespaces::eth::EthNamespaceT; - use zksync_types::api::BlockNumber; + use zksync_types::{api::BlockNumber, fee::Fee, l2::L2Tx}; #[tokio::test] async fn test_set_balance() { @@ -268,7 +336,8 @@ mod tests { #[tokio::test] async fn test_hardhat_mine_custom() { let node = InMemoryNode::::default(); - let hardhat = HardhatNamespaceImpl::new(node.get_inner()); + let hardhat: HardhatNamespaceImpl = + HardhatNamespaceImpl::new(node.get_inner()); let start_block = node .get_block_by_number(zksync_types::api::BlockNumber::Latest, false) @@ -299,4 +368,79 @@ mod tests { ); } } + + #[tokio::test] + async fn test_impersonate_account() { + let node = InMemoryNode::::default(); + let hardhat: HardhatNamespaceImpl = + HardhatNamespaceImpl::new(node.get_inner()); + let to_impersonate = + Address::from_str("0xd8da6bf26964af9d7eed9e03e53415d37aa96045").unwrap(); + + // give impersonated account some balance + let result = hardhat + .set_balance(to_impersonate, U256::exp10(18)) + .await + .unwrap(); + assert!(result); + + // construct a tx + let mut tx = L2Tx::new( + Address::random(), + vec![], + Nonce(0), + Fee { + gas_limit: U256::from(1_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(20000), + }, + to_impersonate, + U256::one(), + None, + Default::default(), + ); + tx.set_input(vec![], H256::random()); + + // try to execute the tx- should fail without signature + assert!(node.apply_txs(vec![tx.clone()]).is_err()); + + // impersonate the account + let result = hardhat + .impersonate_account(to_impersonate) + .await + .expect("impersonate_account"); + + // result should be true + assert!(result); + + // impersonating the same account again should return false + let result = hardhat + .impersonate_account(to_impersonate) + .await + .expect("impersonate_account"); + assert!(!result); + + // execution should now succeed + assert!(node.apply_txs(vec![tx.clone()]).is_ok()); + + // stop impersonating the account + let result = hardhat + .stop_impersonating_account(to_impersonate) + .await + .expect("stop_impersonating_account"); + + // result should be true + assert!(result); + + // stop impersonating the same account again should return false + let result = hardhat + .stop_impersonating_account(to_impersonate) + .await + .expect("stop_impersonating_account"); + assert!(!result); + + // execution should now fail again + assert!(node.apply_txs(vec![tx]).is_err()); + } } diff --git a/src/node.rs b/src/node.rs index e7d3553a..3af49148 100644 --- a/src/node.rs +++ b/src/node.rs @@ -4,7 +4,7 @@ use crate::{ console_log::ConsoleLogHandler, fork::{ForkDetails, ForkSource, ForkStorage}, formatter, - system_contracts::{self, SystemContracts}, + system_contracts::{self, Options, SystemContracts}, utils::{ adjust_l1_gas_price_for_tx, derive_gas_estimation_overhead, to_human_size, IntoBoxedFuture, }, @@ -16,7 +16,7 @@ use futures::FutureExt; use jsonrpc_core::BoxFuture; use std::{ cmp::{self}, - collections::HashMap, + collections::{HashMap, HashSet}, str::FromStr, sync::{Arc, RwLock}, }; @@ -34,7 +34,7 @@ use vm::{ }; use zksync_basic_types::{ web3::{self, signing::keccak256}, - AccountTreeId, Bytes, H160, H256, U256, U64, + AccountTreeId, Address, Bytes, H160, H256, U256, U64, }; use zksync_contracts::BaseSystemContracts; use zksync_core::api_server::web3::backend_jsonrpc::{ @@ -246,6 +246,7 @@ pub struct InMemoryNodeInner { pub resolve_hashes: bool, pub console_log_handler: ConsoleLogHandler, pub system_contracts: SystemContracts, + pub impersonated_accounts: HashSet
, } type L2TxResult = ( @@ -557,6 +558,17 @@ impl InMemoryNodeInner { Some(revert) => Err(revert.revert_reason), } } + + /// Sets the `impersonated_account` field of the node. + /// This field is used to override the `tx.initiator_account` field of the transaction in the `run_l2_tx` method. + pub fn set_impersonated_account(&mut self, address: Address) -> bool { + self.impersonated_accounts.insert(address) + } + + /// Clears the `impersonated_account` field of the node. + pub fn stop_impersonating_account(&mut self, address: Address) -> bool { + self.impersonated_accounts.remove(&address) + } } fn not_implemented( @@ -635,6 +647,7 @@ impl InMemoryNode { resolve_hashes, console_log_handler: ConsoleLogHandler::default(), system_contracts: SystemContracts::from_options(system_contracts_options), + impersonated_accounts: Default::default(), } } else { let mut block_hashes = HashMap::::new(); @@ -664,6 +677,7 @@ impl InMemoryNode { resolve_hashes, console_log_handler: ConsoleLogHandler::default(), system_contracts: SystemContracts::from_options(system_contracts_options), + impersonated_accounts: Default::default(), } }; @@ -935,7 +949,24 @@ impl InMemoryNode { let mut oracle_tools = OracleTools::new(&mut storage_view, HistoryEnabled); - let bootloader_code = inner.system_contracts.contracts(execution_mode); + // 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 block_context = inner.create_block_context(); let block_properties = InMemoryNodeInner::::create_block_properties(bootloader_code); @@ -952,6 +983,7 @@ impl InMemoryNode { let spent_on_pubdata_before = vm.state.local_state.spent_pubdata_counter; let tx: Transaction = l2_tx.clone().into(); + push_transaction_to_bootloader_memory(&mut vm, &tx, execution_mode, None); let tx_result = vm .execute_next_tx(u32::MAX, true) diff --git a/test_endpoints.http b/test_endpoints.http index 51c8ece4..68d38b3d 100644 --- a/test_endpoints.http +++ b/test_endpoints.http @@ -452,6 +452,32 @@ content-type: application/json POST http://localhost:8011 content-type: application/json +{ + "jsonrpc": "2.0", + "id": "2", + "method": "hardhat_impersonateAccount", + "params": [ + "0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6" + ] +} + +### +POST http://localhost:8011 +content-type: application/json + +{ + "jsonrpc": "2.0", + "id": "2", + "method": "hardhat_stopImpersonatingAccount", + "params": [ + "0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6" + ] +} + +### +POST http://localhost:8011 +content-type: application/json + { "jsonrpc": "2.0", "id": "1",