Skip to content

Commit

Permalink
fix: proper handling of halted txs (#463)
Browse files Browse the repository at this point in the history
* proper handling of halted txs

* use upstream anvil alloy provider

* fix compilation errors after merge

* tweak `seal_block` and `apply_txs` logic to match the new flow
  • Loading branch information
itegulov authored Dec 3, 2024
1 parent 7587a7a commit 1578ee4
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 89 deletions.
14 changes: 14 additions & 0 deletions e2e-tests-rust/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 e2e-tests-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ publish = false

[dependencies]
alloy-zksync = { git = "https://github.com/itegulov/alloy-zksync.git", rev = "c43bba1a6c5e744afb975b261cba6e964d6a58c6" }
alloy = { version = "0.6", features = ["full", "rlp", "serde", "sol-types"] }
alloy = { version = "0.6", features = ["full", "rlp", "serde", "sol-types", "provider-anvil-api"] }
anyhow = "1.0"
fs2 = "0.4.3"
tokio = { version = "1", features = ["time", "rt", "process"] }
Expand Down
45 changes: 2 additions & 43 deletions e2e-tests-rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use alloy::network::Network;
use alloy::primitives::{Address, TxHash};
use alloy::providers::{Provider, ProviderCall};
use alloy::rpc::client::NoParams;
use alloy::serde::WithOtherFields;
Expand All @@ -12,48 +11,8 @@ pub trait EraTestNodeApiProvider<T>: Provider<T, Zksync>
where
T: Transport + Clone,
{
fn get_auto_mine(&self) -> ProviderCall<T, NoParams, bool> {
self.client().request_noparams("anvil_getAutomine").into()
}

fn set_auto_mine(&self, enable: bool) -> ProviderCall<T, (bool,), ()> {
self.client().request("anvil_setAutomine", (enable,)).into()
}

fn set_interval_mining(&self, seconds: u64) -> ProviderCall<T, (u64,), ()> {
self.client()
.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()
}

fn mine(
&self,
num_blocks: Option<u64>,
interval: Option<u64>,
) -> ProviderCall<T, (Option<u64>, Option<u64>), ()> {
self.client()
.request("anvil_mine", (num_blocks, interval))
.into()
}

/// Custom version of [`alloy::providers::ext::AnvilApi::anvil_mine_detailed`] that returns
/// block representation with transactions that contain extra custom fields.
fn mine_detailed(
&self,
) -> ProviderCall<
Expand Down
95 changes: 82 additions & 13 deletions e2e-tests-rust/tests/lib.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
use alloy::network::{ReceiptResponse, TransactionBuilder};
use alloy::primitives::{address, Address, U256};
use alloy::providers::ext::AnvilApi;
use alloy::providers::{PendingTransaction, PendingTransactionError, Provider, WalletProvider};
use alloy::signers::local::PrivateKeySigner;
use alloy::transports::http::{reqwest, Http};
use alloy_zksync::network::transaction_request::TransactionRequest;
use alloy_zksync::network::Zksync;
use alloy_zksync::node_bindings::EraTestNode;
use alloy_zksync::provider::{zksync_provider, ProviderBuilderExt};
use alloy_zksync::wallet::ZksyncWallet;
use era_test_node_e2e_tests::utils::LockedPort;
use era_test_node_e2e_tests::EraTestNodeApiProvider;
use std::time::Duration;

async fn init(
f: impl FnOnce(EraTestNode) -> EraTestNode,
) -> anyhow::Result<impl Provider<Http<reqwest::Client>, Zksync> + WalletProvider<Zksync> + Clone> {
) -> anyhow::Result<
impl Provider<Http<reqwest::Client>, Zksync> + WalletProvider<Zksync, Wallet = ZksyncWallet> + Clone,
> {
let locked_port = LockedPort::acquire_unused().await?;
let provider = zksync_provider()
.with_recommended_fillers()
Expand Down Expand Up @@ -133,7 +138,7 @@ async fn no_sealing_timeout() -> anyhow::Result<()> {
assert!(finalization_result.is_err());

// Mine a block manually and assert that the transaction is finalized now
provider.mine(None, None).await?;
provider.anvil_mine(None, None).await?;
let receipt = provider.get_transaction_receipt(tx_hash).await?.unwrap();
assert!(receipt.status());

Expand All @@ -144,11 +149,11 @@ async fn no_sealing_timeout() -> anyhow::Result<()> {
async fn dynamic_sealing_mode() -> anyhow::Result<()> {
// Test that we can successfully switch between different sealing modes
let provider = init(|node| node.no_mine()).await?;
assert_eq!(provider.get_auto_mine().await?, false);
assert_eq!(provider.anvil_get_auto_mine().await?, false);

// Enable immediate block sealing
provider.set_auto_mine(true).await?;
assert_eq!(provider.get_auto_mine().await?, true);
provider.anvil_set_auto_mine(true).await?;
assert_eq!(provider.anvil_get_auto_mine().await?, true);

// Check that we can finalize transactions now
let tx = TransactionRequest::default()
Expand All @@ -158,8 +163,8 @@ async fn dynamic_sealing_mode() -> anyhow::Result<()> {
assert!(receipt.status());

// Enable interval block sealing
provider.set_interval_mining(3).await?;
assert_eq!(provider.get_auto_mine().await?, false);
provider.anvil_set_interval_mining(3).await?;
assert_eq!(provider.anvil_get_auto_mine().await?, false);

// Check that we can finalize two txs in the same block now
test_finalize_two_txs_in_the_same_block(
Expand All @@ -169,8 +174,8 @@ async fn dynamic_sealing_mode() -> anyhow::Result<()> {
.await?;

// Disable block sealing entirely
provider.set_auto_mine(false).await?;
assert_eq!(provider.get_auto_mine().await?, false);
provider.anvil_set_auto_mine(false).await?;
assert_eq!(provider.anvil_get_auto_mine().await?, false);

// Check that transactions do not get finalized now
let tx = TransactionRequest::default()
Expand Down Expand Up @@ -203,7 +208,9 @@ async fn drop_transaction() -> anyhow::Result<()> {
let pending_tx1 = provider.send_transaction(tx1).await?.register().await?;

// Drop first
provider.drop_transaction(*pending_tx0.tx_hash()).await?;
provider
.anvil_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;
Expand Down Expand Up @@ -237,7 +244,7 @@ async fn drop_all_transactions() -> anyhow::Result<()> {
let pending_tx1 = provider.send_transaction(tx1).await?.register().await?;

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

// Neither transaction gets finalized
let finalization_result = tokio::time::timeout(Duration::from_secs(4), pending_tx0).await;
Expand Down Expand Up @@ -268,7 +275,9 @@ async fn remove_pool_transactions() -> anyhow::Result<()> {
let pending_tx1 = provider.send_transaction(tx1).await?.register().await?;

// Drop first
provider.remove_pool_transactions(RICH_WALLET0).await?;
provider
.anvil_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;
Expand Down Expand Up @@ -300,7 +309,7 @@ async fn manual_mining_two_txs_in_one_block() -> anyhow::Result<()> {
let pending_tx1 = provider.send_transaction(tx1).await?.register().await?;

// Mine a block manually and assert that both transactions are finalized now
provider.mine(None, None).await?;
provider.anvil_mine(None, None).await?;
let receipt0 = provider
.get_transaction_receipt(pending_tx0.await?)
.await?
Expand Down Expand Up @@ -347,3 +356,63 @@ async fn detailed_mining_success() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test]
async fn seal_block_ignoring_halted_transaction() -> anyhow::Result<()> {
// Test that we can submit three transactions (1 and 3 are successful, 2 is halting). And then
// observe a block that finalizes 1 and 3 while ignoring 2.
let mut provider = init(|node| node.block_time(3)).await?;
let signer = PrivateKeySigner::random();
let random_account = signer.address();
provider.wallet_mut().register_signer(signer);

// Impersonate random account for now so that gas estimation works as expected
provider.anvil_impersonate_account(random_account).await?;

// Submit three 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(random_account)
.with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
.with_value(U256::from(100));
let pending_tx1 = provider.send_transaction(tx1).await?.register().await?;
let tx2 = TransactionRequest::default()
.with_from(RICH_WALLET1)
.with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
.with_value(U256::from(100));
let pending_tx2 = provider.send_transaction(tx2).await?.register().await?;

// Stop impersonating random account so that tx is going to halt
provider
.anvil_stop_impersonating_account(random_account)
.await?;

// Fetch their receipts
let receipt0 = provider
.get_transaction_receipt(pending_tx0.await?)
.await?
.unwrap();
assert!(receipt0.status());
let receipt2 = provider
.get_transaction_receipt(pending_tx2.await?)
.await?
.unwrap();
assert!(receipt2.status());

// Assert that they are different txs but executed in the same block
assert_eq!(receipt0.from(), RICH_WALLET0);
assert_eq!(receipt2.from(), RICH_WALLET1);
assert_ne!(receipt0.transaction_hash(), receipt2.transaction_hash());
assert_eq!(receipt0.block_hash(), receipt2.block_hash());
assert_eq!(receipt0.block_number(), receipt2.block_number());

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

Ok(())
}
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ async fn main() -> anyhow::Result<()> {
}

if !transactions_to_replay.is_empty() {
let _ = node.apply_txs(transactions_to_replay);
let _ = node.apply_txs(transactions_to_replay, config.max_transactions);
}

for signer in config.genesis_accounts.iter() {
Expand Down
5 changes: 2 additions & 3 deletions src/namespaces/anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use zksync_types::{Address, H256, U256, U64};
pub trait AnvilNamespaceT {
/// Mines a single block in the same way as `evm_mine` but returns extra fields.
///
///
/// # Returns
/// Freshly mined block's representation along with extra fields.
#[rpc(name = "anvil_mine_detailed")]
Expand Down Expand Up @@ -246,7 +245,7 @@ pub trait AnvilNamespaceT {
///
/// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation.
#[rpc(name = "anvil_impersonateAccount")]
fn impersonate_account(&self, address: Address) -> RpcResult<bool>;
fn impersonate_account(&self, address: Address) -> RpcResult<()>;

/// Use this method to stop impersonating an account after having previously used `anvil_impersonateAccount`
/// The method returns `true` if the account was being impersonated and `false` otherwise.
Expand All @@ -259,7 +258,7 @@ pub trait AnvilNamespaceT {
///
/// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation.
#[rpc(name = "anvil_stopImpersonatingAccount")]
fn stop_impersonating_account(&self, address: Address) -> RpcResult<bool>;
fn stop_impersonating_account(&self, address: Address) -> RpcResult<()>;

/// Modifies the bytecode stored at an account's address.
///
Expand Down
6 changes: 4 additions & 2 deletions src/node/anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,17 +204,19 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> AnvilNames
.into_boxed_future()
}

fn impersonate_account(&self, address: Address) -> RpcResult<bool> {
fn impersonate_account(&self, address: Address) -> RpcResult<()> {
self.impersonate_account(address)
.map(|_| ())
.map_err(|err| {
tracing::error!("failed impersonating account: {:?}", err);
into_jsrpc_error(Web3Error::InternalError(err))
})
.into_boxed_future()
}

fn stop_impersonating_account(&self, address: Address) -> RpcResult<bool> {
fn stop_impersonating_account(&self, address: Address) -> RpcResult<()> {
InMemoryNode::<S>::stop_impersonating_account(self, address)
.map(|_| ())
.map_err(|err| {
tracing::error!("failed stopping to impersonate account: {:?}", err);
into_jsrpc_error(Web3Error::InternalError(err))
Expand Down
Loading

0 comments on commit 1578ee4

Please sign in to comment.