+ 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(),