Skip to content

Commit

Permalink
feat: add anvil_dumpState/anvil_loadState (#476)
Browse files Browse the repository at this point in the history
* add `anvil_dumpState`/`anvil_loadState`

* add e2e test

* clippy

* rename `DEFAULT_TX_VALUE`

* explain semantics for `ForkStorage::dump_state`
  • Loading branch information
itegulov authored Dec 6, 2024
1 parent 1485a57 commit 7d0b689
Show file tree
Hide file tree
Showing 20 changed files with 731 additions and 63 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ indexmap = "2.0.1"
chrono = { version = "0.4.31", default-features = false }
time = "0.3.36"
rand = "0.8"
flate2 = "1.0"
thiserror = "1"

[dev-dependencies]
httptest = "0.15.4"
Expand Down
2 changes: 1 addition & 1 deletion 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 @@ -10,7 +10,7 @@ categories = ["cryptography"]
publish = false

[dependencies]
alloy-zksync = { git = "https://github.com/itegulov/alloy-zksync.git", rev = "c43bba1a6c5e744afb975b261cba6e964d6a58c6" }
alloy-zksync = { git = "https://github.com/itegulov/alloy-zksync.git", rev = "692c5c2ca5defc88ac542f420d97c6756dadf9df" }
alloy = { version = "0.6", features = ["full", "rlp", "serde", "sol-types", "getrandom", "provider-anvil-api"] }
anyhow = "1.0"
fs2 = "0.4.3"
Expand Down
10 changes: 10 additions & 0 deletions e2e-tests-rust/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ pub trait ReceiptExt: ReceiptResponse {
})
}

fn sender(&self) -> anyhow::Result<Address> {
self.to().ok_or_else(|| {
anyhow::anyhow!(
"receipt (hash={}) does not have `to` address",
self.transaction_hash()
)
})
}

/// Asserts that receipts belong to a block and that block is the same for both of them.
fn assert_same_block(&self, other: &Self) -> anyhow::Result<()> {
let lhs_number = self.block_number_ext()?;
Expand All @@ -45,6 +54,7 @@ pub trait ReceiptExt: ReceiptResponse {
)
}
}

/// Asserts that receipt is successful.
fn assert_successful(&self) -> anyhow::Result<()> {
if !self.status() {
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests-rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ mod provider;
mod utils;

pub use ext::{ReceiptExt, ZksyncWalletProviderExt};
pub use provider::{init_testing_provider, AnvilZKsyncApi, TestingProvider};
pub use provider::{init_testing_provider, AnvilZKsyncApi, TestingProvider, DEFAULT_TX_VALUE};
2 changes: 1 addition & 1 deletion e2e-tests-rust/src/provider/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ mod anvil_zksync;
mod testing;

pub use anvil_zksync::AnvilZKsyncApi;
pub use testing::{init_testing_provider, TestingProvider};
pub use testing::{init_testing_provider, TestingProvider, DEFAULT_TX_VALUE};
162 changes: 159 additions & 3 deletions e2e-tests-rust/src/provider/testing.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
use crate::utils::LockedPort;
use crate::ReceiptExt;
use alloy::network::{Network, TransactionBuilder};
use alloy::network::primitives::{BlockTransactionsKind, HeaderResponse as _};
use alloy::network::{Network, ReceiptResponse as _, TransactionBuilder};
use alloy::primitives::{Address, U256};
use alloy::providers::{
PendingTransaction, PendingTransactionBuilder, PendingTransactionError, Provider, RootProvider,
SendableTx, WalletProvider,
};
use alloy::rpc::types::TransactionRequest;
use alloy::rpc::types::{Block, TransactionRequest};
use alloy::transports::http::{reqwest, Http};
use alloy::transports::{RpcError, Transport, TransportErrorKind, TransportResult};
use alloy_zksync::network::header_response::HeaderResponse;
use alloy_zksync::network::receipt_response::ReceiptResponse;
use alloy_zksync::network::transaction_response::TransactionResponse;
use alloy_zksync::network::Zksync;
use alloy_zksync::node_bindings::EraTestNode;
use alloy_zksync::provider::{zksync_provider, ProviderBuilderExt};
use alloy_zksync::wallet::ZksyncWallet;
use anyhow::Context as _;
use itertools::Itertools;
use std::future::Future;
use std::marker::PhantomData;
Expand All @@ -23,6 +27,8 @@ use std::task::{Context, Poll};
use std::time::Duration;
use tokio::task::JoinHandle;

pub const DEFAULT_TX_VALUE: u64 = 100;

/// Full requirements for the underlying Zksync provider.
pub trait FullZksyncProvider<T>:
Provider<T, Zksync> + WalletProvider<Zksync, Wallet = ZksyncWallet> + Clone
Expand Down Expand Up @@ -114,7 +120,7 @@ where
pub fn tx(&self) -> TestTxBuilder<P, T> {
let tx = TransactionRequest::default()
.with_to(Address::random())
.with_value(U256::from(100));
.with_value(U256::from(DEFAULT_TX_VALUE));
TestTxBuilder {
inner: tx,
provider: (*self).clone(),
Expand Down Expand Up @@ -161,6 +167,156 @@ where
) -> Result<RacedReceipts<N>, PendingTransactionError> {
self.race_n_txs(|i, tx| tx.with_rich_from(i)).await
}

pub async fn get_block_by_receipt(
&self,
receipt: &ReceiptResponse,
) -> anyhow::Result<Block<TransactionResponse, HeaderResponse>> {
let hash = receipt.block_hash_ext()?;
self.get_block_by_hash(receipt.block_hash_ext()?, BlockTransactionsKind::Full)
.await?
.with_context(|| format!("block (hash={}) not found", hash))
}

pub async fn get_blocks_by_receipts(
&self,
receipts: impl IntoIterator<Item = &ReceiptResponse>,
) -> anyhow::Result<Vec<Block<TransactionResponse, HeaderResponse>>> {
futures::future::join_all(
receipts
.into_iter()
.map(|receipt| self.get_block_by_receipt(receipt)),
)
.await
.into_iter()
.collect()
}

pub async fn assert_has_receipt(
&self,
expected_receipt: &ReceiptResponse,
) -> anyhow::Result<()> {
let Some(actual_receipt) = self
.get_transaction_receipt(expected_receipt.transaction_hash())
.await?
else {
anyhow::bail!(
"receipt (hash={}) not found",
expected_receipt.transaction_hash()
);
};
assert_eq!(expected_receipt, &actual_receipt);
Ok(())
}

pub async fn assert_has_receipts(
&self,
receipts: impl IntoIterator<Item = &ReceiptResponse>,
) -> anyhow::Result<()> {
for receipt in receipts {
self.assert_has_receipt(receipt).await?;
}
Ok(())
}

pub async fn assert_no_receipt(
&self,
expected_receipt: &ReceiptResponse,
) -> anyhow::Result<()> {
if let Some(actual_receipt) = self
.get_transaction_receipt(expected_receipt.transaction_hash())
.await?
{
anyhow::bail!(
"receipt (hash={}) expected to be missing but was found",
actual_receipt.transaction_hash()
);
} else {
Ok(())
}
}

pub async fn assert_no_receipts(
&self,
receipts: impl IntoIterator<Item = &ReceiptResponse>,
) -> anyhow::Result<()> {
for receipt in receipts {
self.assert_no_receipt(receipt).await?;
}
Ok(())
}

pub async fn assert_has_block(
&self,
expected_block: &Block<TransactionResponse, HeaderResponse>,
) -> anyhow::Result<()> {
anyhow::ensure!(
expected_block.transactions.is_full(),
"expected block did not have full transactions"
);
let Some(actual_block) = self
.get_block_by_hash(expected_block.header.hash(), BlockTransactionsKind::Full)
.await?
else {
anyhow::bail!("block (hash={}) not found", expected_block.header.hash());
};
assert_eq!(expected_block, &actual_block);
Ok(())
}

pub async fn assert_has_blocks(
&self,
blocks: impl IntoIterator<Item = &Block<TransactionResponse, HeaderResponse>>,
) -> anyhow::Result<()> {
for block in blocks {
self.assert_has_block(block).await?;
}
Ok(())
}

pub async fn assert_no_block(
&self,
expected_block: &Block<TransactionResponse, HeaderResponse>,
) -> anyhow::Result<()> {
if let Some(actual_block) = self
.get_block_by_hash(expected_block.header.hash(), BlockTransactionsKind::Full)
.await?
{
anyhow::bail!(
"block (hash={}) expected to be missing but was found",
actual_block.header.hash()
);
} else {
Ok(())
}
}

pub async fn assert_no_blocks(
&self,
blocks: impl IntoIterator<Item = &Block<TransactionResponse, HeaderResponse>>,
) -> anyhow::Result<()> {
for block in blocks {
self.assert_no_block(block).await?;
}
Ok(())
}

pub async fn assert_balance(
&self,
address: Address,
expected_balance: u64,
) -> anyhow::Result<()> {
let actual_balance = self.get_balance(address).await?;
let expected_balance = U256::from(expected_balance);
anyhow::ensure!(
actual_balance == expected_balance,
"account's ({}) balance ({}) did not match expected value ({})",
address,
actual_balance,
expected_balance,
);
Ok(())
}
}

#[async_trait::async_trait]
Expand Down
83 changes: 82 additions & 1 deletion e2e-tests-rust/tests/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use alloy::network::ReceiptResponse;
use alloy::providers::ext::AnvilApi;
use anvil_zksync_e2e_tests::{
init_testing_provider, AnvilZKsyncApi, ReceiptExt, ZksyncWalletProviderExt,
init_testing_provider, AnvilZKsyncApi, ReceiptExt, ZksyncWalletProviderExt, DEFAULT_TX_VALUE,
};
use std::convert::identity;
use std::time::Duration;

#[tokio::test]
Expand Down Expand Up @@ -252,3 +253,83 @@ async fn seal_block_ignoring_halted_transaction() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test]
async fn dump_and_load_state() -> anyhow::Result<()> {
// Test that we can submit transactions, then dump state and shutdown the node. Following that we
// should be able to spin up a new node and load state into it. Previous transactions/block should
// be present on the new node along with the old state.
let provider = init_testing_provider(identity).await?;

let receipts = [
provider.tx().finalize().await?,
provider.tx().finalize().await?,
];
let blocks = provider.get_blocks_by_receipts(&receipts).await?;

// Dump node's state, re-create it and load old state
let state = provider.anvil_dump_state().await?;
let provider = init_testing_provider(identity).await?;
provider.anvil_load_state(state).await?;

// Assert that new node has pre-restart receipts, blocks and state
provider.assert_has_receipts(&receipts).await?;
provider.assert_has_blocks(&blocks).await?;
provider
.assert_balance(receipts[0].sender()?, DEFAULT_TX_VALUE)
.await?;
provider
.assert_balance(receipts[1].sender()?, DEFAULT_TX_VALUE)
.await?;

// Assert we can still finalize transactions after loading state
provider.tx().finalize().await?;

Ok(())
}

#[tokio::test]
async fn cant_load_into_existing_state() -> anyhow::Result<()> {
// Test that we can't load new state into a node with existing state.
let provider = init_testing_provider(identity).await?;

let old_receipts = [
provider.tx().finalize().await?,
provider.tx().finalize().await?,
];
let old_blocks = provider.get_blocks_by_receipts(&old_receipts).await?;

// Dump node's state and re-create it
let state = provider.anvil_dump_state().await?;
let provider = init_testing_provider(identity).await?;

let new_receipts = [
provider.tx().finalize().await?,
provider.tx().finalize().await?,
];
let new_blocks = provider.get_blocks_by_receipts(&new_receipts).await?;

// Load state into the new node, make sure it fails and assert that the node still has new
// receipts, blocks and state.
assert!(provider.anvil_load_state(state).await.is_err());
provider.assert_has_receipts(&new_receipts).await?;
provider.assert_has_blocks(&new_blocks).await?;
provider
.assert_balance(new_receipts[0].sender()?, DEFAULT_TX_VALUE)
.await?;
provider
.assert_balance(new_receipts[1].sender()?, DEFAULT_TX_VALUE)
.await?;

// Assert the node does not have old state
provider.assert_no_receipts(&old_receipts).await?;
provider.assert_no_blocks(&old_blocks).await?;
provider
.assert_balance(old_receipts[0].sender()?, 0)
.await?;
provider
.assert_balance(old_receipts[1].sender()?, 0)
.await?;

Ok(())
}
Loading

0 comments on commit 7d0b689

Please sign in to comment.