From 2a5d4902fde2630beb8ff61b7aecb90746472ccb Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Mon, 3 Feb 2025 17:15:55 +0100 Subject: [PATCH 1/6] use existing deployment nonce during storage migration --- crates/forge/tests/it/zk/fork.rs | 62 ++++++++++++++++++- .../zksync/src/cheatcode/runner/mod.rs | 30 +++++++-- crates/test-utils/src/lib.rs | 2 +- crates/test-utils/src/zksync.rs | 50 +++++++++++++-- 4 files changed, 132 insertions(+), 12 deletions(-) diff --git a/crates/forge/tests/it/zk/fork.rs b/crates/forge/tests/it/zk/fork.rs index 2e510a638..cddda20e9 100644 --- a/crates/forge/tests/it/zk/fork.rs +++ b/crates/forge/tests/it/zk/fork.rs @@ -2,7 +2,11 @@ use crate::{config::*, test_helpers::TEST_DATA_DEFAULT}; use forge::revm::primitives::SpecId; -use foundry_test_utils::Filter; +use foundry_test_utils::{ + forgetest_async, + util::{self, OutputExt}, + Filter, Fork, ZkSyncNode, +}; #[tokio::test(flavor = "multi_thread")] async fn test_zk_setup_fork_failure() { @@ -28,3 +32,59 @@ async fn test_zk_consistent_storage_migration_after_fork() { TestConfig::with_filter(runner, filter).spec_id(SpecId::SHANGHAI).run().await; } + +forgetest_async!(test_zk_consistent_nonce_migration_after_fork, |prj, cmd| { + util::initialize(prj.root()); + + // Has deployment nonce (1) and transaction nonce (2) on mainnet block #55159219 + let test_address = "0x076d6da60aAAC6c97A8a0fE8057f9564203Ee545"; + + prj.add_script( + "ZkForkNonceTest.s.sol", + format!(r#" +import "forge-std/Script.sol"; +import "forge-std/Test.sol"; + +interface VmExt {{ + function zkGetTransactionNonce( + address account + ) external view returns (uint64 nonce); + function zkGetDeploymentNonce( + address account + ) external view returns (uint64 nonce); +}} + +contract ZkForkNonceTest is Script {{ + VmExt internal constant vmExt = VmExt(VM_ADDRESS); + + address constant TEST_ADDRESS = {test_address}; + uint128 constant TEST_ADDRESS_TRANSACTION_NONCE = 2; + uint128 constant TEST_ADDRESS_DEPLOYMENT_NONCE = 1; + + function run() external {{ + require(TEST_ADDRESS_TRANSACTION_NONCE == vmExt.zkGetTransactionNonce(TEST_ADDRESS), "failed matching transaction nonce"); + require(TEST_ADDRESS_DEPLOYMENT_NONCE == vmExt.zkGetDeploymentNonce(TEST_ADDRESS), "failed matching deployment nonce"); + }} +}} +"#).as_str(), + ) + .unwrap(); + + let node = ZkSyncNode::start_with_fork(Fork::new_with_block( + String::from("https://mainnet.era.zksync.io"), + 55159219, + )) + .await; + + cmd.arg("script").args([ + "ZkForkNonceTest", + "--zk-startup", + "./script/ForkNonce.s.sol", + "--rpc-url", + node.url().as_str(), + "--sender", + test_address, + ]); + + cmd.assert_success(); +}); diff --git a/crates/strategy/zksync/src/cheatcode/runner/mod.rs b/crates/strategy/zksync/src/cheatcode/runner/mod.rs index 2ae0e7a36..6fde84e97 100644 --- a/crates/strategy/zksync/src/cheatcode/runner/mod.rs +++ b/crates/strategy/zksync/src/cheatcode/runner/mod.rs @@ -38,7 +38,7 @@ use revm::{ SignedAuthorization, KECCAK_EMPTY, }, }; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, trace, warn}; use zksync_types::{ block::{pack_block_info, unpack_block_info}, utils::{decompose_full_nonce, nonces_to_full_nonce}, @@ -883,7 +883,10 @@ impl ZksyncCheatcodeInspectorStrategyRunner { let balance = data.sload(balance_account, balance_key).unwrap_or_default().data; let full_nonce = data.sload(nonce_account, nonce_key).unwrap_or_default(); - let (tx_nonce, _deployment_nonce) = decompose_full_nonce(full_nonce.to_u256()); + let (tx_nonce, deployment_nonce) = decompose_full_nonce(full_nonce.to_u256()); + if !deployment_nonce.is_zero() { + warn!(?address, ?deployment_nonce, "discarding ZKsync deployment nonce for EVM context, might cause inconsistencies"); + } let nonce = tx_nonce.as_u64(); let account_code_key = get_account_code_key(address); @@ -909,7 +912,7 @@ impl ZksyncCheatcodeInspectorStrategyRunner { let _ = std::mem::replace(&mut account.info.nonce, nonce); if test_contract.map(|addr| addr == address).unwrap_or_default() { - tracing::trace!(?address, "ignoring code translation for test contract"); + trace!(?address, "ignoring code translation for test contract"); } else { account.info.code_hash = code_hash; account.info.code.clone_from(&code); @@ -951,21 +954,36 @@ impl ZksyncCheatcodeInspectorStrategyRunner { for address in data.db.persistent_accounts().into_iter().chain([data.env.tx.caller]) { info!(?address, "importing to zk state"); + // Re-use the deployment nonce from storage if present. + let deployment_nonce = { + let nonce_key = get_nonce_key(address); + let nonce_addr = NONCE_HOLDER_ADDRESS.to_address(); + let account = journaled_account(data, nonce_addr).expect("failed to load account"); + if let Some(value) = account.storage.get(&nonce_key) { + let full_nonce = value.original_value.to_u256(); + let (_tx_nonce, deployment_nonce) = decompose_full_nonce(full_nonce); + debug!(?address, ?deployment_nonce, "reuse existing deployment nonce"); + deployment_nonce + } else { + zksync_types::U256::zero() + } + }; + let account = journaled_account(data, address).expect("failed to load account"); let info = &account.info; let balance_key = get_balance_key(address); l2_eth_storage.insert(balance_key, EvmStorageSlot::new(info.balance)); - // TODO we need to find a proper way to handle deploy nonces instead of replicating - let full_nonce = nonces_to_full_nonce(info.nonce.into(), info.nonce.into()); + debug!(?address, ?deployment_nonce, transaction_nonce=?info.nonce, "attempting to fit EVM nonce to ZKsync nonces, might cause inconsistencies"); + let full_nonce = nonces_to_full_nonce(info.nonce.into(), deployment_nonce); let nonce_key = get_nonce_key(address); nonce_storage.insert(nonce_key, EvmStorageSlot::new(full_nonce.to_ru256())); if test_contract.map(|test_address| address == test_address).unwrap_or_default() { // avoid migrating test contract code - tracing::trace!(?address, "ignoring code translation for test contract"); + trace!(?address, "ignoring code translation for test contract"); continue; } diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 2b6495088..a9c8b2d3b 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -31,7 +31,7 @@ pub use script::{ScriptOutcome, ScriptTester}; // TODO: remove once anvil supports zksync node mod zksync; -pub use zksync::ZkSyncNode; +pub use zksync::{Fork, ZkSyncNode}; // re-exports for convenience pub use foundry_compilers; diff --git a/crates/test-utils/src/zksync.rs b/crates/test-utils/src/zksync.rs index 2758974b8..d4440bd1a 100644 --- a/crates/test-utils/src/zksync.rs +++ b/crates/test-utils/src/zksync.rs @@ -2,8 +2,12 @@ use std::{net::SocketAddr, str::FromStr}; use anvil_zksync_api_server::NodeServerBuilder; -use anvil_zksync_config::{types::SystemContractsOptions, TestNodeConfig}; +use anvil_zksync_config::{ + types::{CacheConfig, SystemContractsOptions}, + TestNodeConfig, +}; use anvil_zksync_core::{ + fork::ForkDetails, node::{ BlockProducer, BlockSealer, BlockSealerMode, ImpersonationManager, InMemoryNode, TimestampManager, TxPool, @@ -111,6 +115,25 @@ const RICH_WALLETS: [(&str, &str, &str); 10] = [ ), ]; +/// Represents fork config for [ZkSyncNode]. +#[derive(Debug, Default)] +pub struct Fork { + url: String, + block: Option, +} + +impl Fork { + /// Create a fork config with the provided url and the latest block. + pub fn new(url: String) -> Self { + Fork { url, ..Default::default() } + } + + /// Create a fork config with the provided url and block. + pub fn new_with_block(url: String, block: u64) -> Self { + Fork { url, block: Some(block) } + } +} + /// In-memory anvil-zksync that is stopped when dropped. pub struct ZkSyncNode { port: u16, @@ -118,7 +141,9 @@ pub struct ZkSyncNode { } impl ZkSyncNode { - /// Returns the server url. + /// Start anvil-zksync in memory, binding a random available port + /// + /// The server is automatically stopped when the instance is dropped. #[inline] pub fn url(&self) -> String { format!("http://127.0.0.1:{}", self.port) @@ -128,14 +153,30 @@ impl ZkSyncNode { /// /// The server is automatically stopped when the instance is dropped. pub async fn start() -> Self { + Self::start_inner(None).await + } + + /// Start anvil-zksync in memory, binding a random available port and with the provided fork url + /// and block. + /// + /// The server is automatically stopped when the instance is dropped. + pub async fn start_with_fork(fork: Fork) -> Self { + Self::start_inner(Some(fork)).await + } + + async fn start_inner(fork: Option) -> Self { let (_guard, guard_rx) = tokio::sync::oneshot::channel::<()>(); let (port_tx, port) = tokio::sync::oneshot::channel(); + let fork = fork.map(|fork| { + ForkDetails::from_url(fork.url, fork.block, CacheConfig::Memory) + .expect("failed building ForkDetails") + }); std::thread::spawn(move || { // We need to spawn a thread since `run_inner` future is not `Send`. let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); - runtime.block_on(Self::run_inner(port_tx, guard_rx)); + runtime.block_on(Self::run_inner(port_tx, guard_rx, fork)); }); // wait for server to start @@ -147,6 +188,7 @@ impl ZkSyncNode { async fn run_inner( port_tx: tokio::sync::oneshot::Sender, stop_guard: tokio::sync::oneshot::Receiver<()>, + fork: Option, ) { const MAX_TRANSACTIONS: usize = 100; // Not that important for testing purposes. @@ -159,7 +201,7 @@ impl ZkSyncNode { let block_sealer = BlockSealer::new(sealing_mode); let node: InMemoryNode = InMemoryNode::new( - None, + fork, None, &config, time, From 1fb6e2b0335e83b04455ac5b120b91012196ce8b Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Mon, 3 Feb 2025 17:31:40 +0100 Subject: [PATCH 2/6] clippy --- crates/test-utils/src/zksync.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/test-utils/src/zksync.rs b/crates/test-utils/src/zksync.rs index d4440bd1a..a9117c45f 100644 --- a/crates/test-utils/src/zksync.rs +++ b/crates/test-utils/src/zksync.rs @@ -125,12 +125,12 @@ pub struct Fork { impl Fork { /// Create a fork config with the provided url and the latest block. pub fn new(url: String) -> Self { - Fork { url, ..Default::default() } + Self { url, ..Default::default() } } /// Create a fork config with the provided url and block. pub fn new_with_block(url: String, block: u64) -> Self { - Fork { url, block: Some(block) } + Self { url, block: Some(block) } } } From f00b0a5bf0d78742a61585e07de5a170a96b4d8e Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Mon, 3 Feb 2025 17:33:30 +0100 Subject: [PATCH 3/6] spell --- crates/strategy/zksync/src/cheatcode/runner/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/strategy/zksync/src/cheatcode/runner/mod.rs b/crates/strategy/zksync/src/cheatcode/runner/mod.rs index 6fde84e97..4d0793f58 100644 --- a/crates/strategy/zksync/src/cheatcode/runner/mod.rs +++ b/crates/strategy/zksync/src/cheatcode/runner/mod.rs @@ -954,7 +954,7 @@ impl ZksyncCheatcodeInspectorStrategyRunner { for address in data.db.persistent_accounts().into_iter().chain([data.env.tx.caller]) { info!(?address, "importing to zk state"); - // Re-use the deployment nonce from storage if present. + // Reuse the deployment nonce from storage if present. let deployment_nonce = { let nonce_key = get_nonce_key(address); let nonce_addr = NONCE_HOLDER_ADDRESS.to_address(); From 87d0836675444171b85b71514b5dd808345b240d Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Tue, 4 Feb 2025 13:05:53 +0100 Subject: [PATCH 4/6] remove reliance on fork url --- crates/forge/tests/it/zk/fork.rs | 44 ++++++++++++------- .../zksync/src/cheatcode/runner/mod.rs | 20 +++++---- crates/test-utils/src/zksync.rs | 6 +-- 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/crates/forge/tests/it/zk/fork.rs b/crates/forge/tests/it/zk/fork.rs index cddda20e9..2f5c16d27 100644 --- a/crates/forge/tests/it/zk/fork.rs +++ b/crates/forge/tests/it/zk/fork.rs @@ -1,12 +1,15 @@ //! Fork tests. use crate::{config::*, test_helpers::TEST_DATA_DEFAULT}; +use alloy_provider::Provider; use forge::revm::primitives::SpecId; +use foundry_common::provider::try_get_zksync_http_provider; use foundry_test_utils::{ forgetest_async, - util::{self, OutputExt}, - Filter, Fork, ZkSyncNode, + util::{self}, + Filter, ZkSyncNode, }; +use foundry_zksync_core::state::{get_nonce_storage, new_full_nonce}; #[tokio::test(flavor = "multi_thread")] async fn test_zk_setup_fork_failure() { @@ -34,11 +37,26 @@ async fn test_zk_consistent_storage_migration_after_fork() { } forgetest_async!(test_zk_consistent_nonce_migration_after_fork, |prj, cmd| { + let test_address = alloy_primitives::address!("076d6da60aAAC6c97A8a0fE8057f9564203Ee545"); + let transaction_nonce = 2; + let deployment_nonce = 1; + + let node = ZkSyncNode::start().await; + // set nonce + let (nonce_key_addr, nonce_key_slot) = get_nonce_storage(test_address); + let full_nonce = new_full_nonce(transaction_nonce, deployment_nonce); + let result = try_get_zksync_http_provider(node.url()) + .unwrap() + .raw_request::<_, bool>( + "anvil_setStorageAt".into(), + (nonce_key_addr, nonce_key_slot, full_nonce), + ) + .await + .unwrap(); + assert!(result, "failed setting nonce on anvil-zksync"); + + // prepare script util::initialize(prj.root()); - - // Has deployment nonce (1) and transaction nonce (2) on mainnet block #55159219 - let test_address = "0x076d6da60aAAC6c97A8a0fE8057f9564203Ee545"; - prj.add_script( "ZkForkNonceTest.s.sol", format!(r#" @@ -58,8 +76,8 @@ contract ZkForkNonceTest is Script {{ VmExt internal constant vmExt = VmExt(VM_ADDRESS); address constant TEST_ADDRESS = {test_address}; - uint128 constant TEST_ADDRESS_TRANSACTION_NONCE = 2; - uint128 constant TEST_ADDRESS_DEPLOYMENT_NONCE = 1; + uint128 constant TEST_ADDRESS_TRANSACTION_NONCE = {transaction_nonce}; + uint128 constant TEST_ADDRESS_DEPLOYMENT_NONCE = {deployment_nonce}; function run() external {{ require(TEST_ADDRESS_TRANSACTION_NONCE == vmExt.zkGetTransactionNonce(TEST_ADDRESS), "failed matching transaction nonce"); @@ -70,20 +88,16 @@ contract ZkForkNonceTest is Script {{ ) .unwrap(); - let node = ZkSyncNode::start_with_fork(Fork::new_with_block( - String::from("https://mainnet.era.zksync.io"), - 55159219, - )) - .await; - cmd.arg("script").args([ "ZkForkNonceTest", "--zk-startup", "./script/ForkNonce.s.sol", + "--no-storage-caching", // prevents rpc caching "--rpc-url", node.url().as_str(), + // set address as sender to be migrated on startup, so storage is read immediately "--sender", - test_address, + test_address.to_string().as_str(), ]); cmd.assert_success(); diff --git a/crates/strategy/zksync/src/cheatcode/runner/mod.rs b/crates/strategy/zksync/src/cheatcode/runner/mod.rs index 4d0793f58..3a9f4f4e4 100644 --- a/crates/strategy/zksync/src/cheatcode/runner/mod.rs +++ b/crates/strategy/zksync/src/cheatcode/runner/mod.rs @@ -22,10 +22,11 @@ use foundry_evm::{ use foundry_evm_core::backend::DatabaseExt; use foundry_zksync_core::{ convert::{ConvertAddress, ConvertH160, ConvertH256, ConvertRU256, ConvertU256}, - get_account_code_key, get_balance_key, get_nonce_key, PaymasterParams, ZkTransactionMetadata, - ACCOUNT_CODE_STORAGE_ADDRESS, CONTRACT_DEPLOYER_ADDRESS, DEFAULT_CREATE2_DEPLOYER_ZKSYNC, - KNOWN_CODES_STORAGE_ADDRESS, L2_BASE_TOKEN_ADDRESS, NONCE_HOLDER_ADDRESS, - ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY, + get_account_code_key, get_balance_key, get_nonce_key, + state::parse_full_nonce, + PaymasterParams, ZkTransactionMetadata, ACCOUNT_CODE_STORAGE_ADDRESS, + CONTRACT_DEPLOYER_ADDRESS, DEFAULT_CREATE2_DEPLOYER_ZKSYNC, KNOWN_CODES_STORAGE_ADDRESS, + L2_BASE_TOKEN_ADDRESS, NONCE_HOLDER_ADDRESS, ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY, }; use itertools::Itertools; use revm::{ @@ -960,10 +961,13 @@ impl ZksyncCheatcodeInspectorStrategyRunner { let nonce_addr = NONCE_HOLDER_ADDRESS.to_address(); let account = journaled_account(data, nonce_addr).expect("failed to load account"); if let Some(value) = account.storage.get(&nonce_key) { - let full_nonce = value.original_value.to_u256(); - let (_tx_nonce, deployment_nonce) = decompose_full_nonce(full_nonce); - debug!(?address, ?deployment_nonce, "reuse existing deployment nonce"); - deployment_nonce + let full_nonce = parse_full_nonce(value.original_value); + debug!( + ?address, + deployment_nonce = full_nonce.deploy_nonce, + "reuse existing deployment nonce" + ); + full_nonce.deploy_nonce.into() } else { zksync_types::U256::zero() } diff --git a/crates/test-utils/src/zksync.rs b/crates/test-utils/src/zksync.rs index a9117c45f..445e213ee 100644 --- a/crates/test-utils/src/zksync.rs +++ b/crates/test-utils/src/zksync.rs @@ -141,15 +141,13 @@ pub struct ZkSyncNode { } impl ZkSyncNode { - /// Start anvil-zksync in memory, binding a random available port - /// - /// The server is automatically stopped when the instance is dropped. + /// Returns the server url. #[inline] pub fn url(&self) -> String { format!("http://127.0.0.1:{}", self.port) } - /// Start anvil-zksync in memory, binding a random available port + /// Start anvil-zksync in memory, binding a random available port. /// /// The server is automatically stopped when the instance is dropped. pub async fn start() -> Self { From fca57fd4d43e57080867b6a39cc502d7e69806d9 Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Tue, 4 Feb 2025 15:10:26 +0100 Subject: [PATCH 5/6] fix test --- crates/forge/tests/it/zk/logs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/tests/it/zk/logs.rs b/crates/forge/tests/it/zk/logs.rs index 498183aad..0f393190c 100644 --- a/crates/forge/tests/it/zk/logs.rs +++ b/crates/forge/tests/it/zk/logs.rs @@ -51,7 +51,7 @@ async fn test_zk_logs_work_in_create() { Some(vec![ "print".into(), "outer print".into(), - "0xF9E9ba9Ed9B96AB918c74B21dD0f1D5f2ac38a30".into(), + "0xB5c1DF089600415B21FB76bf89900Adb575947c8".into(), "print".into(), "0xff".into(), "print".into(), From 03f6530a581399ad25cfe24a6739866b19b1e9e0 Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Tue, 4 Feb 2025 16:27:12 +0100 Subject: [PATCH 6/6] fix trafce test --- crates/forge/tests/it/zk/traces.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/forge/tests/it/zk/traces.rs b/crates/forge/tests/it/zk/traces.rs index d2c4500d4..4132e0ba2 100644 --- a/crates/forge/tests/it/zk/traces.rs +++ b/crates/forge/tests/it/zk/traces.rs @@ -14,10 +14,10 @@ use itertools::Itertools; use serde::Deserialize; const ADDRESS_ZK_TRACE_TEST: Address = address!("7fa9385be102ac3eac297483dd6233d62b3e1496"); -const ADDRESS_ADDER: Address = address!("f9e9ba9ed9b96ab918c74b21dd0f1d5f2ac38a30"); -const ADDRESS_NUMBER: Address = address!("f232f12e115391c535fd519b00efadf042fc8be5"); -const ADDRESS_FIRST_INNER_NUMBER: Address = address!("ed570f3f91621894e001df0fb70bfbd123d3c8ad"); -const ADDRESS_SECOND_INNER_NUMBER: Address = address!("abceaeac3d3a2ac3dcffd7a60ca00a3fac9490ca"); +const ADDRESS_ADDER: Address = address!("b5c1df089600415b21fb76bf89900adb575947c8"); +const ADDRESS_NUMBER: Address = address!("d6a7a38ee698efae2f48f3a62dc7a71c3c0930a1"); +const ADDRESS_FIRST_INNER_NUMBER: Address = address!("89c74b24fb24dda42a8465ee0f9ede2c1308deeb"); +const ADDRESS_SECOND_INNER_NUMBER: Address = address!("9359008843d2c083a14e9c17cde01893938047fa"); const ADDRESS_CONSOLE: Address = address!("000000000000000000636f6e736f6c652e6c6f67"); const SELECTOR_TEST_CALL: Bytes = Bytes::from_static(hex!("0d3282c4").as_slice()); const SELECTOR_TEST_CREATE: Bytes = Bytes::from_static(hex!("61bdc916").as_slice());