diff --git a/Cargo.lock b/Cargo.lock index b159b70d..d20aa6eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -733,6 +733,7 @@ dependencies = [ "ethabi 16.0.0", "ethers", "eyre", + "flate2", "fs2", "futures 0.3.31", "hex", @@ -758,6 +759,7 @@ dependencies = [ "tempdir", "test-case", "test-log", + "thiserror", "time", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 70189cd2..df601295 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/e2e-tests-rust/Cargo.lock b/e2e-tests-rust/Cargo.lock index a3437af1..eb2e6284 100644 --- a/e2e-tests-rust/Cargo.lock +++ b/e2e-tests-rust/Cargo.lock @@ -642,7 +642,7 @@ dependencies = [ [[package]] name = "alloy-zksync" version = "0.6.1" -source = "git+https://github.com/itegulov/alloy-zksync.git?rev=c43bba1a6c5e744afb975b261cba6e964d6a58c6#c43bba1a6c5e744afb975b261cba6e964d6a58c6" +source = "git+https://github.com/itegulov/alloy-zksync.git?rev=692c5c2ca5defc88ac542f420d97c6756dadf9df#692c5c2ca5defc88ac542f420d97c6756dadf9df" dependencies = [ "alloy", "async-trait", diff --git a/e2e-tests-rust/Cargo.toml b/e2e-tests-rust/Cargo.toml index 444d4c8d..03503dda 100644 --- a/e2e-tests-rust/Cargo.toml +++ b/e2e-tests-rust/Cargo.toml @@ -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" diff --git a/e2e-tests-rust/src/ext.rs b/e2e-tests-rust/src/ext.rs index 8b08f7e9..7114a6d8 100644 --- a/e2e-tests-rust/src/ext.rs +++ b/e2e-tests-rust/src/ext.rs @@ -26,6 +26,15 @@ pub trait ReceiptExt: ReceiptResponse { }) } + fn sender(&self) -> anyhow::Result
{ + 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()?; @@ -45,6 +54,7 @@ pub trait ReceiptExt: ReceiptResponse { ) } } + /// Asserts that receipt is successful. fn assert_successful(&self) -> anyhow::Result<()> { if !self.status() { diff --git a/e2e-tests-rust/src/lib.rs b/e2e-tests-rust/src/lib.rs index 072c3036..59a34eb4 100644 --- a/e2e-tests-rust/src/lib.rs +++ b/e2e-tests-rust/src/lib.rs @@ -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}; diff --git a/e2e-tests-rust/src/provider/mod.rs b/e2e-tests-rust/src/provider/mod.rs index e13184f1..cf6e0b6d 100644 --- a/e2e-tests-rust/src/provider/mod.rs +++ b/e2e-tests-rust/src/provider/mod.rs @@ -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}; diff --git a/e2e-tests-rust/src/provider/testing.rs b/e2e-tests-rust/src/provider/testing.rs index 1b83f64f..dc5635d2 100644 --- a/e2e-tests-rust/src/provider/testing.rs +++ b/e2e-tests-rust/src/provider/testing.rs @@ -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; @@ -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: Provider + WalletProvider + Clone @@ -114,7 +120,7 @@ where pub fn tx(&self) -> TestTxBuilder { 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(), @@ -161,6 +167,156 @@ where ) -> Result, 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> { + 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, + ) -> anyhow::Result>> { + 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, + ) -> 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, + ) -> anyhow::Result<()> { + for receipt in receipts { + self.assert_no_receipt(receipt).await?; + } + Ok(()) + } + + pub async fn assert_has_block( + &self, + expected_block: &Block, + ) -> 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>, + ) -> anyhow::Result<()> { + for block in blocks { + self.assert_has_block(block).await?; + } + Ok(()) + } + + pub async fn assert_no_block( + &self, + expected_block: &Block, + ) -> 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>, + ) -> 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] diff --git a/e2e-tests-rust/tests/lib.rs b/e2e-tests-rust/tests/lib.rs index e397a523..57b2aab8 100644 --- a/e2e-tests-rust/tests/lib.rs +++ b/e2e-tests-rust/tests/lib.rs @@ -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] @@ -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(()) +} diff --git a/src/fork.rs b/src/fork.rs index fcf6c101..fcaf1ce2 100644 --- a/src/fork.rs +++ b/src/fork.rs @@ -3,6 +3,19 @@ //! There is ForkStorage (that is a wrapper over InMemoryStorage) //! And ForkDetails - that parses network address and fork height from arguments. +use crate::config::{ + cache::CacheConfig, + constants::{ + DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR, DEFAULT_ESTIMATE_GAS_SCALE_FACTOR, + DEFAULT_FAIR_PUBDATA_PRICE, TEST_NODE_NETWORK_ID, + }, +}; +use crate::system_contracts; +use crate::{deps::InMemoryStorage, http_fork_source::HttpForkSource}; +use eyre::eyre; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::iter::FromIterator; use std::{ collections::HashMap, convert::{TryFrom, TryInto}, @@ -12,11 +25,9 @@ use std::{ str::FromStr, sync::{Arc, RwLock}, }; - -use eyre::eyre; use tokio::runtime::Builder; -use zksync_types::{Address, L1BatchNumber, L2BlockNumber, L2ChainId, H256, U256, U64}; - +use zksync_multivm::interface::storage::ReadStorage; +use zksync_types::web3::Bytes; use zksync_types::{ api::{ Block, BlockDetails, BlockIdVariant, BlockNumber, BridgeAddresses, Transaction, @@ -27,27 +38,16 @@ use zksync_types::{ url::SensitiveUrl, ProtocolVersionId, StorageKey, }; - -use zksync_multivm::interface::storage::ReadStorage; +use zksync_types::{ + Address, L1BatchNumber, L2BlockNumber, L2ChainId, StorageValue, H256, U256, U64, +}; use zksync_utils::{bytecode::hash_bytecode, h256_to_u256}; - use zksync_web3_decl::{ client::{Client, L2}, namespaces::ZksNamespaceClient, }; use zksync_web3_decl::{namespaces::EthNamespaceClient, types::Index}; -use crate::config::{ - cache::CacheConfig, - constants::{ - DEFAULT_ESTIMATE_GAS_PRICE_SCALE_FACTOR, DEFAULT_ESTIMATE_GAS_SCALE_FACTOR, - DEFAULT_FAIR_PUBDATA_PRICE, TEST_NODE_NETWORK_ID, - }, -}; -use crate::system_contracts; - -use crate::{deps::InMemoryStorage, http_fork_source::HttpForkSource}; - pub fn block_on(future: F) -> F::Output where F::Output: Send, @@ -262,6 +262,48 @@ impl ForkStorage { // TODO: Update this file to use proper enumeration index value once it's exposed for forks via API Some(0_u64) } + + /// Creates a serializable representation of current storage state. It will contain both locally + /// stored data and cached data read from the fork. + pub fn dump_state(&self) -> SerializableForkStorage { + let inner = self.inner.read().unwrap(); + let mut state = BTreeMap::from_iter(inner.value_read_cache.clone()); + state.extend(inner.raw_storage.state.clone()); + let mut factory_deps = BTreeMap::from_iter( + inner + .factory_dep_cache + .iter() + // Ignore cache misses + .filter_map(|(k, v)| v.as_ref().map(|v| (k, v))) + .map(|(k, v)| (*k, Bytes::from(v.clone()))), + ); + factory_deps.extend( + inner + .raw_storage + .factory_deps + .iter() + .map(|(k, v)| (*k, Bytes::from(v.clone()))), + ); + + SerializableForkStorage { + storage: SerializableStorage(state), + factory_deps, + } + } + + pub fn load_state(&self, state: SerializableForkStorage) { + tracing::trace!( + slots = state.storage.0.len(), + factory_deps = state.factory_deps.len(), + "loading fork storage from supplied state" + ); + let mut inner = self.inner.write().unwrap(); + inner.raw_storage.state.extend(state.storage.0); + inner + .raw_storage + .factory_deps + .extend(state.factory_deps.into_iter().map(|(k, v)| (k, v.0))); + } } impl ReadStorage for ForkStorage { @@ -736,6 +778,85 @@ impl ForkDetails { } } +/// Serializable representation of [`ForkStorage`]'s state. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SerializableForkStorage { + /// Node's current key-value storage state (contains both local and cached fork data if applicable). + pub storage: SerializableStorage, + /// Factory dependencies by their hash. + pub factory_deps: BTreeMap, +} + +/// Wrapper for [`BTreeMap`] to avoid serializing [`StorageKey`] as a struct. +/// JSON does not support non-string keys so we use conversion to [`Bytes`] via [`crate::node::state::SerializableStorageKey`] +/// instead. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde( + into = "BTreeMap", + from = "BTreeMap" +)] +pub struct SerializableStorage(pub BTreeMap); + +mod serde_from { + use crate::fork::SerializableStorage; + use serde::{Deserialize, Serialize}; + use std::collections::BTreeMap; + use std::convert::TryFrom; + use zksync_types::web3::Bytes; + use zksync_types::{AccountTreeId, Address, StorageKey, StorageValue, H256}; + + impl From> for SerializableStorage { + fn from(value: BTreeMap) -> Self { + SerializableStorage(value.into_iter().map(|(k, v)| (k.into(), v)).collect()) + } + } + + impl From for BTreeMap { + fn from(value: SerializableStorage) -> Self { + value.0.into_iter().map(|(k, v)| (k.into(), v)).collect() + } + } + + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] + #[serde(into = "Bytes", try_from = "Bytes")] + pub struct SerializableStorageKey(StorageKey); + + impl From for SerializableStorageKey { + fn from(value: StorageKey) -> Self { + SerializableStorageKey(value) + } + } + + impl From for StorageKey { + fn from(value: SerializableStorageKey) -> Self { + value.0 + } + } + + impl TryFrom for SerializableStorageKey { + type Error = anyhow::Error; + + fn try_from(bytes: Bytes) -> anyhow::Result { + if bytes.0.len() != 52 { + anyhow::bail!("invalid bytes length (expected 52, got {})", bytes.0.len()) + } + let address = Address::from_slice(&bytes.0[0..20]); + let key = H256::from_slice(&bytes.0[20..52]); + Ok(SerializableStorageKey(StorageKey::new( + AccountTreeId::new(address), + key, + ))) + } + } + + impl From for Bytes { + fn from(value: SerializableStorageKey) -> Self { + let bytes = [value.0.address().as_bytes(), value.0.key().as_bytes()].concat(); + bytes.into() + } + } +} + #[cfg(test)] mod tests { use zksync_multivm::interface::storage::ReadStorage; diff --git a/src/namespaces/anvil.rs b/src/namespaces/anvil.rs index 7efeff2d..2a76e678 100644 --- a/src/namespaces/anvil.rs +++ b/src/namespaces/anvil.rs @@ -8,6 +8,30 @@ use zksync_types::{Address, H256, U256, U64}; #[rpc] pub trait AnvilNamespaceT { + /// Create a buffer that represents all state on the chain, which can be loaded to separate + /// process by calling `anvil_loadState`. + /// + /// # Arguments + /// + /// * `preserve_historical_states` - Whether to preserve historical states + /// + /// # Returns + /// Buffer representing the chain state. + #[rpc(name = "anvil_dumpState")] + fn dump_state(&self, preserve_historical_states: Option) -> RpcResult; + + /// Append chain state buffer to current chain. Will overwrite any conflicting addresses or + /// storage. + /// + /// # Arguments + /// + /// * `bytes` - Buffer containing the chain state + /// + /// # Returns + /// `true` if a snapshot was reverted, otherwise `false`. + #[rpc(name = "anvil_loadState")] + fn load_state(&self, bytes: Bytes) -> RpcResult; + /// Mines a single block in the same way as `evm_mine` but returns extra fields. /// /// # Returns diff --git a/src/node/anvil.rs b/src/node/anvil.rs index 7c7141f6..133375c6 100644 --- a/src/node/anvil.rs +++ b/src/node/anvil.rs @@ -1,4 +1,5 @@ use zksync_types::api::Block; +use zksync_types::web3::Bytes; use zksync_types::{Address, H256, U256, U64}; use zksync_web3_decl::error::Web3Error; @@ -14,6 +15,21 @@ use crate::{ impl AnvilNamespaceT for InMemoryNode { + fn dump_state(&self, preserve_historical_states: Option) -> RpcResult { + self.dump_state(preserve_historical_states.unwrap_or(false)) + .map_err(|err| { + tracing::error!("failed dumping state: {:?}", err); + into_jsrpc_error(Web3Error::InternalError(err)) + }) + .into_boxed_future() + } + + fn load_state(&self, bytes: Bytes) -> RpcResult { + self.load_state(bytes) + .map_err(Into::into) + .into_boxed_future() + } + fn mine_detailed(&self) -> RpcResult> { self.mine_detailed() .map_err(|err| { diff --git a/src/node/error.rs b/src/node/error.rs new file mode 100644 index 00000000..745ae156 --- /dev/null +++ b/src/node/error.rs @@ -0,0 +1,36 @@ +use crate::utils::into_jsrpc_error; +use zksync_web3_decl::error::Web3Error; + +#[derive(thiserror::Error, Debug)] +pub enum LoadStateError { + #[error("loading state into a node with existing state is not allowed (please create an issue if you have a valid use case)")] + HasExistingState, + #[error("loading empty state (no blocks) is not allowed")] + EmptyState, + #[error("failed to decompress state: {0}")] + FailedDecompress(std::io::Error), + #[error("failed to deserialize state: {0}")] + FailedDeserialize(serde_json::Error), + #[error("unknown state version `{0}`")] + UnknownStateVersion(u8), + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +impl From for jsonrpc_core::Error { + fn from(value: LoadStateError) -> Self { + match value { + err @ LoadStateError::HasExistingState + | err @ LoadStateError::EmptyState + | err @ LoadStateError::FailedDecompress(_) + | err @ LoadStateError::FailedDeserialize(_) + | err @ LoadStateError::UnknownStateVersion(_) => { + jsonrpc_core::Error::invalid_params(err.to_string()) + } + LoadStateError::Other(err) => { + tracing::error!("failed loading state: {:?}", err); + into_jsrpc_error(Web3Error::InternalError(err)) + } + } + } +} diff --git a/src/node/in_memory.rs b/src/node/in_memory.rs index f57a2700..8b15321a 100644 --- a/src/node/in_memory.rs +++ b/src/node/in_memory.rs @@ -1,7 +1,12 @@ //! In-memory node, that supports forking other networks. use colored::Colorize; +use flate2::read::GzDecoder; +use flate2::write::GzEncoder; +use flate2::Compression; use indexmap::IndexMap; use once_cell::sync::OnceCell; +use serde::{Deserialize, Serialize}; +use std::io::{Read, Write}; use std::sync::{RwLockReadGuard, RwLockWriteGuard}; use std::{ collections::{HashMap, HashSet}, @@ -9,7 +14,6 @@ use std::{ str::FromStr, sync::{Arc, RwLock}, }; - use zksync_contracts::BaseSystemContracts; use zksync_multivm::vm_latest::HistoryEnabled; use zksync_multivm::{ @@ -48,7 +52,10 @@ use zksync_types::{ use zksync_utils::{bytecode::hash_bytecode, h256_to_account_address, h256_to_u256, u256_to_h256}; use zksync_web3_decl::error::Web3Error; +use crate::fork::SerializableStorage; +use crate::node::error::LoadStateError; use crate::node::impersonate::{ImpersonationManager, ImpersonationState}; +use crate::node::state::{StateV1, VersionedState}; use crate::node::time::{AdvanceTime, ReadTime, TimestampManager}; use crate::node::{BlockSealer, TxPool}; use crate::{ @@ -196,17 +203,15 @@ fn create_block( } /// Information about the executed transaction. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TxExecutionInfo { pub tx: L2Tx, // Batch number where transaction was executed. pub batch_number: u32, pub miniblock_number: u64, - #[allow(unused)] - pub result: VmExecutionResultAndLogs, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransactionResult { pub info: TxExecutionInfo, pub receipt: TransactionReceipt, @@ -876,6 +881,119 @@ impl InMemoryNodeInner { Ok(()) } + fn dump_state(&self, preserve_historical_states: bool) -> anyhow::Result { + let fork_storage = self.fork_storage.dump_state(); + let historical_states = if preserve_historical_states { + self.previous_states + .iter() + .map(|(k, v)| (*k, SerializableStorage(v.clone().into_iter().collect()))) + .collect() + } else { + Vec::new() + }; + + Ok(VersionedState::v1(StateV1 { + blocks: self.blocks.values().cloned().collect(), + transactions: self.tx_results.values().cloned().collect(), + fork_storage, + historical_states, + })) + } + + fn load_blocks(&mut self, mut time: T, blocks: Vec>) { + tracing::trace!( + blocks = blocks.len(), + "loading new blocks from supplied state" + ); + for block in blocks { + let number = block.number.as_u64(); + tracing::trace!( + number, + hash = %block.hash, + "loading new block from supplied state" + ); + + self.block_hashes.insert(number, block.hash); + self.blocks.insert(block.hash, block); + } + + // Safe unwrap as there was at least one block in the loaded state + let latest_block = self.blocks.values().max_by_key(|b| b.number).unwrap(); + let latest_number = latest_block.number.as_u64(); + let latest_hash = latest_block.hash; + let Some(latest_batch_number) = latest_block.l1_batch_number.map(|n| n.as_u32()) else { + panic!("encountered a block with no batch; this is not supposed to happen") + }; + let latest_timestamp = latest_block.timestamp.as_u64(); + tracing::info!( + number = latest_number, + hash = %latest_hash, + batch_number = latest_batch_number, + timestamp = latest_timestamp, + "latest block after loading state" + ); + self.current_miniblock = latest_number; + self.current_miniblock_hash = latest_hash; + self.current_batch = latest_batch_number; + time.reset_to(latest_timestamp); + } + + fn load_transactions(&mut self, transactions: Vec) { + tracing::trace!( + transactions = transactions.len(), + "loading new transactions from supplied state" + ); + for transaction in transactions { + tracing::trace!( + hash = %transaction.receipt.transaction_hash, + "loading new transaction from supplied state" + ); + self.tx_results + .insert(transaction.receipt.transaction_hash, transaction); + } + } + + fn load_state( + &mut self, + time: T, + state: VersionedState, + ) -> Result { + if self.blocks.len() > 1 { + tracing::debug!( + blocks = self.blocks.len(), + "node has existing state; refusing to load new state" + ); + return Err(LoadStateError::HasExistingState); + } + let state = match state { + VersionedState::V1 { state, .. } => state, + VersionedState::Unknown { version } => { + return Err(LoadStateError::UnknownStateVersion(version)) + } + }; + if state.blocks.is_empty() { + tracing::debug!("new state has no blocks; refusing to load"); + return Err(LoadStateError::EmptyState); + } + + self.load_blocks(time, state.blocks); + self.load_transactions(state.transactions); + self.fork_storage.load_state(state.fork_storage); + + tracing::trace!( + states = state.historical_states.len(), + "loading historical states from supplied state" + ); + self.previous_states.extend( + state + .historical_states + .into_iter() + .map(|(k, v)| (k, v.0.into_iter().collect())), + ); + + Ok(true) + } + fn apply_block( &mut self, time: &mut T, @@ -1652,7 +1770,6 @@ impl InMemoryNode { tx: l2_tx, batch_number: batch_env.number.0, miniblock_number: block_ctx.miniblock, - result, }, receipt: tx_receipt, debug, @@ -1716,13 +1833,15 @@ impl InMemoryNode { } let mut transactions = Vec::new(); - let mut tx_results = Vec::new(); + let mut tx_receipts = Vec::new(); + let mut debug_calls = Vec::new(); for tx_hash in &executed_tx_hashes { let Some(tx_result) = inner.tx_results.get(tx_hash) else { // Skipping halted transaction continue; }; - tx_results.push(&tx_result.info.result); + tx_receipts.push(&tx_result.receipt); + debug_calls.push(&tx_result.debug); let mut transaction = zksync_types::api::Transaction::from(tx_result.info.tx.clone()); transaction.block_hash = Some(block_ctx.hash); @@ -1741,12 +1860,12 @@ impl InMemoryNode { } // Build bloom hash - let iter = tx_results + let iter = tx_receipts .iter() - .flat_map(|r| r.logs.events.iter()) + .flat_map(|r| r.logs.iter()) .flat_map(|event| { event - .indexed_topics + .topics .iter() .map(|topic| BloomInput::Raw(topic.as_bytes())) .chain([BloomInput::Raw(event.address.as_bytes())]) @@ -1754,9 +1873,9 @@ impl InMemoryNode { let logs_bloom = build_bloom(iter); // Calculate how much gas was used across all txs - let gas_used = tx_results + let gas_used = debug_calls .iter() - .map(|r| U256::from(r.statistics.gas_used)) + .map(|r| r.gas_used) .fold(U256::zero(), |acc, x| acc + x); // Construct the block @@ -1824,6 +1943,43 @@ impl InMemoryNode { Ok(()) } + + pub fn dump_state(&self, preserve_historical_states: bool) -> anyhow::Result { + let state = self + .inner + .read() + .map_err(|_| anyhow::anyhow!("Failed to acquire read lock"))? + .dump_state(preserve_historical_states)?; + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&serde_json::to_vec(&state)?)?; + Ok(encoder.finish()?.into()) + } + + pub fn load_state(&self, buf: Bytes) -> Result { + let orig_buf = &buf.0[..]; + let mut decoder = GzDecoder::new(orig_buf); + let mut decoded_data = Vec::new(); + + // Support both compressed and non-compressed state format + let decoded = if decoder.header().is_some() { + tracing::trace!(bytes = buf.0.len(), "decompressing state"); + decoder + .read_to_end(decoded_data.as_mut()) + .map_err(LoadStateError::FailedDecompress)?; + &decoded_data + } else { + &buf.0 + }; + tracing::trace!(bytes = decoded.len(), "deserializing state"); + let state: VersionedState = + serde_json::from_slice(decoded).map_err(LoadStateError::FailedDeserialize)?; + + let time = self.time.lock(); + self.inner + .write() + .map_err(|_| anyhow::anyhow!("Failed to acquire write lock"))? + .load_state(time, state) + } } /// Keeps track of a block's batch number, miniblock number and timestamp. diff --git a/src/node/in_memory_ext.rs b/src/node/in_memory_ext.rs index e7e66f18..0d89cb9c 100644 --- a/src/node/in_memory_ext.rs +++ b/src/node/in_memory_ext.rs @@ -11,7 +11,7 @@ use crate::{ use anyhow::{anyhow, Context}; use std::convert::TryInto; use std::time::Duration; -use zksync_multivm::interface::{ExecutionResult, TxExecutionMode}; +use zksync_multivm::interface::TxExecutionMode; use zksync_types::api::{Block, TransactionVariant}; use zksync_types::{ get_code_key, get_nonce_key, @@ -111,15 +111,8 @@ impl InMemoryNo .tx_results .get(&tx.hash) .expect("freshly executed tx is missing from storage"); - let (output, revert_reason) = match &tx_result.info.result.result { - ExecutionResult::Success { output } => (Some(output.clone().into()), None), - ExecutionResult::Revert { output } => ( - Some(output.encoded_data().into()), - Some(output.to_user_friendly_string()), - ), - // Halted transaction should never be a part of a block - ExecutionResult::Halt { .. } => unreachable!(), - }; + let output = Some(tx_result.debug.output.clone()); + let revert_reason = tx_result.debug.revert_reason.clone(); DetailedTransaction { inner: tx, output, diff --git a/src/node/mod.rs b/src/node/mod.rs index 10ec1126..0732e9ee 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -5,6 +5,7 @@ mod block_producer; mod call_error_tracer; mod config_api; mod debug; +mod error; mod eth; mod evm; mod fee_model; @@ -15,6 +16,7 @@ mod in_memory_ext; mod net; mod pool; mod sealer; +mod state; mod storage_logs; mod time; mod web3; diff --git a/src/node/state.rs b/src/node/state.rs new file mode 100644 index 00000000..a88fa2f7 --- /dev/null +++ b/src/node/state.rs @@ -0,0 +1,67 @@ +use crate::fork::{SerializableForkStorage, SerializableStorage}; +use crate::node::TransactionResult; +use serde::{Deserialize, Serialize}; +use zksync_types::api::{Block, TransactionVariant}; +use zksync_types::H256; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum VersionedState { + V1 { + version: StateVersion<1>, + #[serde(flatten)] + state: StateV1, + }, + Unknown { + version: u8, + }, +} + +impl VersionedState { + pub fn v1(state: StateV1) -> Self { + VersionedState::V1 { + version: StateVersion::<1>, + state, + } + } +} + +/// Workaround while serde does not allow integer tags in enums (see https://github.com/serde-rs/serde/issues/745). +#[derive(Copy, Clone, Debug)] +pub struct StateVersion; + +impl Serialize for StateVersion { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u8(V) + } +} + +impl<'de, const V: u8> Deserialize<'de> for StateVersion { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = u8::deserialize(deserializer)?; + if value == V { + Ok(StateVersion::) + } else { + Err(serde::de::Error::custom("unknown state version")) + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StateV1 { + /// All blocks sealed on this node up to the current moment. + pub blocks: Vec>, + /// All transactions executed on this node up to the current moment. + pub transactions: Vec, + /// Current node's storage state. + #[serde(flatten)] + pub fork_storage: SerializableForkStorage, + /// Historical states of storage at particular block hashes. + pub historical_states: Vec<(H256, SerializableStorage)>, +} diff --git a/src/node/time.rs b/src/node/time.rs index 379a6e0b..0c5bac92 100644 --- a/src/node/time.rs +++ b/src/node/time.rs @@ -19,6 +19,8 @@ pub trait AdvanceTime: ReadTime { /// Subsequent calls to this method return monotonically increasing values. Time difference /// between calls is implementation-specific. fn advance_timestamp(&mut self) -> u64; + + fn reset_to(&mut self, timestamp: u64); } /// Manages timestamps (in seconds) across the system. @@ -153,11 +155,6 @@ struct TimestampManagerInternal { } impl TimestampManagerInternal { - fn reset_to(&mut self, timestamp: u64) { - self.next_timestamp.take(); - self.current_timestamp = timestamp; - } - fn interval(&self) -> u64 { self.interval.unwrap_or(1) } @@ -184,6 +181,11 @@ impl AdvanceTime for TimestampManagerInternal { self.current_timestamp = next_timestamp; next_timestamp } + + fn reset_to(&mut self, timestamp: u64) { + self.next_timestamp.take(); + self.current_timestamp = timestamp; + } } struct TimeLockWithOffsets<'a> { @@ -222,4 +224,12 @@ impl AdvanceTime for TimeLockWithOffsets<'_> { None => self.guard.advance_timestamp(), } } + + fn reset_to(&mut self, timestamp: u64) { + // Resetting `start_timestamp` to `timestamp` may look weird here but at the same time there + // is no "expected" behavior in this case. + // Also, this is temporary logic that will become irrelevant after block production refactoring. + self.guard.reset_to(timestamp); + self.start_timestamp = timestamp; + } } diff --git a/src/testing.rs b/src/testing.rs index 1ed388d4..55aeec3d 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -19,7 +19,6 @@ use httptest::{ }; use itertools::Itertools; use std::str::FromStr; -use zksync_multivm::interface::{ExecutionResult, VmExecutionResultAndLogs}; use zksync_types::api::{ BlockDetailsBase, BlockIdVariant, BlockStatus, BridgeAddresses, DebugCall, DebugCallType, Log, }; @@ -669,13 +668,6 @@ pub fn default_tx_execution_info() -> TxExecutionInfo { }, batch_number: Default::default(), miniblock_number: Default::default(), - result: VmExecutionResultAndLogs { - result: ExecutionResult::Success { output: vec![] }, - logs: Default::default(), - statistics: Default::default(), - refunds: Default::default(), - new_known_factory_deps: None, - }, } } diff --git a/src/utils.rs b/src/utils.rs index 8ff0032b..9592ef10 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -132,7 +132,7 @@ pub fn create_debug_output( }), ExecutionResult::Revert { output } => Ok(DebugCall { gas_used: result.statistics.gas_used.into(), - output: Default::default(), + output: output.encoded_data().into(), r#type: calltype, from: l2_tx.initiator_account(), to: l2_tx.recipient_account().unwrap_or_default(),