Skip to content

Commit

Permalink
feat: add anvil_ pool manipulation methods (#447)
Browse files Browse the repository at this point in the history
* add `anvil_` pool manipulation methods

* clippy
  • Loading branch information
itegulov authored Nov 29, 2024
1 parent 5c0d204 commit 5aa6e15
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 5 deletions.
6 changes: 6 additions & 0 deletions SUPPORTED_APIS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ The `status` options are:

| Namespace | API | <div style="width:130px">Status</div> | Description |
| --- | --- | --- | --- |
| `ANVIL` | `anvil_dropTransaction` | `SUPPORTED` | Removes a transaction from the pool |
| `ANVIL` | `anvil_dropAllTransactions` | `SUPPORTED` | Remove all transactions from the pool |
| `ANVIL` | `anvil_removePoolTransactions` | `SUPPORTED` | Remove all transactions from the pool by sender address |
| `ANVIL` | `anvil_getAutomine` | `SUPPORTED` | Get node's auto mining status |
| `ANVIL` | `anvil_setAutomine` | `SUPPORTED` | Enable or disables auto mining of new blocks |
| `ANVIL` | `anvil_setIntervalMining` | `SUPPORTED` | Set the mining behavior to interval with the given interval |
| `ANVIL` | `anvil_setBlockTimestampInterval` | `SUPPORTED` | Sets the block timestamp interval |
| `ANVIL` | `anvil_removeBlockTimestampInterval` | `SUPPORTED` | Removes the block timestamp interval |
| `ANVIL` | `anvil_setMinGasPrice` | `NOT IMPLEMENTED` | Set the minimum gas price for the node. Unsupported for ZKsync as it is only relevant for pre-EIP1559 chains |
Expand Down
19 changes: 19 additions & 0 deletions e2e-tests-rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use alloy::primitives::{Address, TxHash};
use alloy::providers::{Provider, ProviderCall};
use alloy::rpc::client::NoParams;
use alloy::transports::Transport;
Expand All @@ -22,6 +23,24 @@ where
.request("anvil_setIntervalMining", (seconds,))
.into()
}

fn drop_transaction(&self, hash: TxHash) -> ProviderCall<T, (TxHash,), Option<TxHash>> {
self.client()
.request("anvil_dropTransaction", (hash,))
.into()
}

fn drop_all_transactions(&self) -> ProviderCall<T, NoParams, ()> {
self.client()
.request_noparams("anvil_dropAllTransactions")
.into()
}

fn remove_pool_transactions(&self, address: Address) -> ProviderCall<T, (Address,), ()> {
self.client()
.request("anvil_removePoolTransactions", (address,))
.into()
}
}

impl<P, T> EraTestNodeApiProvider<T> for P
Expand Down
99 changes: 99 additions & 0 deletions e2e-tests-rust/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,102 @@ async fn dynamic_sealing_mode() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test]
async fn drop_transaction() -> anyhow::Result<()> {
// Test that we can submit two transactions and then remove one from the pool before it gets
// finalized. 3 seconds should be long enough for the entire flow to execute before the first
// block is produced.
let provider = init(|node| node.block_time(3)).await?;

// Submit two transactions
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?;

// Drop first
provider.drop_transaction(*pending_tx0.tx_hash()).await?;

// Assert first never gets finalized but the second one does
let finalization_result = tokio::time::timeout(Duration::from_secs(4), pending_tx0).await;
assert!(finalization_result.is_err());
let receipt = provider
.get_transaction_receipt(pending_tx1.await?)
.await?
.unwrap();
assert!(receipt.status());

Ok(())
}

#[tokio::test]
async fn drop_all_transactions() -> anyhow::Result<()> {
// Test that we can submit two transactions and then remove them from the pool before the get
// finalized. 3 seconds should be long enough for the entire flow to execute before the first
// block is produced.
let provider = init(|node| node.block_time(3)).await?;

// Submit two transactions
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?;

// Drop all transactions
provider.drop_all_transactions().await?;

// Neither transaction gets finalized
let finalization_result = tokio::time::timeout(Duration::from_secs(4), pending_tx0).await;
assert!(finalization_result.is_err());
let finalization_result = tokio::time::timeout(Duration::from_secs(4), pending_tx1).await;
assert!(finalization_result.is_err());

Ok(())
}

#[tokio::test]
async fn remove_pool_transactions() -> anyhow::Result<()> {
// Test that we can submit two transactions from two senders and then remove first sender's
// transaction from the pool before it gets finalized. 3 seconds should be long enough for the
// entire flow to execute before the first block is produced.
let provider = init(|node| node.block_time(3)).await?;

// Submit two transactions
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?;

// Drop first
provider.remove_pool_transactions(RICH_WALLET0).await?;

// Assert first never gets finalized but the second one does
let finalization_result = tokio::time::timeout(Duration::from_secs(4), pending_tx0).await;
assert!(finalization_result.is_err());
let receipt = provider
.get_transaction_receipt(pending_tx1.await?)
.await?
.unwrap();
assert!(receipt.status());

Ok(())
}
2 changes: 1 addition & 1 deletion spec-tests/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion spec-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ itertools = "0.13"
tracing = "0.1"
chrono = "0.4"

zksync_basic_types = { git = "https://github.com/matter-labs/zksync-era.git", rev = "ad5d42e0c7b50068ff0fd501c2b2ee549410adf8" }
zksync_basic_types = { git = "https://github.com/matter-labs/zksync-era.git", rev = "6c034f6e180cc92e99766f14c8840c90efa56cec" }

[dev-dependencies]
test-log = "0.2.16"
Expand Down
25 changes: 24 additions & 1 deletion src/namespaces/anvil.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
use jsonrpc_derive::rpc;
use zksync_types::{Address, U256, U64};
use zksync_types::{Address, H256, U256, U64};

use super::{ResetRequest, RpcResult};
use crate::utils::Numeric;

#[rpc]
pub trait AnvilNamespaceT {
/// Removes a transaction from the pool.
///
/// # Arguments
///
/// * `hash` - Hash of the transaction to be removed from the pool
///
/// # Returns
/// `Some(hash)` if transaction was in the pool before being removed, `None` otherwise
#[rpc(name = "anvil_dropTransaction")]
fn drop_transaction(&self, hash: H256) -> RpcResult<Option<H256>>;

/// Remove all transactions from the pool.
#[rpc(name = "anvil_dropAllTransactions")]
fn drop_all_transactions(&self) -> RpcResult<()>;

/// Remove all transactions from the pool by sender address.
///
/// # Arguments
///
/// * `address` - Sender which transactions should be removed from the pool
#[rpc(name = "anvil_removePoolTransactions")]
fn remove_pool_transactions(&self, address: Address) -> RpcResult<()>;

/// Gets node's auto mining status.
///
/// # Returns
Expand Down
29 changes: 28 additions & 1 deletion src/node/anvil.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use zksync_types::{Address, U256, U64};
use zksync_types::{Address, H256, U256, U64};
use zksync_web3_decl::error::Web3Error;

use crate::utils::Numeric;
Expand All @@ -12,6 +12,33 @@ use crate::{
impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> AnvilNamespaceT
for InMemoryNode<S>
{
fn drop_transaction(&self, hash: H256) -> RpcResult<Option<H256>> {
self.drop_transaction(hash)
.map_err(|err| {
tracing::error!("failed dropping transaction: {:?}", err);
into_jsrpc_error(Web3Error::InternalError(err))
})
.into_boxed_future()
}

fn drop_all_transactions(&self) -> RpcResult<()> {
self.drop_all_transactions()
.map_err(|err| {
tracing::error!("failed dropping all transactions: {:?}", err);
into_jsrpc_error(Web3Error::InternalError(err))
})
.into_boxed_future()
}

fn remove_pool_transactions(&self, address: Address) -> RpcResult<()> {
self.remove_pool_transactions(address)
.map_err(|err| {
tracing::error!("failed removing pool transactions: {:?}", err);
into_jsrpc_error(Web3Error::InternalError(err))
})
.into_boxed_future()
}

fn get_auto_mine(&self) -> RpcResult<bool> {
self.get_immediate_sealing()
.map_err(|err| {
Expand Down
16 changes: 15 additions & 1 deletion src/node/in_memory_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use zksync_types::{
utils::{nonces_to_full_nonce, storage_key_for_eth_balance},
StorageKey,
};
use zksync_types::{AccountTreeId, Address, U256, U64};
use zksync_types::{AccountTreeId, Address, H256, U256, U64};
use zksync_utils::u256_to_h256;

type Result<T> = anyhow::Result<T>;
Expand Down Expand Up @@ -400,6 +400,20 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> InMemoryNo
self.sealer.set_mode(sealing_mode);
Ok(())
}

pub fn drop_transaction(&self, hash: H256) -> Result<Option<H256>> {
Ok(self.pool.drop_transaction(hash).map(|tx| tx.hash()))
}

pub fn drop_all_transactions(&self) -> Result<()> {
self.pool.clear();
Ok(())
}

pub fn remove_pool_transactions(&self, address: Address) -> Result<()> {
self.pool.drop_transactions_by_sender(address);
Ok(())
}
}

#[cfg(test)]
Expand Down
26 changes: 26 additions & 0 deletions src/node/pool.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::node::impersonate::ImpersonationManager;
use itertools::Itertools;
use std::sync::{Arc, RwLock};
use zksync_types::l2::L2Tx;
use zksync_types::{Address, H256};

#[derive(Clone)]
pub struct TxPool {
Expand All @@ -21,6 +23,30 @@ impl TxPool {
guard.push(tx);
}

/// Removes a single transaction from the pool
pub fn drop_transaction(&self, hash: H256) -> Option<L2Tx> {
let mut guard = self.inner.write().expect("TxPool lock is poisoned");
let (position, _) = guard.iter_mut().find_position(|tx| tx.hash() == hash)?;
Some(guard.remove(position))
}

/// Remove transactions by sender
pub fn drop_transactions_by_sender(&self, sender: Address) -> Vec<L2Tx> {
let mut guard = self.inner.write().expect("TxPool lock is poisoned");
let txs = std::mem::take(&mut *guard);
let (sender_txs, other_txs) = txs
.into_iter()
.partition(|tx| tx.common_data.initiator_address == sender);
*guard = other_txs;
sender_txs
}

/// Removes all transactions from the pool
pub fn clear(&self) {
let mut guard = self.inner.write().expect("TxPool lock is poisoned");
guard.clear();
}

/// Take up to `n` continuous transactions from the pool that are all uniform in impersonation
/// type (either all are impersonating or all non-impersonating).
// TODO: We should distinguish ready transactions from non-ready ones. Only ready txs should be takeable.
Expand Down

0 comments on commit 5aa6e15

Please sign in to comment.