From 7f38caa38e881d18fa51385c783bb85fade93988 Mon Sep 17 00:00:00 2001 From: Karrq Date: Wed, 14 Aug 2024 22:31:56 +0200 Subject: [PATCH] test(zk): migrate factory tests & script (#516) * feat(tests:zk): `test_zk` macro * tests(zk): migrate `Factory.t.sol` * refactor(test): move `zksync_node` to `test-utils` * test(zk): factory scripts * chore: lints * chore: fmt * feat(test:zk): support concurrent `InMemoryNode` * chore: renames & imports * chore: remove test_zk --- Cargo.lock | 8 +- crates/forge/Cargo.toml | 9 +- crates/forge/tests/cli/main.rs | 2 - crates/forge/tests/cli/script.rs | 4 +- crates/forge/tests/fixtures/zk/Factory.s.sol | 67 ++++++++++++ crates/forge/tests/it/zk/factory.rs | 102 ++++++++++++++++++ crates/forge/tests/it/zk/mod.rs | 1 + crates/test-utils/Cargo.toml | 7 ++ crates/test-utils/src/lib.rs | 4 + .../src/zksync.rs} | 54 ++++------ testdata/zk/Factory.sol | 77 +++++++++++++ testdata/zk/Factory.t.sol | 52 +++++++++ 12 files changed, 342 insertions(+), 45 deletions(-) create mode 100644 crates/forge/tests/fixtures/zk/Factory.s.sol create mode 100644 crates/forge/tests/it/zk/factory.rs rename crates/{forge/tests/cli/zksync_node.rs => test-utils/src/zksync.rs} (85%) create mode 100644 testdata/zk/Factory.sol create mode 100644 testdata/zk/Factory.t.sol diff --git a/Cargo.lock b/Cargo.lock index bcaa8c141..f56f9d922 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4606,7 +4606,6 @@ dependencies = [ "criterion", "dialoguer", "dunce", - "era_test_node", "ethers-contract-abigen", "evm-disassembler", "eyre", @@ -4634,8 +4633,6 @@ dependencies = [ "hyper 1.4.1", "indicatif", "itertools 0.13.0", - "jsonrpc-core", - "jsonrpc-http-server", "mockall", "once_cell", "opener", @@ -5384,11 +5381,14 @@ version = "0.0.2" dependencies = [ "alloy-primitives", "alloy-provider", + "era_test_node", "eyre", "fd-lock 4.0.2", "foundry-common", "foundry-compilers", "foundry-config", + "jsonrpc-core", + "jsonrpc-http-server", "once_cell", "parking_lot 0.12.3", "rand 0.8.5", @@ -5396,9 +5396,11 @@ dependencies = [ "serde_json", "similar-asserts", "snapbox", + "tokio", "tracing", "tracing-subscriber", "walkdir", + "zksync_types", ] [[package]] diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index c87bb6570..e6e9d6a5d 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -114,8 +114,8 @@ opener = "0.7" soldeer.workspace = true # zk -zksync-web3-rs = { workspace = true } -zksync_types = { workspace = true } +zksync-web3-rs.workspace = true +zksync_types.workspace = true [target.'cfg(unix)'.dependencies] tikv-jemallocator = { workspace = true, optional = true } @@ -124,11 +124,6 @@ tikv-jemallocator = { workspace = true, optional = true } anvil.workspace = true foundry-test-utils.workspace = true -# zk -era_test_node.workspace = true -jsonrpc-core = { git = "https://github.com/matter-labs/jsonrpc.git", branch = "master" } -jsonrpc-http-server = { git = "https://github.com/matter-labs/jsonrpc.git", branch = "master" } - mockall = "0.12" criterion = "0.5" paste = "1.0" diff --git a/crates/forge/tests/cli/main.rs b/crates/forge/tests/cli/main.rs index bf8bdce56..b8bc3db5a 100644 --- a/crates/forge/tests/cli/main.rs +++ b/crates/forge/tests/cli/main.rs @@ -21,6 +21,4 @@ mod svm; mod test_cmd; mod verify; -mod zksync_node; - mod ext_integration; diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index d2475e374..30a04a2f2 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -1,6 +1,6 @@ //! Contains various tests related to `forge script`. -use crate::{constants::TEMPLATE_CONTRACT, zksync_node}; +use crate::constants::TEMPLATE_CONTRACT; use alloy_primitives::{hex, Address, Bytes}; use anvil::{spawn, NodeConfig}; use foundry_test_utils::{rpc, util::OutputExt, ScriptOutcome, ScriptTester}; @@ -1472,7 +1472,7 @@ forgetest_async!(test_zk_can_execute_script_with_arguments, |prj, cmd| { factory_deps: Vec>, } - let node = zksync_node::ZkSyncNode::start(); + let node = foundry_test_utils::ZkSyncNode::start(); cmd.args(["init", "--force"]).arg(prj.root()); cmd.assert_non_empty_stdout(); diff --git a/crates/forge/tests/fixtures/zk/Factory.s.sol b/crates/forge/tests/fixtures/zk/Factory.s.sol new file mode 100644 index 000000000..4f0e9995b --- /dev/null +++ b/crates/forge/tests/fixtures/zk/Factory.s.sol @@ -0,0 +1,67 @@ +import "forge-std/Script.sol"; +import "../src/Factory.sol"; + +contract ZkClassicFactoryScript is Script { + function run() external { + vm.startBroadcast(); + MyClassicFactory factory = new MyClassicFactory(); + factory.create(42); + + vm.stopBroadcast(); + assert(factory.getNumber() == 42); + } +} + +contract ZkConstructorFactoryScript is Script { + function run() external { + vm.startBroadcast(); + MyConstructorFactory factory = new MyConstructorFactory(42); + + vm.stopBroadcast(); + assert(factory.getNumber() == 42); + } +} + +contract ZkNestedFactoryScript is Script{ + function run() external { + vm.startBroadcast(); + MyNestedFactory factory = new MyNestedFactory(); + factory.create(42); + + vm.stopBroadcast(); + assert(factory.getNumber() == 42); + } +} + +contract ZkNestedConstructorFactoryScript is Script{ + function run() external { + vm.startBroadcast(); + MyNestedConstructorFactory factory = new MyNestedConstructorFactory(42); + + vm.stopBroadcast(); + assert(factory.getNumber() == 42); + } +} + +contract ZkUserFactoryScript is Script { + function run() external { + vm.startBroadcast(); + MyClassicFactory factory = new MyClassicFactory(); + MyUserFactory user = new MyUserFactory(); + user.create(address(factory), 42); + + vm.stopBroadcast(); + assert(user.getNumber(address(factory)) == 42); + } +} + +contract ZkUserConstructorFactoryScript is Script{ + function run() external { + vm.startBroadcast(); + MyConstructorFactory factory = new MyConstructorFactory(42); + MyUserFactory user = new MyUserFactory(); + + vm.stopBroadcast(); + assert(user.getNumber(address(factory)) == 42); + } +} diff --git a/crates/forge/tests/it/zk/factory.rs b/crates/forge/tests/it/zk/factory.rs new file mode 100644 index 000000000..bb21b3e65 --- /dev/null +++ b/crates/forge/tests/it/zk/factory.rs @@ -0,0 +1,102 @@ +//! Forge tests for zksync factory contracts. + +use forge::revm::primitives::SpecId; +use foundry_test_utils::{forgetest_async, util, Filter, TestCommand, TestProject, ZkSyncNode}; + +use crate::{config::TestConfig, test_helpers::TEST_DATA_DEFAULT}; + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_can_deploy_in_method() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + { + let filter = Filter::new("testClassicFactory|testNestedFactory", "ZkFactoryTest", ".*"); + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_can_deploy_in_constructor() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + { + let filter = Filter::new( + "testConstructorFactory|testNestedConstructorFactory", + "ZkFactoryTest", + ".*", + ); + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_can_use_predeployed_factory() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + { + let filter = Filter::new("testUser.*", "ZkFactoryTest", ".*"); + TestConfig::with_filter(runner, filter).evm_spec(SpecId::SHANGHAI).run().await; + } +} + +forgetest_async!(script_zk_can_deploy_in_method, |prj, cmd| { + setup_factory_prj(&mut prj); + run_factory_script_test(prj.root(), &mut cmd, "ZkClassicFactoryScript", 2); + run_factory_script_test(prj.root(), &mut cmd, "ZkNestedFactoryScript", 2); +}); + +forgetest_async!(script_zk_can_deploy_in_constructor, |prj, cmd| { + setup_factory_prj(&mut prj); + run_factory_script_test(prj.root(), &mut cmd, "ZkConstructorFactoryScript", 1); + run_factory_script_test(prj.root(), &mut cmd, "ZkNestedConstructorFactoryScript", 1); +}); + +forgetest_async!(script_zk_can_use_predeployed_factory, |prj, cmd| { + setup_factory_prj(&mut prj); + run_factory_script_test(prj.root(), &mut cmd, "ZkUserFactoryScript", 3); + run_factory_script_test(prj.root(), &mut cmd, "ZkUserConstructorFactoryScript", 2); +}); + +fn setup_factory_prj(prj: &mut TestProject) { + util::initialize(prj.root()); + prj.add_source("Factory.sol", include_str!("../../../../../testdata/zk/Factory.sol")).unwrap(); + prj.add_script("Factory.s.sol", include_str!("../../fixtures/zk/Factory.s.sol")).unwrap(); +} + +fn run_factory_script_test( + root: impl AsRef, + cmd: &mut TestCommand, + name: &str, + expected_broadcastable_txs: usize, +) { + let node = ZkSyncNode::start(); + + cmd.arg("script").args([ + "--zk-startup", + &format!("./script/Factory.s.sol:{name}"), + "--broadcast", + "--private-key", + "0x3d3cbc973389cb26f657686445bcc75662b415b656078503592ac8c1abb8810e", + "--chain", + "260", + "--gas-estimate-multiplier", + "310", + "--rpc-url", + node.url().as_str(), + "--slow", + "--evm-version", + "shanghai", + ]); + + assert!(cmd.stdout_lossy().contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL")); + + let run_latest = foundry_common::fs::json_files(root.as_ref().join("broadcast").as_path()) + .find(|file| file.ends_with("run-latest.json")) + .expect("No broadcast artifacts"); + + let content = foundry_common::fs::read_to_string(run_latest).unwrap(); + + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!( + json["transactions"].as_array().expect("broadcastable txs").len(), + expected_broadcastable_txs + ); + cmd.forge_fuse(); +} diff --git a/crates/forge/tests/it/zk/mod.rs b/crates/forge/tests/it/zk/mod.rs index 50b97190f..404375e48 100644 --- a/crates/forge/tests/it/zk/mod.rs +++ b/crates/forge/tests/it/zk/mod.rs @@ -2,6 +2,7 @@ mod basic; mod cheats; mod contracts; +mod factory; mod fuzz; mod invariant; mod logs; diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index e3cb1f542..6e3e2d15a 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -34,6 +34,13 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] } walkdir.workspace = true rand.workspace = true snapbox = { version = "0.6.9", features = ["json"] } +tokio.workspace = true + +# zk +zksync_types.workspace = true +era_test_node.workspace = true +jsonrpc-core = { git = "https://github.com/matter-labs/jsonrpc.git", branch = "master" } +jsonrpc-http-server = { git = "https://github.com/matter-labs/jsonrpc.git", branch = "master" } [features] # feature for integration tests that test external projects diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 2a0093278..bb811d9ce 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -25,6 +25,10 @@ pub use util::{TestCommand, TestProject}; mod script; pub use script::{ScriptOutcome, ScriptTester}; +// TODO: remove once anvil supports zksync node +mod zksync; +pub use zksync::ZkSyncNode; + // re-exports for convenience pub use foundry_compilers; diff --git a/crates/forge/tests/cli/zksync_node.rs b/crates/test-utils/src/zksync.rs similarity index 85% rename from crates/forge/tests/cli/zksync_node.rs rename to crates/test-utils/src/zksync.rs index 640884335..f3833080e 100644 --- a/crates/forge/tests/cli/zksync_node.rs +++ b/crates/test-utils/src/zksync.rs @@ -12,12 +12,9 @@ use era_test_node::{ }, node::InMemoryNode, }; -use futures::{SinkExt, StreamExt}; use jsonrpc_core::IoHandler; use zksync_types::H160; -const DEFAULT_PORT: u16 = 18011; - /// List of legacy wallets (address, private key) that we seed with tokens at start. const LEGACY_RICH_WALLETS: [(&str, &str); 10] = [ ( @@ -118,26 +115,22 @@ const RICH_WALLETS: [(&str, &str, &str); 10] = [ /// In-memory era-test-node that is stopped when dropped. pub struct ZkSyncNode { - close_tx: futures::channel::mpsc::Sender<()>, -} - -impl Drop for ZkSyncNode { - fn drop(&mut self) { - self.stop(); - } + port: u16, + _guard: tokio::sync::oneshot::Sender<()>, } impl ZkSyncNode { /// Returns the server url. #[inline] pub fn url(&self) -> String { - format!("http://127.0.0.1:{DEFAULT_PORT}") + format!("http://127.0.0.1:{}", self.port) } - /// Start era-test-node in memory at the [DEFAULT_PORT]. The server is automatically stopped - /// when the instance is dropped. + /// Start era-test-node in memory, binding a random available port + /// + /// The server is automatically stopped when the instance is dropped. pub fn start() -> Self { - let (tx, mut rx) = futures::channel::mpsc::channel::<()>(1); + let (_guard, _guard_rx) = tokio::sync::oneshot::channel::<()>(); let io_handler = { let node: InMemoryNode = @@ -165,37 +158,36 @@ impl ZkSyncNode { io.extend_with(ZksNamespaceT::to_delegate(node)); io }; - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .worker_threads(1) - .build() - .unwrap(); + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0); + let (port_tx, port) = tokio::sync::oneshot::channel(); - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), DEFAULT_PORT); std::thread::spawn(move || { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .worker_threads(2) + .build() + .unwrap(); + let server = jsonrpc_http_server::ServerBuilder::new(io_handler) .threads(1) .event_loop_executor(runtime.handle().clone()) .start_http(&addr) .unwrap(); - futures::executor::block_on(async { - let _ = rx.next().await; - }); + // if no receiver was ready to receive the spawning thread died + _ = port_tx.send(server.address().port()); + // we only care that the channel is alive + _ = tokio::task::block_in_place(move || runtime.block_on(_guard_rx)); server.close(); }); // wait for server to start std::thread::sleep(std::time::Duration::from_millis(600)); + let port = + tokio::task::block_in_place(move || tokio::runtime::Handle::current().block_on(port)) + .expect("failed to start server"); - Self { close_tx: tx } - } - - /// Stop the running era-test-node. - pub fn stop(&mut self) { - futures::executor::block_on(async { - let _ = self.close_tx.send(()).await; - }); + Self { _guard, port } } } diff --git a/testdata/zk/Factory.sol b/testdata/zk/Factory.sol new file mode 100644 index 000000000..0c75f366f --- /dev/null +++ b/testdata/zk/Factory.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.0; + +/// Set of tests for factory contracts +/// +/// *Constructor factories build their dependencies in their constructors +/// *User factories don't deploy but assume the given address to be a deployed factory + +contract MyContract { + uint256 public number; + + constructor(uint256 _number) { + number = _number; + } +} + +contract MyClassicFactory { + MyContract item; + + function create(uint256 _number) public { + item = new MyContract(_number); + } + + function getNumber() public view returns (uint256) { + return item.number(); + } +} + +contract MyConstructorFactory { + MyContract item; + + constructor(uint256 _number) { + item = new MyContract(_number); + } + + function getNumber() public view returns (uint256) { + return item.number(); + } +} + +contract MyNestedFactory { + MyClassicFactory nested; + + function create(uint256 _number) public { + nested = new MyClassicFactory(); + + nested.create(_number); + } + + function getNumber() public view returns (uint256) { + return nested.getNumber(); + } +} + +contract MyNestedConstructorFactory { + MyClassicFactory nested; + + constructor(uint256 _number) { + nested = new MyClassicFactory(); + + nested.create(_number); + } + + function getNumber() public view returns (uint256) { + return nested.getNumber(); + } +} + +contract MyUserFactory { + function create(address classicFactory, uint256 _number) public { + MyClassicFactory(classicFactory).create(_number); + } + + function getNumber(address classicFactory) public view returns (uint256) { + return MyClassicFactory(classicFactory).getNumber(); + } +} diff --git a/testdata/zk/Factory.t.sol b/testdata/zk/Factory.t.sol new file mode 100644 index 000000000..31ff350c8 --- /dev/null +++ b/testdata/zk/Factory.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.0; + +import "ds-test/test.sol"; +import "../cheats/Vm.sol"; + +import "./Factory.sol"; + +contract ZkFactoryTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function testClassicFactory() public { + MyClassicFactory factory = new MyClassicFactory(); + factory.create(42); + + assert(factory.getNumber() == 42); + } + + function testConstructorFactory() public { + MyConstructorFactory factory = new MyConstructorFactory(42); + + assert(factory.getNumber() == 42); + } + + function testNestedFactory() public { + MyNestedFactory factory = new MyNestedFactory(); + factory.create(42); + + assert(factory.getNumber() == 42); + } + + function testNestedConstructorFactory() public { + MyNestedConstructorFactory factory = new MyNestedConstructorFactory(42); + + assert(factory.getNumber() == 42); + } + + function testUserFactory() public { + MyClassicFactory factory = new MyClassicFactory(); + MyUserFactory user = new MyUserFactory(); + user.create(address(factory), 42); + + assert(user.getNumber(address(factory)) == 42); + } + + function testUserConstructorFactory() public { + MyConstructorFactory factory = new MyConstructorFactory(42); + MyUserFactory user = new MyUserFactory(); + + assert(user.getNumber(address(factory)) == 42); + } +}