diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md index 905d4f0e..1b4878e2 100644 --- a/SUPPORTED_APIS.md +++ b/SUPPORTED_APIS.md @@ -14,6 +14,12 @@ The `status` options are: | Namespace | API |
Status
| 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 | diff --git a/e2e-tests-rust/src/lib.rs b/e2e-tests-rust/src/lib.rs index 7ee02df6..94fd15ff 100644 --- a/e2e-tests-rust/src/lib.rs +++ b/e2e-tests-rust/src/lib.rs @@ -1,3 +1,4 @@ +use alloy::primitives::{Address, TxHash}; use alloy::providers::{Provider, ProviderCall}; use alloy::rpc::client::NoParams; use alloy::transports::Transport; @@ -22,6 +23,24 @@ where .request("anvil_setIntervalMining", (seconds,)) .into() } + + fn drop_transaction(&self, hash: TxHash) -> ProviderCall> { + self.client() + .request("anvil_dropTransaction", (hash,)) + .into() + } + + fn drop_all_transactions(&self) -> ProviderCall { + self.client() + .request_noparams("anvil_dropAllTransactions") + .into() + } + + fn remove_pool_transactions(&self, address: Address) -> ProviderCall { + self.client() + .request("anvil_removePoolTransactions", (address,)) + .into() + } } impl EraTestNodeApiProvider for P diff --git a/e2e-tests-rust/tests/lib.rs b/e2e-tests-rust/tests/lib.rs index 1563e06b..dea3e988 100644 --- a/e2e-tests-rust/tests/lib.rs +++ b/e2e-tests-rust/tests/lib.rs @@ -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(()) +} diff --git a/spec-tests/Cargo.lock b/spec-tests/Cargo.lock index ed8edc00..c4ad9b70 100644 --- a/spec-tests/Cargo.lock +++ b/spec-tests/Cargo.lock @@ -4530,7 +4530,7 @@ dependencies = [ [[package]] name = "zksync_basic_types" version = "0.1.0" -source = "git+https://github.com/matter-labs/zksync-era.git?rev=ad5d42e0c7b50068ff0fd501c2b2ee549410adf8#ad5d42e0c7b50068ff0fd501c2b2ee549410adf8" +source = "git+https://github.com/matter-labs/zksync-era.git?rev=6c034f6e180cc92e99766f14c8840c90efa56cec#6c034f6e180cc92e99766f14c8840c90efa56cec" dependencies = [ "anyhow", "chrono", diff --git a/spec-tests/Cargo.toml b/spec-tests/Cargo.toml index db35a622..d486a236 100644 --- a/spec-tests/Cargo.toml +++ b/spec-tests/Cargo.toml @@ -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" diff --git a/src/namespaces/anvil.rs b/src/namespaces/anvil.rs index b55f1c7c..ec94c25c 100644 --- a/src/namespaces/anvil.rs +++ b/src/namespaces/anvil.rs @@ -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>; + + /// 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 diff --git a/src/node/anvil.rs b/src/node/anvil.rs index ad18e12f..363fe782 100644 --- a/src/node/anvil.rs +++ b/src/node/anvil.rs @@ -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; @@ -12,6 +12,33 @@ use crate::{ impl AnvilNamespaceT for InMemoryNode { + fn drop_transaction(&self, hash: H256) -> RpcResult> { + 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 { self.get_immediate_sealing() .map_err(|err| { diff --git a/src/node/in_memory_ext.rs b/src/node/in_memory_ext.rs index 55b6c505..5b51cd71 100644 --- a/src/node/in_memory_ext.rs +++ b/src/node/in_memory_ext.rs @@ -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 = anyhow::Result; @@ -400,6 +400,20 @@ impl InMemoryNo self.sealer.set_mode(sealing_mode); Ok(()) } + + pub fn drop_transaction(&self, hash: H256) -> Result> { + 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)] diff --git a/src/node/pool.rs b/src/node/pool.rs index e4852692..922ed34d 100644 --- a/src/node/pool.rs +++ b/src/node/pool.rs @@ -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 { @@ -21,6 +23,30 @@ impl TxPool { guard.push(tx); } + /// Removes a single transaction from the pool + pub fn drop_transaction(&self, hash: H256) -> Option { + 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 { + 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.