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: add anvil_autoImpersonateAccount #416

Merged
merged 7 commits into from
Nov 25, 2024
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
3 changes: 2 additions & 1 deletion SUPPORTED_APIS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ The `status` options are:

| Namespace | API | <div style="width:130px">Status</div> | Description |
| --- | --- | --- | --- |
| `ANVIL` | `anvil_autoImpersonateAccount` | `SUPPORTED` | Sets auto impersonation status.|
| `ANVIL` | `anvil_setNonce` | `SUPPORTED` | Sets the nonce of an address.|
| `ANVIL` | `anvil_impersonateAccount` | `SUPPORTED` | Impersonate an account |
| `ANVIL` | `anvil_stopImpersonatingAccount` | `SUPPORTED` | Stop impersonating an account after having previously used `anvil_impersonateAccount` |
| `ANVIL` | `anvil_reset` | `PARTIALLY` | Resets the state of the network; cannot revert to past block numbers, unless they're in a fork |
| `ANVIL` | `anvil_reset` | `SUPPORTED` | Resets the state of the network; cannot revert to past block numbers, unless they're in a fork |
| `ANVIL` | `anvil_mine` | `SUPPORTED` | Mine any number of blocks at once, in constant time |
| `ANVIL` | `anvil_setBalance` | `SUPPORTED` | Modifies the balance of an account |
| `ANVIL` | `anvil_setCode` | `SUPPORTED` | Sets the bytecode of a given account |
Expand Down
56 changes: 52 additions & 4 deletions e2e-tests/test/anvil-apis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,36 @@ describe("anvil_setBalance", function () {
describe("anvil_setNonce", function () {
it("Should update the nonce of an account", async function () {
// Arrange
const richWallet = new Wallet(RichAccounts[0].PrivateKey).connect(provider);
const userWallet = Wallet.createRandom().connect(provider);

// Simply asserts that `richWallet` can still send successful transactions
async function assertCanSendTx() {
const tx = {
to: userWallet.address,
value: ethers.utils.parseEther("0.42"),
};

const txResponse = await richWallet.sendTransaction(tx);
const txReceipt = await txResponse.wait();
expect(txReceipt.status).to.equal(1);
}

const newNonce = 42;

// Act
await provider.send("anvil_setNonce", [userWallet.address, ethers.utils.hexlify(newNonce)]);
// Advance nonce to 42
await provider.send("anvil_setNonce", [richWallet.address, ethers.utils.hexlify(newNonce)]);

// Assert
const nonce = await userWallet.getNonce();
expect(nonce).to.equal(newNonce);
expect(await richWallet.getNonce()).to.equal(newNonce);
await assertCanSendTx();

// Rollback nonce to 0
await provider.send("anvil_setNonce", [richWallet.address, ethers.utils.hexlify(0)]);

// Assert
expect(await richWallet.getNonce()).to.equal(0);
await assertCanSendTx();
});
});

Expand Down Expand Up @@ -88,6 +109,33 @@ describe("anvil_impersonateAccount & anvil_stopImpersonatingAccount", function (
});
});

describe("anvil_autoImpersonateAccount", function () {
it("Should allow transfers of funds without knowing the Private Key", async function () {
// Arrange
const userWallet = Wallet.createRandom().connect(provider);
const richAccount = RichAccounts[6].Account;
const beforeBalance = await provider.getBalance(richAccount);

// Act
await provider.send("anvil_autoImpersonateAccount", [true]);

const signer = await ethers.getSigner(richAccount);
const tx = {
to: userWallet.address,
value: ethers.utils.parseEther("0.42"),
};

const recieptTx = await signer.sendTransaction(tx);
await recieptTx.wait();

await provider.send("anvil_autoImpersonateAccount", [false]);

// Assert
expect((await userWallet.getBalance()).eq(ethers.utils.parseEther("0.42"))).to.true;
expect((await provider.getBalance(richAccount)).eq(beforeBalance.sub(ethers.utils.parseEther("0.42")))).to.true;
});
});

describe("anvil_setCode", function () {
it("Should set code at an address", async function () {
// Arrange
Expand Down
14 changes: 13 additions & 1 deletion src/namespaces/anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ use super::{ResetRequest, RpcResult};

#[rpc]
pub trait AnvilNamespaceT {
/// Sets auto impersonation status.
///
/// # Arguments
///
/// * `enabled` - `true` makes every account impersonated, `false` disables this behavior
///
/// # Returns
///
/// A `BoxFuture` containing a `Result` representing the success of the operation.
#[rpc(name = "anvil_autoImpersonateAccount")]
fn auto_impersonate_account(&self, enabled: bool) -> RpcResult<()>;

/// Sets the balance of the given address to the given balance.
///
/// # Arguments
Expand All @@ -29,7 +41,7 @@ pub trait AnvilNamespaceT {
///
/// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation.
#[rpc(name = "anvil_setNonce")]
fn set_nonce(&self, address: Address, balance: U256) -> RpcResult<bool>;
fn set_nonce(&self, address: Address, nonce: U256) -> RpcResult<bool>;

/// 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.
Expand Down
9 changes: 7 additions & 2 deletions src/node/anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ use crate::{
impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> AnvilNamespaceT
for InMemoryNode<S>
{
fn auto_impersonate_account(&self, enabled: bool) -> RpcResult<()> {
self.auto_impersonate_account(enabled);
Ok(()).into_boxed_future()
}

fn set_balance(&self, address: Address, balance: U256) -> RpcResult<bool> {
self.set_balance(address, balance)
.map_err(|err| {
Expand All @@ -20,8 +25,8 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> AnvilNames
.into_boxed_future()
}

fn set_nonce(&self, address: Address, balance: U256) -> RpcResult<bool> {
self.set_nonce(address, balance)
fn set_nonce(&self, address: Address, nonce: U256) -> RpcResult<bool> {
self.set_nonce(address, nonce)
.map_err(|err| {
tracing::error!("failed setting nonce: {:?}", err);
into_jsrpc_error(Web3Error::InternalError(err))
Expand Down
12 changes: 6 additions & 6 deletions src/node/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2967,7 +2967,7 @@ mod tests {
blocks: inner.blocks.clone(),
block_hashes: inner.block_hashes.clone(),
filters: inner.filters.clone(),
impersonated_accounts: inner.impersonation.impersonated_accounts(),
impersonation_state: inner.impersonation.state(),
rich_accounts: inner.rich_accounts.clone(),
previous_states: inner.previous_states.clone(),
raw_storage: storage.raw_storage.clone(),
Expand Down Expand Up @@ -3004,8 +3004,8 @@ mod tests {
assert_eq!(expected_snapshot.block_hashes, actual_snapshot.block_hashes);
assert_eq!(expected_snapshot.filters, actual_snapshot.filters);
assert_eq!(
expected_snapshot.impersonated_accounts,
actual_snapshot.impersonated_accounts
expected_snapshot.impersonation_state,
actual_snapshot.impersonation_state
);
assert_eq!(
expected_snapshot.rich_accounts,
Expand Down Expand Up @@ -3074,7 +3074,7 @@ mod tests {
blocks: inner.blocks.clone(),
block_hashes: inner.block_hashes.clone(),
filters: inner.filters.clone(),
impersonated_accounts: inner.impersonation.impersonated_accounts(),
impersonation_state: inner.impersonation.state(),
rich_accounts: inner.rich_accounts.clone(),
previous_states: inner.previous_states.clone(),
raw_storage: storage.raw_storage.clone(),
Expand Down Expand Up @@ -3144,8 +3144,8 @@ mod tests {
assert_eq!(expected_snapshot.block_hashes, inner.block_hashes);
assert_eq!(expected_snapshot.filters, inner.filters);
assert_eq!(
expected_snapshot.impersonated_accounts,
inner.impersonation.impersonated_accounts()
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);
Expand Down
40 changes: 30 additions & 10 deletions src/node/impersonate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,20 @@ use zksync_types::Address;
/// instances.
#[derive(Clone, Debug, Default)]
pub struct ImpersonationManager {
state: Arc<RwLock<HashSet<Address>>>,
state: Arc<RwLock<ImpersonationState>>,
}

impl ImpersonationManager {
/// Sets the auto impersonation flag, when `true` it makes all accounts impersonated by default.
/// Setting to `false` disabled this behavior.
pub fn set_auto_impersonation(&self, enabled: bool) {
tracing::trace!(enabled, "auto impersonation status set");
self.state
.write()
.expect("ImpersonationManager lock is poisoned")
.auto = enabled
}

/// Starts impersonation for the provided account.
///
/// Returns `true` if the account was not impersonated before.
Expand All @@ -21,7 +31,7 @@ impl ImpersonationManager {
.state
.write()
.expect("ImpersonationManager lock is poisoned");
state.insert(addr)
state.accounts.insert(addr)
}

/// Stops impersonation for the provided account.
Expand All @@ -32,30 +42,40 @@ impl ImpersonationManager {
self.state
.write()
.expect("ImpersonationManager lock is poisoned")
.accounts
.remove(addr)
}

/// Returns whether the provided account is currently impersonated.
pub fn is_impersonating(&self, addr: &Address) -> bool {
self.state
let state = self
.state
.read()
.expect("ImpersonationManager lock is poisoned")
.contains(addr)
.expect("ImpersonationManager lock is poisoned");
state.auto || state.accounts.contains(addr)
}

/// Returns all accounts that are currently being impersonated.
pub fn impersonated_accounts(&self) -> HashSet<Address> {
/// Returns internal state representation.
pub fn state(&self) -> ImpersonationState {
self.state
.read()
.expect("ImpersonationManager lock is poisoned")
.clone()
}

/// Overrides currently impersonated accounts with the provided value.
pub fn set_impersonated_accounts(&self, accounts: HashSet<Address>) {
/// Overrides current internal state with the provided value.
pub fn set_state(&self, state: ImpersonationState) {
*self
.state
.write()
.expect("ImpersonationManager lock is poisoned") = accounts;
.expect("ImpersonationManager lock is poisoned") = state;
}
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct ImpersonationState {
/// If `true` then all accounts are impersonated regardless of `accounts` contents
pub auto: bool,
/// Accounts that are currently impersonated
pub accounts: HashSet<Address>,
}
9 changes: 4 additions & 5 deletions src/node/in_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ use zksync_types::{
use zksync_utils::{bytecode::hash_bytecode, h256_to_account_address, h256_to_u256, u256_to_h256};
use zksync_web3_decl::error::Web3Error;

use crate::node::impersonate::ImpersonationManager;
use crate::node::impersonate::{ImpersonationManager, ImpersonationState};
use crate::node::time::TimestampManager;
use crate::{
bootloader_debug::{BootloaderDebug, BootloaderDebugTracer},
Expand Down Expand Up @@ -805,7 +805,7 @@ impl<S: std::fmt::Debug + ForkSource> InMemoryNodeInner<S> {
blocks: self.blocks.clone(),
block_hashes: self.block_hashes.clone(),
filters: self.filters.clone(),
impersonated_accounts: self.impersonation.impersonated_accounts(),
impersonation_state: self.impersonation.state(),
rich_accounts: self.rich_accounts.clone(),
previous_states: self.previous_states.clone(),
raw_storage: storage.raw_storage.clone(),
Expand All @@ -832,8 +832,7 @@ impl<S: std::fmt::Debug + ForkSource> InMemoryNodeInner<S> {
self.blocks = snapshot.blocks;
self.block_hashes = snapshot.block_hashes;
self.filters = snapshot.filters;
self.impersonation
.set_impersonated_accounts(snapshot.impersonated_accounts);
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;
Expand All @@ -859,7 +858,7 @@ pub struct Snapshot {
pub(crate) blocks: HashMap<H256, Block<TransactionVariant>>,
pub(crate) block_hashes: HashMap<u64, H256>,
pub(crate) filters: EthFilters,
pub(crate) impersonated_accounts: HashSet<Address>,
pub(crate) impersonation_state: ImpersonationState,
pub(crate) rich_accounts: HashSet<H160>,
pub(crate) previous_states: IndexMap<H256, HashMap<StorageKey, StorageValue>>,
pub(crate) raw_storage: InMemoryStorage,
Expand Down
47 changes: 15 additions & 32 deletions src/node/in_memory_ext.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use anyhow::anyhow;
use zksync_types::{
get_code_key, get_nonce_key,
utils::{decompose_full_nonce, nonces_to_full_nonce, storage_key_for_eth_balance},
utils::{nonces_to_full_nonce, storage_key_for_eth_balance},
StorageKey,
};
use zksync_types::{AccountTreeId, Address, U256, U64};
use zksync_utils::{h256_to_u256, u256_to_h256};
use zksync_utils::u256_to_h256;

use crate::{
fork::{ForkDetails, ForkSource},
Expand Down Expand Up @@ -179,33 +179,9 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> InMemoryNo
self.get_inner()
.write()
.map_err(|err| anyhow!("failed acquiring lock: {:?}", err))
.and_then(|mut writer| {
.map(|mut writer| {
let nonce_key = get_nonce_key(&address);
let full_nonce = match writer.fork_storage.read_value_internal(&nonce_key) {
Ok(full_nonce) => full_nonce,
Err(error) => {
return Err(anyhow!(error.to_string()));
}
};
let (mut account_nonce, mut deployment_nonce) =
decompose_full_nonce(h256_to_u256(full_nonce));
if account_nonce >= nonce {
return Err(anyhow!(
"Account Nonce is already set to a higher value ({}, requested {})",
account_nonce,
nonce
));
}
account_nonce = nonce;
if deployment_nonce >= nonce {
return Err(anyhow!(
"Deployment Nonce is already set to a higher value ({}, requested {})",
deployment_nonce,
nonce
));
}
deployment_nonce = nonce;
let enforced_full_nonce = nonces_to_full_nonce(account_nonce, deployment_nonce);
let enforced_full_nonce = nonces_to_full_nonce(nonce, nonce);
tracing::info!(
"👷 Nonces for address {:?} have been set to {}",
address,
Expand All @@ -214,7 +190,7 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> InMemoryNo
writer
.fork_storage
.set_value(nonce_key, u256_to_h256(enforced_full_nonce));
Ok(true)
true
})
}

Expand Down Expand Up @@ -302,6 +278,10 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> InMemoryNo
}
}

pub fn auto_impersonate_account(&self, enabled: bool) {
self.impersonation.set_auto_impersonation(enabled);
}

pub fn impersonate_account(&self, address: Address) -> Result<bool> {
if self.impersonation.impersonate(address) {
tracing::info!("🕵️ Account {:?} has been impersonated", address);
Expand Down Expand Up @@ -378,6 +358,7 @@ mod tests {
use zksync_multivm::interface::storage::ReadStorage;
use zksync_types::{api::BlockNumber, fee::Fee, l2::L2Tx, PackedEthSignature};
use zksync_types::{Nonce, H256};
use zksync_utils::h256_to_u256;

#[tokio::test]
async fn test_set_balance() {
Expand Down Expand Up @@ -408,9 +389,11 @@ mod tests {
assert_eq!(nonce_after, U256::from(1337));
assert_ne!(nonce_before, nonce_after);

// setting nonce lower than the current one should fail
let result = node.set_nonce(address, U256::from(1336));
assert!(result.is_err());
let result = node.set_nonce(address, U256::from(1336)).unwrap();
assert!(result);

let nonce_after = node.get_transaction_count(address, None).await.unwrap();
assert_eq!(nonce_after, U256::from(1336));
}

#[tokio::test]
Expand Down
Loading