Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: impl hardhat_impersonateAccount and hardhat_stopImpersonationAccount #125

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions SUPPORTED_APIS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ The `status` options are:
| `EVM` | `evm_snapshot` | `NOT IMPLEMENTED`<br />[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`<br />[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 |
Expand All @@ -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`<br />[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 <br />_(default is `260`)_ |
| [`NETWORK`](#network-namespace) | [`net_peerCount`](#net_peercount) | `SUPPORTED` | Returns the number of peers currently connected to the client <br/>_(hard-coded to `0`)_ |
| [`NETWORK`](#network-namespace) | [`net_listening`](#net_listening) | `SUPPORTED` | Returns `true` if the client is actively listening for network connections <br />_(hard-coded to `false`)_ |
Expand Down Expand Up @@ -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`
Expand Down
148 changes: 146 additions & 2 deletions src/hardhat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,34 @@ pub trait HardhatNamespaceT {
num_blocks: Option<U64>,
interval: Option<U64>,
) -> BoxFuture<Result<bool>>;

/// 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<Result<bool>>;

/// 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<Result<bool>>;
}

impl<S: Send + Sync + 'static + ForkSource + std::fmt::Debug> HardhatNamespaceT
Expand Down Expand Up @@ -174,15 +202,55 @@ impl<S: Send + Sync + 'static + ForkSource + std::fmt::Debug> HardhatNamespaceT
}
})
}

fn impersonate_account(&self, address: Address) -> BoxFuture<jsonrpc_core::Result<bool>> {
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<Result<bool>> {
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)]
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() {
Expand Down Expand Up @@ -268,7 +336,8 @@ mod tests {
#[tokio::test]
async fn test_hardhat_mine_custom() {
let node = InMemoryNode::<HttpForkSource>::default();
let hardhat = HardhatNamespaceImpl::new(node.get_inner());
let hardhat: HardhatNamespaceImpl<HttpForkSource> =
HardhatNamespaceImpl::new(node.get_inner());

let start_block = node
.get_block_by_number(zksync_types::api::BlockNumber::Latest, false)
Expand Down Expand Up @@ -299,4 +368,79 @@ mod tests {
);
}
}

#[tokio::test]
async fn test_impersonate_account() {
let node = InMemoryNode::<HttpForkSource>::default();
let hardhat: HardhatNamespaceImpl<HttpForkSource> =
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());
}
}
40 changes: 36 additions & 4 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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},
};
Expand All @@ -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::{
Expand Down Expand Up @@ -246,6 +246,7 @@ pub struct InMemoryNodeInner<S> {
pub resolve_hashes: bool,
pub console_log_handler: ConsoleLogHandler,
pub system_contracts: SystemContracts,
pub impersonated_accounts: HashSet<Address>,
}

type L2TxResult = (
Expand Down Expand Up @@ -557,6 +558,17 @@ impl<S: std::fmt::Debug + ForkSource> InMemoryNodeInner<S> {
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<T: Send + 'static>(
Expand Down Expand Up @@ -635,6 +647,7 @@ impl<S: ForkSource + std::fmt::Debug> InMemoryNode<S> {
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::<u64, H256>::new();
Expand Down Expand Up @@ -664,6 +677,7 @@ impl<S: ForkSource + std::fmt::Debug> InMemoryNode<S> {
resolve_hashes,
console_log_handler: ConsoleLogHandler::default(),
system_contracts: SystemContracts::from_options(system_contracts_options),
impersonated_accounts: Default::default(),
}
};

Expand Down Expand Up @@ -935,7 +949,24 @@ impl<S: ForkSource + std::fmt::Debug> InMemoryNode<S> {

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)
MexicanAce marked this conversation as resolved.
Show resolved Hide resolved
} else {
inner.system_contracts.contracts(execution_mode)
}
};

let block_context = inner.create_block_context();
let block_properties = InMemoryNodeInner::<S>::create_block_properties(bootloader_code);
Expand All @@ -952,6 +983,7 @@ impl<S: ForkSource + std::fmt::Debug> InMemoryNode<S> {
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)
Expand Down
26 changes: 26 additions & 0 deletions test_endpoints.http
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down