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

fix: proper handling of halted txs #463

Merged
merged 5 commits into from
Dec 3, 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
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
Loading