From 904f5453988136055d32c2a3e16686bdf22ff852 Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Fri, 15 Dec 2023 14:10:05 +0100 Subject: [PATCH 1/4] feat: decouple env, use storage for era_test_node block and timestamp, enable fuzz testing (#195) --- crates/common/src/zk_compile.rs | 4 +- crates/evm/core/src/era_revm/db.rs | 42 ++++++++++++++++---- crates/evm/core/src/era_revm/transactions.rs | 28 +++++-------- crates/evm/evm/src/executors/mod.rs | 22 +++------- 4 files changed, 51 insertions(+), 45 deletions(-) diff --git a/crates/common/src/zk_compile.rs b/crates/common/src/zk_compile.rs index 4bcda1827..5cca6536a 100644 --- a/crates/common/src/zk_compile.rs +++ b/crates/common/src/zk_compile.rs @@ -203,7 +203,7 @@ pub fn compile_smart_contracts( match zksolc.compile() { Ok(_) => { - println!("Compiled Successfully"); + info!("Compiled Successfully"); Ok(()) } Err(err) => { @@ -718,7 +718,7 @@ impl ZkSolc { if has_error { exit(1); } else if has_warning { - println!("Compiler run completed with warnings"); + warn!("Compiler run completed with warnings"); } } /// Handles and formats the errors present in the output JSON from the zksolc compiler. diff --git a/crates/evm/core/src/era_revm/db.rs b/crates/evm/core/src/era_revm/db.rs index 02a7ac6ed..27d258e55 100644 --- a/crates/evm/core/src/era_revm/db.rs +++ b/crates/evm/core/src/era_revm/db.rs @@ -33,7 +33,7 @@ use zksync_utils::{address_to_h256, h256_to_u256, u256_to_h256}; #[derive(Default)] pub struct RevmDatabaseForEra { pub db: Arc>>, - pub current_block: u64, + current_block: u64, } impl Clone for RevmDatabaseForEra { @@ -55,9 +55,26 @@ impl RevmDatabaseForEra where ::Error: Debug, { - /// Returns the current block number and timestamp from the database. + /// Create a new instance of [RevmDatabaseForEra] caching the current l2 block. + pub fn new(db: Arc>>) -> Self { + let db_inner = db.clone(); + let current_block = { + let mut db = db_inner.lock().expect("failed aquiring lock on the database"); + let result = db + .storage(h160_to_address(SYSTEM_CONTEXT_ADDRESS), u256_to_revm_u256(U256::from(9))) + .unwrap(); + let num_and_ts = revm_u256_to_h256(result); + let num_and_ts_bytes = num_and_ts.as_fixed_bytes(); + let num: [u8; 8] = num_and_ts_bytes[24..32].try_into().unwrap(); + u64::from_be_bytes(num) + }; + + Self { db, current_block } + } + + /// Returns the current L1 block number and timestamp from the database. /// Reads it directly from the SYSTEM_CONTEXT storage. - pub fn block_number_and_timestamp(&self) -> (u64, u64) { + pub fn get_l1_block_number_and_timestamp(&self) -> (u64, u64) { let num_and_ts = self.read_storage_internal(SYSTEM_CONTEXT_ADDRESS, U256::from(7)); let num_and_ts_bytes = num_and_ts.as_fixed_bytes(); let num: [u8; 8] = num_and_ts_bytes[24..32].try_into().unwrap(); @@ -66,6 +83,17 @@ where (u64::from_be_bytes(num), u64::from_be_bytes(ts)) } + /// Returns the current L2 block number and timestamp from the database. + /// Reads it directly from the SYSTEM_CONTEXT storage. + pub fn get_l2_block_number_and_timestamp(&self) -> (u64, u64) { + let num_and_ts = self.read_storage_internal(SYSTEM_CONTEXT_ADDRESS, U256::from(9)); + let num_and_ts_bytes = num_and_ts.as_fixed_bytes(); + let num: [u8; 8] = num_and_ts_bytes[24..32].try_into().unwrap(); + let ts: [u8; 8] = num_and_ts_bytes[8..16].try_into().unwrap(); + + (u64::from_be_bytes(num), u64::from_be_bytes(ts)) + } + /// Returns the nonce for a given account from NonceHolder storage. pub fn get_nonce_for_address(&self, address: H160) -> u64 { // Nonce is stored in the first mapping of the Nonce contract. @@ -155,14 +183,12 @@ where idx: U256, block: Option, ) -> eyre::Result { - // We cannot support historical lookups. Only the most recent block is supported. - let current_block = self.current_block; + // We cannot support historical lookups. Only the most recent L2 block is supported. if let Some(block) = &block { match block { BlockIdVariant::BlockNumber(zksync_types::api::BlockNumber::Number(num)) => { - let current_block_number_l2 = current_block * 2; - if num.as_u64() != current_block_number_l2 { - eyre::bail!("Only fetching of the most recent L2 block {} is supported - but queried for {}", current_block_number_l2, num) + if num.as_u64() != self.current_block { + eyre::bail!("Only fetching of the most recent L2 block {} is supported - but queried for {}", self.current_block, num) } } _ => eyre::bail!("Only fetching most recent block is implemented"), diff --git a/crates/evm/core/src/era_revm/transactions.rs b/crates/evm/core/src/era_revm/transactions.rs index 7563cb373..5537f9268 100644 --- a/crates/evm/core/src/era_revm/transactions.rs +++ b/crates/evm/core/src/era_revm/transactions.rs @@ -135,19 +135,11 @@ where ::Error: Debug, INSP: ToTracerPointer>>, HistoryDisabled>, { - let (num, ts) = (env.block.number.to::(), env.block.timestamp.to::()); - let era_db = RevmDatabaseForEra { db: Arc::new(Mutex::new(Box::new(db))), current_block: num }; - - let nonces = era_db.get_nonce_for_address(address_to_h160(env.tx.caller)); - - debug!( - "*** Starting ERA transaction: block: {:?} timestamp: {:?} - but using {:?} and {:?} instead with nonce {:?}", - env.block.number.to::(), - env.block.timestamp.to::(), - num, - ts, - nonces - ); + let era_db = RevmDatabaseForEra::new(Arc::new(Mutex::new(Box::new(db)))); + let nonce = era_db.get_nonce_for_address(address_to_h160(env.tx.caller)); + let (num, ts) = era_db.get_l2_block_number_and_timestamp(); + + debug!("Starting ERA transaction: block={:?} timestamp={:?} nonce={:?}", num, ts, nonce); // Update the environment timestamp and block number. // Check if this should be done at the end? @@ -161,14 +153,14 @@ where 31337 }; - let (l2_num, l2_ts) = (num * 2, ts * 2); + let l1_num = num / 2; let fork_details = ForkDetails { fork_source: era_db.clone(), - l1_block: L1BatchNumber(num as u32), + l1_block: L1BatchNumber(l1_num as u32), l2_block: Block::default(), - l2_miniblock: l2_num, + l2_miniblock: num, l2_miniblock_hash: Default::default(), - block_timestamp: l2_ts, + block_timestamp: ts, overwrite_chain_id: Some(L2ChainId::from(chain_id_u32)), // Make sure that l1 gas price is set to reasonable values. l1_gas_price: u64::max(env.block.basefee.to::(), 1000), @@ -184,7 +176,7 @@ where }; let node = InMemoryNode::new(Some(fork_details), None, config); - let mut l2_tx = tx_env_to_era_tx(env.tx.clone(), nonces); + let mut l2_tx = tx_env_to_era_tx(env.tx.clone(), nonce); if l2_tx.common_data.signature.is_empty() { // FIXME: This is a hack to make sure that the signature is not empty. diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 5f58cc53f..996d01a3a 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -147,15 +147,6 @@ impl Executor { self } - // Record any changes made to the block's environment during setup, - // and also the chainid, which can be set manually. - pub fn record_env_changes(&mut self, env: &Env) { - // record any changes made to the block's environment during setup - self.env.block = env.block.clone(); - // and also the chainid, which can be set manually - self.env.cfg.chain_id = env.cfg.chain_id; - } - /// Calls the `setUp()` function on a contract. /// /// This will commit any state changes to the underlying database. @@ -169,7 +160,10 @@ impl Executor { self.backend.set_test_contract(to).set_caller(from); let res = self.call_committing::<_, _>(from, to, "setUp()", vec![], U256::ZERO, None)?; - self.record_env_changes(&res.env); + // record any changes made to the block's environment during setup + self.env.block = res.env.block.clone(); + // and also the chainid, which can be set manually + self.env.cfg.chain_id = res.env.cfg.chain_id; match res.state_changeset.as_ref() { Some(changeset) => { @@ -249,7 +243,6 @@ impl Executor { // execute the call let env = self.build_test_env(from, TransactTo::Call(test_contract), calldata, value); let call_result = self.call_raw_with_env(env)?; - self.record_env_changes(&call_result.env); convert_call_result(abi, &func, call_result) } @@ -432,12 +425,7 @@ impl Executor { abi: Option<&Abi>, ) -> Result { let env = self.build_test_env(from, TransactTo::Create(CreateScheme::Create), code, value); - let res = self.deploy_with_env(env, abi); - if let Ok(DeployResult { env, .. }) = &res { - self.record_env_changes(env); - } - - res + self.deploy_with_env(env, abi) } /// Check if a call to a test contract was successful. From 1f8590e4dd10322b6448cb8c19f9a8f5cfd863c2 Mon Sep 17 00:00:00 2001 From: Agustin Aon <21188659+aon@users.noreply.github.com> Date: Fri, 15 Dec 2023 10:49:39 -0300 Subject: [PATCH 2/4] feat: migrate era revm cheatcodes (#202) Co-authored-by: Nisheeth Barthwal --- crates/era-cheatcodes/.gitignore | 2 + crates/era-cheatcodes/src/cheatcodes.rs | 157 +++++++++++++++++- .../tests/src/cheatcodes/Ffi.t.sol | 45 +++++ .../tests/src/cheatcodes/Fs.t.sol | 50 ++++++ .../tests/src/cheatcodes/ReadCallers.t.sol | 66 ++++++++ .../tests/src/cheatcodes/Serialize.t.sol | 23 +-- .../tests/src/cheatcodes/ToString.t.sol | 31 +--- .../tests/src/cheatcodes/TryFfi.t.sol | 51 ++++++ .../tests/src/cheatcodes/Utils.sol | 20 +++ .../tests/src/cheatcodes/WriteJson.t.sol | 91 ++++++++++ .../tests/src/fixtures/File/read.txt | 2 + .../tests/src/fixtures/Json/.gitkeep | 0 12 files changed, 494 insertions(+), 44 deletions(-) create mode 100644 crates/era-cheatcodes/tests/src/cheatcodes/Ffi.t.sol create mode 100644 crates/era-cheatcodes/tests/src/cheatcodes/Fs.t.sol create mode 100644 crates/era-cheatcodes/tests/src/cheatcodes/ReadCallers.t.sol create mode 100644 crates/era-cheatcodes/tests/src/cheatcodes/TryFfi.t.sol create mode 100644 crates/era-cheatcodes/tests/src/cheatcodes/Utils.sol create mode 100644 crates/era-cheatcodes/tests/src/cheatcodes/WriteJson.t.sol create mode 100644 crates/era-cheatcodes/tests/src/fixtures/File/read.txt create mode 100644 crates/era-cheatcodes/tests/src/fixtures/Json/.gitkeep diff --git a/crates/era-cheatcodes/.gitignore b/crates/era-cheatcodes/.gitignore index c9b090484..5b9efc3a6 100644 --- a/crates/era-cheatcodes/.gitignore +++ b/crates/era-cheatcodes/.gitignore @@ -1,6 +1,8 @@ /target /Cargo.lock tests/* +tests/src/fixtures/File/write_file.txt +tests/src/fixtures/Json/write_test.json !tests/src !tests/lib !tests/test.sh diff --git a/crates/era-cheatcodes/src/cheatcodes.rs b/crates/era-cheatcodes/src/cheatcodes.rs index 02b0934b5..310b2eb02 100644 --- a/crates/era-cheatcodes/src/cheatcodes.rs +++ b/crates/era-cheatcodes/src/cheatcodes.rs @@ -1,5 +1,5 @@ use crate::utils::{ToH160, ToH256}; -use alloy_sol_types::SolInterface; +use alloy_sol_types::{SolInterface, SolValue}; use era_test_node::{fork::ForkStorage, utils::bytecode_to_factory_dep}; use ethers::utils::to_checksum; use foundry_cheatcodes_spec::Vm; @@ -17,7 +17,7 @@ use multivm::{ }, }, }; -use std::{cell::RefMut, collections::HashMap, fmt::Debug}; +use std::{cell::RefMut, collections::HashMap, fmt::Debug, fs, process::Command}; use zksync_basic_types::{AccountTreeId, H160, H256, U256}; use zksync_state::{ReadStorage, StoragePtr, StorageView, WriteStorage}; use zksync_types::{ @@ -253,6 +253,34 @@ impl CheatcodeTracer { self.store_factory_dep(hash, code); self.write_storage(code_key, u256_to_h256(hash), &mut storage.borrow_mut()); } + ffi(ffiCall { commandInput: command_input }) => { + tracing::info!("👷 Running ffi: {command_input:?}"); + let Some(first_arg) = command_input.get(0) else { + tracing::error!("Failed to run ffi: no args"); + return + }; + // TODO: set directory to root + let Ok(output) = Command::new(first_arg).args(&command_input[1..]).output() else { + tracing::error!("Failed to run ffi"); + return + }; + + // The stdout might be encoded on valid hex, or it might just be a string, + // so we need to determine which it is to avoid improperly encoding later. + let Ok(trimmed_stdout) = String::from_utf8(output.stdout) else { + tracing::error!("Failed to parse ffi output"); + return + }; + let trimmed_stdout = trimmed_stdout.trim(); + let encoded_stdout = + if let Ok(hex) = hex::decode(trimmed_stdout.trim_start_matches("0x")) { + hex + } else { + trimmed_stdout.as_bytes().to_vec() + }; + + self.add_trimmed_return_data(&encoded_stdout); + } getNonce_0(getNonce_0Call { account }) => { tracing::info!("👷 Getting nonce for {account:?}"); let mut storage = storage.borrow_mut(); @@ -275,6 +303,52 @@ impl CheatcodeTracer { let value = storage.read_value(&key); self.return_data = Some(vec![h256_to_u256(value)]); } + readCallers(readCallersCall {}) => { + tracing::info!("👷 Reading callers"); + + let current_origin = { + let key = StorageKey::new( + AccountTreeId::new(zksync_types::SYSTEM_CONTEXT_ADDRESS), + zksync_types::SYSTEM_CONTEXT_TX_ORIGIN_POSITION, + ); + + storage.borrow_mut().read_value(&key) + }; + + let mut mode = CallerMode::None; + let mut new_caller = current_origin; + + if let Some(prank) = &self.permanent_actions.start_prank { + //TODO: vm.prank -> CallerMode::Prank + println!("PRANK"); + mode = CallerMode::RecurrentPrank; + new_caller = prank.sender.into(); + } + // TODO: vm.broadcast / vm.startBroadcast section + // else if let Some(broadcast) = broadcast { + // mode = if broadcast.single_call { + // CallerMode::Broadcast + // } else { + // CallerMode::RecurrentBroadcast + // }; + // new_caller = &broadcast.new_origin; + // new_origin = &broadcast.new_origin; + // } + + let caller_mode = (mode as u8).into(); + let message_sender = h256_to_u256(new_caller); + let tx_origin = h256_to_u256(current_origin); + + self.return_data = Some(vec![caller_mode, message_sender, tx_origin]); + } + readFile(readFileCall { path }) => { + tracing::info!("👷 Reading file in path {}", path); + let Ok(data) = fs::read(path) else { + tracing::error!("Failed to read file"); + return + }; + self.add_trimmed_return_data(&data); + } roll(rollCall { newHeight: new_height }) => { tracing::info!("👷 Setting block number to {}", new_height); let key = StorageKey::new( @@ -459,6 +533,42 @@ impl CheatcodeTracer { let int_value = value.to_string(); self.add_trimmed_return_data(int_value.as_bytes()); } + tryFfi(tryFfiCall { commandInput: command_input }) => { + tracing::info!("👷 Running try ffi: {command_input:?}"); + let Some(first_arg) = command_input.get(0) else { + tracing::error!("Failed to run ffi: no args"); + return + }; + // TODO: set directory to root + let Ok(output) = Command::new(first_arg).args(&command_input[1..]).output() else { + tracing::error!("Failed to run ffi"); + return + }; + + // The stdout might be encoded on valid hex, or it might just be a string, + // so we need to determine which it is to avoid improperly encoding later. + let Ok(trimmed_stdout) = String::from_utf8(output.stdout) else { + tracing::error!("Failed to parse ffi output"); + return + }; + let trimmed_stdout = trimmed_stdout.trim(); + let encoded_stdout = + if let Ok(hex) = hex::decode(trimmed_stdout.trim_start_matches("0x")) { + hex + } else { + trimmed_stdout.as_bytes().to_vec() + }; + + let ffi_result = FfiResult { + exitCode: output.status.code().unwrap_or(69), // Default from foundry + stdout: encoded_stdout, + stderr: output.stderr, + }; + let encoded_ffi_result: Vec = ffi_result.abi_encode(); + let return_data: Vec = + encoded_ffi_result.chunks(32).map(|b| b.into()).collect_vec(); + self.return_data = Some(return_data); + } warp(warpCall { newTimestamp: new_timestamp }) => { tracing::info!("👷 Setting block timestamp {}", new_timestamp); @@ -474,6 +584,49 @@ impl CheatcodeTracer { &mut storage, ); } + writeFile(writeFileCall { path, data }) => { + tracing::info!("👷 Writing data to file in path {}", path); + if fs::write(path, data).is_err() { + tracing::error!("Failed to write file"); + } + } + writeJson_0(writeJson_0Call { json, path }) => { + tracing::info!("👷 Writing json data to file in path {}", path); + let Ok(json) = serde_json::from_str::(&json) else { + tracing::error!("Failed to parse json"); + return + }; + let Ok(formatted_json) = serde_json::to_string_pretty(&json) else { + tracing::error!("Failed to format json"); + return + }; + if fs::write(path, formatted_json).is_err() { + tracing::error!("Failed to write file"); + } + } + writeJson_1(writeJson_1Call { json, path, valueKey: value_key }) => { + tracing::info!("👷 Writing json data to file in path {path} with key {value_key}"); + let Ok(file) = fs::read_to_string(&path) else { + tracing::error!("Failed to read file"); + return + }; + let Ok(mut file_json) = serde_json::from_str::(&file) else { + tracing::error!("Failed to parse json"); + return + }; + let Ok(json) = serde_json::from_str::(&json) else { + tracing::error!("Failed to parse json"); + return + }; + file_json[value_key] = json; + let Ok(formatted_json) = serde_json::to_string_pretty(&file_json) else { + tracing::error!("Failed to format json"); + return + }; + if fs::write(path, formatted_json).is_err() { + tracing::error!("Failed to write file"); + } + } _ => { tracing::error!("👷 Unrecognized cheatcode"); } diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Ffi.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Ffi.t.sol new file mode 100644 index 000000000..a70ba44e8 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Ffi.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; +import {Utils} from "./Utils.sol"; + +contract FfiTest is Test { + function testFfi() public { + string[] memory inputs = new string[](3); + inputs[0] = "bash"; + inputs[1] = "-c"; + inputs[ + 2 + ] = "echo -n 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000966666920776f726b730000000000000000000000000000000000000000000000"; + + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("ffi(string[])", inputs) + ); + require(success, "ffi failed"); + + bytes memory data = Utils.trimReturnBytes(rawData); + string memory output = abi.decode(data, (string)); + require( + keccak256(bytes(output)) == keccak256(bytes("ffi works")), + "ffi failed" + ); + + console.log("failed?", failed()); + } + + function testFfiString() public { + string[] memory inputs = new string[](3); + inputs[0] = "echo"; + inputs[1] = "-n"; + inputs[2] = "gm"; + + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("ffi(string[])", inputs) + ); + require(success, "ffi failed"); + bytes memory data = Utils.trimReturnBytes(rawData); + require(keccak256(data) == keccak256(bytes("gm")), "ffi failed"); + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Fs.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Fs.t.sol new file mode 100644 index 000000000..5f6cb6b79 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Fs.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; +import {Utils} from "./Utils.sol"; + +contract FsTest is Test { + function testReadFile() public { + string memory path = "src/fixtures/File/read.txt"; + + (bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("readFile(string)", path) + ); + require(success, "readFile failed"); + + bytes memory data = Utils.trimReturnBytes(rawData); + + require( + keccak256(data) == + keccak256("hello readable world\nthis is the second line!\n"), + "read data did not match expected data" + ); + console.log("failed?", failed()); + } + + function testWriteFile() public { + string memory path = "src/fixtures/File/write_file.txt"; + string memory writeData = "hello writable world"; + + (bool success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("writeFile(string,string)", path, writeData) + ); + require(success, "writeFile failed"); + + bytes memory readRawData; + (success, readRawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("readFile(string)", path) + ); + require(success, "readFile failed"); + + bytes memory readData = Utils.trimReturnBytes(readRawData); + + require( + keccak256(readData) == keccak256(bytes(writeData)), + "read data did not match write data" + ); + console.log("failed?", failed()); + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/ReadCallers.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/ReadCallers.t.sol new file mode 100644 index 000000000..e63c72d9f --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/ReadCallers.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +contract CheatcodeReadCallers is Test { + address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a; + address constant TEST_ORIGIN = 0xdEBe90b7BFD87Af696B1966082F6515a6E72F3d8; + + // enum CallerMode { + // None, + // Broadcast, + // RecurrentBroadcast, + // Prank, + // RecurrentPrank + // } + + function testNormalReadCallers() public { + (bool success, bytes memory data) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("readCallers()")); + require(success, "readCallers failed"); + + (uint8 mode, address sender, address origin) = abi.decode(data, (uint8, address, address)); + require(mode == 0, "normal call mode"); + require(sender == msg.sender, "sender not overridden"); + require(origin == tx.origin, "origin not overridden"); + } + + function testPrankedReadCallers() public { + (bool success1, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("startPrank(address)", TEST_ADDRESS) + ); + require(success1, "startPrank failed"); + + (bool success, bytes memory data) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("readCallers()")); + require(success, "readCallers failed"); + + (uint8 mode, address sender, address origin) = abi.decode(data, (uint8, address, address)); + require(mode == 4, "recurrent prank call mode"); + require(sender == TEST_ADDRESS, "sender overridden"); + require(origin == tx.origin, "origin not overridden"); + + console.log("failed?", failed()); + } + + function testFullyPrankedReadCallers() public { + (bool success1, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("startPrank(address,address)", TEST_ADDRESS, TEST_ORIGIN) + ); + require(success1, "startPrank failed"); + + (bool success, bytes memory data) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("readCallers()")); + require(success, "readCallers failed"); + + (uint8 mode, address sender, address origin) = abi.decode(data, (uint8, address, address)); + + require(mode == 4, "recurrent prank call mode"); + require(sender == TEST_ADDRESS, "sender overridden"); + require(origin == TEST_ORIGIN, "origin overridden"); + + console.log("failed?", failed()); + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Serialize.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Serialize.t.sol index c2c95b1c8..e2109f41f 100644 --- a/crates/era-cheatcodes/tests/src/cheatcodes/Serialize.t.sol +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Serialize.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; import {Constants} from "./Constants.sol"; +import {Utils} from "./Utils.sol"; contract CheatcodeSerializeTest is Test { address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a; @@ -17,7 +18,7 @@ contract CheatcodeSerializeTest is Test { ) ); require(success, "serializeAddress failed"); - bytes memory data = trimReturnBytes(rawData); + bytes memory data = Utils.trimReturnBytes(rawData); string memory testString = string(abi.encodePacked(data)); require( keccak256(bytes(testString)) == @@ -37,7 +38,7 @@ contract CheatcodeSerializeTest is Test { ) ); require(success, "serializeBool failed"); - bytes memory data = trimReturnBytes(rawData); + bytes memory data = Utils.trimReturnBytes(rawData); string memory testString = string(abi.encodePacked(data)); require( keccak256(bytes(testString)) == keccak256(bytes("true")), @@ -56,7 +57,7 @@ contract CheatcodeSerializeTest is Test { ) ); require(success, "serializeUint failed"); - bytes memory data = trimReturnBytes(rawData); + bytes memory data = Utils.trimReturnBytes(rawData); string memory testString = string(abi.encodePacked(data)); require( keccak256(bytes(testString)) == keccak256(bytes("99")), @@ -64,20 +65,4 @@ contract CheatcodeSerializeTest is Test { ); console.log("failed?", failed()); } - - function trimReturnBytes( - bytes memory rawData - ) internal pure returns (bytes memory) { - uint256 lengthStartingPos = rawData.length - 32; - bytes memory lengthSlice = new bytes(32); - for (uint256 i = 0; i < 32; i++) { - lengthSlice[i] = rawData[lengthStartingPos + i]; - } - uint256 length = abi.decode(lengthSlice, (uint256)); - bytes memory data = new bytes(length); - for (uint256 i = 0; i < length; i++) { - data[i] = rawData[i]; - } - return data; - } } diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/ToString.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/ToString.t.sol index ff9305123..e5f78f2fc 100644 --- a/crates/era-cheatcodes/tests/src/cheatcodes/ToString.t.sol +++ b/crates/era-cheatcodes/tests/src/cheatcodes/ToString.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; import {Constants} from "./Constants.sol"; +import {Utils} from "./Utils.sol"; contract CheatcodeToStringTest is Test { function testToStringFromAddress() external { @@ -11,7 +12,7 @@ contract CheatcodeToStringTest is Test { abi.encodeWithSignature("toString(address)", testAddress) ); require(success, "toString failed"); - bytes memory data = trimReturnBytes(rawData); + bytes memory data = Utils.trimReturnBytes(rawData); string memory testString = string(abi.encodePacked(data)); require( keccak256(bytes(testString)) == @@ -26,7 +27,7 @@ contract CheatcodeToStringTest is Test { abi.encodeWithSignature("toString(bool)", false) ); require(success, "toString failed"); - bytes memory data = trimReturnBytes(rawData); + bytes memory data = Utils.trimReturnBytes(rawData); string memory testString = string(abi.encodePacked(data)); require( keccak256(bytes(testString)) == keccak256(bytes("false")), @@ -37,7 +38,7 @@ contract CheatcodeToStringTest is Test { abi.encodeWithSignature("toString(bool)", true) ); require(success, "toString failed"); - data = trimReturnBytes(rawData); + data = Utils.trimReturnBytes(rawData); testString = string(abi.encodePacked(data)); require( keccak256(bytes(testString)) == keccak256(bytes("true")), @@ -53,7 +54,7 @@ contract CheatcodeToStringTest is Test { abi.encodeWithSignature("toString(uint256)", value) ); require(success, "toString failed"); - bytes memory data = trimReturnBytes(rawData); + bytes memory data = Utils.trimReturnBytes(rawData); string memory testString = string(abi.encodePacked(data)); require( keccak256(bytes(testString)) == keccak256(bytes(stringValue)), @@ -69,7 +70,7 @@ contract CheatcodeToStringTest is Test { abi.encodeWithSignature("toString(int256)", value) ); require(success, "toString failed"); - bytes memory data = trimReturnBytes(rawData); + bytes memory data = Utils.trimReturnBytes(rawData); string memory testString = string(abi.encodePacked(data)); require( keccak256(bytes(testString)) == keccak256(bytes(stringValue)), @@ -84,7 +85,7 @@ contract CheatcodeToStringTest is Test { abi.encodeWithSignature("toString(bytes32)", testBytes) ); require(success, "toString failed"); - bytes memory data = trimReturnBytes(rawData); + bytes memory data = Utils.trimReturnBytes(rawData); string memory testString = string(abi.encodePacked(data)); require( keccak256(bytes(testString)) == @@ -105,7 +106,7 @@ contract CheatcodeToStringTest is Test { abi.encodeWithSignature("toString(bytes)", testBytes) ); require(success, "toString failed"); - bytes memory data = trimReturnBytes(rawData); + bytes memory data = Utils.trimReturnBytes(rawData); string memory testString = string(abi.encodePacked(data)); require( keccak256(bytes(testString)) == @@ -118,20 +119,4 @@ contract CheatcodeToStringTest is Test { ); console.log("failed?", failed()); } - - function trimReturnBytes( - bytes memory rawData - ) internal pure returns (bytes memory) { - uint256 lengthStartingPos = rawData.length - 32; - bytes memory lengthSlice = new bytes(32); - for (uint256 i = 0; i < 32; i++) { - lengthSlice[i] = rawData[lengthStartingPos + i]; - } - uint256 length = abi.decode(lengthSlice, (uint256)); - bytes memory data = new bytes(length); - for (uint256 i = 0; i < length; i++) { - data[i] = rawData[i]; - } - return data; - } } diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/TryFfi.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/TryFfi.t.sol new file mode 100644 index 000000000..feb29769b --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/TryFfi.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; +import {Utils} from "./Utils.sol"; + +contract FfiTest is Test { + struct FfiResult { + int32 exitCode; + bytes stdout; + bytes stderr; + } + + function testTryFfi() public { + string[] memory inputs = new string[](3); + inputs[0] = "bash"; + inputs[1] = "-c"; + inputs[ + 2 + ] = "echo -n 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000966666920776f726b730000000000000000000000000000000000000000000000"; + + (bool success, bytes memory data) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("tryFfi(string[])", inputs) + ); + require(success, "tryFfi failed"); + + FfiResult memory f = abi.decode(data, (FfiResult)); + string memory output = abi.decode(f.stdout, (string)); + + require( + keccak256(bytes(output)) == keccak256(bytes("ffi works")), + "ffi failed" + ); + require(f.exitCode == 0, "ffi failed"); + } + + function testTryFfiFail() public { + string[] memory inputs = new string[](2); + inputs[0] = "ls"; + inputs[1] = "wad"; + + (bool success, bytes memory data) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("tryFfi(string[])", inputs) + ); + require(success, "tryFfi failed"); + + FfiResult memory f = abi.decode(data, (FfiResult)); + require(f.exitCode != 0, "ffi failed"); + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Utils.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Utils.sol new file mode 100644 index 000000000..a33c9152d --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Utils.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +library Utils { + function trimReturnBytes( + bytes memory rawData + ) internal pure returns (bytes memory) { + uint256 lengthStartingPos = rawData.length - 32; + bytes memory lengthSlice = new bytes(32); + for (uint256 i = 0; i < 32; i++) { + lengthSlice[i] = rawData[lengthStartingPos + i]; + } + uint256 length = abi.decode(lengthSlice, (uint256)); + bytes memory data = new bytes(length); + for (uint256 i = 0; i < length; i++) { + data[i] = rawData[i]; + } + return data; + } +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/WriteJson.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/WriteJson.t.sol new file mode 100644 index 000000000..91f46c3f1 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/WriteJson.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; +import {Utils} from "./Utils.sol"; + +contract FsTest is Test { + function testWriteJson() public { + string + memory json = '{"boolean": true, "number": 342, "object": { "title": "finally json serialization" } }'; + string memory path = "src/fixtures/Json/write_test.json"; + + // Write json to file + (bool success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("writeJson(string,string)", json, path) + ); + require(success, "writeJson failed"); + + bytes memory readRawData; + (success, readRawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("readFile(string)", path) + ); + require(success, "readFile failed"); + bytes memory readData = Utils.trimReturnBytes(readRawData); + + require( + keccak256(readData) == + keccak256( + bytes( + '{\n "boolean": true,\n "number": 342,\n "object": {\n "title": "finally json serialization"\n }\n}' + ) + ), + "read data did not match write data" + ); + + // Write json to key b + (success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "writeJson(string,string,string)", + json, + path, + "b" + ) + ); + require(success, "writeJson to key failed"); + + (success, readRawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("readFile(string)", path) + ); + require(success, "readFile failed"); + readData = Utils.trimReturnBytes(readRawData); + + require( + keccak256(readData) == + keccak256( + bytes( + '{\n "boolean": true,\n "number": 342,\n "object": {\n "title": "finally json serialization"\n },\n "b": {\n "boolean": true,\n "number": 342,\n "object": {\n "title": "finally json serialization"\n }\n }\n}' + ) + ), + "read data did not match write data" + ); + + // Replace the key b with single value + (success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature( + "writeJson(string,string,string)", + '"test"', + path, + "b" + ) + ); + require(success, "writeJson to key failed"); + + (success, readRawData) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("readFile(string)", path) + ); + require(success, "readFile failed"); + readData = Utils.trimReturnBytes(readRawData); + + require( + keccak256(readData) == + keccak256( + bytes( + '{\n "boolean": true,\n "number": 342,\n "object": {\n "title": "finally json serialization"\n },\n "b": "test"\n}' + ) + ), + "read data did not match write data" + ); + } +} diff --git a/crates/era-cheatcodes/tests/src/fixtures/File/read.txt b/crates/era-cheatcodes/tests/src/fixtures/File/read.txt new file mode 100644 index 000000000..ea7ec8648 --- /dev/null +++ b/crates/era-cheatcodes/tests/src/fixtures/File/read.txt @@ -0,0 +1,2 @@ +hello readable world +this is the second line! diff --git a/crates/era-cheatcodes/tests/src/fixtures/Json/.gitkeep b/crates/era-cheatcodes/tests/src/fixtures/Json/.gitkeep new file mode 100644 index 000000000..e69de29bb From ed1d84c804845113edf8f07addc87e7b6189d0cb Mon Sep 17 00:00:00 2001 From: Nisheeth Barthwal Date: Fri, 15 Dec 2023 16:40:52 +0100 Subject: [PATCH 3/4] chore: move smoke test to foundry (#203) --- .github/workflows/test.yml | 15 ++---- .gitmodules | 3 ++ smoke-test/.gitignore | 3 ++ smoke-test/foundry.toml | 12 +++++ smoke-test/lib/forge-std | 1 + smoke-test/src/Counter.sol | 56 ++++++++++++++++++++ smoke-test/src/foundry.toml | 9 ++++ smoke-test/test.sh | 102 ++++++++++++++++++++++++++++++++++++ 8 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 smoke-test/.gitignore create mode 100644 smoke-test/foundry.toml create mode 160000 smoke-test/lib/forge-std create mode 100644 smoke-test/src/Counter.sol create mode 100644 smoke-test/src/foundry.toml create mode 100755 smoke-test/test.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b75477987..e905ce2a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -93,30 +93,23 @@ jobs: smoke-test: name: smoke-test runs-on: ubuntu-22.04-github-hosted-16core - env: - TEST_REPO_DIR: test-repo - + steps: - name: Checkout code uses: actions/checkout@v4 with: - path: ${{ env.TEST_REPO_DIR }} + submodules: recursive ref: ${{ github.event.pull_request.head.sha }} - name: Install Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: nightly-2023-09-21 - + - name: Run smoke-test env: - TEST_REPO: ${{ github.event.repository.name }} - TEST_REPO_DIR: "../${{ env.TEST_REPO_DIR }}" RUST_BACKTRACE: full - run: | - git clone https://github.com/matter-labs/zkfoundry-smoke-test - cd zkfoundry-smoke-test - ./smoke-test.sh + run: cd smoke-test && ./test.sh cheatcodes: name: cheatcode-test diff --git a/.gitmodules b/.gitmodules index c0e956c61..87c9122a7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "crates/era-cheatcodes/tests/lib/forge-std"] path = crates/era-cheatcodes/tests/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "smoke-test/lib/forge-std"] + path = smoke-test/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/smoke-test/.gitignore b/smoke-test/.gitignore new file mode 100644 index 000000000..2cb09d77f --- /dev/null +++ b/smoke-test/.gitignore @@ -0,0 +1,3 @@ +zkout/ +solc-v* +*.log diff --git a/smoke-test/foundry.toml b/smoke-test/foundry.toml new file mode 100644 index 000000000..5add7e07c --- /dev/null +++ b/smoke-test/foundry.toml @@ -0,0 +1,12 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['lib'] + +[rpc_endpoints] +local = "${ERA_TEST_NODE_RPC_URL}" + +[invariant] +fail_on_revert = false + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/smoke-test/lib/forge-std b/smoke-test/lib/forge-std new file mode 160000 index 000000000..80a8f6ea9 --- /dev/null +++ b/smoke-test/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 80a8f6ea9362849b2a8f2dc28df40c77a64f9c16 diff --git a/smoke-test/src/Counter.sol b/smoke-test/src/Counter.sol new file mode 100644 index 000000000..26e2c3f43 --- /dev/null +++ b/smoke-test/src/Counter.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; + +contract Counter { + uint256 public number = 0; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + console.log("increment"); + number += 1; + } + + function incrementBy(uint64 amount) public { + console.log("incrementBy"); + number += uint256(amount); + } +} + +contract CounterTest is Test { + Counter counter; + + function setUp() public { + counter = new Counter(); + } + + function test_Increment() public { + counter.increment(); + if (counter.number() == 1) { + console.log("[INT-TEST] PASS"); + } else { + console.log("[INT-TEST] FAIL"); + } + } + + function test_FailIncrement() public { + counter.increment(); + assertEq(counter.number(), 200); + } + + function testFail_Increment() public { + counter.increment(); + assertEq(counter.number(), 200); + } + + function testFuzz_Increment(uint64 amount) public { + uint256 numBefore = counter.number(); + counter.incrementBy(amount); + uint256 numAfter = counter.number(); + assertEq(numBefore + amount, numAfter); + } +} diff --git a/smoke-test/src/foundry.toml b/smoke-test/src/foundry.toml new file mode 100644 index 000000000..ea122abec --- /dev/null +++ b/smoke-test/src/foundry.toml @@ -0,0 +1,9 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['lib'] + +[rpc_endpoints] +local = "${ERA_TEST_NODE_RPC_URL}" + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/smoke-test/test.sh b/smoke-test/test.sh new file mode 100755 index 000000000..3cbae3af2 --- /dev/null +++ b/smoke-test/test.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +# Fail fast and on piped commands +set -o pipefail -e + +SOLC_VERSION=${SOLC_VERSION:-"v0.8.20"} +SOLC="solc-${SOLC_VERSION}" +BINARY_PATH="../target/release/zkforge" + +function cleanup() { + echo "Cleaning up..." + rm -f "./${SOLC}" +} + +function success() { + echo '' + echo '=================================' + printf "\e[32m> [SUCCESS]\e[0m\n" + echo '=================================' + echo '' + cleanup + exit 0 +} + +function fail() { + echo "Displaying run.log..." + cat run.log + echo '' + echo '==================================' + printf "\e[31m> [FAILURE]\e[0m %s\n" "$1" + echo '==================================' + echo '' + cleanup + exit 1 +} + +function download_solc() { + wget --quiet -O "${SOLC}" "https://github.com/ethereum/solidity/releases/download/${1}/solc-static-linux" + chmod +x "${SOLC}" +} + +function wait_for_build() { + local timeout=$1 + while ! [ -x "${BINARY_PATH}" ]; do + ((timeout--)) + if [ $timeout -le 0 ]; then + echo "Build timed out waiting for binary to be created." + exit 1 + fi + sleep 1 + done +} + +# We want this to fail-fast and hence are put on separate lines +# See https://unix.stackexchange.com/questions/312631/bash-script-with-set-e-doesnt-stop-on-command +function build_zkforge() { + echo "Building..." + cargo build --manifest-path="../Cargo.toml" --release + wait_for_build 30 +} + +trap cleanup ERR + +echo "Solc: ${SOLC_VERSION}" +echo "Zkforge binary: ${BINARY_PATH}" + +# Download solc +download_solc "${SOLC_VERSION}" + +# Check for necessary tools +command -v cargo &>/dev/null || { + echo "cargo not found, exiting" + exit 1 +} +command -v git &>/dev/null || { + echo "git not found, exiting" + exit 1 +} + +build_zkforge + +echo "Running tests..." + +echo "[1] Check test suite passed" +RUST_LOG=debug "${BINARY_PATH}" test --use "./${SOLC}" --match-test 'test_Increment' &>run.log || fail "zkforge test failed" + +echo "[2] Check console logs are printed in era-test-node" +grep '\[INT-TEST\] PASS' run.log &>/dev/null || fail "zkforge test console output failed" + +echo "[3] Check asserts fail tests" +set +e +if RUST_LOG=debug "${BINARY_PATH}" test --use "./${SOLC}" --match-test 'test_FailIncrement' &>run.log; then + fail "zkforge test did not fail" +fi + +echo "[4] Check testFail works" +RUST_LOG=debug "${BINARY_PATH}" test --use "./${SOLC}" --match-test 'testFail_Increment' &>run.log || fail "zkforge testFail failed" + +echo "[5] Check fuzz test works" +RUST_LOG=debug "${BINARY_PATH}" test --use "./${SOLC}" --match-test 'testFuzz_Increment' &>run.log || fail "zkforge fuzz test failed" + +success From 74083adce839d7028e12836e51489119c5830cb8 Mon Sep 17 00:00:00 2001 From: Juan Rigada <62958725+Jrigada@users.noreply.github.com> Date: Fri, 15 Dec 2023 13:48:23 -0300 Subject: [PATCH 4/4] feat: Implement log cheatcodes (#197) --- crates/era-cheatcodes/src/cheatcodes.rs | 96 ++++++++++++++++++- .../tests/src/cheatcodes/Logs.t.sol | 55 +++++++++++ 2 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 crates/era-cheatcodes/tests/src/cheatcodes/Logs.t.sol diff --git a/crates/era-cheatcodes/src/cheatcodes.rs b/crates/era-cheatcodes/src/cheatcodes.rs index 310b2eb02..605b2ebc0 100644 --- a/crates/era-cheatcodes/src/cheatcodes.rs +++ b/crates/era-cheatcodes/src/cheatcodes.rs @@ -17,14 +17,21 @@ use multivm::{ }, }, }; -use std::{cell::RefMut, collections::HashMap, fmt::Debug, fs, process::Command}; +use serde::Serialize; +use std::{ + cell::RefMut, + collections::{HashMap, HashSet}, + fmt::Debug, + fs, + process::Command, +}; use zksync_basic_types::{AccountTreeId, H160, H256, U256}; use zksync_state::{ReadStorage, StoragePtr, StorageView, WriteStorage}; use zksync_types::{ block::{pack_block_info, unpack_block_info}, get_code_key, get_nonce_key, utils::{decompose_full_nonce, nonces_to_full_nonce, storage_key_for_eth_balance}, - LogQuery, StorageKey, Timestamp, + EventMessage, LogQuery, StorageKey, Timestamp, }; use zksync_utils::{h256_to_u256, u256_to_h256}; @@ -66,6 +73,22 @@ pub struct CheatcodeTracer { return_ptr: Option, near_calls: usize, serialized_objects: HashMap, + recorded_logs: HashSet, + recording_logs: bool, + recording_timestamp: u32, +} + +#[derive(Debug, Clone, Serialize, Eq, Hash, PartialEq)] +struct LogEntry { + topic: H256, + data: H256, + emitter: H160, +} + +impl LogEntry { + fn new(topic: H256, data: H256, emitter: H160) -> Self { + LogEntry { topic, data, emitter } + } } #[derive(Debug, Clone)] @@ -155,6 +178,23 @@ impl VmTracer, H> for CheatcodeT state: &mut ZkSyncVmState, H>, _bootloader_state: &mut BootloaderState, ) -> TracerExecutionStatus { + let emitter = state.local_state.callstack.current.this_address; + if self.recording_logs { + let logs = transform_to_logs( + state + .event_sink + .get_events_and_l2_l1_logs_after_timestamp(Timestamp(self.recording_timestamp)) + .0, + emitter, + ); + if !logs.is_empty() { + let mut unique_set: HashSet = HashSet::new(); + + // Filter out duplicates and extend the unique entries to the vector + self.recorded_logs + .extend(logs.into_iter().filter(|log| unique_set.insert(log.clone()))); + } + } while let Some(action) = self.one_time_actions.pop() { match action { FinishCycleOneTimeActions::StorageWrite { key, read_value, write_value } => { @@ -214,12 +254,15 @@ impl CheatcodeTracer { return_data: None, return_ptr: None, serialized_objects: HashMap::new(), + recorded_logs: HashSet::new(), + recording_logs: false, + recording_timestamp: 0, } } pub fn dispatch_cheatcode( &mut self, - _state: VmLocalStateData<'_>, + state: VmLocalStateData<'_>, _data: AfterExecutionData, _memory: &SimpleMemory, storage: StoragePtr>, @@ -296,6 +339,30 @@ impl CheatcodeTracer { tracing::info!("👷 Returndata is {:?}", account_nonce); self.return_data = Some(vec![account_nonce]); } + getRecordedLogs(getRecordedLogsCall {}) => { + tracing::info!("👷 Getting recorded logs"); + let logs: Vec = self + .recorded_logs + .iter() + .map(|log| Log { + topics: vec![log.topic.to_fixed_bytes().into()], + data: log.data.to_fixed_bytes().into(), + emitter: log.emitter.to_fixed_bytes().into(), + }) + .collect_vec(); + + let result = getRecordedLogsReturn { logs }; + + let return_data: Vec = + result.logs.abi_encode().chunks(32).map(|b| b.into()).collect_vec(); + + self.return_data = Some(return_data); + + //clean up logs + self.recorded_logs = HashSet::new(); + //disable flag of recording logs + self.recording_logs = false; + } load(loadCall { target, slot }) => { tracing::info!("👷 Getting storage slot {:?} for account {:?}", slot, target); let key = StorageKey::new(AccountTreeId::new(target.to_h160()), H256(*slot)); @@ -303,6 +370,16 @@ impl CheatcodeTracer { let value = storage.read_value(&key); self.return_data = Some(vec![h256_to_u256(value)]); } + recordLogs(recordLogsCall {}) => { + tracing::info!("👷 Recording logs"); + tracing::info!( + "👷 Logs will be with the timestamp {}", + state.vm_local_state.timestamp + ); + + self.recording_timestamp = state.vm_local_state.timestamp; + self.recording_logs = true; + } readCallers(readCallersCall {}) => { tracing::info!("👷 Reading callers"); @@ -533,6 +610,7 @@ impl CheatcodeTracer { let int_value = value.to_string(); self.add_trimmed_return_data(int_value.as_bytes()); } + tryFfi(tryFfiCall { commandInput: command_input }) => { tracing::info!("👷 Running try ffi: {command_input:?}"); let Some(first_arg) = command_input.get(0) else { @@ -669,3 +747,15 @@ impl CheatcodeTracer { self.return_data = Some(data); } } +fn transform_to_logs(events: Vec, emitter: H160) -> Vec { + events + .iter() + .filter_map(|event| { + if event.address == zksync_types::EVENT_WRITER_ADDRESS { + Some(LogEntry::new(u256_to_h256(event.key), u256_to_h256(event.value), emitter)) + } else { + None + } + }) + .collect() +} diff --git a/crates/era-cheatcodes/tests/src/cheatcodes/Logs.t.sol b/crates/era-cheatcodes/tests/src/cheatcodes/Logs.t.sol new file mode 100644 index 000000000..ed060c41d --- /dev/null +++ b/crates/era-cheatcodes/tests/src/cheatcodes/Logs.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "./Constants.sol"; + +struct Log { + bytes32[] topics; + bytes data; + address emitter; +} + +contract LogsTest is Test { + event LogTopic1(uint256 indexed topic1, bytes data); + + function testRecordAndGetLogs() public { + bytes memory testData1 = "test"; + + (bool success, ) = Constants.CHEATCODE_ADDRESS.call( + abi.encodeWithSignature("recordLogs()") + ); + require(success, "recordLogs failed"); + + emit LogTopic1(1, testData1); + + (bool success2, bytes memory rawData) = Constants + .CHEATCODE_ADDRESS + .call(abi.encodeWithSignature("getRecordedLogs()")); + require(success2, "getRecordedLogs failed"); + + Log[] memory logs = abi.decode(rawData, (Log[])); + console.log("logs length: %d", logs.length); + require(logs.length == 8, "logs length should be 8"); + } + + function trimReturnBytes( + bytes memory rawData + ) internal pure returns (bytes memory) { + uint256 lengthStartingPos = rawData.length - 32; + bytes memory lengthSlice = new bytes(32); + + for (uint256 i = 0; i < 32; i++) { + lengthSlice[i] = rawData[lengthStartingPos + i]; + } + + uint256 length = abi.decode(lengthSlice, (uint256)); + bytes memory data = new bytes(length); + + for (uint256 i = 0; i < length; i++) { + data[i] = rawData[i]; + } + + return data; + } +}