From d6489ae1ca0da9f54d723fb87bb77c4a893dd42b Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Fri, 20 Dec 2024 21:53:04 +0100 Subject: [PATCH 01/61] feat(zk): zksolc linking --- Cargo.lock | 27 +-- crates/common/Cargo.toml | 1 - crates/common/src/compile.rs | 78 +------- crates/forge/bin/cmd/create.rs | 12 +- crates/forge/src/multi_runner.rs | 123 +++++++----- crates/linking/Cargo.toml | 5 + crates/linking/src/lib.rs | 228 ++++++++++++++++++++++- crates/zksync/compiler/src/lib.rs | 4 +- crates/zksync/compiler/src/link.rs | 135 ++++++++++++++ crates/zksync/compiler/src/zksolc/mod.rs | 35 +++- crates/zksync/core/src/lib.rs | 14 ++ 11 files changed, 510 insertions(+), 152 deletions(-) create mode 100644 crates/zksync/compiler/src/link.rs diff --git a/Cargo.lock b/Cargo.lock index 872764abb..f99c0f476 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1974,7 +1974,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.11.0", "lazy_static", "lazycell", "log", @@ -4921,7 +4921,6 @@ dependencies = [ "foundry-common-fmt", "foundry-compilers", "foundry-config", - "foundry-zksync-compiler", "itertools 0.13.0", "num-format", "reqwest 0.12.9", @@ -5321,8 +5320,11 @@ version = "0.0.2" dependencies = [ "alloy-primitives", "foundry-compilers", + "foundry-zksync-compiler", + "foundry-zksync-core", "semver 1.0.23", "thiserror 1.0.69", + "tracing", ] [[package]] @@ -7043,15 +7045,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -8372,7 +8365,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 3.2.0", + "proc-macro-crate 1.3.1", "proc-macro2 1.0.92", "quote 1.0.37", "syn 2.0.89", @@ -9456,7 +9449,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", "heck", - "itertools 0.12.1", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -9476,7 +9469,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.11.0", "proc-macro2 1.0.92", "quote 1.0.37", "syn 2.0.89", @@ -9489,7 +9482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.11.0", "proc-macro2 1.0.92", "quote 1.0.37", "syn 2.0.89", @@ -11685,7 +11678,7 @@ dependencies = [ "const-hex", "derive_builder", "dunce", - "itertools 0.13.0", + "itertools 0.11.0", "itoa", "lasso", "match_cfg", @@ -11721,7 +11714,7 @@ dependencies = [ "alloy-primitives", "bitflags 2.6.0", "bumpalo", - "itertools 0.13.0", + "itertools 0.11.0", "memchr", "num-bigint 0.4.6", "num-rational", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 12b952fda..efb141dfb 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -14,7 +14,6 @@ workspace = true [dependencies] foundry-block-explorers = { workspace = true, features = ["foundry-compilers"] } -foundry-zksync-compiler.workspace = true foundry-common-fmt.workspace = true foundry-compilers.workspace = true foundry-config.workspace = true diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 81e45f67f..dbde04c56 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -24,10 +24,9 @@ use foundry_compilers::{ }, Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig, }; -use foundry_zksync_compiler::libraries::{self, ZkMissingLibrary}; use num_format::{Locale, ToFormattedString}; use std::{ - collections::{BTreeMap, HashSet}, + collections::BTreeMap, fmt::Display, io::IsTerminal, path::{Path, PathBuf}, @@ -325,7 +324,7 @@ impl ProjectCompiler { let zksolc_version = ZkSolc::get_version_for_path(&project.compiler.zksolc)?; Report::new(SpinnerReporter::spawn_with(format!("Using zksolc-{zksolc_version}"))); } - self.zksync_compile_with(&project.paths.root, || { + self.zksync_compile_with(|| { let files_to_compile = if !files.is_empty() { files } else { project.paths.input_files() }; let sources = Source::read_all(files_to_compile)?; @@ -338,11 +337,7 @@ impl ProjectCompiler { } #[instrument(target = "forge::compile", skip_all)] - fn zksync_compile_with( - self, - root_path: impl AsRef, - f: F, - ) -> Result + fn zksync_compile_with(self, f: F) -> Result where F: FnOnce() -> Result, { @@ -385,80 +380,17 @@ impl ProjectCompiler { sh_println!("{output}")?; } - self.zksync_handle_output(root_path, &output)?; + self.zksync_handle_output(&output)?; } Ok(output) } /// If configured, this will print sizes or names - fn zksync_handle_output( - &self, - root_path: impl AsRef, - output: &ZkProjectCompileOutput, - ) -> Result<()> { + fn zksync_handle_output(&self, output: &ZkProjectCompileOutput) -> Result<()> { let print_names = self.print_names.unwrap_or(false); let print_sizes = self.print_sizes.unwrap_or(false); - // Process missing libraries - // TODO: skip this if project was not compiled using --detect-missing-libraries - let mut missing_libs_unique: HashSet = HashSet::new(); - for (artifact_id, artifact) in output.artifact_ids() { - // TODO: when compiling specific files, the output might still add cached artifacts - // that are not part of the file list to the output, which may cause missing libraries - // error to trigger for files that were not intended to be compiled. - // This behaviour needs to be investigated better on the foundry-compilers side. - // For now we filter, checking only the files passed to compile. - let is_target_file = - self.files.is_empty() || self.files.iter().any(|f| artifact_id.path == *f); - if is_target_file { - if let Some(mls) = artifact.missing_libraries() { - missing_libs_unique.extend(mls.clone()); - } - } - } - - let missing_libs: Vec = missing_libs_unique - .into_iter() - .map(|ml| { - let mut split = ml.split(':'); - let contract_path = - split.next().expect("Failed to extract contract path for missing library"); - let contract_name = - split.next().expect("Failed to extract contract name for missing library"); - - let mut abs_path_buf = PathBuf::new(); - abs_path_buf.push(root_path.as_ref()); - abs_path_buf.push(contract_path); - - let art = output.find(abs_path_buf.as_path(), contract_name).unwrap_or_else(|| { - panic!( - "Could not find contract {contract_name} at path {contract_path} for compilation output" - ) - }); - - ZkMissingLibrary { - contract_path: contract_path.to_string(), - contract_name: contract_name.to_string(), - missing_libraries: art.missing_libraries().cloned().unwrap_or_default(), - } - }) - .collect(); - - if !missing_libs.is_empty() { - libraries::add_dependencies_to_missing_libraries_cache( - root_path, - missing_libs.as_slice(), - ) - .expect("Error while adding missing libraries"); - let missing_libs_list = missing_libs - .iter() - .map(|ml| format!("{}:{}", ml.contract_path, ml.contract_name)) - .collect::>() - .join(", "); - eyre::bail!("Missing libraries detected: {missing_libs_list}\n\nRun the following command in order to deploy each missing library:\n\nforge create --private-key --rpc-url --chain --zksync\n\nThen pass the library addresses using the --libraries option"); - } - // print any sizes or names if print_names { let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new(); diff --git a/crates/forge/bin/cmd/create.rs b/crates/forge/bin/cmd/create.rs index 15ebbb87a..e676420aa 100644 --- a/crates/forge/bin/cmd/create.rs +++ b/crates/forge/bin/cmd/create.rs @@ -156,10 +156,10 @@ impl CreateArgs { let (artifact, id) = remove_zk_contract(&mut zk_output, &target_path, &self.contract.name)?; - let ZkContractArtifact { bytecode, factory_dependencies, abi, .. } = artifact; + let ZkContractArtifact { bytecode, abi, factory_dependencies, .. } = &artifact; - let abi = abi.expect("Abi not found"); - let bin = bytecode.expect("Bytecode not found"); + let abi = abi.clone().expect("Abi not found"); + let bin = bytecode.as_ref().expect("Bytecode not found"); let bytecode = match bin.object() { BytecodeObject::Bytecode(bytes) => bytes.to_vec(), @@ -206,7 +206,7 @@ impl CreateArgs { let factory_deps: Vec> = { let factory_dependencies_map = - factory_dependencies.expect("factory deps not found"); + factory_dependencies.as_ref().expect("factory deps not found"); let mut visited_paths = HashSet::new(); let mut visited_bytecodes = HashSet::new(); let mut queue = VecDeque::new(); @@ -234,12 +234,12 @@ impl CreateArgs { ) }); let fdep_fdeps_map = - fdep_art.factory_dependencies.clone().expect("factory deps not found"); + fdep_art.factory_dependencies.as_ref().expect("factory deps not found"); for dep in fdep_fdeps_map.values() { queue.push_back(dep.clone()) } - // TODO(zk): ensure factory deps are also linked + // NOTE(zk): unlinked factory deps don't show up in `factory_dependencies` let fdep_bytecode = fdep_art .bytecode .clone() diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index c6a43c1c5..76af31b5e 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -5,14 +5,13 @@ use crate::{ TestFilter, TestOptions, }; use alloy_json_abi::{Function, JsonAbi}; -use alloy_primitives::{Address, Bytes, U256}; +use alloy_primitives::{keccak256, Address, Bytes, B256, U256}; use eyre::Result; use foundry_common::{get_contract_name, ContractsByArtifact, TestFunctionExt}; use foundry_compilers::{ - artifacts::{CompactBytecode, CompactContractBytecode, CompactDeployedBytecode, Libraries}, - compilers::Compiler, - zksync::compile::output::ProjectCompileOutput as ZkProjectCompileOutput, - Artifact, ArtifactId, ProjectCompileOutput, + artifacts::Libraries, compilers::Compiler, + zksync::compile::output::ProjectCompileOutput as ZkProjectCompileOutput, Artifact, ArtifactId, + ProjectCompileOutput, }; use foundry_config::Config; use foundry_evm::{ @@ -26,9 +25,11 @@ use foundry_evm::{ traces::{InternalTraceMode, TraceMode}, }; use foundry_linking::{LinkOutput, Linker}; -use foundry_zksync_compiler::DualCompiledContracts; +use foundry_zksync_compiler::{DualCompiledContract, DualCompiledContracts}; +use foundry_zksync_core::hash_bytecode; use rayon::prelude::*; use revm::primitives::SpecId; +use zksync_types::H256; use std::{ borrow::Borrow, @@ -414,13 +415,18 @@ impl MultiContractRunnerBuilder { zk_output: Option, env: revm::primitives::Env, evm_opts: EvmOpts, - dual_compiled_contracts: DualCompiledContracts, + mut dual_compiled_contracts: DualCompiledContracts, ) -> Result { let use_zk = zk_output.is_some(); let mut known_contracts = ContractsByArtifact::default(); + let output = output.with_stripped_file_prefixes(root); let linker = Linker::new(root, output.artifact_ids().collect()); + let zk_output = zk_output.map(|zk| zk.with_stripped_file_prefixes(&root)); + let zk_linker = + zk_output.as_ref().map(|output| Linker::new(root, output.artifact_ids().collect())); + // Build revert decoder from ABIs of all artifacts. let abis = linker .contracts @@ -435,11 +441,74 @@ impl MultiContractRunnerBuilder { linker.contracts.keys(), )?; + let zk_libs = zk_linker + .as_ref() + .map(|zk| { + zk.zk_link_with_nonce_or_address( + Default::default(), + LIBRARY_DEPLOYER, + // NOTE(zk): normally 0, this way we avoid overlaps with EVM libs + // FIXME: needed or 0 is ok? + libs_to_deploy.len() as u64, + zk.contracts.keys(), + ) + .map(|output| + + // NOTE(zk): zk_linked_contracts later will also contain + // `libs_to_deploy` bytecodes, so those will + // get registered in DualCompiledContracts + + output.libraries) + }) + .transpose()?; + let linked_contracts = linker.get_linked_artifacts(&libraries)?; + let zk_linked_contracts = zk_linker + .as_ref() + .and_then(|linker| zk_libs.as_ref().map(|libs| (linker, libs))) + .map(|(zk, libs)| zk.zk_get_linked_artifacts(zk.contracts.keys(), libs)) + .transpose()?; - // Create a mapping of name => (abi, deployment code, Vec) - let mut deployable_contracts = DeployableContracts::default(); + let newly_linked_dual_compiled_contracts = zk_linked_contracts + .iter() + .flat_map(|arts| arts.iter()) + .flat_map(|(needle, zk)| { + linked_contracts + .iter() + .find(|(id, _)| id.source == needle.source && id.name == needle.name) + .map(|(_, evm)| (needle, zk, evm)) + }) + .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) + .map(|(id, linked_zk, evm)| { + let (_, unlinked_zk_artifact) = + zk_output.as_ref().unwrap().artifact_ids().find(|(id, _)| id == id).unwrap(); + let zk_bytecode = linked_zk.get_bytecode_bytes().unwrap(); + let zk_hash = hash_bytecode(&zk_bytecode); + let evm = evm.get_bytecode_bytes().unwrap(); + let contract = DualCompiledContract { + name: id.name.clone(), + zk_bytecode_hash: zk_hash, + zk_deployed_bytecode: zk_bytecode.to_vec(), + // FIXME: retrieve unlinked factory deps (1.5.9) + zk_factory_deps: vec![zk_bytecode.to_vec()], + evm_bytecode_hash: B256::from_slice(&keccak256(evm.as_ref())[..]), + evm_deployed_bytecode: evm.to_vec(), // FIXME: is this ok? not really used + evm_bytecode: evm.to_vec(), + }; + + // populate factory deps that were already linked + dual_compiled_contracts.extend_factory_deps_by_hash( + contract, + unlinked_zk_artifact.factory_dependencies.iter().flatten().map(|(_, hash)| { + H256::from_slice(alloy_primitives::hex::decode(hash).unwrap().as_slice()) + }), + ) + }); + dual_compiled_contracts.extend(newly_linked_dual_compiled_contracts.collect::>()); + // FIXME: is this comment outdated? I don't see the library deployment code anywhere + // Create a mapping of name => (abi, deployment code, Vec) + let mut contracts = DeployableContracts::default(); for (id, contract) in linked_contracts.iter() { let Some(abi) = &contract.abi else { continue }; @@ -453,43 +522,13 @@ impl MultiContractRunnerBuilder { continue; }; - deployable_contracts - .insert(id.clone(), TestContract { abi: abi.clone(), bytecode }); + contracts.insert(id.clone(), TestContract { abi: abi.clone(), bytecode }); } } if !use_zk { known_contracts = ContractsByArtifact::new(linked_contracts); - } else if let Some(zk_output) = zk_output { - let zk_contracts = zk_output.with_stripped_file_prefixes(root).into_artifacts(); - let mut zk_contracts_map = BTreeMap::new(); - - for (id, contract) in zk_contracts { - if let Some(abi) = contract.abi { - let bytecode = contract.bytecode.as_ref(); - - // TODO(zk): retrieve link_references - if let Some(bytecode_object) = bytecode.map(|b| b.object()) { - let compact_bytecode = CompactBytecode { - object: bytecode_object.clone(), - source_map: None, - link_references: BTreeMap::new(), - }; - let compact_contract = CompactContractBytecode { - abi: Some(abi), - bytecode: Some(compact_bytecode.clone()), - deployed_bytecode: Some(CompactDeployedBytecode { - bytecode: Some(compact_bytecode), - immutable_references: BTreeMap::new(), - }), - }; - zk_contracts_map.insert(id.clone(), compact_contract); - } - } else { - warn!("Abi not found for contract {}", id.identifier()); - } - } - + } else if let Some(mut zk_contracts_map) = zk_linked_contracts { // Extend zk contracts with solc contracts as well. This is required for traces to // accurately detect contract names deployed in EVM mode, and when using // `vm.zkVmSkip()` cheatcode. @@ -499,7 +538,7 @@ impl MultiContractRunnerBuilder { } Ok(MultiContractRunner { - contracts: deployable_contracts, + contracts, evm_opts, env, evm_spec: self.evm_spec.unwrap_or(SpecId::CANCUN), diff --git a/crates/linking/Cargo.toml b/crates/linking/Cargo.toml index 15d0d113b..eeadd30fa 100644 --- a/crates/linking/Cargo.toml +++ b/crates/linking/Cargo.toml @@ -18,3 +18,8 @@ foundry-compilers = { workspace = true, features = ["full"] } semver.workspace = true alloy-primitives = { workspace = true, features = ["rlp"] } thiserror.workspace = true +tracing.workspace = true + +# zk linking utils +foundry-zksync-core.workspace = true +foundry-zksync-compiler.workspace = true diff --git a/crates/linking/src/lib.rs b/crates/linking/src/lib.rs index e44ee7748..daea95c4f 100644 --- a/crates/linking/src/lib.rs +++ b/crates/linking/src/lib.rs @@ -5,15 +5,24 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -use alloy_primitives::{Address, Bytes, B256}; +use alloy_primitives::{ + hex::FromHex, + map::{HashMap, HashSet}, + Address, Bytes, B256, +}; use foundry_compilers::{ - artifacts::{CompactContractBytecodeCow, Libraries}, + artifacts::{ + BytecodeObject, CompactBytecode, CompactContractBytecode, CompactContractBytecodeCow, + CompactDeployedBytecode, Libraries, + }, contracts::ArtifactContracts, + zksolc::{ZkSolc, ZkSolcCompiler}, Artifact, ArtifactId, }; +use foundry_zksync_compiler::link::{self as zk_link, MissingLibrary}; use semver::Version; use std::{ - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, VecDeque}, path::{Path, PathBuf}, str::FromStr, }; @@ -31,6 +40,18 @@ pub enum LinkerError { CyclicDependency, } +/// Errors that can occur during linking. +#[derive(Debug, thiserror::Error)] +pub enum ZkLinkerError { + #[error(transparent)] + Inner(#[from] LinkerError), + #[error("unable to fully link due to missing libraries")] + MissingLibraries(BTreeSet), + #[error("unable to fully link due to unlinked factory dependencies")] + MissingFactoryDeps(BTreeSet), +} + +#[derive(Debug)] pub struct Linker<'a> { /// Root of the project, used to determine whether artifact/library path can be stripped. pub root: PathBuf, @@ -181,6 +202,60 @@ impl<'a> Linker<'a> { Ok(LinkOutput { libraries, libs_to_deploy }) } + /// Links given artifact with either given library addresses or address computed from sender and + /// nonce. + /// + /// Each key in `libraries` should either be a global path or relative to project root. All + /// remappings should be resolved. + /// + /// When calling for `target` being an external library itself, you should check that `target` + /// does not appear in `libs_to_deploy` to avoid deploying it twice. It may happen in cases + /// when there is a dependency cycle including `target`. + pub fn zk_link_with_nonce_or_address( + &'a self, + libraries: Libraries, + sender: Address, + mut nonce: u64, + targets: impl IntoIterator, + ) -> Result { + // Library paths in `link_references` keys are always stripped, so we have to strip + // user-provided paths to be able to match them correctly. + let mut libraries = libraries.with_stripped_file_prefixes(self.root.as_path()); + + let mut needed_libraries = BTreeSet::new(); + for target in targets { + self.collect_dependencies(target, &mut needed_libraries)?; + } + + let mut libs_to_deploy = Vec::new(); + + // If `libraries` does not contain needed dependency, compute its address and add to + // `libs_to_deploy`. + for id in needed_libraries { + let (lib_path, lib_name) = self.convert_artifact_id_to_lib_path(id); + + libraries.libs.entry(lib_path).or_default().entry(lib_name).or_insert_with(|| { + let address = foundry_zksync_core::compute_create_address(sender, nonce); + libs_to_deploy.push((id, address)); + nonce += 1; + + address.to_checksum(None) + }); + } + + // Link and collect bytecodes for `libs_to_deploy`. + let libs_to_deploy = self + .zk_get_linked_artifacts(libs_to_deploy.into_iter().map(|(id, _)| id), &libraries)? + .into_iter() + .map(|(_, linked)| linked.get_bytecode_bytes().unwrap().into_owned()) + .collect(); + + Ok(LinkOutput { libraries, libs_to_deploy }) + } + + // TODO(zk): zk_link_with_create2 + // a bit more difficult due to the lack of bytecode + // until the contract is fully linked pub fn link_with_create2( &'a self, libraries: Libraries, @@ -265,6 +340,100 @@ impl<'a> Linker<'a> { Ok(contract) } + /// Links given artifact with given libraries. + // TODO(zk): improve interface to reflect batching operation (all bytecodes in all bytecodes + // out) + pub fn zk_link( + contracts: &ArtifactContracts>, + target: &ArtifactId, + libraries: &Libraries, + ) -> Result, ZkLinkerError> { + let artifact_to_link_id = |id: &ArtifactId| format!("{}:{}", id.source.display(), id.name); + + // collect bytecodes & libraries for input to zksolc_link + let bytecodes = contracts + .iter() + .filter_map(|(id, bytecode)| { + let link_id = artifact_to_link_id(id); + let object = bytecode.bytecode.as_ref().map(|bc| bc.object.clone())?; + + let bytes = match object { + BytecodeObject::Bytecode(bytes) => bytes, + BytecodeObject::Unlinked(unlinked) => { + alloy_primitives::hex::decode(unlinked).unwrap().into() + } + }; + + Some((link_id, bytes)) + }) + .collect::>(); + + let libraries = libraries + .libs + .iter() + .flat_map(|(file, libs)| { + libs.iter() + .map(|(name, address)| (file.to_string_lossy(), name.clone(), address.clone())) + }) + .map(|(filename, name, address)| zk_link::Library { + filename: filename.into_owned(), + name, + address: Address::from_hex(address).unwrap(), + }) + .collect::>(); + + let zksolc = ZkSolcCompiler { + // NOTE(zk): zksolc --link --standard-json requires >1.5.8 + // FIXME(zk): compiler from config + zksolc: ZkSolc::get_path_for_version(&Version::new(1, 5, 8)).unwrap(), + solc: Default::default(), + }; + let mut link_output = + zk_link::zksolc_link(&zksolc, zk_link::LinkJsonInput { bytecodes, libraries }) + .expect("able to call zksolc --link"); // FIXME: proper error check + + let link_id = &artifact_to_link_id(target); + + let mut contract = contracts.get(target).ok_or(LinkerError::MissingTargetArtifact)?.clone(); + + if let Some(unlinked) = link_output.unlinked.remove(link_id) { + tracing::error!(factory_dependencies = ?unlinked.factory_dependencies, libraries = ?unlinked.linker_symbols, "unmet linking dependencies"); + + if !unlinked.linker_symbols.is_empty() { + return Err(ZkLinkerError::MissingLibraries( + unlinked.linker_symbols.into_iter().collect(), + )); + } + return Err(ZkLinkerError::MissingFactoryDeps( + unlinked.factory_dependencies.into_iter().collect(), + )); + } + + let linked_output = + link_output.linked.remove(link_id).or_else(|| link_output.ignored.remove(link_id)); + + // NOTE(zk): covers intermittent issue where fully linked bytecode was + // not being returned in `ignored` (or `linked`). + // The check above should catch if the bytecode remains unlinked + let Some(linked) = linked_output else { + return Ok(contract); + }; + + let mut compact_bytecode = CompactBytecode::empty(); + compact_bytecode.object = BytecodeObject::Bytecode( + alloy_primitives::hex::decode(&linked.bytecode).unwrap().into(), + ); + + let mut compact_deployed_bytecode = CompactDeployedBytecode::empty(); + compact_deployed_bytecode.bytecode.replace(compact_bytecode.clone()); + + // TODO(zk): maybe return bytecode hash? + contract.bytecode.replace(std::borrow::Cow::Owned(compact_bytecode)); + contract.deployed_bytecode.replace(std::borrow::Cow::Owned(compact_deployed_bytecode)); + + Ok(contract) + } + pub fn get_linked_artifacts( &self, libraries: &Libraries, @@ -272,6 +441,59 @@ impl<'a> Linker<'a> { self.contracts.keys().map(|id| Ok((id.clone(), self.link(id, libraries)?))).collect() } + pub fn zk_get_linked_artifacts<'b>( + &self, + targets: impl IntoIterator, + libraries: &Libraries, + ) -> Result { + let mut targets = targets.into_iter().cloned().collect::>(); + let mut contracts = self.contracts.clone(); + let mut linked_artifacts = vec![]; + + // FIXME(zk): determine if this loop is still needed like this + while let Some(id) = targets.pop_front() { + match Self::zk_link(&contracts, &id, &libraries) { + Ok(linked) => { + // persist linked contract for successive iterations + *contracts.entry(id.clone()).or_default() = linked.clone(); + + linked_artifacts.push((id.clone(), CompactContractBytecode::from(linked))); + } + Err(ZkLinkerError::MissingFactoryDeps(fdeps)) => { + // attempt linking again if some factory dep remains unlinked + // this is just in the case where a previously unlinked factory dep + // is linked with the same run as `id` would be linked + // and instead `id` remains unlinked + // FIXME(zk): might be unnecessary, observed when paths were wrong + let mut ids = fdeps + .into_iter() + .inspect(|fdep| { + dbg!(&fdep); + }) + .flat_map(|fdep| { + contracts.iter().find(|(id, _)| { + id.source.as_path() == Path::new(fdep.filename.as_str()) && + &id.name == &fdep.library + }) + }) + .map(|(id, _)| id.clone()) + .peekable(); + + // if we have no dep ids then we avoid + // queueing our own id to avoid infinite loop + // TODO(zk): find a better way to avoid issues later + if let Some(_) = ids.peek() { + targets.extend(ids); // queue factory deps for linking + targets.push_back(id); // reque original target + } + } + Err(err) => return Err(err), + } + } + + Ok(linked_artifacts.into_iter().collect()) + } + pub fn get_linked_artifacts_cow( &self, libraries: &Libraries, diff --git a/crates/zksync/compiler/src/lib.rs b/crates/zksync/compiler/src/lib.rs index 69b086b04..a9448b609 100644 --- a/crates/zksync/compiler/src/lib.rs +++ b/crates/zksync/compiler/src/lib.rs @@ -5,15 +5,17 @@ /// ZKSolc specific logic. mod zksolc; +pub use zksolc::*; use std::path::PathBuf; use foundry_config::{Config, SkipBuildFilters, SolcReq}; use semver::Version; -pub use zksolc::*; pub mod libraries; +pub mod link; + use foundry_compilers::{ artifacts::Severity, error::SolcError, diff --git a/crates/zksync/compiler/src/link.rs b/crates/zksync/compiler/src/link.rs new file mode 100644 index 000000000..080e0eb0a --- /dev/null +++ b/crates/zksync/compiler/src/link.rs @@ -0,0 +1,135 @@ +//! Contains items and functions to link via zksolc + +use std::{ + path::Path, + process::{Command, Stdio}, +}; + +use alloy_primitives::{ + map::{HashMap, HashSet}, + Address, Bytes, +}; +use foundry_compilers::{error::SolcError, zksolc::ZkSolcCompiler}; +use serde::{Deserialize, Serialize}; + +type LinkId = String; + +#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)] +#[serde(into = "String")] +/// A library that zksolc will link against +pub struct Library { + /// Path to the library source + pub filename: String, + /// Name of the library + pub name: String, + /// Address of the library + pub address: Address, +} + +impl Into for Library { + fn into(self) -> String { + format!("{}:{}={}", self.filename, self.name, self.address) + } +} + +#[derive(Debug, Clone, Serialize)] +/// JSON Input for `zksolc link` +pub struct LinkJsonInput { + /// List of input bytecodes (linked or unlinked) + pub bytecodes: HashMap, + /// List of libraries to link against + pub libraries: HashSet, +} + +#[derive(Debug, Clone, Deserialize)] +/// Representation of a linked object given by zksolc +pub struct LinkedObject { + // FIXME: obtain factoryDeps from output + // might come in handy to have the libraries used as well + /// Fully linked bytecode + pub bytecode: String, + /// Bytecode hash of the fully linked object + pub hash: String, +} + +#[derive(Debug, Clone, Deserialize)] +/// Representation of a linked object given by zksolc +pub struct UnlinkedObject { + /// List of unlinked libraries + pub linker_symbols: HashSet, + /// List of factory dependencies missing from input + pub factory_dependencies: HashSet, +} + +/// Represent a missing library returned by the compiler +/// +/// Deserialized from: ":" +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)] +#[serde(try_from = "String")] +pub struct MissingLibrary { + /// Source path of the contract + pub filename: String, + /// Name of the contract + pub library: String, +} + +impl TryFrom for MissingLibrary { + type Error = &'static str; + + fn try_from(value: String) -> Result { + let mut split = value.split(':'); + let path = split.next().ok_or("failed to parse unlinked library filename")?.to_string(); + let name = split.next().ok_or("failed to parse unlinked library name")?.to_string(); + + Ok(Self { filename: path, library: name }) + } +} + +#[derive(Debug, Clone, Deserialize)] +/// JSON Output for `zksolc link` +pub struct LinkJsonOutput { + /// Fully linked bytecodes resulting from given input + #[serde(default)] + pub linked: HashMap, + /// Not fully linked bytecodes + #[serde(default)] + pub unlinked: HashMap, + /// List of fully linked bytecodes in input + #[serde(default)] + pub ignored: HashMap, +} + +// taken fom compilers +fn map_io_err(zksolc_path: &Path) -> impl FnOnce(std::io::Error) -> SolcError + '_ { + move |err| SolcError::io(err, zksolc_path) +} + +/// Invoke `zksolc link` given the `zksolc` binary and json input to use +#[tracing::instrument(level = tracing::Level::TRACE, ret)] +pub fn zksolc_link( + zksolc: &ZkSolcCompiler, + input: LinkJsonInput, +) -> Result { + let zksolc = &zksolc.zksolc; + let mut cmd = Command::new(&zksolc); + + cmd.arg("--standard-json") + .arg("--link") + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()); + + let mut child = cmd.spawn().map_err(map_io_err(&zksolc))?; + + let stdin = child.stdin.as_mut().unwrap(); + let _ = serde_json::to_writer(stdin, &input); + + let output = child.wait_with_output().map_err(map_io_err(&zksolc))?; + tracing::trace!(?output); + + if output.status.success() { + serde_json::from_slice(&output.stdout).map_err(Into::into) + } else { + Err(SolcError::solc_output(&output)) + } +} diff --git a/crates/zksync/compiler/src/zksolc/mod.rs b/crates/zksync/compiler/src/zksolc/mod.rs index f4e3b0699..757c1deed 100644 --- a/crates/zksync/compiler/src/zksolc/mod.rs +++ b/crates/zksync/compiler/src/zksolc/mod.rs @@ -132,10 +132,8 @@ impl DualCompiledContracts { let mut zksolc_all_bytecodes: HashMap> = Default::default(); for (_, zk_artifact) in zk_output.artifacts() { if let (Some(hash), Some(bytecode)) = (&zk_artifact.hash, &zk_artifact.bytecode) { - // TODO: we can do this because no bytecode object could be unlinked - // at this stage for zksolc, and BytecodeObject as ref will get the bytecode bytes. - // We should be careful however and check/handle errors in - // case an Unlinked BytecodeObject gets here somehow + // NOTE(zk): unlinked objects are _still_ encoded as valid hex + // but the hash wouldn't be present let bytes = bytecode.object().into_bytes().unwrap(); zksolc_all_bytecodes.insert(hash.clone(), bytes.to_vec()); } @@ -162,11 +160,8 @@ impl DualCompiledContracts { if let Some((solc_bytecode, solc_deployed_bytecode)) = solc_bytecodes.get(&contract_file) { - // TODO: we can do this because no bytecode object could be unlinked - // at this stage for zksolc, and BytecodeObject as ref will get the bytecode - // bytes. However, we should check and - // handle errors in case an Unlinked BytecodeObject gets - // here somehow + // NOTE(zk): unlinked objects are _still_ encoded as valid hex + // but the hash wouldn't be present in the artifact let bytecode_vec = bytecode.object().into_bytes().unwrap().to_vec(); let mut factory_deps_vec: Vec> = factory_deps_map .keys() @@ -299,4 +294,26 @@ impl DualCompiledContracts { pub fn push(&mut self, contract: DualCompiledContract) { self.contracts.push(contract); } + + /// Extend the inner set of contracts with the given iterator + pub fn extend(&mut self, iter: impl IntoIterator) { + self.contracts.extend(iter.into_iter()); + self.contracts.sort_by(|a, b| a.name.cmp(&b.name)); + self.contracts.dedup_by(|a, b| a.name == b.name); + } + + /// Populate the target's factory deps based on the new list + pub fn extend_factory_deps_by_hash( + &self, + mut target: DualCompiledContract, + factory_deps: impl IntoIterator, + ) -> DualCompiledContract { + let deps_bytecodes = factory_deps + .into_iter() + .flat_map(|hash| self.find_by_zk_bytecode_hash(hash)) + .map(|contract| contract.zk_deployed_bytecode.clone()); + + target.zk_factory_deps.extend(deps_bytecodes); + target + } } diff --git a/crates/zksync/core/src/lib.rs b/crates/zksync/core/src/lib.rs index 7218ff60e..ef4114fe0 100644 --- a/crates/zksync/core/src/lib.rs +++ b/crates/zksync/core/src/lib.rs @@ -176,6 +176,20 @@ pub fn try_decode_create2(data: &[u8]) -> Result<(H256, H256, Vec)> { Ok((H256(salt.0), H256(bytecode_hash.0), constructor_args.to_vec())) } +/// Compute a CREATE address according to zksync +pub fn compute_create_address(sender: Address, nonce: u64) -> Address { + const CREATE_PREFIX: &'static [u8] = b"zksyncCreate"; + let sender = sender.to_h256(); + let nonce = H256::from_low_u64_be(nonce); + let prefix = keccak256(CREATE_PREFIX); + + let payload = [prefix.as_slice(), sender.0.as_slice(), nonce.0.as_slice()].concat(); + let hash = keccak256(payload); + let address = &hash[..20]; + + Address::from_slice(address) +} + /// Try decoding the provided transaction data into create parameters. pub fn try_decode_create(data: &[u8]) -> Result<(H256, Vec)> { let decoded_calldata = From e5b805d13abce7acfccd26ed2a4b81bdeb5b3a99 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 7 Jan 2025 17:53:29 +0100 Subject: [PATCH 02/61] fix(zk:libs): calculate addresses w/ proper nonce chore: identify action to deploy libs in EraVM --- crates/forge/src/multi_runner.rs | 6 +++--- crates/forge/src/runner.rs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 76af31b5e..17864c0ea 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -447,9 +447,9 @@ impl MultiContractRunnerBuilder { zk.zk_link_with_nonce_or_address( Default::default(), LIBRARY_DEPLOYER, - // NOTE(zk): normally 0, this way we avoid overlaps with EVM libs - // FIXME: needed or 0 is ok? - libs_to_deploy.len() as u64, + // NOTE(zk): match with EVM nonces as we will be doing a duplex deployment for + // the libs + 0, zk.contracts.keys(), ) .map(|output| diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 094ce9e67..f663a5712 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -102,6 +102,7 @@ impl ContractRunner<'_> { U256::ZERO, Some(self.revert_decoder), ); + // TODO(zk): redo but entirely in zk let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?; result.extend(raw, TraceKind::Deployment); if reason.is_some() { From 262d2a7e95be7d3a5961b312f85457e022af0599 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Wed, 8 Jan 2025 19:05:21 +0100 Subject: [PATCH 03/61] fix: don't always assume DCC is present --- crates/forge/src/multi_runner.rs | 90 ++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 821433301..3081fb804 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -541,46 +541,56 @@ impl MultiContractRunnerBuilder { .map(|(zk, libs)| zk.zk_get_linked_artifacts(zk.contracts.keys(), libs)) .transpose()?; - let dual_compiled_contracts = strategy - .runner - .zksync_get_mut_dual_compiled_contracts(strategy.context.as_mut()) - .expect("dual compiled contracts present"); - let newly_linked_dual_compiled_contracts = zk_linked_contracts - .iter() - .flat_map(|arts| arts.iter()) - .flat_map(|(needle, zk)| { - linked_contracts - .iter() - .find(|(id, _)| id.source == needle.source && id.name == needle.name) - .map(|(_, evm)| (needle, zk, evm)) - }) - .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) - .map(|(id, linked_zk, evm)| { - let (_, unlinked_zk_artifact) = - zk_output.as_ref().unwrap().artifact_ids().find(|(id, _)| id == id).unwrap(); - let zk_bytecode = linked_zk.get_bytecode_bytes().unwrap(); - let zk_hash = hash_bytecode(&zk_bytecode); - let evm = evm.get_bytecode_bytes().unwrap(); - let contract = DualCompiledContract { - name: id.name.clone(), - zk_bytecode_hash: zk_hash, - zk_deployed_bytecode: zk_bytecode.to_vec(), - // FIXME: retrieve unlinked factory deps (1.5.9) - zk_factory_deps: vec![zk_bytecode.to_vec()], - evm_bytecode_hash: B256::from_slice(&keccak256(evm.as_ref())[..]), - evm_deployed_bytecode: evm.to_vec(), // FIXME: is this ok? not really used - evm_bytecode: evm.to_vec(), - }; - - // populate factory deps that were already linked - dual_compiled_contracts.extend_factory_deps_by_hash( - contract, - unlinked_zk_artifact.factory_dependencies.iter().flatten().map(|(_, hash)| { - H256::from_slice(alloy_primitives::hex::decode(hash).unwrap().as_slice()) - }), - ) - }); - dual_compiled_contracts.extend(newly_linked_dual_compiled_contracts.collect::>()); + if let Some(dual_compiled_contracts) = + strategy.runner.zksync_get_mut_dual_compiled_contracts(strategy.context.as_mut()) + { + let newly_linked_dual_compiled_contracts = zk_linked_contracts + .iter() + .flat_map(|arts| arts.iter()) + .flat_map(|(needle, zk)| { + linked_contracts + .iter() + .find(|(id, _)| id.source == needle.source && id.name == needle.name) + .map(|(_, evm)| (needle, zk, evm)) + }) + .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) + .map(|(id, linked_zk, evm)| { + let (_, unlinked_zk_artifact) = zk_output + .as_ref() + .unwrap() + .artifact_ids() + .find(|(id, _)| id == id) + .unwrap(); + let zk_bytecode = linked_zk.get_bytecode_bytes().unwrap(); + let zk_hash = hash_bytecode(&zk_bytecode); + let evm = evm.get_bytecode_bytes().unwrap(); + let contract = DualCompiledContract { + name: id.name.clone(), + zk_bytecode_hash: zk_hash, + zk_deployed_bytecode: zk_bytecode.to_vec(), + // FIXME: retrieve unlinked factory deps (1.5.9) + zk_factory_deps: vec![zk_bytecode.to_vec()], + evm_bytecode_hash: B256::from_slice(&keccak256(evm.as_ref())[..]), + evm_deployed_bytecode: evm.to_vec(), // FIXME: is this ok? not really used + evm_bytecode: evm.to_vec(), + }; + + // populate factory deps that were already linked + dual_compiled_contracts.extend_factory_deps_by_hash( + contract, + unlinked_zk_artifact.factory_dependencies.iter().flatten().map( + |(_, hash)| { + H256::from_slice( + alloy_primitives::hex::decode(hash).unwrap().as_slice(), + ) + }, + ), + ) + }); + + dual_compiled_contracts + .extend(newly_linked_dual_compiled_contracts.collect::>()); + } // FIXME: is this comment outdated? I don't see the library deployment code anywhere // Create a mapping of name => (abi, deployment code, Vec) From b79cada31e3647091601a725f6da4bb681742244 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Thu, 9 Jan 2025 21:33:27 +0100 Subject: [PATCH 04/61] feat(executor): `deploy_library` in strategy --- crates/evm/evm/src/executors/mod.rs | 16 +++- crates/evm/evm/src/executors/strategy.rs | 43 ++++++++- crates/forge/src/multi_runner.rs | 2 + crates/forge/src/runner.rs | 3 +- crates/strategy/zksync/src/executor.rs | 112 +++++++++++++++++++++-- 5 files changed, 164 insertions(+), 12 deletions(-) diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 158fdac8d..2839dfc83 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -303,6 +303,20 @@ impl Executor { self.deploy_with_env(env, rd) } + /// Deploys a library contract and commits the new state to the underlying database. + /// + /// Executes a CREATE transaction with the contract `code` and persistent database state + /// modifications. + pub fn deploy_library( + &mut self, + from: Address, + code: Bytes, + value: U256, + rd: Option<&RevertDecoder>, + ) -> Result { + self.strategy.runner.deploy_library(self, from, code, value, rd) + } + /// Deploys a contract using the given `env` and commits the new state to the underlying /// database. /// @@ -672,7 +686,7 @@ impl Executor { /// /// If using a backend with cheatcodes, `tx.gas_price` and `block.number` will be overwritten by /// the cheatcode state in between calls. - fn build_test_env( + pub fn build_test_env( &self, caller: Address, transact_to: TxKind, diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index 96ed8d484..0bd2b9334 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -1,12 +1,15 @@ use std::{any::Any, fmt::Debug}; -use alloy_primitives::{Address, U256}; +use alloy_primitives::{Address, Bytes, U256}; use alloy_serde::OtherFields; use eyre::Result; use foundry_cheatcodes::strategy::{ CheatcodeInspectorStrategy, EvmCheatcodeInspectorStrategyRunner, }; -use foundry_evm_core::backend::{strategy::BackendStrategy, Backend, BackendResult, CowBackend}; +use foundry_evm_core::{ + backend::{strategy::BackendStrategy, Backend, BackendResult, CowBackend}, + decode::RevertDecoder, +}; use foundry_zksync_compilers::dual_compiled_contracts::DualCompiledContracts; use revm::{ primitives::{Env, EnvWithHandlerCfg, ResultAndState}, @@ -15,7 +18,7 @@ use revm::{ use crate::inspectors::InspectorStack; -use super::Executor; +use super::{DeployResult, EvmError, Executor}; pub trait ExecutorStrategyContext: Debug + Send + Sync + Any { /// Clone the strategy context. @@ -68,9 +71,23 @@ pub trait ExecutorStrategyRunner: Debug + Send + Sync + ExecutorStrategyExt { amount: U256, ) -> BackendResult<()>; + fn get_balance(&self, executor: &mut Executor, address: Address) -> BackendResult; + fn set_nonce(&self, executor: &mut Executor, address: Address, nonce: u64) -> BackendResult<()>; + fn get_nonce(&self, executor: &mut Executor, address: Address) -> BackendResult; + + /// Deploys a library, applying state changes + fn deploy_library( + &self, + executor: &mut Executor, + from: Address, + code: Bytes, + value: U256, + rd: Option<&RevertDecoder>, + ) -> Result; + /// Execute a transaction and *WITHOUT* applying state changes. fn call( &self, @@ -160,6 +177,10 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { Ok(()) } + fn get_balance(&self, executor: &mut Executor, address: Address) -> BackendResult { + executor.get_balance(address) + } + fn set_nonce( &self, executor: &mut Executor, @@ -173,6 +194,22 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { Ok(()) } + fn get_nonce(&self, executor: &mut Executor, address: Address) -> BackendResult { + executor.get_nonce(address) + } + + /// Deploys a library, applying state changes + fn deploy_library( + &self, + executor: &mut Executor, + from: Address, + code: Bytes, + value: U256, + rd: Option<&RevertDecoder>, + ) -> Result { + executor.deploy(from, code, value, rd) + } + fn call( &self, _ctx: &dyn ExecutorStrategyContext, diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 3081fb804..fe381944f 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -489,6 +489,8 @@ impl MultiContractRunnerBuilder { evm_opts: EvmOpts, mut strategy: ExecutorStrategy, ) -> Result { + // TODO(zk): move linking to executor strategy + let contracts = output .artifact_ids() .map(|(id, v)| (id.with_stripped_file_prefixes(root), v)) diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 1c297f695..0ccb08d7f 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -124,13 +124,12 @@ impl<'a> ContractRunner<'a> { let mut result = TestSetup::default(); for code in self.mcr.libs_to_deploy.iter() { - let deploy_result = self.executor.deploy( + let deploy_result = self.executor.deploy_library( LIBRARY_DEPLOYER, code.clone(), U256::ZERO, Some(&self.mcr.revert_decoder), ); - // TODO(zk): redo but entirely in zk // Record deployed library address. if let Ok(deployed) = &deploy_result { diff --git a/crates/strategy/zksync/src/executor.rs b/crates/strategy/zksync/src/executor.rs index 47c9f0479..69f1279f1 100644 --- a/crates/strategy/zksync/src/executor.rs +++ b/crates/strategy/zksync/src/executor.rs @@ -1,23 +1,29 @@ -use alloy_primitives::{Address, U256}; +use alloy_primitives::{Address, Bytes, TxKind, U256}; use alloy_rpc_types::serde_helpers::OtherFields; -use alloy_zksync::provider::{zksync_provider, ZksyncProvider}; +use alloy_zksync::{ + contracts::l2::contract_deployer::CONTRACT_DEPLOYER_ADDRESS, + provider::{zksync_provider, ZksyncProvider}, +}; use eyre::Result; use foundry_evm::{ - backend::{Backend, BackendResult, CowBackend}, + backend::{Backend, BackendResult, CowBackend, DatabaseExt}, + decode::RevertDecoder, executors::{ strategy::{ EvmExecutorStrategyRunner, ExecutorStrategy, ExecutorStrategyContext, ExecutorStrategyExt, ExecutorStrategyRunner, }, - Executor, + DeployResult, EvmError, Executor, }, inspectors::InspectorStack, }; use foundry_zksync_compilers::dual_compiled_contracts::DualCompiledContracts; -use foundry_zksync_core::{vm::ZkEnv, ZkTransactionMetadata, ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY}; +use foundry_zksync_core::{ + encode_create_params, vm::ZkEnv, ZkTransactionMetadata, ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY, +}; use revm::{ - primitives::{Env, EnvWithHandlerCfg, ResultAndState}, + primitives::{CreateScheme, Env, EnvWithHandlerCfg, Output, ResultAndState}, Database, }; @@ -52,6 +58,23 @@ impl ExecutorStrategyContext for ZksyncExecutorStrategyContext { #[derive(Debug, Default, Clone)] pub struct ZksyncExecutorStrategyRunner; +impl ZksyncExecutorStrategyRunner { + fn set_deployment_nonce( + executor: &mut Executor, + address: Address, + nonce: u64, + ) -> BackendResult<()> { + let (address, slot) = foundry_zksync_core::state::get_nonce_storage(address); + // fetch the full nonce to preserve account's tx nonce + let full_nonce = executor.backend.storage(address, slot)?; + let full_nonce = foundry_zksync_core::state::parse_full_nonce(full_nonce); + let new_full_nonce = foundry_zksync_core::state::new_full_nonce(full_nonce.tx_nonce, nonce); + executor.backend.insert_account_storage(address, slot, new_full_nonce)?; + + Ok(()) + } +} + fn get_context_ref(ctx: &dyn ExecutorStrategyContext) -> &ZksyncExecutorStrategyContext { ctx.as_any_ref().downcast_ref().expect("expected ZksyncExecutorStrategyContext") } @@ -75,6 +98,13 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { Ok(()) } + fn get_balance(&self, executor: &mut Executor, address: Address) -> BackendResult { + let (address, slot) = foundry_zksync_core::state::get_balance_storage(address); + let balance = executor.backend.storage(address, slot)?; + + Ok(balance) + } + fn set_nonce( &self, executor: &mut Executor, @@ -94,6 +124,76 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { Ok(()) } + fn get_nonce(&self, executor: &mut Executor, address: Address) -> BackendResult { + let (address, slot) = foundry_zksync_core::state::get_nonce_storage(address); + let full_nonce = executor.backend.storage(address, slot)?; + let full_nonce = foundry_zksync_core::state::parse_full_nonce(full_nonce); + + Ok(full_nonce.tx_nonce) + } + + fn deploy_library( + &self, + executor: &mut Executor, + from: Address, + code: Bytes, + value: U256, + rd: Option<&RevertDecoder>, + ) -> Result { + // sync deployer account info + let nonce = EvmExecutorStrategyRunner.get_nonce(executor, from).expect("deployer to exist"); + let balance = + EvmExecutorStrategyRunner.get_balance(executor, from).expect("deployer to exist"); + + Self::set_deployment_nonce(executor, from, nonce).map_err(|err| eyre::eyre!(err))?; + self.set_balance(executor, from, balance).map_err(|err| eyre::eyre!(err))?; + tracing::debug!(?nonce, ?balance, sender = ?from, "deploying lib in EraVM"); + + // TODO(zk): determine how to return also relevant information for the EVM deployment + let evm_deployment = EvmExecutorStrategyRunner.deploy_library( + executor, + from, + code.clone(), + value, + rd.clone(), + )?; + + let ctx = get_context(executor.strategy.context.as_mut()); + + // lookup dual compiled contract based on EVM bytecode + let Some(dual_contract) = ctx.dual_compiled_contracts.find_by_evm_bytecode(code.as_ref()) + else { + // we don't know what the equivalent zk contract would be + return Ok(evm_deployment); + }; + + // no need for constructor args as it's a lib + let create_params = + encode_create_params(&CreateScheme::Create, dual_contract.zk_bytecode_hash, vec![]); + + // populate ctx.transaction_context with factory deps + // we also populate the ctx so the deployment is executed + // entirely in EraVM + let factory_deps = ctx.dual_compiled_contracts.fetch_all_factory_deps(dual_contract); + + // persist existing paymaster data (needed?) + let paymaster_data = + ctx.transaction_context.take().and_then(|metadata| metadata.paymaster_data); + ctx.transaction_context = Some(ZkTransactionMetadata { factory_deps, paymaster_data }); + + // eravm_env: call to ContractDeployer w/ properly encoded calldata + let env = executor.build_test_env( + from, + // foundry_zksync_core::vm::runner::transact takes care of using the ContractDeployer + // address + TxKind::Create, + create_params.into(), + value, + ); + + executor.deploy_with_env(env, rd) + } + fn new_backend_strategy(&self) -> foundry_evm_core::backend::strategy::BackendStrategy { foundry_evm_core::backend::strategy::BackendStrategy::new_zksync() } From 1941e33826a5ebf8aef2ff056f291108b160ce20 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Fri, 10 Jan 2025 12:24:42 +0100 Subject: [PATCH 05/61] fix(zk): create address computation --- crates/zksync/core/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zksync/core/src/lib.rs b/crates/zksync/core/src/lib.rs index 5ce2409a6..b515c25d1 100644 --- a/crates/zksync/core/src/lib.rs +++ b/crates/zksync/core/src/lib.rs @@ -190,7 +190,7 @@ pub fn compute_create_address(sender: Address, nonce: u64) -> Address { let payload = [prefix.as_slice(), sender.0.as_slice(), nonce.0.as_slice()].concat(); let hash = keccak256(payload); - let address = &hash[..20]; + let address = &hash[12..]; Address::from_slice(address) } From 93babb95e0ad79a1472b12a101491f4a958ddd0b Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Fri, 10 Jan 2025 13:04:09 +0100 Subject: [PATCH 06/61] chore: cleanup unused imports --- crates/strategy/zksync/src/executor.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/strategy/zksync/src/executor.rs b/crates/strategy/zksync/src/executor.rs index 69f1279f1..a10cf2457 100644 --- a/crates/strategy/zksync/src/executor.rs +++ b/crates/strategy/zksync/src/executor.rs @@ -1,13 +1,10 @@ use alloy_primitives::{Address, Bytes, TxKind, U256}; use alloy_rpc_types::serde_helpers::OtherFields; -use alloy_zksync::{ - contracts::l2::contract_deployer::CONTRACT_DEPLOYER_ADDRESS, - provider::{zksync_provider, ZksyncProvider}, -}; +use alloy_zksync::provider::{zksync_provider, ZksyncProvider}; use eyre::Result; use foundry_evm::{ - backend::{Backend, BackendResult, CowBackend, DatabaseExt}, + backend::{Backend, BackendResult, CowBackend}, decode::RevertDecoder, executors::{ strategy::{ @@ -23,7 +20,7 @@ use foundry_zksync_core::{ encode_create_params, vm::ZkEnv, ZkTransactionMetadata, ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY, }; use revm::{ - primitives::{CreateScheme, Env, EnvWithHandlerCfg, Output, ResultAndState}, + primitives::{CreateScheme, Env, EnvWithHandlerCfg, ResultAndState}, Database, }; From 5ed51542c4c60eb1d41982d3988002db501f147d Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Fri, 10 Jan 2025 13:04:24 +0100 Subject: [PATCH 07/61] test(zk): deploy-time linking (script/test) --- .../forge/tests/fixtures/zk/Libraries.s.sol | 11 +++++- crates/forge/tests/it/test_helpers.rs | 2 +- crates/forge/tests/it/zk/linking.rs | 39 +++++++++++++++---- crates/script/src/build.rs | 2 + testdata/zk/WithLibraries.sol | 2 +- testdata/zk/WithLibraries.t.sol | 25 ++++++++++++ 6 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 testdata/zk/WithLibraries.t.sol diff --git a/crates/forge/tests/fixtures/zk/Libraries.s.sol b/crates/forge/tests/fixtures/zk/Libraries.s.sol index 195d4dd7b..b892a4a56 100644 --- a/crates/forge/tests/fixtures/zk/Libraries.s.sol +++ b/crates/forge/tests/fixtures/zk/Libraries.s.sol @@ -5,9 +5,18 @@ pragma solidity >=0.8.7 <0.9.0; import {UsesFoo} from "../src/WithLibraries.sol"; import "forge-std/Script.sol"; -contract DeployUsesFoo is Script { +contract GetCodeUnlinked is Script { function run () external { // should fail because `UsesFoo` is unlinked bytes memory _code = vm.getCode("UsesFoo"); } } + +contract DeployTimeLinking is Script { + function run() external { + vm.broadcast(); + UsesFoo user = new UsesFoo(); + + assert(user.number() == 42); + } +} diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index d7472c6a7..4f8c77cd6 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -201,7 +201,7 @@ impl ForgeTestProfile { zk_config.zksync.startup = true; zk_config.zksync.fallback_oz = true; zk_config.zksync.optimizer_mode = '3'; - zk_config.zksync.zksolc = Some(foundry_config::SolcReq::Version(Version::new(1, 5, 7))); + zk_config.zksync.zksolc = Some(foundry_config::SolcReq::Version(Version::new(1, 5, 8))); zk_config.fuzz.no_zksync_reserved_addresses = true; zk_config.invariant.depth = 15; diff --git a/crates/forge/tests/it/zk/linking.rs b/crates/forge/tests/it/zk/linking.rs index 679e34ca5..872e1ba46 100644 --- a/crates/forge/tests/it/zk/linking.rs +++ b/crates/forge/tests/it/zk/linking.rs @@ -1,21 +1,44 @@ -use foundry_test_utils::{forgetest_async, util, TestProject}; +use forge::revm::primitives::SpecId; +use foundry_test_utils::{forgetest_async, util, Filter, TestProject}; -use crate::test_helpers::{deploy_zk_contract, run_zk_script_test}; +use crate::{config::TestConfig, test_helpers::{deploy_zk_contract, run_zk_script_test, TEST_DATA_DEFAULT}}; -// TODO(zk): add test that actually does the deployment -// of the unlinked contract via script, once recursive linking is supported -// and once we also support doing deploy-time linking +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_deploy_time_linking() { + let runner = TEST_DATA_DEFAULT.runner_zksync(); + let filter = Filter::new(".*", ".*", ".*WithLibraries.t.sol"); + + TestConfig::with_filter(runner, filter).spec_id(SpecId::SHANGHAI).run().await; +} forgetest_async!( #[should_panic = "no bytecode for contract; is it abstract or unlinked?"] - script_using_unlinked_fails, + script_zk_fails_indirect_reference_to_unlinked, + |prj, cmd| { + setup_libs_prj(&mut prj); + run_zk_script_test( + prj.root(), + &mut cmd, + "./script/Libraries.s.sol", + "GetCodeUnlinked", + None, + 1, + Some(&["-vvvvv"]), + ); + } +); + +// FIXME(zk): add deploy-time linking to scripting +forgetest_async!( + #[should_panic = "has no bytecode hash, as it may require library linkage"] + script_zk_deploy_time_linking_fails, |prj, cmd| { setup_libs_prj(&mut prj); run_zk_script_test( prj.root(), &mut cmd, "./script/Libraries.s.sol", - "DeployUsesFoo", + "DeployTimeLinking", None, 1, Some(&["-vvvvv"]), @@ -25,7 +48,7 @@ forgetest_async!( forgetest_async!( #[should_panic = "Dynamic linking not supported"] - create_using_unlinked_fails, + create_zk_using_unlinked_fails, |prj, cmd| { setup_libs_prj(&mut prj); diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index ba89cbfd0..b2a3775f8 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -64,6 +64,8 @@ impl BuildData { let known_libraries = script_config.config.libraries_with_remappings()?; + // TODO(zk): linking updating self.dual_compiled_contracts + let maybe_create2_link_output = can_use_create2 .then(|| { self.get_linker() diff --git a/testdata/zk/WithLibraries.sol b/testdata/zk/WithLibraries.sol index 51ca18879..bffda5a2b 100644 --- a/testdata/zk/WithLibraries.sol +++ b/testdata/zk/WithLibraries.sol @@ -9,7 +9,7 @@ library Foo { } contract UsesFoo { - uint256 number; + uint256 public number; constructor() { number = Foo.add(42, 0); diff --git a/testdata/zk/WithLibraries.t.sol b/testdata/zk/WithLibraries.t.sol new file mode 100644 index 000000000..bd22841bc --- /dev/null +++ b/testdata/zk/WithLibraries.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity >=0.8.7; + +import "ds-test/test.sol"; +import "../cheats/Vm.sol"; + +import {UsesFoo} from "./WithLibraries.sol"; + +contract GetCodeUnlinked is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function testGetCodeUnlinked() external { + bytes memory _code = vm.getCode("UsesFoo"); + } +} + +contract DeployTimeLinking is DSTest { + function testUseUnlinkedContract() external { + // we check that `UsesFoo` is fully linked + // and that the inner library is usable + UsesFoo user = new UsesFoo(); + assertEq(user.number(), 42); + } +} From 018f3f0cfe7b1ffe39f832b850b6ad199234fccc Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 13 Jan 2025 13:37:29 +0100 Subject: [PATCH 08/61] chore: default zksolc to 1.5.8 --- crates/config/src/zksync.rs | 2 +- crates/forge/tests/fixtures/zk/Libraries.s.sol | 2 +- crates/forge/tests/it/zk/linking.rs | 7 +++++-- crates/zksync/compilers/src/compilers/zksolc/mod.rs | 2 +- testdata/zk/WithLibraries.t.sol | 8 -------- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/crates/config/src/zksync.rs b/crates/config/src/zksync.rs index 1f501282f..0d3c60cce 100644 --- a/crates/config/src/zksync.rs +++ b/crates/config/src/zksync.rs @@ -204,7 +204,7 @@ pub fn config_create_project( { zksolc } else if !config.offline { - let default_version = semver::Version::new(1, 5, 7); + let default_version = semver::Version::new(1, 5, 8); let mut zksolc = ZkSolc::find_installed_version(&default_version)?; if zksolc.is_none() { ZkSolc::blocking_install(&default_version)?; diff --git a/crates/forge/tests/fixtures/zk/Libraries.s.sol b/crates/forge/tests/fixtures/zk/Libraries.s.sol index b892a4a56..25d6bd4bc 100644 --- a/crates/forge/tests/fixtures/zk/Libraries.s.sol +++ b/crates/forge/tests/fixtures/zk/Libraries.s.sol @@ -6,7 +6,7 @@ import {UsesFoo} from "../src/WithLibraries.sol"; import "forge-std/Script.sol"; contract GetCodeUnlinked is Script { - function run () external { + function run() external { // should fail because `UsesFoo` is unlinked bytes memory _code = vm.getCode("UsesFoo"); } diff --git a/crates/forge/tests/it/zk/linking.rs b/crates/forge/tests/it/zk/linking.rs index 872e1ba46..43c16cf4f 100644 --- a/crates/forge/tests/it/zk/linking.rs +++ b/crates/forge/tests/it/zk/linking.rs @@ -1,12 +1,15 @@ use forge::revm::primitives::SpecId; use foundry_test_utils::{forgetest_async, util, Filter, TestProject}; -use crate::{config::TestConfig, test_helpers::{deploy_zk_contract, run_zk_script_test, TEST_DATA_DEFAULT}}; +use crate::{ + config::TestConfig, + test_helpers::{deploy_zk_contract, run_zk_script_test, TEST_DATA_DEFAULT}, +}; #[tokio::test(flavor = "multi_thread")] async fn test_zk_deploy_time_linking() { let runner = TEST_DATA_DEFAULT.runner_zksync(); - let filter = Filter::new(".*", ".*", ".*WithLibraries.t.sol"); + let filter = Filter::new(".*", "DeployTimeLinking", ".*"); TestConfig::with_filter(runner, filter).spec_id(SpecId::SHANGHAI).run().await; } diff --git a/crates/zksync/compilers/src/compilers/zksolc/mod.rs b/crates/zksync/compilers/src/compilers/zksolc/mod.rs index 203a0fd45..865b36a59 100644 --- a/crates/zksync/compilers/src/compilers/zksolc/mod.rs +++ b/crates/zksync/compilers/src/compilers/zksolc/mod.rs @@ -40,7 +40,7 @@ pub const ZKSOLC: &str = "zksolc"; /// ZKsync solc release used for all ZKsync solc versions pub const ZKSYNC_SOLC_RELEASE: Version = Version::new(1, 0, 1); /// Default zksolc version -pub const ZKSOLC_VERSION: Version = Version::new(1, 5, 7); +pub const ZKSOLC_VERSION: Version = Version::new(1, 5, 8); #[cfg(test)] macro_rules! take_solc_installer_lock { diff --git a/testdata/zk/WithLibraries.t.sol b/testdata/zk/WithLibraries.t.sol index bd22841bc..34e81313d 100644 --- a/testdata/zk/WithLibraries.t.sol +++ b/testdata/zk/WithLibraries.t.sol @@ -7,14 +7,6 @@ import "../cheats/Vm.sol"; import {UsesFoo} from "./WithLibraries.sol"; -contract GetCodeUnlinked is DSTest { - Vm constant vm = Vm(HEVM_ADDRESS); - - function testGetCodeUnlinked() external { - bytes memory _code = vm.getCode("UsesFoo"); - } -} - contract DeployTimeLinking is DSTest { function testUseUnlinkedContract() external { // we check that `UsesFoo` is fully linked From c7f4f3542ff1a3bbcdd37868924fe5b39dc474dd Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 13 Jan 2025 19:02:30 +0100 Subject: [PATCH 09/61] chore: lints --- crates/linking/src/lib.rs | 10 ++++++---- crates/zksync/compilers/src/link.rs | 8 ++++---- crates/zksync/core/src/lib.rs | 13 +++---------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/crates/linking/src/lib.rs b/crates/linking/src/lib.rs index 0b2e7d3d5..a087029dd 100644 --- a/crates/linking/src/lib.rs +++ b/crates/linking/src/lib.rs @@ -361,9 +361,9 @@ impl<'a> Linker<'a> { let bytes = match object { BytecodeObject::Bytecode(bytes) => bytes, - BytecodeObject::Unlinked(unlinked) => { - alloy_primitives::hex::decode(unlinked).unwrap().into() - } + BytecodeObject::Unlinked(unlinked) => alloy_primitives::hex::decode(unlinked) + .expect("malformed unlinked bytecode object") + .into(), }; Some((link_id, bytes)) @@ -423,7 +423,9 @@ impl<'a> Linker<'a> { let mut compact_bytecode = CompactBytecode::empty(); compact_bytecode.object = BytecodeObject::Bytecode( - alloy_primitives::hex::decode(&linked.bytecode).unwrap().into(), + alloy_primitives::hex::decode(&linked.bytecode) + .expect("malformed unlinked bytecode object") + .into(), ); let mut compact_deployed_bytecode = CompactDeployedBytecode::empty(); diff --git a/crates/zksync/compilers/src/link.rs b/crates/zksync/compilers/src/link.rs index 591e6d98a..90a9da540 100644 --- a/crates/zksync/compilers/src/link.rs +++ b/crates/zksync/compilers/src/link.rs @@ -16,9 +16,9 @@ use crate::compilers::zksolc::ZkSolcCompiler; type LinkId = String; +/// A library that zksolc will link against #[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)] #[serde(into = "String")] -/// A library that zksolc will link against pub struct Library { /// Path to the library source pub filename: String, @@ -43,8 +43,8 @@ pub struct LinkJsonInput { pub libraries: HashSet, } -#[derive(Debug, Clone, Deserialize)] /// Representation of a linked object given by zksolc +#[derive(Debug, Clone, Deserialize)] pub struct LinkedObject { // FIXME: obtain factoryDeps from output // might come in handy to have the libraries used as well @@ -54,8 +54,8 @@ pub struct LinkedObject { pub hash: String, } -#[derive(Debug, Clone, Deserialize)] /// Representation of a linked object given by zksolc +#[derive(Debug, Clone, Deserialize)] pub struct UnlinkedObject { /// List of unlinked libraries pub linker_symbols: HashSet, @@ -87,8 +87,8 @@ impl TryFrom for MissingLibrary { } } -#[derive(Debug, Clone, Deserialize)] /// JSON Output for `zksolc link` +#[derive(Debug, Clone, Deserialize)] pub struct LinkJsonOutput { /// Fully linked bytecodes resulting from given input #[serde(default)] diff --git a/crates/zksync/core/src/lib.rs b/crates/zksync/core/src/lib.rs index b515c25d1..cf27f129f 100644 --- a/crates/zksync/core/src/lib.rs +++ b/crates/zksync/core/src/lib.rs @@ -29,6 +29,8 @@ use convert::{ConvertAddress, ConvertH160, ConvertH256, ConvertRU256, ConvertU25 use eyre::eyre; use revm::{Database, InnerEvmContext}; use serde::{Deserialize, Serialize}; +use zksync_multivm::vm_m6::test_utils::get_create_zksync_address; +use zksync_types::Nonce; use std::fmt::Debug; pub use utils::{fix_l2_gas_limit, fix_l2_gas_price}; @@ -183,16 +185,7 @@ pub fn try_decode_create2(data: &[u8]) -> Result<(H256, H256, Vec)> { /// Compute a CREATE address according to zksync pub fn compute_create_address(sender: Address, nonce: u64) -> Address { - const CREATE_PREFIX: &'static [u8] = b"zksyncCreate"; - let sender = sender.to_h256(); - let nonce = H256::from_low_u64_be(nonce); - let prefix = keccak256(CREATE_PREFIX); - - let payload = [prefix.as_slice(), sender.0.as_slice(), nonce.0.as_slice()].concat(); - let hash = keccak256(payload); - let address = &hash[12..]; - - Address::from_slice(address) + get_create_zksync_address(sender.to_h160(), Nonce(nonce as u32)).to_address() } /// Try decoding the provided transaction data into create parameters. From c1c2615287380296695ee35c1a7a9874b32ec57a Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 13 Jan 2025 20:16:24 +0100 Subject: [PATCH 10/61] refactor: allow multiple lib deployments --- crates/evm/evm/src/executors/mod.rs | 2 +- crates/evm/evm/src/executors/strategy.rs | 6 +++--- crates/forge/src/runner.rs | 25 +++++++++++++++--------- crates/strategy/zksync/src/executor.rs | 10 ++++++---- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 2839dfc83..0a6db6f5b 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -313,7 +313,7 @@ impl Executor { code: Bytes, value: U256, rd: Option<&RevertDecoder>, - ) -> Result { + ) -> Result, EvmError> { self.strategy.runner.deploy_library(self, from, code, value, rd) } diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index 0bd2b9334..ebb5b68df 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -86,7 +86,7 @@ pub trait ExecutorStrategyRunner: Debug + Send + Sync + ExecutorStrategyExt { code: Bytes, value: U256, rd: Option<&RevertDecoder>, - ) -> Result; + ) -> Result, EvmError>; /// Execute a transaction and *WITHOUT* applying state changes. fn call( @@ -206,8 +206,8 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { code: Bytes, value: U256, rd: Option<&RevertDecoder>, - ) -> Result { - executor.deploy(from, code, value, rd) + ) -> Result, EvmError> { + executor.deploy(from, code, value, rd).map(|dr| vec![dr]) } fn call( diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 0ccb08d7f..4af7e45c5 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -131,16 +131,23 @@ impl<'a> ContractRunner<'a> { Some(&self.mcr.revert_decoder), ); - // Record deployed library address. - if let Ok(deployed) = &deploy_result { - result.deployed_libs.push(deployed.address); - } + let deployments = match deploy_result { + Err(err) => vec![Err(err)], + Ok(deployments) => deployments.into_iter().map(|d| Ok(d)).collect(), + }; - let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?; - result.extend(raw, TraceKind::Deployment); - if reason.is_some() { - result.reason = reason; - return Ok(result); + for deploy_result in deployments { + // Record deployed library address. + if let Ok(deployed) = &deploy_result { + result.deployed_libs.push(deployed.address); + } + + let (raw, reason) = RawCallResult::from_evm_result(deploy_result.map(Into::into))?; + result.extend(raw, TraceKind::Deployment); + if reason.is_some() { + result.reason = reason; + return Ok(result); + } } } diff --git a/crates/strategy/zksync/src/executor.rs b/crates/strategy/zksync/src/executor.rs index a10cf2457..427b418a0 100644 --- a/crates/strategy/zksync/src/executor.rs +++ b/crates/strategy/zksync/src/executor.rs @@ -136,7 +136,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { code: Bytes, value: U256, rd: Option<&RevertDecoder>, - ) -> Result { + ) -> Result, EvmError> { // sync deployer account info let nonce = EvmExecutorStrategyRunner.get_nonce(executor, from).expect("deployer to exist"); let balance = @@ -146,8 +146,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { self.set_balance(executor, from, balance).map_err(|err| eyre::eyre!(err))?; tracing::debug!(?nonce, ?balance, sender = ?from, "deploying lib in EraVM"); - // TODO(zk): determine how to return also relevant information for the EVM deployment - let evm_deployment = EvmExecutorStrategyRunner.deploy_library( + let mut evm_deployment = EvmExecutorStrategyRunner.deploy_library( executor, from, code.clone(), @@ -188,7 +187,10 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { value, ); - executor.deploy_with_env(env, rd) + executor.deploy_with_env(env, rd).map(move |dr| { + evm_deployment.push(dr); + evm_deployment + }) } fn new_backend_strategy(&self) -> foundry_evm_core::backend::strategy::BackendStrategy { From 1ec71e44a9d0e5919fafdb07fb888e8d5cfc4a81 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 13 Jan 2025 21:21:57 +0100 Subject: [PATCH 11/61] refactor(link): move to executor strategy fix(strategy): remove get_mut for `DualCompiledContracts` --- Cargo.lock | 3 + crates/evm/evm/Cargo.toml | 1 + crates/evm/evm/src/executors/strategy.rs | 93 +++++++++++++- crates/forge/src/multi_runner.rs | 156 +++-------------------- crates/strategy/zksync/Cargo.toml | 2 + crates/strategy/zksync/src/executor.rs | 127 ++++++++++++++++-- 6 files changed, 231 insertions(+), 151 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fab62262..386931ce0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5202,6 +5202,7 @@ dependencies = [ "foundry-evm-coverage", "foundry-evm-fuzz", "foundry-evm-traces", + "foundry-linking", "foundry-zksync-compilers", "foundry-zksync-core", "foundry-zksync-inspectors", @@ -5394,9 +5395,11 @@ dependencies = [ "eyre", "foundry-cheatcodes", "foundry-common", + "foundry-compilers", "foundry-config", "foundry-evm", "foundry-evm-core", + "foundry-linking", "foundry-zksync-compilers", "foundry-zksync-core", "itertools 0.13.0", diff --git a/crates/evm/evm/Cargo.toml b/crates/evm/evm/Cargo.toml index e870512bd..d626b32e7 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -22,6 +22,7 @@ foundry-evm-core.workspace = true foundry-evm-coverage.workspace = true foundry-evm-fuzz.workspace = true foundry-evm-traces.workspace = true +foundry-linking.workspace = true foundry-zksync-core.workspace = true foundry-zksync-compilers.workspace = true foundry-zksync-inspectors.workspace = true diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index ebb5b68df..acfa23aaf 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -1,16 +1,26 @@ -use std::{any::Any, fmt::Debug}; +use std::{any::Any, borrow::Borrow, collections::BTreeMap, fmt::Debug, path::Path}; +use alloy_json_abi::JsonAbi; use alloy_primitives::{Address, Bytes, U256}; use alloy_serde::OtherFields; use eyre::Result; use foundry_cheatcodes::strategy::{ CheatcodeInspectorStrategy, EvmCheatcodeInspectorStrategyRunner, }; +use foundry_common::{ContractsByArtifact, TestFunctionExt}; +use foundry_compilers::{ + artifacts::Libraries, contracts::ArtifactContracts, Artifact, ArtifactId, ProjectCompileOutput, +}; use foundry_evm_core::{ backend::{strategy::BackendStrategy, Backend, BackendResult, CowBackend}, decode::RevertDecoder, + opts::EvmOpts, +}; +use foundry_linking::{Linker, LinkerError}; +use foundry_zksync_compilers::{ + compilers::{artifact_output::zk::ZkArtifactOutput, zksolc::ZkSolcCompiler}, + dual_compiled_contracts::DualCompiledContracts, }; -use foundry_zksync_compilers::dual_compiled_contracts::DualCompiledContracts; use revm::{ primitives::{Env, EnvWithHandlerCfg, ResultAndState}, DatabaseRef, @@ -51,6 +61,15 @@ pub struct ExecutorStrategy { pub context: Box, } +pub struct LinkOutput { + pub deployable_contracts: BTreeMap, + pub revert_decoder: RevertDecoder, + pub linked_contracts: ArtifactContracts, + pub known_contracts: ContractsByArtifact, + pub libs_to_deploy: Vec, + pub libraries: Libraries, +} + impl ExecutorStrategy { pub fn new_evm() -> Self { Self { runner: &EvmExecutorStrategyRunner, context: Box::new(()) } @@ -78,6 +97,14 @@ pub trait ExecutorStrategyRunner: Debug + Send + Sync + ExecutorStrategyExt { fn get_nonce(&self, executor: &mut Executor, address: Address) -> BackendResult; + fn link( + &self, + ctx: &mut dyn ExecutorStrategyContext, + root: &Path, + input: ProjectCompileOutput, + deployer: Address, + ) -> Result; + /// Deploys a library, applying state changes fn deploy_library( &self, @@ -127,11 +154,11 @@ pub trait ExecutorStrategyExt { ) { } - fn zksync_get_mut_dual_compiled_contracts<'a>( + fn zksync_set_compilation_output( &self, - _ctx: &'a mut dyn ExecutorStrategyContext, - ) -> Option<&'a mut DualCompiledContracts> { - None + ctx: &mut dyn ExecutorStrategyContext, + output: ProjectCompileOutput, + ) { } /// Set the fork environment on the context. @@ -198,6 +225,60 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { executor.get_nonce(address) } + fn link( + &self, + _: &mut dyn ExecutorStrategyContext, + root: &Path, + input: ProjectCompileOutput, + deployer: Address, + ) -> Result { + let contracts = + input.artifact_ids().map(|(id, v)| (id.with_stripped_file_prefixes(root), v)).collect(); + let linker = Linker::new(root, contracts); + + // Build revert decoder from ABIs of all artifacts. + let abis = linker + .contracts + .iter() + .filter_map(|(_, contract)| contract.abi.as_ref().map(|abi| abi.borrow())); + let revert_decoder = RevertDecoder::new().with_abis(abis); + + let foundry_linking::LinkOutput { libraries, libs_to_deploy } = linker + .link_with_nonce_or_address(Default::default(), deployer, 0, linker.contracts.keys())?; + + let linked_contracts = linker.get_linked_artifacts(&libraries)?; + + // Create a mapping of name => (abi, deployment code, Vec) + let mut deployable_contracts = BTreeMap::default(); + for (id, contract) in linked_contracts.iter() { + let Some(abi) = &contract.abi else { continue }; + + // if it's a test, link it and add to deployable contracts + if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && + abi.functions().any(|func| func.name.is_any_test()) + { + let Some(bytecode) = + contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty()) + else { + continue; + }; + + deployable_contracts.insert(id.clone(), (abi.clone(), bytecode)); + } + } + + let known_contracts = ContractsByArtifact::new(linked_contracts.clone()); + + Ok(LinkOutput { + deployable_contracts, + revert_decoder, + linked_contracts, + known_contracts, + libs_to_deploy, + libraries, + }) + } + /// Deploys a library, applying state changes fn deploy_library( &self, diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index fe381944f..031ee6ea3 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -17,14 +17,16 @@ use foundry_config::{Config, InlineConfig}; use foundry_evm::{ backend::Backend, decode::RevertDecoder, - executors::{strategy::ExecutorStrategy, Executor, ExecutorBuilder}, + executors::{ + strategy::{ExecutorStrategy, LinkOutput}, + Executor, ExecutorBuilder, + }, fork::CreateFork, inspectors::CheatsConfig, opts::EvmOpts, revm, traces::{InternalTraceMode, TraceMode}, }; -use foundry_linking::{LinkOutput, Linker}; use foundry_zksync_core::hash_bytecode; use rayon::prelude::*; use revm::primitives::SpecId; @@ -489,146 +491,26 @@ impl MultiContractRunnerBuilder { evm_opts: EvmOpts, mut strategy: ExecutorStrategy, ) -> Result { - // TODO(zk): move linking to executor strategy - - let contracts = output - .artifact_ids() - .map(|(id, v)| (id.with_stripped_file_prefixes(root), v)) - .collect(); - let linker = Linker::new(root, contracts); - - let zk_output = zk_output.map(|zk| zk.with_stripped_file_prefixes(&root)); - let zk_linker = - zk_output.as_ref().map(|output| Linker::new(root, output.artifact_ids().collect())); - - // Build revert decoder from ABIs of all artifacts. - let abis = linker - .contracts - .iter() - .filter_map(|(_, contract)| contract.abi.as_ref().map(|abi| abi.borrow())); - let revert_decoder = RevertDecoder::new().with_abis(abis); - - let LinkOutput { libraries, libs_to_deploy } = linker.link_with_nonce_or_address( - Default::default(), - LIBRARY_DEPLOYER, - 0, - linker.contracts.keys(), - )?; - - let zk_libs = zk_linker - .as_ref() - .map(|zk| { - zk.zk_link_with_nonce_or_address( - Default::default(), - LIBRARY_DEPLOYER, - // NOTE(zk): match with EVM nonces as we will be doing a duplex deployment for - // the libs - 0, - zk.contracts.keys(), - ) - .map(|output| - - // NOTE(zk): zk_linked_contracts later will also contain - // `libs_to_deploy` bytecodes, so those will - // get registered in DualCompiledContracts - - output.libraries) - }) - .transpose()?; - - let linked_contracts = linker.get_linked_artifacts(&libraries)?; - let zk_linked_contracts = zk_linker - .as_ref() - .and_then(|linker| zk_libs.as_ref().map(|libs| (linker, libs))) - .map(|(zk, libs)| zk.zk_get_linked_artifacts(zk.contracts.keys(), libs)) - .transpose()?; - - if let Some(dual_compiled_contracts) = - strategy.runner.zksync_get_mut_dual_compiled_contracts(strategy.context.as_mut()) - { - let newly_linked_dual_compiled_contracts = zk_linked_contracts - .iter() - .flat_map(|arts| arts.iter()) - .flat_map(|(needle, zk)| { - linked_contracts - .iter() - .find(|(id, _)| id.source == needle.source && id.name == needle.name) - .map(|(_, evm)| (needle, zk, evm)) - }) - .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) - .map(|(id, linked_zk, evm)| { - let (_, unlinked_zk_artifact) = zk_output - .as_ref() - .unwrap() - .artifact_ids() - .find(|(id, _)| id == id) - .unwrap(); - let zk_bytecode = linked_zk.get_bytecode_bytes().unwrap(); - let zk_hash = hash_bytecode(&zk_bytecode); - let evm = evm.get_bytecode_bytes().unwrap(); - let contract = DualCompiledContract { - name: id.name.clone(), - zk_bytecode_hash: zk_hash, - zk_deployed_bytecode: zk_bytecode.to_vec(), - // FIXME: retrieve unlinked factory deps (1.5.9) - zk_factory_deps: vec![zk_bytecode.to_vec()], - evm_bytecode_hash: B256::from_slice(&keccak256(evm.as_ref())[..]), - evm_deployed_bytecode: evm.to_vec(), // FIXME: is this ok? not really used - evm_bytecode: evm.to_vec(), - }; - - // populate factory deps that were already linked - dual_compiled_contracts.extend_factory_deps_by_hash( - contract, - unlinked_zk_artifact.factory_dependencies.iter().flatten().map( - |(_, hash)| { - H256::from_slice( - alloy_primitives::hex::decode(hash).unwrap().as_slice(), - ) - }, - ), - ) - }); - - dual_compiled_contracts - .extend(newly_linked_dual_compiled_contracts.collect::>()); + if let Some(zk) = zk_output { + strategy.runner.set_compilation_output(strategy.context.as_mut(), zk); } - // FIXME: is this comment outdated? I don't see the library deployment code anywhere - // Create a mapping of name => (abi, deployment code, Vec) - let mut deployable_contracts = DeployableContracts::default(); - for (id, contract) in linked_contracts.iter() { - let Some(abi) = &contract.abi else { continue }; - - // if it's a test, link it and add to deployable contracts - if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && - abi.functions().any(|func| func.name.is_any_test()) - { - let Some(bytecode) = - contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty()) - else { - continue; - }; - - deployable_contracts - .insert(id.clone(), TestContract { abi: abi.clone(), bytecode }); - } - } - - let mut known_contracts = ContractsByArtifact::default(); - if zk_output.is_none() { - known_contracts = ContractsByArtifact::new(linked_contracts); - } else if let Some(mut zk_contracts_map) = zk_linked_contracts { - // Extend zk contracts with solc contracts as well. This is required for traces to - // accurately detect contract names deployed in EVM mode, and when using - // `vm.zkVmSkip()` cheatcode. - zk_contracts_map.extend(linked_contracts); + let LinkOutput { + deployable_contracts, + revert_decoder, + linked_contracts, + known_contracts, + libs_to_deploy, + libraries, + } = strategy.runner.link(strategy.context.as_mut(), root, output, LIBRARY_DEPLOYER)?; - known_contracts = ContractsByArtifact::new(zk_contracts_map); - } + let contracts = deployable_contracts + .into_iter() + .map(|id, (abi, bytecode)| (id, TestContract { abi, bytecode })) + .collect(); Ok(MultiContractRunner { - contracts: deployable_contracts, + contracts, revert_decoder, known_contracts, libs_to_deploy, diff --git a/crates/strategy/zksync/Cargo.toml b/crates/strategy/zksync/Cargo.toml index c9b7f9e3e..3b61abaf2 100644 --- a/crates/strategy/zksync/Cargo.toml +++ b/crates/strategy/zksync/Cargo.toml @@ -17,10 +17,12 @@ alloy-sol-types.workspace = true alloy-json-abi.workspace = true alloy-zksync.workspace = true foundry-common.workspace = true +foundry-compilers.workspace = true foundry-config.workspace = true foundry-evm.workspace = true foundry-evm-core.workspace = true foundry-cheatcodes.workspace = true +foundry-linking.workspace = true foundry-zksync-core.workspace = true foundry-zksync-compilers.workspace = true diff --git a/crates/strategy/zksync/src/executor.rs b/crates/strategy/zksync/src/executor.rs index 427b418a0..6827bff22 100644 --- a/crates/strategy/zksync/src/executor.rs +++ b/crates/strategy/zksync/src/executor.rs @@ -1,28 +1,38 @@ -use alloy_primitives::{Address, Bytes, TxKind, U256}; +use std::path::Path; + +use alloy_primitives::{keccak256, Address, Bytes, TxKind, B256, U256}; use alloy_rpc_types::serde_helpers::OtherFields; use alloy_zksync::provider::{zksync_provider, ZksyncProvider}; use eyre::Result; +use foundry_common::ContractsByArtifact; +use foundry_compilers::{contracts::ArtifactContracts, Artifact, ProjectCompileOutput}; use foundry_evm::{ backend::{Backend, BackendResult, CowBackend}, decode::RevertDecoder, executors::{ strategy::{ EvmExecutorStrategyRunner, ExecutorStrategy, ExecutorStrategyContext, - ExecutorStrategyExt, ExecutorStrategyRunner, + ExecutorStrategyExt, ExecutorStrategyRunner, LinkOutput, }, DeployResult, EvmError, Executor, }, inspectors::InspectorStack, }; -use foundry_zksync_compilers::dual_compiled_contracts::DualCompiledContracts; +use foundry_linking::{Linker, LinkerError, ZkLinkerError}; +use foundry_zksync_compilers::{ + compilers::{artifact_output::zk::ZkArtifactOutput, zksolc::ZkSolcCompiler}, + dual_compiled_contracts::{DualCompiledContract, DualCompiledContracts}, +}; use foundry_zksync_core::{ - encode_create_params, vm::ZkEnv, ZkTransactionMetadata, ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY, + encode_create_params, hash_bytecode, vm::ZkEnv, ZkTransactionMetadata, + ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY, }; use revm::{ primitives::{CreateScheme, Env, EnvWithHandlerCfg, ResultAndState}, Database, }; +use zksync_types::H256; use crate::{ backend::{ZksyncBackendStrategyBuilder, ZksyncInspectContext}, @@ -33,6 +43,7 @@ use crate::{ #[derive(Debug, Default, Clone)] pub struct ZksyncExecutorStrategyContext { transaction_context: Option, + compilation_output: Option>, dual_compiled_contracts: DualCompiledContracts, zk_env: ZkEnv, } @@ -129,6 +140,105 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { Ok(full_nonce.tx_nonce) } + fn link( + &self, + ctx: &mut dyn ExecutorStrategyContext, + root: &Path, + input: ProjectCompileOutput, + deployer: Address, + ) -> Result { + let evm_link = EvmExecutorStrategyRunner.link(ctx, root, input, deployer)?; + + let ctx = get_context(ctx); + let Some(input) = ctx.compilation_output.as_ref() else { + return Err(LinkerError::MissingTargetArtifact) + }; + + let input = input.with_stripped_file_prefixes(root); + let linker = Linker::new(root, input.artifact_ids().collect()); + + let zk_linker_error_to_linker = |zk_error| match zk_error { + ZkLinkerError::Inner(err) => err, + // FIXME: better error value + ZkLinkerError::MissingLibraries(libs) => LinkerError::MissingLibraryArtifact { + file: "libraries".to_string(), + name: libs.len().to_string(), + }, + ZkLinkerError::MissingFactoryDeps(libs) => LinkerError::MissingLibraryArtifact { + file: "factoryDeps".to_string(), + name: libs.len().to_string(), + }, + }; + + let foundry_linking::LinkOutput { libraries, libs_to_deploy } = linker + .zk_link_with_nonce_or_address( + Default::default(), + deployer, + // NOTE(zk): match with EVM nonces as we will be doing a duplex deployment for + // the libs + 0, + linker.contracts.keys(), + ) + .map_err(zk_linker_error_to_linker)?; + + let mut linked_contracts = linker + .zk_get_linked_artifacts(linker.contracts.keys(), &libraries) + .map_err(zk_linker_error_to_linker)?; + + let newly_linked_dual_compiled_contracts = linked_contracts + .iter() + .flat_map(|(needle, zk)| { + evm_link + .linked_contracts + .iter() + .find(|(id, _)| id.source == needle.source && id.name == needle.name) + .map(|(_, evm)| (needle, zk, evm)) + }) + .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) + .map(|(id, linked_zk, evm)| { + let (_, unlinked_zk_artifact) = + input.artifact_ids().find(|(id, _)| id == id).unwrap(); + let zk_bytecode = linked_zk.get_bytecode_bytes().unwrap(); + let zk_hash = hash_bytecode(&zk_bytecode); + let evm = evm.get_bytecode_bytes().unwrap(); + let contract = DualCompiledContract { + name: id.name.clone(), + zk_bytecode_hash: zk_hash, + zk_deployed_bytecode: zk_bytecode.to_vec(), + // FIXME: retrieve unlinked factory deps (1.5.9) + zk_factory_deps: vec![zk_bytecode.to_vec()], + evm_bytecode_hash: B256::from_slice(&keccak256(evm.as_ref())[..]), + evm_deployed_bytecode: evm.to_vec(), // FIXME: is this ok? not really used + evm_bytecode: evm.to_vec(), + }; + + // populate factory deps that were already linked + ctx.dual_compiled_contracts.extend_factory_deps_by_hash( + contract, + unlinked_zk_artifact.factory_dependencies.iter().flatten().map(|(_, hash)| { + H256::from_slice(alloy_primitives::hex::decode(hash).unwrap().as_slice()) + }), + ) + }); + + ctx.dual_compiled_contracts + .extend(newly_linked_dual_compiled_contracts.collect::>()); + + // Extend zk contracts with solc contracts as well. This is required for traces to + // accurately detect contract names deployed in EVM mode, and when using + // `vm.zkVmSkip()` cheatcode. + linked_contracts.extend(evm_link.linked_contracts); + + Ok(LinkOutput { + deployable_contracts: evm_link.deployable_contracts, + revert_decoder: evm_link.revert_decoder, + known_contracts: ContractsByArtifact::new(linked_contracts.clone()), + linked_contracts, + libs_to_deploy: evm_link.libs_to_deploy, + libraries: evm_link.libraries, + }) + } + fn deploy_library( &self, executor: &mut Executor, @@ -274,12 +384,13 @@ impl ExecutorStrategyExt for ZksyncExecutorStrategyRunner { ctx.dual_compiled_contracts = dual_compiled_contracts; } - fn zksync_get_mut_dual_compiled_contracts<'a>( + fn zksync_set_compilation_output( &self, - ctx: &'a mut dyn ExecutorStrategyContext, - ) -> Option<&'a mut DualCompiledContracts> { + ctx: &mut dyn ExecutorStrategyContext, + output: ProjectCompileOutput, + ) { let ctx = get_context(ctx); - Some(&mut ctx.dual_compiled_contracts) + ctx.compilation_output.replace(output); } fn zksync_set_fork_env( From 624119de35544466be99ef2e627e9019b7bd913a Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 13 Jan 2025 21:33:13 +0100 Subject: [PATCH 12/61] fix: compilation --- crates/evm/evm/src/executors/strategy.rs | 8 ++++---- crates/forge/src/multi_runner.rs | 4 ++-- crates/strategy/zksync/src/executor.rs | 17 +++++++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index acfa23aaf..55989210a 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -101,7 +101,7 @@ pub trait ExecutorStrategyRunner: Debug + Send + Sync + ExecutorStrategyExt { &self, ctx: &mut dyn ExecutorStrategyContext, root: &Path, - input: ProjectCompileOutput, + input: &ProjectCompileOutput, deployer: Address, ) -> Result; @@ -156,8 +156,8 @@ pub trait ExecutorStrategyExt { fn zksync_set_compilation_output( &self, - ctx: &mut dyn ExecutorStrategyContext, - output: ProjectCompileOutput, + _ctx: &mut dyn ExecutorStrategyContext, + _output: ProjectCompileOutput, ) { } @@ -229,7 +229,7 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { &self, _: &mut dyn ExecutorStrategyContext, root: &Path, - input: ProjectCompileOutput, + input: &ProjectCompileOutput, deployer: Address, ) -> Result { let contracts = diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 031ee6ea3..3d5395c98 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -492,7 +492,7 @@ impl MultiContractRunnerBuilder { mut strategy: ExecutorStrategy, ) -> Result { if let Some(zk) = zk_output { - strategy.runner.set_compilation_output(strategy.context.as_mut(), zk); + strategy.runner.zksync_set_compilation_output(strategy.context.as_mut(), zk); } let LinkOutput { @@ -506,7 +506,7 @@ impl MultiContractRunnerBuilder { let contracts = deployable_contracts .into_iter() - .map(|id, (abi, bytecode)| (id, TestContract { abi, bytecode })) + .map(|(id, (abi, bytecode))| (id, TestContract { abi, bytecode })) .collect(); Ok(MultiContractRunner { diff --git a/crates/strategy/zksync/src/executor.rs b/crates/strategy/zksync/src/executor.rs index 6827bff22..b33c41a3a 100644 --- a/crates/strategy/zksync/src/executor.rs +++ b/crates/strategy/zksync/src/executor.rs @@ -144,7 +144,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { &self, ctx: &mut dyn ExecutorStrategyContext, root: &Path, - input: ProjectCompileOutput, + input: &ProjectCompileOutput, deployer: Address, ) -> Result { let evm_link = EvmExecutorStrategyRunner.link(ctx, root, input, deployer)?; @@ -154,8 +154,9 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { return Err(LinkerError::MissingTargetArtifact) }; - let input = input.with_stripped_file_prefixes(root); - let linker = Linker::new(root, input.artifact_ids().collect()); + let contracts = + input.artifact_ids().map(|(id, v)| (id.with_stripped_file_prefixes(root), v)).collect(); + let linker = Linker::new(root, contracts); let zk_linker_error_to_linker = |zk_error| match zk_error { ZkLinkerError::Inner(err) => err, @@ -170,7 +171,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { }, }; - let foundry_linking::LinkOutput { libraries, libs_to_deploy } = linker + let foundry_linking::LinkOutput { libraries, libs_to_deploy: _ } = linker .zk_link_with_nonce_or_address( Default::default(), deployer, @@ -196,8 +197,12 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { }) .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) .map(|(id, linked_zk, evm)| { - let (_, unlinked_zk_artifact) = - input.artifact_ids().find(|(id, _)| id == id).unwrap(); + let (_, unlinked_zk_artifact) = input + .artifact_ids() + .find(|(contract_id, _)| { + contract_id.clone().with_stripped_file_prefixes(root) == id.clone() + }) + .unwrap(); let zk_bytecode = linked_zk.get_bytecode_bytes().unwrap(); let zk_hash = hash_bytecode(&zk_bytecode); let evm = evm.get_bytecode_bytes().unwrap(); From 8ba1e1cf07dc39f68fd51f68c03d7ef2f701d9cd Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 14 Jan 2025 11:49:38 +0100 Subject: [PATCH 13/61] feat(strategy:link): pass config --- crates/evm/evm/src/executors/strategy.rs | 3 +++ crates/forge/src/multi_runner.rs | 10 ++++++++-- crates/strategy/zksync/src/executor.rs | 4 +++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index 55989210a..0c374bdc5 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -11,6 +11,7 @@ use foundry_common::{ContractsByArtifact, TestFunctionExt}; use foundry_compilers::{ artifacts::Libraries, contracts::ArtifactContracts, Artifact, ArtifactId, ProjectCompileOutput, }; +use foundry_config::Config; use foundry_evm_core::{ backend::{strategy::BackendStrategy, Backend, BackendResult, CowBackend}, decode::RevertDecoder, @@ -100,6 +101,7 @@ pub trait ExecutorStrategyRunner: Debug + Send + Sync + ExecutorStrategyExt { fn link( &self, ctx: &mut dyn ExecutorStrategyContext, + config: &Config, root: &Path, input: &ProjectCompileOutput, deployer: Address, @@ -228,6 +230,7 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { fn link( &self, _: &mut dyn ExecutorStrategyContext, + _: &Config, root: &Path, input: &ProjectCompileOutput, deployer: Address, diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 3d5395c98..1b6b7831f 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -498,11 +498,17 @@ impl MultiContractRunnerBuilder { let LinkOutput { deployable_contracts, revert_decoder, - linked_contracts, + linked_contracts: _, known_contracts, libs_to_deploy, libraries, - } = strategy.runner.link(strategy.context.as_mut(), root, output, LIBRARY_DEPLOYER)?; + } = strategy.runner.link( + strategy.context.as_mut(), + &self.config, + root, + output, + LIBRARY_DEPLOYER, + )?; let contracts = deployable_contracts .into_iter() diff --git a/crates/strategy/zksync/src/executor.rs b/crates/strategy/zksync/src/executor.rs index b33c41a3a..1673d38b6 100644 --- a/crates/strategy/zksync/src/executor.rs +++ b/crates/strategy/zksync/src/executor.rs @@ -7,6 +7,7 @@ use eyre::Result; use foundry_common::ContractsByArtifact; use foundry_compilers::{contracts::ArtifactContracts, Artifact, ProjectCompileOutput}; +use foundry_config::Config; use foundry_evm::{ backend::{Backend, BackendResult, CowBackend}, decode::RevertDecoder, @@ -143,11 +144,12 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { fn link( &self, ctx: &mut dyn ExecutorStrategyContext, + config: &Config, root: &Path, input: &ProjectCompileOutput, deployer: Address, ) -> Result { - let evm_link = EvmExecutorStrategyRunner.link(ctx, root, input, deployer)?; + let evm_link = EvmExecutorStrategyRunner.link(ctx, config, root, input, deployer)?; let ctx = get_context(ctx); let Some(input) = ctx.compilation_output.as_ref() else { From 98c56fcde60d455538b78cffa8aadf7066809c34 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 14 Jan 2025 11:50:17 +0100 Subject: [PATCH 14/61] feat(zk:link): dedicated linker module fix(zk:link): use version from config refactor(zk:config): extract config -> zksolc compiler logic into function chore: lints --- crates/config/src/zksync.rs | 42 +++-- crates/linking/src/lib.rs | 231 +---------------------- crates/linking/src/zksync.rs | 244 +++++++++++++++++++++++++ crates/strategy/zksync/src/executor.rs | 25 ++- 4 files changed, 292 insertions(+), 250 deletions(-) create mode 100644 crates/linking/src/zksync.rs diff --git a/crates/config/src/zksync.rs b/crates/config/src/zksync.rs index 0d3c60cce..402e84177 100644 --- a/crates/config/src/zksync.rs +++ b/crates/config/src/zksync.rs @@ -172,6 +172,30 @@ pub fn config_zksolc_settings(config: &Config) -> Result Result { + let zksolc = if let Some(zksolc) = + config_ensure_zksolc(config.zksync.zksolc.as_ref(), config.offline)? + { + zksolc + } else if !config.offline { + let default_version = semver::Version::new(1, 5, 8); + let mut zksolc = ZkSolc::find_installed_version(&default_version)?; + if zksolc.is_none() { + ZkSolc::blocking_install(&default_version)?; + zksolc = ZkSolc::find_installed_version(&default_version)?; + } + zksolc.unwrap_or_else(|| panic!("Could not install zksolc v{default_version}")) + } else { + "zksolc".into() + }; + + Ok(ZkSolcCompiler { zksolc, solc: config_solc_compiler(config)? }) +} + /// Create a new zkSync project pub fn config_create_project( config: &Config, @@ -199,23 +223,7 @@ pub fn config_create_project( builder = builder.sparse_output(filter); } - let zksolc = if let Some(zksolc) = - config_ensure_zksolc(config.zksync.zksolc.as_ref(), config.offline)? - { - zksolc - } else if !config.offline { - let default_version = semver::Version::new(1, 5, 8); - let mut zksolc = ZkSolc::find_installed_version(&default_version)?; - if zksolc.is_none() { - ZkSolc::blocking_install(&default_version)?; - zksolc = ZkSolc::find_installed_version(&default_version)?; - } - zksolc.unwrap_or_else(|| panic!("Could not install zksolc v{default_version}")) - } else { - "zksolc".into() - }; - - let zksolc_compiler = ZkSolcCompiler { zksolc, solc: config_solc_compiler(config)? }; + let zksolc_compiler = config_zksolc_compiler(config)?; let project = builder.build(zksolc_compiler)?; diff --git a/crates/linking/src/lib.rs b/crates/linking/src/lib.rs index a087029dd..127fcc4a7 100644 --- a/crates/linking/src/lib.rs +++ b/crates/linking/src/lib.rs @@ -5,30 +5,22 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -use alloy_primitives::{ - hex::FromHex, - map::{HashMap, HashSet}, - Address, Bytes, B256, -}; +use alloy_primitives::{Address, Bytes, B256}; use foundry_compilers::{ - artifacts::{ - BytecodeObject, CompactBytecode, CompactContractBytecode, CompactContractBytecodeCow, - CompactDeployedBytecode, Libraries, - }, + artifacts::{CompactContractBytecodeCow, Libraries}, contracts::ArtifactContracts, Artifact, ArtifactId, }; -use foundry_zksync_compilers::{ - compilers::zksolc::{ZkSolc, ZkSolcCompiler}, - link::{self as zk_link, MissingLibrary}, -}; use semver::Version; use std::{ - collections::{BTreeMap, BTreeSet, VecDeque}, + collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}, str::FromStr, }; +mod zksync; +pub use zksync::*; + /// Errors that can occur during linking. #[derive(Debug, thiserror::Error)] pub enum LinkerError { @@ -42,17 +34,6 @@ pub enum LinkerError { CyclicDependency, } -/// Errors that can occur during linking. -#[derive(Debug, thiserror::Error)] -pub enum ZkLinkerError { - #[error(transparent)] - Inner(#[from] LinkerError), - #[error("unable to fully link due to missing libraries")] - MissingLibraries(BTreeSet), - #[error("unable to fully link due to unlinked factory dependencies")] - MissingFactoryDeps(BTreeSet), -} - #[derive(Debug)] pub struct Linker<'a> { /// Root of the project, used to determine whether artifact/library path can be stripped. @@ -204,57 +185,6 @@ impl<'a> Linker<'a> { Ok(LinkOutput { libraries, libs_to_deploy }) } - /// Links given artifact with either given library addresses or address computed from sender and - /// nonce. - /// - /// Each key in `libraries` should either be a global path or relative to project root. All - /// remappings should be resolved. - /// - /// When calling for `target` being an external library itself, you should check that `target` - /// does not appear in `libs_to_deploy` to avoid deploying it twice. It may happen in cases - /// when there is a dependency cycle including `target`. - pub fn zk_link_with_nonce_or_address( - &'a self, - libraries: Libraries, - sender: Address, - mut nonce: u64, - targets: impl IntoIterator, - ) -> Result { - // Library paths in `link_references` keys are always stripped, so we have to strip - // user-provided paths to be able to match them correctly. - let mut libraries = libraries.with_stripped_file_prefixes(self.root.as_path()); - - let mut needed_libraries = BTreeSet::new(); - for target in targets { - self.collect_dependencies(target, &mut needed_libraries)?; - } - - let mut libs_to_deploy = Vec::new(); - - // If `libraries` does not contain needed dependency, compute its address and add to - // `libs_to_deploy`. - for id in needed_libraries { - let (lib_path, lib_name) = self.convert_artifact_id_to_lib_path(id); - - libraries.libs.entry(lib_path).or_default().entry(lib_name).or_insert_with(|| { - let address = foundry_zksync_core::compute_create_address(sender, nonce); - libs_to_deploy.push((id, address)); - nonce += 1; - - address.to_checksum(None) - }); - } - - // Link and collect bytecodes for `libs_to_deploy`. - let libs_to_deploy = self - .zk_get_linked_artifacts(libs_to_deploy.into_iter().map(|(id, _)| id), &libraries)? - .into_iter() - .map(|(_, linked)| linked.get_bytecode_bytes().unwrap().into_owned()) - .collect(); - - Ok(LinkOutput { libraries, libs_to_deploy }) - } - // TODO(zk): zk_link_with_create2 // a bit more difficult due to the lack of bytecode // until the contract is fully linked @@ -342,102 +272,6 @@ impl<'a> Linker<'a> { Ok(contract) } - /// Links given artifact with given libraries. - // TODO(zk): improve interface to reflect batching operation (all bytecodes in all bytecodes - // out) - pub fn zk_link( - contracts: &ArtifactContracts>, - target: &ArtifactId, - libraries: &Libraries, - ) -> Result, ZkLinkerError> { - let artifact_to_link_id = |id: &ArtifactId| format!("{}:{}", id.source.display(), id.name); - - // collect bytecodes & libraries for input to zksolc_link - let bytecodes = contracts - .iter() - .filter_map(|(id, bytecode)| { - let link_id = artifact_to_link_id(id); - let object = bytecode.bytecode.as_ref().map(|bc| bc.object.clone())?; - - let bytes = match object { - BytecodeObject::Bytecode(bytes) => bytes, - BytecodeObject::Unlinked(unlinked) => alloy_primitives::hex::decode(unlinked) - .expect("malformed unlinked bytecode object") - .into(), - }; - - Some((link_id, bytes)) - }) - .collect::>(); - - let libraries = libraries - .libs - .iter() - .flat_map(|(file, libs)| { - libs.iter() - .map(|(name, address)| (file.to_string_lossy(), name.clone(), address.clone())) - }) - .map(|(filename, name, address)| zk_link::Library { - filename: filename.into_owned(), - name, - address: Address::from_hex(address).unwrap(), - }) - .collect::>(); - - let zksolc = ZkSolcCompiler { - // NOTE(zk): zksolc --link --standard-json requires >1.5.8 - // FIXME(zk): compiler from config - zksolc: ZkSolc::get_path_for_version(&Version::new(1, 5, 8)).unwrap(), - solc: Default::default(), - }; - let mut link_output = - zk_link::zksolc_link(&zksolc, zk_link::LinkJsonInput { bytecodes, libraries }) - .expect("able to call zksolc --link"); // FIXME: proper error check - - let link_id = &artifact_to_link_id(target); - - let mut contract = contracts.get(target).ok_or(LinkerError::MissingTargetArtifact)?.clone(); - - if let Some(unlinked) = link_output.unlinked.remove(link_id) { - tracing::error!(factory_dependencies = ?unlinked.factory_dependencies, libraries = ?unlinked.linker_symbols, "unmet linking dependencies"); - - if !unlinked.linker_symbols.is_empty() { - return Err(ZkLinkerError::MissingLibraries( - unlinked.linker_symbols.into_iter().collect(), - )); - } - return Err(ZkLinkerError::MissingFactoryDeps( - unlinked.factory_dependencies.into_iter().collect(), - )); - } - - let linked_output = - link_output.linked.remove(link_id).or_else(|| link_output.ignored.remove(link_id)); - - // NOTE(zk): covers intermittent issue where fully linked bytecode was - // not being returned in `ignored` (or `linked`). - // The check above should catch if the bytecode remains unlinked - let Some(linked) = linked_output else { - return Ok(contract); - }; - - let mut compact_bytecode = CompactBytecode::empty(); - compact_bytecode.object = BytecodeObject::Bytecode( - alloy_primitives::hex::decode(&linked.bytecode) - .expect("malformed unlinked bytecode object") - .into(), - ); - - let mut compact_deployed_bytecode = CompactDeployedBytecode::empty(); - compact_deployed_bytecode.bytecode.replace(compact_bytecode.clone()); - - // TODO(zk): maybe return bytecode hash? - contract.bytecode.replace(std::borrow::Cow::Owned(compact_bytecode)); - contract.deployed_bytecode.replace(std::borrow::Cow::Owned(compact_deployed_bytecode)); - - Ok(contract) - } - pub fn get_linked_artifacts( &self, libraries: &Libraries, @@ -445,59 +279,6 @@ impl<'a> Linker<'a> { self.contracts.keys().map(|id| Ok((id.clone(), self.link(id, libraries)?))).collect() } - pub fn zk_get_linked_artifacts<'b>( - &self, - targets: impl IntoIterator, - libraries: &Libraries, - ) -> Result { - let mut targets = targets.into_iter().cloned().collect::>(); - let mut contracts = self.contracts.clone(); - let mut linked_artifacts = vec![]; - - // FIXME(zk): determine if this loop is still needed like this - while let Some(id) = targets.pop_front() { - match Self::zk_link(&contracts, &id, &libraries) { - Ok(linked) => { - // persist linked contract for successive iterations - *contracts.entry(id.clone()).or_default() = linked.clone(); - - linked_artifacts.push((id.clone(), CompactContractBytecode::from(linked))); - } - Err(ZkLinkerError::MissingFactoryDeps(fdeps)) => { - // attempt linking again if some factory dep remains unlinked - // this is just in the case where a previously unlinked factory dep - // is linked with the same run as `id` would be linked - // and instead `id` remains unlinked - // FIXME(zk): might be unnecessary, observed when paths were wrong - let mut ids = fdeps - .into_iter() - .inspect(|fdep| { - dbg!(&fdep); - }) - .flat_map(|fdep| { - contracts.iter().find(|(id, _)| { - id.source.as_path() == Path::new(fdep.filename.as_str()) && - &id.name == &fdep.library - }) - }) - .map(|(id, _)| id.clone()) - .peekable(); - - // if we have no dep ids then we avoid - // queueing our own id to avoid infinite loop - // TODO(zk): find a better way to avoid issues later - if let Some(_) = ids.peek() { - targets.extend(ids); // queue factory deps for linking - targets.push_back(id); // reque original target - } - } - Err(err) => return Err(err), - } - } - - Ok(linked_artifacts.into_iter().collect()) - } - pub fn get_linked_artifacts_cow( &self, libraries: &Libraries, diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs new file mode 100644 index 000000000..9d9dbf41d --- /dev/null +++ b/crates/linking/src/zksync.rs @@ -0,0 +1,244 @@ +use std::{ + collections::{BTreeSet, VecDeque}, + path::{Path, PathBuf}, +}; + +use alloy_primitives::{ + hex::FromHex, + map::{HashMap, HashSet}, + Address, +}; +use foundry_compilers::{ + artifacts::{ + BytecodeObject, CompactBytecode, CompactContractBytecode, CompactContractBytecodeCow, + CompactDeployedBytecode, Libraries, + }, + contracts::ArtifactContracts, + Artifact, ArtifactId, +}; +use foundry_zksync_compilers::{ + compilers::zksolc::ZkSolcCompiler, + link::{self as zk_link, MissingLibrary}, +}; + +use crate::{LinkOutput, Linker, LinkerError}; + +/// Errors that can occur during linking. +#[derive(Debug, thiserror::Error)] +pub enum ZkLinkerError { + #[error(transparent)] + Inner(#[from] LinkerError), + #[error("unable to fully link due to missing libraries")] + MissingLibraries(BTreeSet), + #[error("unable to fully link due to unlinked factory dependencies")] + MissingFactoryDeps(BTreeSet), +} + +#[derive(Debug)] +pub struct ZkLinker<'a> { + pub linker: Linker<'a>, + pub compiler: ZkSolcCompiler, +} + +impl<'a> ZkLinker<'a> { + pub fn new( + root: impl Into, + contracts: ArtifactContracts>, + compiler: ZkSolcCompiler, + ) -> Self { + Self { linker: Linker::new(root, contracts), compiler } + } + + /// Links given artifact with either given library addresses or address computed from sender and + /// nonce. + /// + /// Each key in `libraries` should either be a global path or relative to project root. All + /// remappings should be resolved. + /// + /// When calling for `target` being an external library itself, you should check that `target` + /// does not appear in `libs_to_deploy` to avoid deploying it twice. It may happen in cases + /// when there is a dependency cycle including `target`. + pub fn zk_link_with_nonce_or_address( + &'a self, + libraries: Libraries, + sender: Address, + mut nonce: u64, + targets: impl IntoIterator, + ) -> Result { + // Library paths in `link_references` keys are always stripped, so we have to strip + // user-provided paths to be able to match them correctly. + let mut libraries = libraries.with_stripped_file_prefixes(self.linker.root.as_path()); + + let mut needed_libraries = BTreeSet::new(); + for target in targets { + self.linker.collect_dependencies(target, &mut needed_libraries)?; + } + + let mut libs_to_deploy = Vec::new(); + + // If `libraries` does not contain needed dependency, compute its address and add to + // `libs_to_deploy`. + for id in needed_libraries { + let (lib_path, lib_name) = self.linker.convert_artifact_id_to_lib_path(id); + + libraries.libs.entry(lib_path).or_default().entry(lib_name).or_insert_with(|| { + let address = foundry_zksync_core::compute_create_address(sender, nonce); + libs_to_deploy.push((id, address)); + nonce += 1; + + address.to_checksum(None) + }); + } + + // Link and collect bytecodes for `libs_to_deploy`. + let libs_to_deploy = self + .zk_get_linked_artifacts(libs_to_deploy.into_iter().map(|(id, _)| id), &libraries)? + .into_iter() + .map(|(_, linked)| linked.get_bytecode_bytes().unwrap().into_owned()) + .collect(); + + Ok(LinkOutput { libraries, libs_to_deploy }) + } + + /// Links given artifact with given libraries. + // TODO(zk): improve interface to reflect batching operation (all bytecodes in all bytecodes + // out) + pub fn zk_link( + contracts: &ArtifactContracts>, + target: &ArtifactId, + libraries: &Libraries, + zksolc: &ZkSolcCompiler, + ) -> Result, ZkLinkerError> { + let artifact_to_link_id = |id: &ArtifactId| format!("{}:{}", id.source.display(), id.name); + + // collect bytecodes & libraries for input to zksolc_link + let bytecodes = contracts + .iter() + .filter_map(|(id, bytecode)| { + let link_id = artifact_to_link_id(id); + let object = bytecode.bytecode.as_ref().map(|bc| bc.object.clone())?; + + let bytes = match object { + BytecodeObject::Bytecode(bytes) => bytes, + BytecodeObject::Unlinked(unlinked) => alloy_primitives::hex::decode(unlinked) + .expect("malformed unlinked bytecode object") + .into(), + }; + + Some((link_id, bytes)) + }) + .collect::>(); + + let libraries = libraries + .libs + .iter() + .flat_map(|(file, libs)| { + libs.iter() + .map(|(name, address)| (file.to_string_lossy(), name.clone(), address.clone())) + }) + .map(|(filename, name, address)| zk_link::Library { + filename: filename.into_owned(), + name, + address: Address::from_hex(address).unwrap(), + }) + .collect::>(); + + let mut link_output = + zk_link::zksolc_link(zksolc, zk_link::LinkJsonInput { bytecodes, libraries }) + .expect("able to call zksolc --link"); // TODO(zk): proper error check + + let link_id = &artifact_to_link_id(target); + + let mut contract = contracts.get(target).ok_or(LinkerError::MissingTargetArtifact)?.clone(); + + if let Some(unlinked) = link_output.unlinked.remove(link_id) { + tracing::error!(factory_dependencies = ?unlinked.factory_dependencies, libraries = ?unlinked.linker_symbols, "unmet linking dependencies"); + + if !unlinked.linker_symbols.is_empty() { + return Err(ZkLinkerError::MissingLibraries( + unlinked.linker_symbols.into_iter().collect(), + )); + } + return Err(ZkLinkerError::MissingFactoryDeps( + unlinked.factory_dependencies.into_iter().collect(), + )); + } + + let linked_output = + link_output.linked.remove(link_id).or_else(|| link_output.ignored.remove(link_id)); + + // NOTE(zk): covers intermittent issue where fully linked bytecode was + // not being returned in `ignored` (or `linked`). + // The check above should catch if the bytecode remains unlinked + let Some(linked) = linked_output else { + return Ok(contract); + }; + + let mut compact_bytecode = CompactBytecode::empty(); + compact_bytecode.object = BytecodeObject::Bytecode( + alloy_primitives::hex::decode(&linked.bytecode) + .expect("malformed unlinked bytecode object") + .into(), + ); + + let mut compact_deployed_bytecode = CompactDeployedBytecode::empty(); + compact_deployed_bytecode.bytecode.replace(compact_bytecode.clone()); + + // TODO(zk): maybe return bytecode hash? + contract.bytecode.replace(std::borrow::Cow::Owned(compact_bytecode)); + contract.deployed_bytecode.replace(std::borrow::Cow::Owned(compact_deployed_bytecode)); + + Ok(contract) + } + + pub fn zk_get_linked_artifacts<'b>( + &self, + targets: impl IntoIterator, + libraries: &Libraries, + ) -> Result { + let mut targets = targets.into_iter().cloned().collect::>(); + let mut contracts = self.linker.contracts.clone(); + let mut linked_artifacts = vec![]; + + // TODO(zk): determine if this loop is still needed like this + // explanation below + while let Some(id) = targets.pop_front() { + match Self::zk_link(&contracts, &id, &libraries, &self.compiler) { + Ok(linked) => { + // persist linked contract for successive iterations + *contracts.entry(id.clone()).or_default() = linked.clone(); + + linked_artifacts.push((id.clone(), CompactContractBytecode::from(linked))); + } + Err(ZkLinkerError::MissingFactoryDeps(fdeps)) => { + // attempt linking again if some factory dep remains unlinked + // this is just in the case where a previously unlinked factory dep + // is linked with the same run as `id` would be linked + // and instead `id` remains unlinked + // TODO(zk): might be unnecessary, observed when paths were wrong + let mut ids = fdeps + .into_iter() + .flat_map(|fdep| { + contracts.iter().find(|(id, _)| { + id.source.as_path() == Path::new(fdep.filename.as_str()) && + &id.name == &fdep.library + }) + }) + .map(|(id, _)| id.clone()) + .peekable(); + + // if we have no dep ids then we avoid + // queueing our own id to avoid infinite loop + // TODO(zk): find a better way to avoid issues later + if let Some(_) = ids.peek() { + targets.extend(ids); // queue factory deps for linking + targets.push_back(id); // reque original target + } + } + Err(err) => return Err(err), + } + } + + Ok(linked_artifacts.into_iter().collect()) + } +} diff --git a/crates/strategy/zksync/src/executor.rs b/crates/strategy/zksync/src/executor.rs index 1673d38b6..86225f9b4 100644 --- a/crates/strategy/zksync/src/executor.rs +++ b/crates/strategy/zksync/src/executor.rs @@ -20,7 +20,7 @@ use foundry_evm::{ }, inspectors::InspectorStack, }; -use foundry_linking::{Linker, LinkerError, ZkLinkerError}; +use foundry_linking::{Linker, LinkerError, ZkLinker, ZkLinkerError}; use foundry_zksync_compilers::{ compilers::{artifact_output::zk::ZkArtifactOutput, zksolc::ZkSolcCompiler}, dual_compiled_contracts::{DualCompiledContract, DualCompiledContracts}, @@ -158,11 +158,18 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { let contracts = input.artifact_ids().map(|(id, v)| (id.with_stripped_file_prefixes(root), v)).collect(); - let linker = Linker::new(root, contracts); + + let Ok(zksolc) = foundry_config::zksync::config_zksolc_compiler(config) else { + tracing::error!("unable to determine zksolc compiler to be used for linking"); + // TODO(zk): better error + return Err(LinkerError::CyclicDependency); + }; + + let linker = ZkLinker::new(root, contracts, zksolc); let zk_linker_error_to_linker = |zk_error| match zk_error { ZkLinkerError::Inner(err) => err, - // FIXME: better error value + // TODO(zk): better error value ZkLinkerError::MissingLibraries(libs) => LinkerError::MissingLibraryArtifact { file: "libraries".to_string(), name: libs.len().to_string(), @@ -180,12 +187,12 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { // NOTE(zk): match with EVM nonces as we will be doing a duplex deployment for // the libs 0, - linker.contracts.keys(), + linker.linker.contracts.keys(), ) .map_err(zk_linker_error_to_linker)?; let mut linked_contracts = linker - .zk_get_linked_artifacts(linker.contracts.keys(), &libraries) + .zk_get_linked_artifacts(linker.linker.contracts.keys(), &libraries) .map_err(zk_linker_error_to_linker)?; let newly_linked_dual_compiled_contracts = linked_contracts @@ -212,10 +219,12 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { name: id.name.clone(), zk_bytecode_hash: zk_hash, zk_deployed_bytecode: zk_bytecode.to_vec(), - // FIXME: retrieve unlinked factory deps (1.5.9) + // TODO(zk): retrieve unlinked factory deps (1.5.9) zk_factory_deps: vec![zk_bytecode.to_vec()], evm_bytecode_hash: B256::from_slice(&keccak256(evm.as_ref())[..]), - evm_deployed_bytecode: evm.to_vec(), // FIXME: is this ok? not really used + // TODO(zk): determine if this is ok, as it's + // not really used in dual compiled contracts + evm_deployed_bytecode: evm.to_vec(), evm_bytecode: evm.to_vec(), }; @@ -289,7 +298,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { // entirely in EraVM let factory_deps = ctx.dual_compiled_contracts.fetch_all_factory_deps(dual_contract); - // persist existing paymaster data (needed?) + // persist existing paymaster data (TODO(zk): is this needed?) let paymaster_data = ctx.transaction_context.take().and_then(|metadata| metadata.paymaster_data); ctx.transaction_context = Some(ZkTransactionMetadata { factory_deps, paymaster_data }); From a81ee0c596e54300d5b8c1fb903a1931a46d3761 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 14 Jan 2025 11:51:32 +0100 Subject: [PATCH 15/61] chore: more lints chore: remove accidental file re-inclusion --- crates/forge/tests/it/zk/linking.rs | 2 +- crates/zksync/compiler/src/lib.rs | 230 ---------------------------- crates/zksync/compilers/src/link.rs | 2 +- 3 files changed, 2 insertions(+), 232 deletions(-) delete mode 100644 crates/zksync/compiler/src/lib.rs diff --git a/crates/forge/tests/it/zk/linking.rs b/crates/forge/tests/it/zk/linking.rs index 43c16cf4f..d3521ab0c 100644 --- a/crates/forge/tests/it/zk/linking.rs +++ b/crates/forge/tests/it/zk/linking.rs @@ -31,7 +31,7 @@ forgetest_async!( } ); -// FIXME(zk): add deploy-time linking to scripting +// TODO(zk): add deploy-time linking to scripting forgetest_async!( #[should_panic = "has no bytecode hash, as it may require library linkage"] script_zk_deploy_time_linking_fails, diff --git a/crates/zksync/compiler/src/lib.rs b/crates/zksync/compiler/src/lib.rs deleted file mode 100644 index a9448b609..000000000 --- a/crates/zksync/compiler/src/lib.rs +++ /dev/null @@ -1,230 +0,0 @@ -//! # foundry-zksync -//! -//! Main Foundry ZKSync implementation. -#![warn(missing_docs, unused_crate_dependencies)] - -/// ZKSolc specific logic. -mod zksolc; -pub use zksolc::*; - -use std::path::PathBuf; - -use foundry_config::{Config, SkipBuildFilters, SolcReq}; -use semver::Version; - -pub mod libraries; - -pub mod link; - -use foundry_compilers::{ - artifacts::Severity, - error::SolcError, - solc::{Solc, SolcCompiler, SolcLanguage}, - zksolc::{get_solc_version_info, ZkSolc, ZkSolcCompiler, ZkSolcSettings}, - zksync::artifact_output::zk::ZkArtifactOutput, - Project, ProjectBuilder, ProjectPathsConfig, -}; - -/// Filename for zksync cache -pub const ZKSYNC_SOLIDITY_FILES_CACHE_FILENAME: &str = "zksync-solidity-files-cache.json"; - -/// Directory for zksync artifacts -pub const ZKSYNC_ARTIFACTS_DIR: &str = "zkout"; - -// Config overrides to create zksync specific foundry-compilers data structures - -/// Returns the configured `zksolc` `Settings` that includes: -/// - all libraries -/// - the optimizer (including details, if configured) -/// - evm version -pub fn config_zksolc_settings(config: &Config) -> Result { - let libraries = match config.parsed_libraries() { - Ok(libs) => config.project_paths::().apply_lib_remappings(libs), - Err(e) => return Err(SolcError::msg(format!("Failed to parse libraries: {e}"))), - }; - - Ok(config.zksync.settings(libraries, config.evm_version, config.via_ir)) -} - -/// Create a new zkSync project -pub fn config_create_project( - config: &Config, - cached: bool, - no_artifacts: bool, -) -> Result, SolcError> { - let mut builder = ProjectBuilder::::default() - .artifacts(ZkArtifactOutput {}) - .paths(config_project_paths(config)) - .settings(config_zksolc_settings(config)?) - .ignore_error_codes(config.ignored_error_codes.iter().copied().map(Into::into)) - .ignore_paths(config.ignored_file_paths.clone()) - .set_compiler_severity_filter(if config.deny_warnings { - Severity::Warning - } else { - Severity::Error - }) - .set_offline(config.offline) - .set_cached(cached) - .set_build_info(!no_artifacts && config.build_info) - .set_no_artifacts(no_artifacts); - - if !config.skip.is_empty() { - let filter = SkipBuildFilters::new(config.skip.clone(), config.root.0.clone()); - builder = builder.sparse_output(filter); - } - - let zksolc = if let Some(zksolc) = - config_ensure_zksolc(config.zksync.zksolc.as_ref(), config.offline)? - { - zksolc - } else if !config.offline { - let default_version = semver::Version::new(1, 5, 7); - let mut zksolc = ZkSolc::find_installed_version(&default_version)?; - if zksolc.is_none() { - ZkSolc::blocking_install(&default_version)?; - zksolc = ZkSolc::find_installed_version(&default_version)?; - } - zksolc.unwrap_or_else(|| panic!("Could not install zksolc v{}", default_version)) - } else { - "zksolc".into() - }; - - let zksolc_compiler = ZkSolcCompiler { zksolc, solc: config_solc_compiler(config)? }; - - let project = builder.build(zksolc_compiler)?; - - if config.force { - config.cleanup(&project)?; - } - - Ok(project) -} - -/// Returns solc compiler to use along zksolc using the following rules: -/// 1. If `solc_path` in zksync config options is set, use it. -/// 2. If `solc_path` is not set, check the `solc` requirements: a. If a version is specified, use -/// zkVm solc matching that version. b. If a path is specified, use it. -/// 3. If none of the above, use autodetect which will match source files to a compiler version and -/// use zkVm solc matching that version. -fn config_solc_compiler(config: &Config) -> Result { - if let Some(path) = &config.zksync.solc_path { - if !path.is_file() { - return Err(SolcError::msg(format!("`solc` {} does not exist", path.display()))) - } - let version = get_solc_version_info(path)?.version; - let solc = - Solc::new_with_version(path, Version::new(version.major, version.minor, version.patch)); - return Ok(SolcCompiler::Specific(solc)) - } - - if let Some(ref solc) = config.solc { - let solc = match solc { - SolcReq::Version(version) => { - let solc_version_without_metadata = - format!("{}.{}.{}", version.major, version.minor, version.patch); - let maybe_solc = - ZkSolc::find_solc_installed_version(&solc_version_without_metadata)?; - let path = if let Some(solc) = maybe_solc { - solc - } else { - ZkSolc::solc_blocking_install(&solc_version_without_metadata)? - }; - Solc::new_with_version( - path, - Version::new(version.major, version.minor, version.patch), - ) - } - SolcReq::Local(path) => { - if !path.is_file() { - return Err(SolcError::msg(format!("`solc` {} does not exist", path.display()))) - } - let version = get_solc_version_info(path)?.version; - Solc::new_with_version( - path, - Version::new(version.major, version.minor, version.patch), - ) - } - }; - Ok(SolcCompiler::Specific(solc)) - } else { - Ok(SolcCompiler::AutoDetect) - } -} - -/// Returns the `ProjectPathsConfig` sub set of the config. -pub fn config_project_paths(config: &Config) -> ProjectPathsConfig { - let builder = ProjectPathsConfig::builder() - .cache(config.cache_path.join(ZKSYNC_SOLIDITY_FILES_CACHE_FILENAME)) - .sources(&config.src) - .tests(&config.test) - .scripts(&config.script) - .artifacts(config.root.0.join(ZKSYNC_ARTIFACTS_DIR)) - .libs(config.libs.iter()) - .remappings(config.get_all_remappings()) - .allowed_path(&config.root.0) - .allowed_paths(&config.libs) - .allowed_paths(&config.allow_paths) - .include_paths(&config.include_paths); - - builder.build_with_root(&config.root.0) -} - -/// Ensures that the configured version is installed if explicitly set -/// -/// If `zksolc` is [`SolcReq::Version`] then this will download and install the solc version if -/// it's missing, unless the `offline` flag is enabled, in which case an error is thrown. -/// -/// If `zksolc` is [`SolcReq::Local`] then this will ensure that the path exists. -pub fn config_ensure_zksolc( - zksolc: Option<&SolcReq>, - offline: bool, -) -> Result, SolcError> { - if let Some(ref zksolc) = zksolc { - let zksolc = match zksolc { - SolcReq::Version(version) => { - let mut zksolc = ZkSolc::find_installed_version(version)?; - if zksolc.is_none() { - if offline { - return Err(SolcError::msg(format!( - "can't install missing zksolc {version} in offline mode" - ))) - } - ZkSolc::blocking_install(version)?; - zksolc = ZkSolc::find_installed_version(version)?; - } - zksolc - } - SolcReq::Local(zksolc) => { - if !zksolc.is_file() { - return Err(SolcError::msg(format!( - "`zksolc` {} does not exist", - zksolc.display() - ))) - } - Some(zksolc.clone()) - } - }; - return Ok(zksolc) - } - - Ok(None) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn zksync_project_has_zksync_solc_when_solc_req_is_a_version() { - let config = - Config { solc: Some(SolcReq::Version(Version::new(0, 8, 26))), ..Default::default() }; - let project = config_create_project(&config, false, true).unwrap(); - let solc_compiler = project.compiler.solc; - if let SolcCompiler::Specific(path) = solc_compiler { - let version = get_solc_version_info(&path.solc).unwrap(); - assert!(version.zksync_version.is_some()); - } else { - panic!("Expected SolcCompiler::Specific"); - } - } -} diff --git a/crates/zksync/compilers/src/link.rs b/crates/zksync/compilers/src/link.rs index 90a9da540..78020b940 100644 --- a/crates/zksync/compilers/src/link.rs +++ b/crates/zksync/compilers/src/link.rs @@ -46,7 +46,7 @@ pub struct LinkJsonInput { /// Representation of a linked object given by zksolc #[derive(Debug, Clone, Deserialize)] pub struct LinkedObject { - // FIXME: obtain factoryDeps from output + // TODO(zk): obtain factoryDeps from output // might come in handy to have the libraries used as well /// Fully linked bytecode pub bytecode: String, From 2152648df9b6c7f19d8ecfe76922cfb4c6e4415f Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 14 Jan 2025 12:03:21 +0100 Subject: [PATCH 16/61] feat(zk:link): version check --- crates/strategy/zksync/src/executor.rs | 26 ++++++++++++++----- .../compilers/src/compilers/zksolc/mod.rs | 5 ++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/crates/strategy/zksync/src/executor.rs b/crates/strategy/zksync/src/executor.rs index 86225f9b4..b1b43be85 100644 --- a/crates/strategy/zksync/src/executor.rs +++ b/crates/strategy/zksync/src/executor.rs @@ -4,9 +4,15 @@ use alloy_primitives::{keccak256, Address, Bytes, TxKind, B256, U256}; use alloy_rpc_types::serde_helpers::OtherFields; use alloy_zksync::provider::{zksync_provider, ZksyncProvider}; use eyre::Result; +use revm::{ + primitives::{CreateScheme, Env, EnvWithHandlerCfg, ResultAndState}, + Database, +}; +use semver::VersionReq; +use zksync_types::H256; use foundry_common::ContractsByArtifact; -use foundry_compilers::{contracts::ArtifactContracts, Artifact, ProjectCompileOutput}; +use foundry_compilers::{Artifact, ProjectCompileOutput}; use foundry_config::Config; use foundry_evm::{ backend::{Backend, BackendResult, CowBackend}, @@ -20,7 +26,7 @@ use foundry_evm::{ }, inspectors::InspectorStack, }; -use foundry_linking::{Linker, LinkerError, ZkLinker, ZkLinkerError}; +use foundry_linking::{LinkerError, ZkLinker, ZkLinkerError}; use foundry_zksync_compilers::{ compilers::{artifact_output::zk::ZkArtifactOutput, zksolc::ZkSolcCompiler}, dual_compiled_contracts::{DualCompiledContract, DualCompiledContracts}, @@ -29,11 +35,6 @@ use foundry_zksync_core::{ encode_create_params, hash_bytecode, vm::ZkEnv, ZkTransactionMetadata, ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY, }; -use revm::{ - primitives::{CreateScheme, Env, EnvWithHandlerCfg, ResultAndState}, - Database, -}; -use zksync_types::H256; use crate::{ backend::{ZksyncBackendStrategyBuilder, ZksyncInspectContext}, @@ -165,6 +166,17 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { return Err(LinkerError::CyclicDependency); }; + // TODO(zk): better error + zksolc.version().map_err(|_| LinkerError::CyclicDependency).and_then(|version| { + let version_req = VersionReq::parse(">=1.5.8").unwrap(); + if !version_req.matches(&version) { + tracing::error!(?version, "linking requires zksolc >= v1.5.8"); + Err(LinkerError::CyclicDependency) + } else { + Ok(()) + } + })?; + let linker = ZkLinker::new(root, contracts, zksolc); let zk_linker_error_to_linker = |zk_error| match zk_error { diff --git a/crates/zksync/compilers/src/compilers/zksolc/mod.rs b/crates/zksync/compilers/src/compilers/zksolc/mod.rs index 865b36a59..691639443 100644 --- a/crates/zksync/compilers/src/compilers/zksolc/mod.rs +++ b/crates/zksync/compilers/src/compilers/zksolc/mod.rs @@ -244,6 +244,11 @@ impl ZkSolcCompiler { Ok(zksolc) } + + /// Retrieve the version of the specified `zksolc` + pub fn version(&self) -> Result { + ZkSolc::get_version_for_path(self.zksolc.as_ref()) + } } /// Version metadata. Will include `zksync_version` if compiler is zksync solc. From 71b78bdc82727232d53076013603c8b50d70c94f Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 14 Jan 2025 12:45:15 +0100 Subject: [PATCH 17/61] chore: lints & fmt --- crates/config/src/zksync.rs | 12 ++++++------ crates/evm/evm/src/executors/strategy.rs | 4 ++-- crates/forge/src/runner.rs | 2 +- crates/forge/tests/it/zk/linking.rs | 2 +- crates/linking/src/zksync.rs | 6 +++--- .../src/cheatcode/runner/cheatcode_handlers.rs | 2 +- crates/strategy/zksync/src/cheatcode/runner/mod.rs | 12 ++++++------ crates/strategy/zksync/src/executor/runner.rs | 2 +- .../compilers/src/dual_compiled_contracts.rs | 2 +- crates/zksync/compilers/src/link.rs | 14 +++++++------- crates/zksync/core/src/lib.rs | 5 ++--- 11 files changed, 31 insertions(+), 32 deletions(-) diff --git a/crates/config/src/zksync.rs b/crates/config/src/zksync.rs index 2cce858f5..adc14e9c9 100644 --- a/crates/config/src/zksync.rs +++ b/crates/config/src/zksync.rs @@ -237,12 +237,12 @@ pub fn config_create_project( fn config_solc_compiler(config: &Config) -> Result { if let Some(path) = &config.zksync.solc_path { if !path.is_file() { - return Err(SolcError::msg(format!("`solc` {} does not exist", path.display()))) + return Err(SolcError::msg(format!("`solc` {} does not exist", path.display()))); } let version = get_solc_version_info(path)?.version; let solc = Solc::new_with_version(path, Version::new(version.major, version.minor, version.patch)); - return Ok(SolcCompiler::Specific(solc)) + return Ok(SolcCompiler::Specific(solc)); } if let Some(ref solc) = config.solc { @@ -264,7 +264,7 @@ fn config_solc_compiler(config: &Config) -> Result { } SolcReq::Local(path) => { if !path.is_file() { - return Err(SolcError::msg(format!("`solc` {} does not exist", path.display()))) + return Err(SolcError::msg(format!("`solc` {} does not exist", path.display()))); } let version = get_solc_version_info(path)?.version; Solc::new_with_version( @@ -315,7 +315,7 @@ pub fn config_ensure_zksolc( if offline { return Err(SolcError::msg(format!( "can't install missing zksolc {version} in offline mode" - ))) + ))); } ZkSolc::blocking_install(version)?; zksolc = ZkSolc::find_installed_version(version)?; @@ -327,12 +327,12 @@ pub fn config_ensure_zksolc( return Err(SolcError::msg(format!( "`zksolc` {} does not exist", zksolc.display() - ))) + ))); } Some(zksolc.clone()) } }; - return Ok(zksolc) + return Ok(zksolc); } Ok(None) diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index 5747e0349..0d4ab07af 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -256,8 +256,8 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { let Some(abi) = &contract.abi else { continue }; // if it's a test, link it and add to deployable contracts - if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) - && abi.functions().any(|func| func.name.is_any_test()) + if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && + abi.functions().any(|func| func.name.is_any_test()) { let Some(bytecode) = contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty()) diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 4af7e45c5..093b0fe09 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -133,7 +133,7 @@ impl<'a> ContractRunner<'a> { let deployments = match deploy_result { Err(err) => vec![Err(err)], - Ok(deployments) => deployments.into_iter().map(|d| Ok(d)).collect(), + Ok(deployments) => deployments.into_iter().map(Ok).collect(), }; for deploy_result in deployments { diff --git a/crates/forge/tests/it/zk/linking.rs b/crates/forge/tests/it/zk/linking.rs index b9a67aae3..dc9f8568d 100644 --- a/crates/forge/tests/it/zk/linking.rs +++ b/crates/forge/tests/it/zk/linking.rs @@ -27,7 +27,7 @@ forgetest_async!( None, 1, Some(&["-vvvvv"]), - ); + ).await; } ); diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index 9d9dbf41d..4ff4d645c 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -203,7 +203,7 @@ impl<'a> ZkLinker<'a> { // TODO(zk): determine if this loop is still needed like this // explanation below while let Some(id) = targets.pop_front() { - match Self::zk_link(&contracts, &id, &libraries, &self.compiler) { + match Self::zk_link(&contracts, &id, libraries, &self.compiler) { Ok(linked) => { // persist linked contract for successive iterations *contracts.entry(id.clone()).or_default() = linked.clone(); @@ -221,7 +221,7 @@ impl<'a> ZkLinker<'a> { .flat_map(|fdep| { contracts.iter().find(|(id, _)| { id.source.as_path() == Path::new(fdep.filename.as_str()) && - &id.name == &fdep.library + id.name == fdep.library }) }) .map(|(id, _)| id.clone()) @@ -230,7 +230,7 @@ impl<'a> ZkLinker<'a> { // if we have no dep ids then we avoid // queueing our own id to avoid infinite loop // TODO(zk): find a better way to avoid issues later - if let Some(_) = ids.peek() { + if ids.peek().is_some() { targets.extend(ids); // queue factory deps for linking targets.push_back(id); // reque original target } diff --git a/crates/strategy/zksync/src/cheatcode/runner/cheatcode_handlers.rs b/crates/strategy/zksync/src/cheatcode/runner/cheatcode_handlers.rs index ffd99282d..73e640e7f 100644 --- a/crates/strategy/zksync/src/cheatcode/runner/cheatcode_handlers.rs +++ b/crates/strategy/zksync/src/cheatcode/runner/cheatcode_handlers.rs @@ -210,7 +210,7 @@ impl ZksyncCheatcodeInspectorStrategyRunner { name = existing.name, "contract already exists with the given bytecode hashes" ); - return Ok(Default::default()) + return Ok(Default::default()); } ctx.dual_compiled_contracts.push(new_contract); diff --git a/crates/strategy/zksync/src/cheatcode/runner/mod.rs b/crates/strategy/zksync/src/cheatcode/runner/mod.rs index b90ed80f8..51e0a3bd2 100644 --- a/crates/strategy/zksync/src/cheatcode/runner/mod.rs +++ b/crates/strategy/zksync/src/cheatcode/runner/mod.rs @@ -436,7 +436,7 @@ impl CheatcodeInspectorStrategyExt for ZksyncCheatcodeInspectorStrategyRunner { ctx.skip_zk_vm = false; // handled the skip, reset flag ctx.record_next_create_address = true; info!("running create in EVM, instead of zkEVM (skipped)"); - return None + return None; } if let Some(CreateScheme::Create) = input.scheme() { @@ -451,14 +451,14 @@ impl CheatcodeInspectorStrategyExt for ZksyncCheatcodeInspectorStrategyRunner { let address = caller.create(nonce); if ecx.db.get_test_contract_address().map(|addr| address == addr).unwrap_or_default() { info!("running create in EVM, instead of zkEVM (Test Contract) {:#?}", address); - return None + return None; } } let init_code = input.init_code(); if init_code.0 == DEFAULT_CREATE2_DEPLOYER_CODE { info!("running create in EVM, instead of zkEVM (DEFAULT_CREATE2_DEPLOYER_CODE)"); - return None + return None; } info!("running create in zkEVM"); @@ -679,7 +679,7 @@ impl CheatcodeInspectorStrategyExt for ZksyncCheatcodeInspectorStrategyRunner { "running call in EVM, instead of zkEVM (Test Contract) {:#?}", call.bytecode_address ); - return None + return None; } info!("running call in zkEVM {:#?}", call); @@ -833,7 +833,7 @@ impl ZksyncCheatcodeInspectorStrategyRunner { ) { if !ctx.using_zk_vm { tracing::info!("already in EVM"); - return + return; } tracing::info!("switching to EVM"); @@ -910,7 +910,7 @@ impl ZksyncCheatcodeInspectorStrategyRunner { ) { if ctx.using_zk_vm { tracing::info!("already in ZK-VM"); - return + return; } tracing::info!("switching to ZK-VM"); diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 5eb4272fb..8cf997428 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -264,7 +264,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { from, code.clone(), value, - rd.clone(), + rd, )?; let ctx = get_context(executor.strategy.context.as_mut()); diff --git a/crates/zksync/compilers/src/dual_compiled_contracts.rs b/crates/zksync/compilers/src/dual_compiled_contracts.rs index 4caaab7dc..998957c2e 100644 --- a/crates/zksync/compilers/src/dual_compiled_contracts.rs +++ b/crates/zksync/compilers/src/dual_compiled_contracts.rs @@ -308,7 +308,7 @@ impl DualCompiledContracts { /// Extend the inner set of contracts with the given iterator pub fn extend(&mut self, iter: impl IntoIterator) { - self.contracts.extend(iter.into_iter()); + self.contracts.extend(iter); self.contracts.sort_by(|a, b| a.name.cmp(&b.name)); self.contracts.dedup_by(|a, b| a.name == b.name); } diff --git a/crates/zksync/compilers/src/link.rs b/crates/zksync/compilers/src/link.rs index 78020b940..05d84c5e7 100644 --- a/crates/zksync/compilers/src/link.rs +++ b/crates/zksync/compilers/src/link.rs @@ -28,9 +28,9 @@ pub struct Library { pub address: Address, } -impl Into for Library { - fn into(self) -> String { - format!("{}:{}={}", self.filename, self.name, self.address) +impl From for String { + fn from(val: Library) -> Self { + format!("{}:{}={}", val.filename, val.name, val.address) } } @@ -101,7 +101,7 @@ pub struct LinkJsonOutput { pub ignored: HashMap, } -// taken fom compilers +// taken from compilers fn map_io_err(zksolc_path: &Path) -> impl FnOnce(std::io::Error) -> SolcError + '_ { move |err| SolcError::io(err, zksolc_path) } @@ -113,7 +113,7 @@ pub fn zksolc_link( input: LinkJsonInput, ) -> Result { let zksolc = &zksolc.zksolc; - let mut cmd = Command::new(&zksolc); + let mut cmd = Command::new(zksolc); cmd.arg("--standard-json") .arg("--link") @@ -121,12 +121,12 @@ pub fn zksolc_link( .stderr(Stdio::piped()) .stdout(Stdio::piped()); - let mut child = cmd.spawn().map_err(map_io_err(&zksolc))?; + let mut child = cmd.spawn().map_err(map_io_err(zksolc))?; let stdin = child.stdin.as_mut().unwrap(); let _ = serde_json::to_writer(stdin, &input); - let output = child.wait_with_output().map_err(map_io_err(&zksolc))?; + let output = child.wait_with_output().map_err(map_io_err(zksolc))?; tracing::trace!(?output); if output.status.success() { diff --git a/crates/zksync/core/src/lib.rs b/crates/zksync/core/src/lib.rs index 1146151dd..a450b45f5 100644 --- a/crates/zksync/core/src/lib.rs +++ b/crates/zksync/core/src/lib.rs @@ -29,10 +29,9 @@ use convert::{ConvertAddress, ConvertH160, ConvertH256, ConvertRU256, ConvertU25 use eyre::eyre; use revm::{Database, InnerEvmContext}; use serde::{Deserialize, Serialize}; -use zksync_multivm::vm_m6::test_utils::get_create_zksync_address; -use zksync_types::Nonce; use std::fmt::Debug; -use zksync_types::bytecode::BytecodeHash; +use zksync_multivm::vm_m6::test_utils::get_create_zksync_address; +use zksync_types::{bytecode::BytecodeHash, Nonce}; pub use utils::{fix_l2_gas_limit, fix_l2_gas_price}; pub use vm::{balance, encode_create_params, nonce}; From 14b2b0a9974c36334f2630a4dbb1f0886274e523 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 14 Jan 2025 15:52:14 +0100 Subject: [PATCH 18/61] chore: more formatting --- crates/forge/src/multi_runner.rs | 4 ++-- crates/forge/tests/it/zk/linking.rs | 3 ++- crates/strategy/zksync/src/executor/runner.rs | 9 ++------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 8205c6d58..e603a3ad3 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -540,8 +540,8 @@ impl MultiContractRunnerBuilder { } pub fn matches_contract(id: &ArtifactId, abi: &JsonAbi, filter: &dyn TestFilter) -> bool { - (filter.matches_path(&id.source) && filter.matches_contract(&id.name)) - && abi.functions().any(|func| is_matching_test(func, filter)) + (filter.matches_path(&id.source) && filter.matches_contract(&id.name)) && + abi.functions().any(|func| is_matching_test(func, filter)) } /// Returns `true` if the function is a test function that matches the given filter. diff --git a/crates/forge/tests/it/zk/linking.rs b/crates/forge/tests/it/zk/linking.rs index dc9f8568d..4a635d1ca 100644 --- a/crates/forge/tests/it/zk/linking.rs +++ b/crates/forge/tests/it/zk/linking.rs @@ -27,7 +27,8 @@ forgetest_async!( None, 1, Some(&["-vvvvv"]), - ).await; + ) + .await; } ); diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 8cf997428..8c3b8ed84 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -259,13 +259,8 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { self.set_balance(executor, from, balance).map_err(|err| eyre::eyre!(err))?; tracing::debug!(?nonce, ?balance, sender = ?from, "deploying lib in EraVM"); - let mut evm_deployment = EvmExecutorStrategyRunner.deploy_library( - executor, - from, - code.clone(), - value, - rd, - )?; + let mut evm_deployment = + EvmExecutorStrategyRunner.deploy_library(executor, from, code.clone(), value, rd)?; let ctx = get_context(executor.strategy.context.as_mut()); From 51590b936536d1254940ebe5371e08a0a78f3c37 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 14 Jan 2025 17:23:07 +0100 Subject: [PATCH 19/61] fix(zk:link): retrieve factory dep hash refactor(zk:link): avoid attempting to link fully-linked contracts --- crates/linking/src/zksync.rs | 1 + crates/strategy/zksync/src/executor/runner.rs | 61 ++++++++++++++----- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index 4ff4d645c..5b0de95d4 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -210,6 +210,7 @@ impl<'a> ZkLinker<'a> { linked_artifacts.push((id.clone(), CompactContractBytecode::from(linked))); } + // contract was ignored, no need to add it to the list of linked contracts Err(ZkLinkerError::MissingFactoryDeps(fdeps)) => { // attempt linking again if some factory dep remains unlinked // this is just in the case where a previously unlinked factory dep diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 8c3b8ed84..2aa8a20d2 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -13,7 +13,10 @@ use semver::VersionReq; use zksync_types::H256; use foundry_common::ContractsByArtifact; -use foundry_compilers::{Artifact, ProjectCompileOutput}; +use foundry_compilers::{ + artifacts::CompactContractBytecodeCow, contracts::ArtifactContracts, Artifact, + ProjectCompileOutput, +}; use foundry_config::Config; use foundry_evm::{ backend::{Backend, BackendResult, CowBackend}, @@ -132,7 +135,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { return Err(LinkerError::MissingTargetArtifact); }; - let contracts = + let contracts: ArtifactContracts> = input.artifact_ids().map(|(id, v)| (id.with_stripped_file_prefixes(root), v)).collect(); let Ok(zksolc) = foundry_config::zksync::config_zksolc_compiler(config) else { @@ -152,7 +155,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { } })?; - let linker = ZkLinker::new(root, contracts, zksolc); + let linker = ZkLinker::new(root, contracts.clone(), zksolc); let zk_linker_error_to_linker = |zk_error| match zk_error { ZkLinkerError::Inner(err) => err, @@ -178,8 +181,24 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { ) .map_err(zk_linker_error_to_linker)?; - let mut linked_contracts = linker - .zk_get_linked_artifacts(linker.linker.contracts.keys(), &libraries) + let linked_contracts = linker + .zk_get_linked_artifacts( + linker + .linker + .contracts + .iter() + // we add this filter to avoid linking contracts that don't need linking + .filter(|(_, art)| { + // NOTE(zk): no need to check `deployed_bytecode` + // as those `link_references` would be the same as `bytecode` + art.bytecode + .as_ref() + .map(|bc| !bc.link_references.is_empty()) + .unwrap_or_default() + }) + .map(|(id, _)| id), + &libraries, + ) .map_err(zk_linker_error_to_linker)?; let newly_linked_dual_compiled_contracts = linked_contracts @@ -193,15 +212,17 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { }) .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) .map(|(id, linked_zk, evm)| { - let (_, unlinked_zk_artifact) = input + let (unlinked_id, unlinked_zk_artifact) = input .artifact_ids() .find(|(contract_id, _)| { contract_id.clone().with_stripped_file_prefixes(root) == id.clone() }) - .unwrap(); - let zk_bytecode = linked_zk.get_bytecode_bytes().unwrap(); + .expect("unable to find original (pre-linking) artifact"); + dbg!(unlinked_id, &unlinked_zk_artifact); + let zk_bytecode = + linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); let zk_hash = hash_bytecode(&zk_bytecode); - let evm = evm.get_bytecode_bytes().unwrap(); + let evm = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); let contract = DualCompiledContract { name: id.name.clone(), zk_bytecode_hash: zk_hash, @@ -218,8 +239,12 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { // populate factory deps that were already linked ctx.dual_compiled_contracts.extend_factory_deps_by_hash( contract, - unlinked_zk_artifact.factory_dependencies.iter().flatten().map(|(_, hash)| { - H256::from_slice(alloy_primitives::hex::decode(hash).unwrap().as_slice()) + unlinked_zk_artifact.factory_dependencies.iter().flatten().map(|(hash, _)| { + H256::from_slice( + alloy_primitives::hex::decode(dbg!(hash)) + .expect("malformed factory dep hash") + .as_slice(), + ) }), ) }); @@ -227,10 +252,16 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { ctx.dual_compiled_contracts .extend(newly_linked_dual_compiled_contracts.collect::>()); - // Extend zk contracts with solc contracts as well. This is required for traces to - // accurately detect contract names deployed in EVM mode, and when using - // `vm.zkVmSkip()` cheatcode. - linked_contracts.extend(evm_link.linked_contracts); + let linked_contracts: ArtifactContracts = contracts + .into_iter() + .map(|(id, art)| (id, foundry_compilers::artifacts::CompactContractBytecode::from(art))) + // Extend original zk contracts with newly linked ones + .chain(linked_contracts) + // Extend zk contracts with solc contracts as well. This is required for traces to + // accurately detect contract names deployed in EVM mode, and when using + // `vm.zkVmSkip()` cheatcode. + .chain(evm_link.linked_contracts) + .collect(); Ok(LinkOutput { deployable_contracts: evm_link.deployable_contracts, From ef6b750b3aa253b7cc156133d251faf55e206292 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 14 Jan 2025 17:27:57 +0100 Subject: [PATCH 20/61] fix(zk:compilers): remove dead `libraries` module --- crates/zksync/compilers/Cargo.toml | 1 - crates/zksync/compilers/src/lib.rs | 1 - crates/zksync/compilers/src/libraries.rs | 129 ----------------------- 3 files changed, 131 deletions(-) delete mode 100644 crates/zksync/compilers/src/libraries.rs diff --git a/crates/zksync/compilers/Cargo.toml b/crates/zksync/compilers/Cargo.toml index 40c345eb5..550df7b11 100644 --- a/crates/zksync/compilers/Cargo.toml +++ b/crates/zksync/compilers/Cargo.toml @@ -21,7 +21,6 @@ serde_json.workspace = true serde.workspace = true semver.workspace = true itertools.workspace = true -eyre.workspace = true walkdir.workspace = true reqwest.workspace = true yansi.workspace = true diff --git a/crates/zksync/compilers/src/lib.rs b/crates/zksync/compilers/src/lib.rs index 952bd49f2..4fd584b52 100644 --- a/crates/zksync/compilers/src/lib.rs +++ b/crates/zksync/compilers/src/lib.rs @@ -6,7 +6,6 @@ pub mod artifacts; pub mod compilers; pub mod dual_compiled_contracts; -pub mod libraries; pub mod link; // TODO: Used in integration tests. diff --git a/crates/zksync/compilers/src/libraries.rs b/crates/zksync/compilers/src/libraries.rs deleted file mode 100644 index b3db8823c..000000000 --- a/crates/zksync/compilers/src/libraries.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! Handles resolution and storage of missing libraries emitted by zksolc - -use std::{ - fs, - io::Write, - path::{Path, PathBuf}, -}; - -use serde::{Deserialize, Serialize}; -use tracing::{trace, warn}; - -use foundry_compilers::info::ContractInfo; - -/// Missing Library entry -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct ZkMissingLibrary { - /// Contract name - pub contract_name: String, - /// Contract path - pub contract_path: String, - /// Missing Libraries - pub missing_libraries: Vec, -} - -/// Return the missing libraries cache path -pub(crate) fn get_missing_libraries_cache_path(project_root: impl AsRef) -> PathBuf { - project_root.as_ref().join(".zksolc-libraries-cache/missing_library_dependencies.json") -} - -/// Add libraries to missing libraries cache -pub fn add_dependencies_to_missing_libraries_cache( - project_root: impl AsRef, - libraries: &[ZkMissingLibrary], -) -> eyre::Result<()> { - let file_path = get_missing_libraries_cache_path(project_root); - fs::create_dir_all(file_path.parent().unwrap()).unwrap(); - fs::File::create(file_path)? - .write_all(serde_json::to_string_pretty(libraries).unwrap().as_bytes())?; - Ok(()) -} - -/// Returns the detected missing libraries from previous compilation -pub fn get_detected_missing_libraries( - project_root: impl AsRef, -) -> eyre::Result> { - let library_paths = get_missing_libraries_cache_path(project_root); - if !library_paths.exists() { - eyre::bail!("No missing libraries found"); - } - - Ok(serde_json::from_reader(fs::File::open(&library_paths)?)?) -} - -/// Performs cleanup of cached missing libraries -pub fn cleanup_detected_missing_libraries(project_root: impl AsRef) -> eyre::Result<()> { - fs::remove_file(get_missing_libraries_cache_path(project_root))?; - Ok(()) -} - -/// Retrieve ordered list of libraries to deploy -/// -/// Libraries are grouped in batches, where the next batch -/// may have dependencies on the previous one, thus -/// it's recommended to build & deploy one batch before moving onto the next -pub fn resolve_libraries( - mut missing_libraries: Vec, - already_deployed_libraries: &[ContractInfo], -) -> eyre::Result>> { - trace!(?missing_libraries, ?already_deployed_libraries, "filtering out missing libraries"); - missing_libraries.retain(|lib| { - !already_deployed_libraries.contains(&ContractInfo { - path: Some(lib.contract_path.to_string()), - name: lib.contract_name.to_string(), - }) - }); - - let mut batches = Vec::new(); - loop { - if missing_libraries.is_empty() { - break Ok(batches); - } - - let mut batch = Vec::new(); - loop { - // find library with no further dependencies - let Some(next_lib) = missing_libraries - .iter() - .enumerate() - .find(|(_, lib)| lib.missing_libraries.is_empty()) - .map(|(i, _)| i) - .map(|i| missing_libraries.remove(i)) - else { - // no such library, and we didn't collect any library already - if batch.is_empty() { - warn!( - ?missing_libraries, - ?batches, - "unable to find library ready to be deployed" - ); - //TODO: determine if this error message is accurate - eyre::bail!("Library dependency cycle detected"); - } - - break; - }; - - let info = - ContractInfo { path: Some(next_lib.contract_path), name: next_lib.contract_name }; - batch.push(info); - } - - // remove this batch from each library's missing_library if listed as dependency - // this potentially allows more libraries to be included in the next batch - for lib in &mut missing_libraries { - lib.missing_libraries.retain(|maybe_missing_lib| { - let mut split = maybe_missing_lib.split(':'); - let lib_path = split.next().unwrap(); - let lib_name = split.next().unwrap(); - - !batch.contains(&ContractInfo { - path: Some(lib_path.to_string()), - name: lib_name.to_string(), - }) - }) - } - - batches.push(batch); - } -} From 119b94dc759b47dc9fc45b19fe007d166f09b61c Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 14 Jan 2025 21:52:16 +0100 Subject: [PATCH 21/61] feat(link:zk): create2 linking feat(script:zk): link with zksolc --- Cargo.lock | 1 - crates/linking/src/lib.rs | 3 - crates/linking/src/zksync.rs | 145 ++++++++- crates/script/src/build.rs | 285 ++++++++++++++---- crates/strategy/zksync/src/executor/runner.rs | 5 +- crates/zksync/core/src/lib.rs | 17 +- 6 files changed, 386 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f497fe57b..9a73f17ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5163,7 +5163,6 @@ dependencies = [ "alloy-json-abi", "alloy-primitives", "dirs 5.0.1", - "eyre", "fd-lock", "foundry-compilers", "foundry-compilers-artifacts-solc", diff --git a/crates/linking/src/lib.rs b/crates/linking/src/lib.rs index 127fcc4a7..aaf40285c 100644 --- a/crates/linking/src/lib.rs +++ b/crates/linking/src/lib.rs @@ -185,9 +185,6 @@ impl<'a> Linker<'a> { Ok(LinkOutput { libraries, libs_to_deploy }) } - // TODO(zk): zk_link_with_create2 - // a bit more difficult due to the lack of bytecode - // until the contract is fully linked pub fn link_with_create2( &'a self, libraries: Libraries, diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index 5b0de95d4..8f71adc34 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -1,12 +1,12 @@ use std::{ - collections::{BTreeSet, VecDeque}, + collections::{BTreeMap, BTreeSet, VecDeque}, path::{Path, PathBuf}, }; use alloy_primitives::{ hex::FromHex, map::{HashMap, HashSet}, - Address, + Address, B256, }; use foundry_compilers::{ artifacts::{ @@ -14,12 +14,16 @@ use foundry_compilers::{ CompactDeployedBytecode, Libraries, }, contracts::ArtifactContracts, - Artifact, ArtifactId, + Artifact, ArtifactId, ProjectCompileOutput, }; use foundry_zksync_compilers::{ - compilers::zksolc::ZkSolcCompiler, + compilers::{ + artifact_output::zk::{ZkArtifactOutput, ZkContractArtifact}, + zksolc::ZkSolcCompiler, + }, link::{self as zk_link, MissingLibrary}, }; +use foundry_zksync_core::hash_bytecode; use crate::{LinkOutput, Linker, LinkerError}; @@ -38,6 +42,7 @@ pub enum ZkLinkerError { pub struct ZkLinker<'a> { pub linker: Linker<'a>, pub compiler: ZkSolcCompiler, + pub compiler_output: &'a ProjectCompileOutput, } impl<'a> ZkLinker<'a> { @@ -45,8 +50,50 @@ impl<'a> ZkLinker<'a> { root: impl Into, contracts: ArtifactContracts>, compiler: ZkSolcCompiler, + compiler_output: &'a ProjectCompileOutput, ) -> Self { - Self { linker: Linker::new(root, contracts), compiler } + Self { linker: Linker::new(root, contracts), compiler, compiler_output } + } + + /// Performs DFS on the graph of link references, and populates `deps` with all found libraries, including ones of factory deps. + fn zk_collect_dependencies( + &'a self, + target: &'a ArtifactId, + deps: &mut BTreeSet<&'a ArtifactId>, + ) -> Result<(), LinkerError> { + let (_, artifact) = self + .compiler_output + .artifact_ids() + .find(|(id, _)| { + let id = id.clone().with_stripped_file_prefixes(self.linker.root.as_ref()); + id.source == target.source && id.name == target.name + }) + .ok_or(LinkerError::MissingTargetArtifact)?; + + let mut references = BTreeMap::new(); + if let Some(bytecode) = &artifact.bytecode { + references.extend(bytecode.link_references()); + } + + // TODO(zk): should instead use unlinked factory dependencies + // zksolc 1.5.9 + + for (file, libs) in &references { + for contract in libs.keys() { + let id = self + .linker + .find_artifact_id_by_library_path(file, contract, Some(&target.version)) + .ok_or_else(|| LinkerError::MissingLibraryArtifact { + file: file.to_string(), + name: contract.to_string(), + })?; + if deps.insert(id) { + self.zk_collect_dependencies(id, deps)?; + } + } + } + + Ok(()) } /// Links given artifact with either given library addresses or address computed from sender and @@ -71,7 +118,7 @@ impl<'a> ZkLinker<'a> { let mut needed_libraries = BTreeSet::new(); for target in targets { - self.linker.collect_dependencies(target, &mut needed_libraries)?; + self.zk_collect_dependencies(target, &mut needed_libraries)?; } let mut libs_to_deploy = Vec::new(); @@ -100,6 +147,88 @@ impl<'a> ZkLinker<'a> { Ok(LinkOutput { libraries, libs_to_deploy }) } + pub fn zk_link_with_create2( + &'a self, + libraries: Libraries, + sender: Address, + salt: B256, + target: &'a ArtifactId, + ) -> Result { + // Library paths in `link_references` keys are always stripped, so we have to strip + // user-provided paths to be able to match them correctly. + let mut libraries = libraries.with_stripped_file_prefixes(self.linker.root.as_path()); + + let mut contracts = self.linker.contracts.clone(); + + let mut needed_libraries = BTreeSet::new(); + self.zk_collect_dependencies(target, &mut needed_libraries)?; + + let attempt_link = |contracts: &mut ArtifactContracts>, + id, + libraries: &Libraries, + zksolc| { + let original = contracts.get(id).expect("library present in list of contracts"); + // Link library with provided libs and extract bytecode object (possibly unlinked). + match Self::zk_link(&contracts, id, libraries, zksolc) { + Ok(linked) => { + // persist linked contract for successive iterations + *contracts.entry(id.clone()).or_default() = linked.clone(); + linked.bytecode.expect("library should have bytecode") + } + // the library remains unlinked at this time + Err(_) => original.bytecode.as_ref().expect("library should have bytecode").clone(), + } + }; + + let mut needed_libraries = needed_libraries + .into_iter() + .filter(|id| { + // Filter out already provided libraries. + let (file, name) = self.linker.convert_artifact_id_to_lib_path(id); + !libraries.libs.contains_key(&file) || !libraries.libs[&file].contains_key(&name) + }) + .map(|id| (id, attempt_link(&mut contracts, id, &libraries, &self.compiler))) + .collect::>(); + + let mut libs_to_deploy = Vec::new(); + + // Iteratively compute addresses and link libraries until we have no unlinked libraries + // left. + while !needed_libraries.is_empty() { + // Find any library which is fully linked. + let deployable = needed_libraries + .iter() + .enumerate() + .find(|(_, (_, bytecode))| !bytecode.object.is_unlinked()); + + // If we haven't found any deployable library, it means we have a cyclic dependency. + let Some((index, &(id, _))) = deployable else { + return Err(LinkerError::CyclicDependency); + }; + let (_, library_bytecode) = needed_libraries.swap_remove(index); + + let code = library_bytecode.bytes().expect("fully linked bytecode"); + let bytecode_hash = hash_bytecode(code); + + let address = + foundry_zksync_core::compute_create2_address(sender, bytecode_hash, salt, &[]); + + let (file, name) = self.linker.convert_artifact_id_to_lib_path(id); + + // NOTE(zk): doesn't really matter since we use the EVM + // bytecode to determine what EraVM bytecode to deploy + dbg!(&file, &name, &address); + libs_to_deploy.push(code.clone()); + libraries.libs.entry(file).or_default().insert(name, address.to_checksum(None)); + + for (id, bytecode) in &mut needed_libraries { + *bytecode = attempt_link(&mut contracts, id, &libraries, &self.compiler) + } + } + + Ok(LinkOutput { libraries, libs_to_deploy }) + } + /// Links given artifact with given libraries. // TODO(zk): improve interface to reflect batching operation (all bytecodes in all bytecodes // out) @@ -221,8 +350,8 @@ impl<'a> ZkLinker<'a> { .into_iter() .flat_map(|fdep| { contracts.iter().find(|(id, _)| { - id.source.as_path() == Path::new(fdep.filename.as_str()) && - id.name == fdep.library + id.source.as_path() == Path::new(fdep.filename.as_str()) + && id.name == fdep.library }) }) .map(|(id, _)| id.clone()) diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index b2a3775f8..e6652d29e 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -2,32 +2,32 @@ use crate::{ broadcast::BundledState, execute::LinkedState, multi_sequence::MultiChainSequence, sequence::ScriptSequenceKind, ScriptArgs, ScriptConfig, }; -use alloy_primitives::{Bytes, B256}; +use alloy_primitives::{keccak256, Bytes, B256}; use alloy_provider::Provider; -use eyre::{OptionExt, Result}; +use eyre::{Context, OptionExt, Result}; use forge_script_sequence::ScriptSequence; use foundry_cheatcodes::Wallets; use foundry_common::{ compile::ProjectCompiler, provider::try_get_http_provider, ContractData, ContractsByArtifact, }; use foundry_compilers::{ - artifacts::{ - BytecodeObject, CompactBytecode, CompactContractBytecode, CompactDeployedBytecode, - Libraries, - }, + artifacts::{BytecodeObject, CompactContractBytecode, CompactContractBytecodeCow, Libraries}, compilers::{multi::MultiCompilerLanguage, Language}, + contracts::ArtifactContracts, info::ContractInfo, solc::SolcLanguage, utils::source_files_iter, - ArtifactId, ProjectCompileOutput, + Artifact, ArtifactId, ProjectCompileOutput, }; use foundry_evm::traces::debug::ContractSources; -use foundry_linking::Linker; +use foundry_linking::{Linker, ZkLinker}; use foundry_zksync_compilers::{ compilers::{artifact_output::zk::ZkArtifactOutput, zksolc::ZkSolcCompiler}, - dual_compiled_contracts::DualCompiledContracts, + dual_compiled_contracts::{DualCompiledContract, DualCompiledContracts}, }; -use std::{collections::BTreeMap, path::PathBuf, str::FromStr, sync::Arc}; +use foundry_zksync_core::{hash_bytecode, DEFAULT_CREATE2_DEPLOYER_ZKSYNC, H256}; +use semver::VersionReq; +use std::{path::PathBuf, str::FromStr, sync::Arc}; /// Container for the compiled contracts. #[derive(Debug)] @@ -48,9 +48,195 @@ impl BuildData { Linker::new(self.project_root.clone(), self.output.artifact_ids().collect()) } + fn get_zk_linker(&self, script_config: &ScriptConfig) -> Result> { + let zksolc = foundry_config::zksync::config_zksolc_compiler(&script_config.config) + .context("retrieving zksolc compiler to be used for linking")?; + + let version_req = VersionReq::parse(">=1.5.8").unwrap(); + if !version_req.matches(&zksolc.version().context("trying to determine zksolc version")?) { + eyre::bail!("linking requires zksolc >= 1.5.8"); + } + + let Some(input) = self.zk_output.as_ref() else { + eyre::bail!("unable to link zk artifacts if no zk compilation output is provided") + }; + + Ok(ZkLinker::new( + self.project_root.clone(), + input + .artifact_ids() + .map(|(id, v)| (id.with_stripped_file_prefixes(self.project_root.as_ref()), v)) + .collect(), + zksolc, + input + )) + } + + /// Will attempt linking via `zksolc` + /// + /// Will attempt linking with a CREATE2 deployer if possible first, otherwise + /// just using CREATE. + /// After linking is done it will update the list of `DualCompiledContracts` with + /// the newly linked contracts (and their EVM equivalent). + /// Finally, return the list of known contracts + /// + /// If compilation for zksync is not enabled will return the + /// given EVM linked artifacts + async fn zk_link( + &mut self, + script_config: &ScriptConfig, + known_libraries: Libraries, + evm_linked_artifacts: ArtifactContracts, + ) -> Result { + if !script_config.config.zksync.should_compile() { + return Ok(evm_linked_artifacts); + } + + let Some(input) = self.zk_output.as_ref() else { + eyre::bail!("unable to link zk artifacts if no zk compilation output is provided"); + }; + let mut dual_compiled_contracts = self.dual_compiled_contracts.take().unwrap_or_default(); + + let linker = self.get_zk_linker(script_config)?; + + // NOTE(zk): translate solc ArtifactId to zksolc otherwise + // we won't be able to find it in the zksolc output + let target = self.target.clone().with_stripped_file_prefixes(self.project_root.as_ref()); + let Some(target) = linker + .linker + .contracts + .keys() + .find(|id| id.source == target.source && id.name == target.name) + else { + eyre::bail!("unable to find zk target artifact for linking"); + }; + + let create2_deployer = DEFAULT_CREATE2_DEPLOYER_ZKSYNC; + let can_use_create2 = if let Some(fork_url) = &script_config.evm_opts.fork_url { + let provider = try_get_http_provider(fork_url)?; + let deployer_code = provider.get_code_at(create2_deployer).await?; + + !deployer_code.is_empty() + } else { + // If --fork-url is not provided, we are just simulating the script. + true + }; + + let maybe_create2_link_output = can_use_create2 + .then(|| { + linker + .zk_link_with_create2( + known_libraries.clone(), + create2_deployer, + script_config.config.create2_library_salt, + target, + ) + .ok() + }) + .flatten(); + + let libraries = if let Some(output) = maybe_create2_link_output { + output.libraries + } else { + let output = linker.zk_link_with_nonce_or_address( + known_libraries, + script_config.evm_opts.sender, + script_config.sender_nonce, + [target], + )?; + + output.libraries + }; + + let linked_contracts = linker + .zk_get_linked_artifacts( + linker + .linker + .contracts + .iter() + // we add this filter to avoid linking contracts that don't need linking + .filter(|(_, art)| { + // NOTE(zk): no need to check `deployed_bytecode` + // as those `link_references` would be the same as `bytecode` + art.bytecode + .as_ref() + .map(|bc| !bc.link_references.is_empty()) + .unwrap_or_default() + }) + .map(|(id, _)| id), + &libraries, + ) + .context("retrieving all fully linked contracts")?; + + let newly_linked_dual_compiled_contracts = linked_contracts + .iter() + .flat_map(|(needle, zk)| { + evm_linked_artifacts + .iter() + .find(|(id, _)| id.source == needle.source && id.name == needle.name) + .map(|(_, evm)| (needle, zk, evm)) + }) + .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) + .map(|(id, linked_zk, evm)| { + let (_, unlinked_zk_artifact) = input + .artifact_ids() + .find(|(contract_id, _)| { + contract_id.clone().with_stripped_file_prefixes(self.project_root.as_ref()) + == id.clone() + }) + .expect("unable to find original (pre-linking) artifact"); + let zk_bytecode = + linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); + let zk_hash = hash_bytecode(&zk_bytecode); + let evm = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); + let contract = DualCompiledContract { + name: id.name.clone(), + zk_bytecode_hash: zk_hash, + zk_deployed_bytecode: zk_bytecode.to_vec(), + // TODO(zk): retrieve unlinked factory deps (1.5.9) + zk_factory_deps: vec![zk_bytecode.to_vec()], + evm_bytecode_hash: B256::from_slice(&keccak256(evm.as_ref())[..]), + // TODO(zk): determine if this is ok, as it's + // not really used in dual compiled contracts + evm_deployed_bytecode: evm.to_vec(), + evm_bytecode: evm.to_vec(), + }; + + // populate factory deps that were already linked + dual_compiled_contracts.extend_factory_deps_by_hash( + contract, + unlinked_zk_artifact.factory_dependencies.iter().flatten().map(|(hash, _)| { + H256::from_slice( + alloy_primitives::hex::decode(dbg!(hash)) + .expect("malformed factory dep hash") + .as_slice(), + ) + }), + ) + }); + + dual_compiled_contracts.extend(newly_linked_dual_compiled_contracts.collect::>()); + self.dual_compiled_contracts.replace(dual_compiled_contracts); + + // base zksolc contracts + newly linked + evm contracts + let contracts = input + .artifact_ids() + .map(|(id, v)| { + ( + id.with_stripped_file_prefixes(self.project_root.as_ref()), + CompactContractBytecode::from(CompactContractBytecodeCow::from(v)), + ) + }) + .chain(linked_contracts) + .chain(evm_linked_artifacts) + .collect(); + + Ok(contracts) + } + /// Links contracts. Uses CREATE2 linking when possible, otherwise falls back to /// default linking with sender nonce and address. - pub async fn link(self, script_config: &ScriptConfig) -> Result { + pub async fn link(mut self, script_config: &ScriptConfig) -> Result { let create2_deployer = script_config.evm_opts.create2_deployer; let can_use_create2 = if let Some(fork_url) = &script_config.evm_opts.fork_url { let provider = try_get_http_provider(fork_url)?; @@ -64,7 +250,7 @@ impl BuildData { let known_libraries = script_config.config.libraries_with_remappings()?; - // TODO(zk): linking updating self.dual_compiled_contracts + // TODO(zk): evaluate using strategies here as well let maybe_create2_link_output = can_use_create2 .then(|| { @@ -89,7 +275,7 @@ impl BuildData { ) } else { let output = self.get_linker().link_with_nonce_or_address( - known_libraries, + known_libraries.clone(), script_config.evm_opts.sender, script_config.sender_nonce, [&self.target], @@ -98,13 +284,37 @@ impl BuildData { (output.libraries, ScriptPredeployLibraries::Default(output.libs_to_deploy)) }; - LinkedBuildData::new(libraries, predeploy_libs, self) + let known_contracts = self + .get_linker() + .get_linked_artifacts(&libraries) + .context("retrieving fully linked artifacts")?; + let known_contracts = self.zk_link(script_config, known_libraries, known_contracts).await?; + + LinkedBuildData::new( + libraries, + predeploy_libs, + ContractsByArtifact::new(known_contracts), + self, + ) } /// Links the build data with the given libraries. Expects supplied libraries set being enough /// to fully link target contract. - pub fn link_with_libraries(self, libraries: Libraries) -> Result { - LinkedBuildData::new(libraries, ScriptPredeployLibraries::Default(Vec::new()), self) + pub async fn link_with_libraries( + mut self, + script_config: &ScriptConfig, + libraries: Libraries, + ) -> Result { + let known_contracts = self.get_linker().get_linked_artifacts(&libraries)?; + let known_contracts = + self.zk_link(script_config, libraries.clone(), known_contracts).await?; + + LinkedBuildData::new( + libraries, + ScriptPredeployLibraries::Default(Vec::new()), + ContractsByArtifact::new(known_contracts), + self, + ) } } @@ -142,7 +352,8 @@ impl LinkedBuildData { pub fn new( libraries: Libraries, predeploy_libraries: ScriptPredeployLibraries, - mut build_data: BuildData, + known_contracts: ContractsByArtifact, + build_data: BuildData, ) -> Result { let sources = ContractSources::from_project_output( &build_data.output, @@ -150,40 +361,6 @@ impl LinkedBuildData { Some(&libraries), )?; - let mut known_artifacts = build_data.get_linker().get_linked_artifacts(&libraries)?; - // Extend known_artifacts with zk artifacts if available - if let Some(zk_output) = build_data.zk_output.take() { - let zk_contracts = - zk_output.with_stripped_file_prefixes(&build_data.project_root).into_artifacts(); - - for (id, contract) in zk_contracts { - if let Some(abi) = contract.abi { - let bytecode = contract.bytecode.as_ref(); - // TODO(zk): retrieve link_references - if let Some(bytecode_object) = bytecode.map(|b| b.object()) { - let compact_bytecode = CompactBytecode { - object: bytecode_object.clone(), - source_map: None, - link_references: BTreeMap::new(), - }; - let compact_contract = CompactContractBytecode { - abi: Some(abi), - bytecode: Some(compact_bytecode.clone()), - deployed_bytecode: Some(CompactDeployedBytecode { - bytecode: Some(compact_bytecode), - immutable_references: BTreeMap::new(), - }), - }; - known_artifacts.insert(id.clone(), compact_contract); - } - } else { - warn!("Abi not found for contract {}", id.identifier()); - } - } - } - - let known_contracts = ContractsByArtifact::new(known_artifacts); - Ok(Self { build_data, known_contracts, libraries, predeploy_libraries, sources }) } @@ -268,8 +445,8 @@ impl PreprocessedState { if id.name != *name { continue; } - } else if contract.abi.as_ref().is_none_or(|abi| abi.is_empty()) || - contract.bytecode.as_ref().is_none_or(|b| match &b.object { + } else if contract.abi.as_ref().is_none_or(|abi| abi.is_empty()) + || contract.bytecode.as_ref().is_none_or(|b| match &b.object { BytecodeObject::Bytecode(b) => b.is_empty(), BytecodeObject::Unlinked(_) => false, }) @@ -394,7 +571,7 @@ impl CompiledState { ScriptSequenceKind::Multi(_) => Libraries::default(), }; - let linked_build_data = build_data.link_with_libraries(libraries)?; + let linked_build_data = build_data.link_with_libraries(&script_config, libraries).await?; Ok(BundledState { args, diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 2aa8a20d2..0657b7e44 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -155,7 +155,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { } })?; - let linker = ZkLinker::new(root, contracts.clone(), zksolc); + let linker = ZkLinker::new(root, contracts.clone(), zksolc, input); let zk_linker_error_to_linker = |zk_error| match zk_error { ZkLinkerError::Inner(err) => err, @@ -212,13 +212,12 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { }) .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) .map(|(id, linked_zk, evm)| { - let (unlinked_id, unlinked_zk_artifact) = input + let (_, unlinked_zk_artifact) = input .artifact_ids() .find(|(contract_id, _)| { contract_id.clone().with_stripped_file_prefixes(root) == id.clone() }) .expect("unable to find original (pre-linking) artifact"); - dbg!(unlinked_id, &unlinked_zk_artifact); let zk_bytecode = linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); let zk_hash = hash_bytecode(&zk_bytecode); diff --git a/crates/zksync/core/src/lib.rs b/crates/zksync/core/src/lib.rs index a450b45f5..fcf0bfb25 100644 --- a/crates/zksync/core/src/lib.rs +++ b/crates/zksync/core/src/lib.rs @@ -19,7 +19,7 @@ pub mod vm; pub mod state; use alloy_network::TransactionBuilder; -use alloy_primitives::{address, hex, keccak256, Address, Bytes, U256 as rU256}; +use alloy_primitives::{address, hex, keccak256, Address, Bytes, B256, U256 as rU256}; use alloy_transport::Transport; use alloy_zksync::{ network::transaction_request::TransactionRequest as ZkTransactionRequest, @@ -204,6 +204,21 @@ pub fn compute_create_address(sender: Address, nonce: u64) -> Address { get_create_zksync_address(sender.to_h160(), Nonce(nonce as u32)).to_address() } +/// Compute a CREATE2 address according to zksync +pub fn compute_create2_address(sender: Address, bytecode_hash: H256, salt: B256, constructor_input: &[u8]) -> Address { + const CREATE2_PREFIX: &'static [u8] = b"zksyncCreate2"; + let prefix = keccak256(CREATE2_PREFIX); + let sender = sender.to_h256(); + let constructor_input_hash = keccak256(constructor_input); + + let payload = [prefix.as_slice(), sender.0.as_slice(), salt.0.as_slice(), bytecode_hash.0.as_slice(), constructor_input_hash.as_slice()].concat(); + let hash = keccak256(payload); + + let address = &hash[12..]; + + Address::from_slice(address) +} + /// Try decoding the provided transaction data into create parameters. pub fn try_decode_create(data: &[u8]) -> Result<(H256, Vec)> { let decoded_calldata = From b3de768c0f719709cead4eddc16b3c218cba7b4c Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 14 Jan 2025 21:53:18 +0100 Subject: [PATCH 22/61] chore: formatting --- crates/script/src/build.rs | 23 +++++++++++++---------- crates/zksync/core/src/lib.rs | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index e6652d29e..fb70207e9 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -68,7 +68,7 @@ impl BuildData { .map(|(id, v)| (id.with_stripped_file_prefixes(self.project_root.as_ref()), v)) .collect(), zksolc, - input + input, )) } @@ -178,13 +178,16 @@ impl BuildData { }) .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) .map(|(id, linked_zk, evm)| { - let (_, unlinked_zk_artifact) = input - .artifact_ids() - .find(|(contract_id, _)| { - contract_id.clone().with_stripped_file_prefixes(self.project_root.as_ref()) - == id.clone() - }) - .expect("unable to find original (pre-linking) artifact"); + let (_, unlinked_zk_artifact) = + input + .artifact_ids() + .find(|(contract_id, _)| { + contract_id + .clone() + .with_stripped_file_prefixes(self.project_root.as_ref()) == + id.clone() + }) + .expect("unable to find original (pre-linking) artifact"); let zk_bytecode = linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); let zk_hash = hash_bytecode(&zk_bytecode); @@ -445,8 +448,8 @@ impl PreprocessedState { if id.name != *name { continue; } - } else if contract.abi.as_ref().is_none_or(|abi| abi.is_empty()) - || contract.bytecode.as_ref().is_none_or(|b| match &b.object { + } else if contract.abi.as_ref().is_none_or(|abi| abi.is_empty()) || + contract.bytecode.as_ref().is_none_or(|b| match &b.object { BytecodeObject::Bytecode(b) => b.is_empty(), BytecodeObject::Unlinked(_) => false, }) diff --git a/crates/zksync/core/src/lib.rs b/crates/zksync/core/src/lib.rs index fcf0bfb25..e7ffbd89c 100644 --- a/crates/zksync/core/src/lib.rs +++ b/crates/zksync/core/src/lib.rs @@ -205,13 +205,25 @@ pub fn compute_create_address(sender: Address, nonce: u64) -> Address { } /// Compute a CREATE2 address according to zksync -pub fn compute_create2_address(sender: Address, bytecode_hash: H256, salt: B256, constructor_input: &[u8]) -> Address { +pub fn compute_create2_address( + sender: Address, + bytecode_hash: H256, + salt: B256, + constructor_input: &[u8], +) -> Address { const CREATE2_PREFIX: &'static [u8] = b"zksyncCreate2"; let prefix = keccak256(CREATE2_PREFIX); let sender = sender.to_h256(); let constructor_input_hash = keccak256(constructor_input); - let payload = [prefix.as_slice(), sender.0.as_slice(), salt.0.as_slice(), bytecode_hash.0.as_slice(), constructor_input_hash.as_slice()].concat(); + let payload = [ + prefix.as_slice(), + sender.0.as_slice(), + salt.0.as_slice(), + bytecode_hash.0.as_slice(), + constructor_input_hash.as_slice(), + ] + .concat(); let hash = keccak256(payload); let address = &hash[12..]; From 6d8dc25b38217851c51ec97d2a74ed1e269cc6b1 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Wed, 15 Jan 2025 20:15:59 +0100 Subject: [PATCH 23/61] feat(compiler:zk): `factory_dependencies_unlinked` test(zk:link): use 1.5.9 for deploy-time linking feat(zk:link): detect factory dependencies as link references for library lookup fix(zk:link): require 1.5.9 for deploy-time-linking --- crates/forge/tests/it/zk/linking.rs | 43 ++++++++++++++++--- crates/linking/src/zksync.rs | 35 +++++++++++++-- crates/script/src/build.rs | 9 ++-- crates/strategy/zksync/src/executor/runner.rs | 14 +++--- .../compilers/src/artifacts/contract.rs | 26 +++++++++-- .../src/compilers/artifact_output/zk.rs | 17 +++++++- .../src/compilers/zksolc/settings.rs | 2 + 7 files changed, 121 insertions(+), 25 deletions(-) diff --git a/crates/forge/tests/it/zk/linking.rs b/crates/forge/tests/it/zk/linking.rs index 4a635d1ca..4fddd0695 100644 --- a/crates/forge/tests/it/zk/linking.rs +++ b/crates/forge/tests/it/zk/linking.rs @@ -1,5 +1,7 @@ use forge::revm::primitives::SpecId; use foundry_test_utils::{forgetest_async, util, Filter, TestProject}; +use foundry_zksync_compilers::compilers::zksolc::settings::ZkSolcError; +use semver::Version; use crate::{ config::TestConfig, @@ -18,7 +20,7 @@ forgetest_async!( #[should_panic = "no bytecode for contract; is it abstract or unlinked?"] script_zk_fails_indirect_reference_to_unlinked, |prj, cmd| { - setup_libs_prj(&mut prj); + setup_libs_prj(&mut prj, Some(Version::new(1, 5, 9))); run_zk_script_test( prj.root(), &mut cmd, @@ -32,12 +34,29 @@ forgetest_async!( } ); -// TODO(zk): add deploy-time linking to scripting forgetest_async!( - #[should_panic = "has no bytecode hash, as it may require library linkage"] - script_zk_deploy_time_linking_fails, + script_zk_deploy_time_linking, |prj, cmd| { - setup_libs_prj(&mut prj); + setup_libs_prj(&mut prj, Some(Version::new(1, 5, 9))); + run_zk_script_test( + prj.root(), + &mut cmd, + "./script/Libraries.s.sol", + "DeployTimeLinking", + None, + 1, + Some(&["-vvvvv"]), + ) + .await; + } +); + +forgetest_async!( + #[ignore] + #[should_panic = "deploy-time linking not supported"] + script_zk_deploy_time_linking_fails_older_version, + |prj, cmd| { + setup_libs_prj(&mut prj, Some(Version::new(1, 5, 8))); run_zk_script_test( prj.root(), &mut cmd, @@ -55,7 +74,7 @@ forgetest_async!( #[should_panic = "Dynamic linking not supported"] create_zk_using_unlinked_fails, |prj, cmd| { - setup_libs_prj(&mut prj); + setup_libs_prj(&mut prj, None); // we don't really connect to the rpc because // we expect to fail before that point @@ -71,8 +90,18 @@ forgetest_async!( } ); -fn setup_libs_prj(prj: &mut TestProject) { +fn setup_libs_prj(prj: &mut TestProject, zksolc: Option) { util::initialize(prj.root()); + + let zksolc = zksolc.unwrap_or_else(|| Version::new(1, 5, 9)); + // force zksolc 1.5.9 + let mut config = prj.config_from_output::<_, &str>([]); + if zksolc >= Version::new(1,5,9) { + config.zksync.suppressed_errors.insert(ZkSolcError::AssemblyCreate); + } + config.zksync.zksolc.replace(foundry_config::SolcReq::Version(zksolc)); + prj.write_config(config); + prj.add_script("Libraries.s.sol", include_str!("../../fixtures/zk/Libraries.s.sol")).unwrap(); prj.add_source( "WithLibraries.sol", diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index 8f71adc34..9a9afe7c9 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -24,6 +24,7 @@ use foundry_zksync_compilers::{ link::{self as zk_link, MissingLibrary}, }; use foundry_zksync_core::hash_bytecode; +use semver::Version; use crate::{LinkOutput, Linker, LinkerError}; @@ -38,6 +39,8 @@ pub enum ZkLinkerError { MissingFactoryDeps(BTreeSet), } +pub const DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION: Version = Version::new(1, 5, 9); + #[derive(Debug)] pub struct ZkLinker<'a> { pub linker: Linker<'a>, @@ -55,7 +58,29 @@ impl<'a> ZkLinker<'a> { Self { linker: Linker::new(root, contracts), compiler, compiler_output } } - /// Performs DFS on the graph of link references, and populates `deps` with all found libraries, including ones of factory deps. + fn zk_collect_factory_deps(&self, target: &ZkContractArtifact) -> Vec { + let mut contracts = + self.compiler_output.clone().with_stripped_file_prefixes(self.linker.root.as_ref()); + + let parse_factory_dep = |dep: &str| { + let mut split = dep.split(':'); + let path = split.next().expect("malformed factory dep path"); + let name = split.next().expect("malformed factory dep name"); + + (path.to_string(), name.to_string()) + }; + + target + .factory_dependencies_unlinked + .iter() + .flatten() + .map(|dep| parse_factory_dep(dep)) + .filter_map(|(path, name)| contracts.remove(Path::new(&path), &name)) + .collect() + } + + /// Performs DFS on the graph of link references, and populates `deps` with all found libraries, + /// including ones of factory deps. fn zk_collect_dependencies( &'a self, target: &'a ArtifactId, @@ -75,8 +100,12 @@ impl<'a> ZkLinker<'a> { references.extend(bytecode.link_references()); } - // TODO(zk): should instead use unlinked factory dependencies - // zksolc 1.5.9 + let factory_deps = self.zk_collect_factory_deps(artifact); + for fdep in factory_deps { + if let Some(bytecode) = &fdep.bytecode { + references.extend(bytecode.link_references()); + } + } for (file, libs) in &references { for contract in libs.keys() { diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index fb70207e9..59926d570 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -20,13 +20,12 @@ use foundry_compilers::{ Artifact, ArtifactId, ProjectCompileOutput, }; use foundry_evm::traces::debug::ContractSources; -use foundry_linking::{Linker, ZkLinker}; +use foundry_linking::{Linker, ZkLinker, DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION}; use foundry_zksync_compilers::{ compilers::{artifact_output::zk::ZkArtifactOutput, zksolc::ZkSolcCompiler}, dual_compiled_contracts::{DualCompiledContract, DualCompiledContracts}, }; use foundry_zksync_core::{hash_bytecode, DEFAULT_CREATE2_DEPLOYER_ZKSYNC, H256}; -use semver::VersionReq; use std::{path::PathBuf, str::FromStr, sync::Arc}; /// Container for the compiled contracts. @@ -52,9 +51,9 @@ impl BuildData { let zksolc = foundry_config::zksync::config_zksolc_compiler(&script_config.config) .context("retrieving zksolc compiler to be used for linking")?; - let version_req = VersionReq::parse(">=1.5.8").unwrap(); - if !version_req.matches(&zksolc.version().context("trying to determine zksolc version")?) { - eyre::bail!("linking requires zksolc >= 1.5.8"); + let version = zksolc.version().context("trying to determine zksolc version")?; + if version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { + eyre::bail!("deploy-time linking not supported. minimum: {}, given: {}", DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, &version); } let Some(input) = self.zk_output.as_ref() else { diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 0657b7e44..731ef3249 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -4,12 +4,13 @@ use alloy_primitives::{keccak256, Address, Bytes, TxKind, B256, U256}; use alloy_rpc_types::serde_helpers::OtherFields; use alloy_zksync::provider::{zksync_provider, ZksyncProvider}; use eyre::Result; -use foundry_linking::{LinkerError, ZkLinker, ZkLinkerError}; +use foundry_linking::{ + LinkerError, ZkLinker, ZkLinkerError, DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, +}; use revm::{ primitives::{CreateScheme, Env, EnvWithHandlerCfg, ResultAndState}, Database, }; -use semver::VersionReq; use zksync_types::H256; use foundry_common::ContractsByArtifact; @@ -146,9 +147,12 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { // TODO(zk): better error zksolc.version().map_err(|_| LinkerError::CyclicDependency).and_then(|version| { - let version_req = VersionReq::parse(">=1.5.8").unwrap(); - if !version_req.matches(&version) { - tracing::error!(?version, "linking requires zksolc >= v1.5.8"); + if version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { + tracing::error!( + %version, + minimum_version = %DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, + "deploy-time linking not supported" + ); Err(LinkerError::CyclicDependency) } else { Ok(()) diff --git a/crates/zksync/compilers/src/artifacts/contract.rs b/crates/zksync/compilers/src/artifacts/contract.rs index 0e64e2ab7..cd38886b3 100644 --- a/crates/zksync/compilers/src/artifacts/contract.rs +++ b/crates/zksync/compilers/src/artifacts/contract.rs @@ -6,7 +6,10 @@ use foundry_compilers_artifacts_solc::{ CompactContractRef, CompactDeployedBytecode, DevDoc, Evm, Offsets, StorageLayout, UserDoc, }; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, collections::BTreeMap}; +use std::{ + borrow::Cow, + collections::{BTreeMap, HashSet}, +}; /// Represents a compiled solidity contract #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -32,9 +35,17 @@ pub struct Contract { /// The contract EraVM bytecode hash. #[serde(default, skip_serializing_if = "Option::is_none")] pub hash: Option, - /// The contract factory dependencies. + /// Map of factory dependencies, encoded as => : + /// + /// Only contains fully linked factory dependencies, as + /// unlinked factory dependencies do not have a bytecode hash #[serde(default, skip_serializing_if = "Option::is_none")] pub factory_dependencies: Option>, + /// Complete list of factory dependencies, encoded as : + /// + /// Contains both linked and unlinked factory dependencies + #[serde(default, skip_serializing_if = "Option::is_none")] + pub factory_dependencies_unlinked: Option>, /// EraVM-related outputs #[serde(default, skip_serializing_if = "Option::is_none")] pub eravm: Option, @@ -53,7 +64,16 @@ fn storage_layout_is_empty(storage_layout: &StorageLayout) -> bool { impl Contract { /// Returns true if contract is not linked pub fn is_unlinked(&self) -> bool { - self.hash.is_none() || !self.missing_libraries.is_empty() + self.hash.is_none() + || !self.missing_libraries.is_empty() + || self + .factory_dependencies_unlinked + .as_ref() + .and_then(|unlinked| { + self.factory_dependencies.as_ref().map(|linked| unlinked.len() - linked.len()) + }) + .unwrap_or_default() + > 0 } /// takes missing libraries output and transforms into link references diff --git a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs index ec146cfe0..99793e9c3 100644 --- a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs +++ b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs @@ -11,7 +11,11 @@ use foundry_compilers_artifacts_solc::{ CompactDeployedBytecode, }; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, collections::BTreeMap, path::Path}; +use std::{ + borrow::Cow, + collections::{BTreeMap, HashSet}, + path::Path, +}; mod bytecode; pub use bytecode::ZkArtifactBytecode; @@ -46,9 +50,16 @@ pub struct ZkContractArtifact { /// contract hash #[serde(default, skip_serializing_if = "Option::is_none")] pub hash: Option, - /// contract factory dependencies + /// List of factory dependencies, encoded as => : + /// + /// Only contains fully linked factory dependencies #[serde(default, skip_serializing_if = "Option::is_none")] pub factory_dependencies: Option>, + /// Complete list of factory dependencies, encoded as : + /// + /// Contains both linked and unlinked factory dependencies + #[serde(default, skip_serializing_if = "Option::is_none")] + pub factory_dependencies_unlinked: Option>, /// The identifier of the source file #[serde(default, skip_serializing_if = "Option::is_none")] pub id: Option, @@ -130,6 +141,7 @@ impl ArtifactOutput for ZkArtifactOutput { ir_optimized, hash, factory_dependencies, + factory_dependencies_unlinked, missing_libraries, } = contract; @@ -144,6 +156,7 @@ impl ArtifactOutput for ZkArtifactOutput { abi, hash, factory_dependencies, + factory_dependencies_unlinked, storage_layout: Some(storage_layout), bytecode, assembly, diff --git a/crates/zksync/compilers/src/compilers/zksolc/settings.rs b/crates/zksync/compilers/src/compilers/zksolc/settings.rs index e111a4eb8..5815c71cb 100644 --- a/crates/zksync/compilers/src/compilers/zksolc/settings.rs +++ b/crates/zksync/compilers/src/compilers/zksolc/settings.rs @@ -54,6 +54,7 @@ pub enum ZkSolcError { /// `sendtransfer` error: Using `send()` or `transfer()` methods on `address payable` instead /// of `call()`. SendTransfer, + AssemblyCreate, } impl FromStr for ZkSolcError { @@ -61,6 +62,7 @@ impl FromStr for ZkSolcError { fn from_str(s: &str) -> Result { match s { "sendtransfer" => Ok(Self::SendTransfer), + "assemblycreate" => Ok(Self::AssemblyCreate), s => Err(format!("Unknown zksolc error: {s}")), } } From 63621815f66081422a3252ada570c9e0af16fe51 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Thu, 16 Jan 2025 14:40:20 +0100 Subject: [PATCH 24/61] refactor: dual compiled contracts as map --- crates/script/src/build.rs | 30 ++-- .../strategy/zksync/src/cheatcode/context.rs | 47 ++--- .../cheatcode/runner/cheatcode_handlers.rs | 15 +- .../zksync/src/cheatcode/runner/mod.rs | 7 +- crates/strategy/zksync/src/executor/runner.rs | 37 ++-- .../compilers/src/artifacts/contract.rs | 11 +- .../compilers/src/dual_compiled_contracts.rs | 163 ++++++++++-------- 7 files changed, 174 insertions(+), 136 deletions(-) diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 59926d570..5628c8574 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -191,8 +191,11 @@ impl BuildData { linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); let zk_hash = hash_bytecode(&zk_bytecode); let evm = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); - let contract = DualCompiledContract { + let contract_info = ContractInfo { name: id.name.clone(), + path: Some(id.source.to_string_lossy().into_owned()), + }; + let contract = DualCompiledContract { zk_bytecode_hash: zk_hash, zk_deployed_bytecode: zk_bytecode.to_vec(), // TODO(zk): retrieve unlinked factory deps (1.5.9) @@ -204,16 +207,21 @@ impl BuildData { evm_bytecode: evm.to_vec(), }; - // populate factory deps that were already linked - dual_compiled_contracts.extend_factory_deps_by_hash( - contract, - unlinked_zk_artifact.factory_dependencies.iter().flatten().map(|(hash, _)| { - H256::from_slice( - alloy_primitives::hex::decode(dbg!(hash)) - .expect("malformed factory dep hash") - .as_slice(), - ) - }), + ( + contract_info, + // populate factory deps that were already linked + dual_compiled_contracts.extend_factory_deps_by_hash( + contract, + unlinked_zk_artifact.factory_dependencies.iter().flatten().map( + |(hash, _)| { + H256::from_slice( + alloy_primitives::hex::decode(dbg!(hash)) + .expect("malformed factory dep hash") + .as_slice(), + ) + }, + ), + ), ) }); diff --git a/crates/strategy/zksync/src/cheatcode/context.rs b/crates/strategy/zksync/src/cheatcode/context.rs index 7f69a155d..987f0708e 100644 --- a/crates/strategy/zksync/src/cheatcode/context.rs +++ b/crates/strategy/zksync/src/cheatcode/context.rs @@ -3,6 +3,7 @@ use std::collections::HashSet; use alloy_primitives::{keccak256, map::HashMap, Address, Bytes, B256}; use alloy_sol_types::SolValue; use foundry_cheatcodes::strategy::CheatcodeInspectorStrategyContext; +use foundry_compilers::info::ContractInfo; use foundry_evm_core::constants::{CHEATCODE_ADDRESS, CHEATCODE_CONTRACT_HASH}; use foundry_zksync_compilers::dual_compiled_contracts::{ DualCompiledContract, DualCompiledContracts, @@ -68,32 +69,38 @@ impl ZksyncCheatcodeInspectorStrategyContext { let zk_deployed_bytecode = foundry_zksync_core::EMPTY_CODE.to_vec(); let mut dual_compiled_contracts = dual_compiled_contracts; - dual_compiled_contracts.push(DualCompiledContract { - name: String::from("EmptyEVMBytecode"), - zk_bytecode_hash, - zk_deployed_bytecode: zk_deployed_bytecode.clone(), - zk_factory_deps: Default::default(), - evm_bytecode_hash: B256::from_slice(&keccak256(&empty_bytes)[..]), - evm_deployed_bytecode: Bytecode::new_raw(empty_bytes.clone()).bytecode().to_vec(), - evm_bytecode: Bytecode::new_raw(empty_bytes).bytecode().to_vec(), - }); + dual_compiled_contracts.insert( + ContractInfo::new("EmptyEVMBytecode"), + DualCompiledContract { + zk_bytecode_hash, + zk_deployed_bytecode: zk_deployed_bytecode.clone(), + zk_factory_deps: Default::default(), + evm_bytecode_hash: B256::from_slice(&keccak256(&empty_bytes)[..]), + evm_deployed_bytecode: Bytecode::new_raw(empty_bytes.clone()).bytecode().to_vec(), + evm_bytecode: Bytecode::new_raw(empty_bytes).bytecode().to_vec(), + }, + ); let cheatcodes_bytecode = { let mut bytecode = CHEATCODE_ADDRESS.abi_encode_packed(); bytecode.append(&mut [0; 12].to_vec()); Bytes::from(bytecode) }; - dual_compiled_contracts.push(DualCompiledContract { - name: String::from("CheatcodeBytecode"), - // we put a different bytecode hash here so when importing back to EVM - // we avoid collision with EmptyEVMBytecode for the cheatcodes - zk_bytecode_hash: foundry_zksync_core::hash_bytecode(CHEATCODE_CONTRACT_HASH.as_ref()), - zk_deployed_bytecode: cheatcodes_bytecode.to_vec(), - zk_factory_deps: Default::default(), - evm_bytecode_hash: CHEATCODE_CONTRACT_HASH, - evm_deployed_bytecode: cheatcodes_bytecode.to_vec(), - evm_bytecode: cheatcodes_bytecode.to_vec(), - }); + dual_compiled_contracts.insert( + ContractInfo::new("CheatcodeBytecode"), + DualCompiledContract { + // we put a different bytecode hash here so when importing back to EVM + // we avoid collision with EmptyEVMBytecode for the cheatcodes + zk_bytecode_hash: foundry_zksync_core::hash_bytecode( + CHEATCODE_CONTRACT_HASH.as_ref(), + ), + zk_deployed_bytecode: cheatcodes_bytecode.to_vec(), + zk_factory_deps: Default::default(), + evm_bytecode_hash: CHEATCODE_CONTRACT_HASH, + evm_deployed_bytecode: cheatcodes_bytecode.to_vec(), + evm_bytecode: cheatcodes_bytecode.to_vec(), + }, + ); let mut persisted_factory_deps = HashMap::new(); persisted_factory_deps.insert(zk_bytecode_hash, zk_deployed_bytecode); diff --git a/crates/strategy/zksync/src/cheatcode/runner/cheatcode_handlers.rs b/crates/strategy/zksync/src/cheatcode/runner/cheatcode_handlers.rs index 73e640e7f..54ff365dc 100644 --- a/crates/strategy/zksync/src/cheatcode/runner/cheatcode_handlers.rs +++ b/crates/strategy/zksync/src/cheatcode/runner/cheatcode_handlers.rs @@ -12,6 +12,7 @@ use foundry_cheatcodes::{ zkUseFactoryDepCall, zkUsePaymasterCall, zkVmCall, zkVmSkipCall, }, }; +use foundry_compilers::info::ContractInfo; use foundry_evm::backend::LocalForkId; use foundry_zksync_compilers::dual_compiled_contracts::DualCompiledContract; use foundry_zksync_core::{ZkPaymasterData, H256}; @@ -192,8 +193,8 @@ impl ZksyncCheatcodeInspectorStrategyRunner { let ctx = get_context(ccx.state.strategy.context.as_mut()); let zk_factory_deps = vec![]; //TODO: add argument to cheatcode + let new_contract_info = ContractInfo::new(name); let new_contract = DualCompiledContract { - name: name.clone(), zk_bytecode_hash: H256(zkBytecodeHash.0), zk_deployed_bytecode: zkDeployedBytecode.to_vec(), zk_factory_deps, @@ -202,10 +203,12 @@ impl ZksyncCheatcodeInspectorStrategyRunner { evm_bytecode: evmBytecode.to_vec(), }; - if let Some(existing) = ctx.dual_compiled_contracts.iter().find(|contract| { - contract.evm_bytecode_hash == new_contract.evm_bytecode_hash && - contract.zk_bytecode_hash == new_contract.zk_bytecode_hash - }) { + if let Some((existing, _)) = + ctx.dual_compiled_contracts.iter().find(|(_, contract)| { + contract.evm_bytecode_hash == new_contract.evm_bytecode_hash && + contract.zk_bytecode_hash == new_contract.zk_bytecode_hash + }) + { warn!( name = existing.name, "contract already exists with the given bytecode hashes" @@ -213,7 +216,7 @@ impl ZksyncCheatcodeInspectorStrategyRunner { return Ok(Default::default()); } - ctx.dual_compiled_contracts.push(new_contract); + ctx.dual_compiled_contracts.insert(new_contract_info, new_contract); Ok(Default::default()) } diff --git a/crates/strategy/zksync/src/cheatcode/runner/mod.rs b/crates/strategy/zksync/src/cheatcode/runner/mod.rs index 51e0a3bd2..6a3e3945e 100644 --- a/crates/strategy/zksync/src/cheatcode/runner/mod.rs +++ b/crates/strategy/zksync/src/cheatcode/runner/mod.rs @@ -469,6 +469,7 @@ impl CheatcodeInspectorStrategyExt for ZksyncCheatcodeInspectorStrategyRunner { .unwrap_or_else(|| panic!("failed finding contract for {init_code:?}")); let constructor_args = find_contract.constructor_args(); + let info = find_contract.info(); let contract = find_contract.contract(); let zk_create_input = foundry_zksync_core::encode_create_params( @@ -502,7 +503,7 @@ impl CheatcodeInspectorStrategyExt for ZksyncCheatcodeInspectorStrategyRunner { // NOTE(zk): Clear injected factory deps so that they are not sent on further transactions ctx.zk_use_factory_deps.clear(); - tracing::debug!(contract = contract.name, "using dual compiled contract"); + tracing::debug!(contract = info.name, "using dual compiled contract"); let zk_persist_nonce_update = ctx.zk_persist_nonce_update.check(); let ccx = foundry_zksync_core::vm::CheatcodeTracerContext { @@ -876,7 +877,7 @@ impl ZksyncCheatcodeInspectorStrategyRunner { .and_then(|zk_bytecode_hash| { ctx.dual_compiled_contracts .find_by_zk_bytecode_hash(zk_bytecode_hash.to_h256()) - .map(|contract| { + .map(|(_, contract)| { ( contract.evm_bytecode_hash, Some(Bytecode::new_raw(Bytes::from( @@ -952,7 +953,7 @@ impl ZksyncCheatcodeInspectorStrategyRunner { continue; } - if let Some(contract) = ctx.dual_compiled_contracts.iter().find(|contract| { + if let Some((_, contract)) = ctx.dual_compiled_contracts.iter().find(|(_, contract)| { info.code_hash != KECCAK_EMPTY && info.code_hash == contract.evm_bytecode_hash }) { account_code_storage.insert( diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 731ef3249..3a39a7db0 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -15,8 +15,8 @@ use zksync_types::H256; use foundry_common::ContractsByArtifact; use foundry_compilers::{ - artifacts::CompactContractBytecodeCow, contracts::ArtifactContracts, Artifact, - ProjectCompileOutput, + artifacts::CompactContractBytecodeCow, contracts::ArtifactContracts, info::ContractInfo, + Artifact, ProjectCompileOutput, }; use foundry_config::Config; use foundry_evm::{ @@ -226,8 +226,11 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); let zk_hash = hash_bytecode(&zk_bytecode); let evm = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); - let contract = DualCompiledContract { + let contract_info = ContractInfo { name: id.name.clone(), + path: Some(id.source.to_string_lossy().into_owned()), + }; + let contract = DualCompiledContract { zk_bytecode_hash: zk_hash, zk_deployed_bytecode: zk_bytecode.to_vec(), // TODO(zk): retrieve unlinked factory deps (1.5.9) @@ -239,16 +242,21 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { evm_bytecode: evm.to_vec(), }; - // populate factory deps that were already linked - ctx.dual_compiled_contracts.extend_factory_deps_by_hash( - contract, - unlinked_zk_artifact.factory_dependencies.iter().flatten().map(|(hash, _)| { - H256::from_slice( - alloy_primitives::hex::decode(dbg!(hash)) - .expect("malformed factory dep hash") - .as_slice(), - ) - }), + ( + contract_info, + // populate factory deps that were already linked + ctx.dual_compiled_contracts.extend_factory_deps_by_hash( + contract, + unlinked_zk_artifact.factory_dependencies.iter().flatten().map( + |(hash, _)| { + H256::from_slice( + alloy_primitives::hex::decode(dbg!(hash)) + .expect("malformed factory dep hash") + .as_slice(), + ) + }, + ), + ), ) }); @@ -299,7 +307,8 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { let ctx = get_context(executor.strategy.context.as_mut()); // lookup dual compiled contract based on EVM bytecode - let Some(dual_contract) = ctx.dual_compiled_contracts.find_by_evm_bytecode(code.as_ref()) + let Some((_, dual_contract)) = + ctx.dual_compiled_contracts.find_by_evm_bytecode(code.as_ref()) else { // we don't know what the equivalent zk contract would be return Ok(evm_deployment); diff --git a/crates/zksync/compilers/src/artifacts/contract.rs b/crates/zksync/compilers/src/artifacts/contract.rs index cd38886b3..37672baca 100644 --- a/crates/zksync/compilers/src/artifacts/contract.rs +++ b/crates/zksync/compilers/src/artifacts/contract.rs @@ -64,16 +64,15 @@ fn storage_layout_is_empty(storage_layout: &StorageLayout) -> bool { impl Contract { /// Returns true if contract is not linked pub fn is_unlinked(&self) -> bool { - self.hash.is_none() - || !self.missing_libraries.is_empty() - || self - .factory_dependencies_unlinked + self.hash.is_none() || + !self.missing_libraries.is_empty() || + self.factory_dependencies_unlinked .as_ref() .and_then(|unlinked| { self.factory_dependencies.as_ref().map(|linked| unlinked.len() - linked.len()) }) - .unwrap_or_default() - > 0 + .unwrap_or_default() > + 0 } /// takes missing libraries output and transforms into link references diff --git a/crates/zksync/compilers/src/dual_compiled_contracts.rs b/crates/zksync/compilers/src/dual_compiled_contracts.rs index 998957c2e..9023ad19d 100644 --- a/crates/zksync/compilers/src/dual_compiled_contracts.rs +++ b/crates/zksync/compilers/src/dual_compiled_contracts.rs @@ -5,8 +5,9 @@ use std::{ str::FromStr, }; +use foundry_compilers::ConfigurableArtifacts; use foundry_compilers::{ - solc::SolcLanguage, Artifact, ArtifactId, ArtifactOutput, ConfigurableArtifacts, + info::ContractInfo, solc::SolcLanguage, Artifact, ArtifactId, ArtifactOutput, ProjectCompileOutput, ProjectPathsConfig, }; @@ -28,13 +29,11 @@ pub enum ContractType { /// Defines a contract that has been dual compiled with both zksolc and solc #[derive(Debug, Default, Clone)] pub struct DualCompiledContract { - /// Contract name - pub name: String, /// Deployed bytecode with zksolc pub zk_bytecode_hash: H256, /// Deployed bytecode hash with zksolc pub zk_deployed_bytecode: Vec, - /// Deployed bytecode factory deps + /// Bytecodes of the factory deps for zksolc's deployed bytecode pub zk_factory_deps: Vec>, /// Deployed bytecode hash with solc pub evm_bytecode_hash: B256, @@ -47,11 +46,17 @@ pub struct DualCompiledContract { /// Couple contract type with contract and init code pub struct FindBytecodeResult<'a> { r#type: ContractType, + info: &'a ContractInfo, contract: &'a DualCompiledContract, init_code: &'a [u8], } impl<'a> FindBytecodeResult<'a> { + /// Retrieve the found contract's info + pub fn info(&self) -> &'a ContractInfo { + self.info + } + /// Retrieve the found contract pub fn contract(self) -> &'a DualCompiledContract { self.contract @@ -69,7 +74,7 @@ impl<'a> FindBytecodeResult<'a> { /// A collection of `[DualCompiledContract]`s #[derive(Debug, Default, Clone)] pub struct DualCompiledContracts { - contracts: Vec, + contracts: HashMap, /// ZKvm artifacts path pub zk_artifact_path: PathBuf, /// EVM artifacts path @@ -84,37 +89,29 @@ impl DualCompiledContracts { layout: &ProjectPathsConfig, zk_layout: &ProjectPathsConfig, ) -> Self { - let mut dual_compiled_contracts = vec![]; + let mut dual_compiled_contracts = HashMap::new(); let mut solc_bytecodes = HashMap::new(); - let output_artifacts = output - .cached_artifacts() - .artifact_files() - .chain(output.compiled_artifacts().artifact_files()) - .filter_map(|artifact| { - ConfigurableArtifacts::contract_name(&artifact.file) - .map(|name| (name, (&artifact.file, &artifact.artifact))) - }); - let zk_output_artifacts = zk_output - .cached_artifacts() - .artifact_files() - .chain(zk_output.compiled_artifacts().artifact_files()) - .filter_map(|artifact| { - ConfigurableArtifacts::contract_name(&artifact.file) - .map(|name| (name, (&artifact.file, &artifact.artifact))) - }); - - for (_contract_name, (artifact_path, artifact)) in output_artifacts { - let contract_file = artifact_path - .strip_prefix(&layout.artifacts) - .unwrap_or_else(|_| { - panic!( - "failed stripping solc artifact path '{:?}' from '{:?}'", - layout.artifacts, artifact_path - ) - }) - .to_path_buf(); - + let output_artifacts = output.artifact_ids().map(|(id, artifact)| { + ( + ContractInfo { + name: id.name, + path: Some(id.source.to_string_lossy().into_owned()), + }, + artifact, + ) + }); + let zk_output_artifacts = zk_output.artifact_ids().map(|(id, artifact)| { + ( + ContractInfo { + name: id.name, + path: Some(id.source.to_string_lossy().into_owned()), + }, + artifact, + ) + }); + + for (contract_info, artifact) in output_artifacts { let deployed_bytecode = artifact.get_deployed_bytecode(); let deployed_bytecode = deployed_bytecode .as_ref() @@ -122,7 +119,7 @@ impl DualCompiledContracts { let bytecode = artifact.get_bytecode().and_then(|b| b.object.as_bytes().cloned()); if let Some(bytecode) = bytecode { if let Some(deployed_bytecode) = deployed_bytecode { - solc_bytecodes.insert(contract_file, (bytecode, deployed_bytecode.clone())); + solc_bytecodes.insert(contract_info, (bytecode, deployed_bytecode.clone())); } } } @@ -140,17 +137,7 @@ impl DualCompiledContracts { } } - for (contract_name, (artifact_path, artifact)) in zk_output_artifacts { - let contract_file = artifact_path - .strip_prefix(&zk_layout.artifacts) - .unwrap_or_else(|_| { - panic!( - "failed stripping zksolc artifact path '{:?}' from '{:?}'", - zk_layout.artifacts, artifact_path - ) - }) - .to_path_buf(); - + for (contract_info, artifact) in zk_output_artifacts { let maybe_bytecode = &artifact.bytecode; let maybe_hash = &artifact.hash; let maybe_factory_deps = &artifact.factory_dependencies; @@ -159,7 +146,7 @@ impl DualCompiledContracts { (maybe_bytecode, maybe_hash, maybe_factory_deps) { if let Some((solc_bytecode, solc_deployed_bytecode)) = - solc_bytecodes.get(&contract_file) + solc_bytecodes.get(&contract_info) { // NOTE(zk): unlinked objects are _still_ encoded as valid hex // but the hash wouldn't be present in the artifact @@ -176,17 +163,19 @@ impl DualCompiledContracts { factory_deps_vec.push(bytecode_vec.clone()); - dual_compiled_contracts.push(DualCompiledContract { - name: contract_name, - zk_bytecode_hash: H256::from_str(hash).unwrap(), - zk_deployed_bytecode: bytecode_vec, - zk_factory_deps: factory_deps_vec, - evm_bytecode_hash: keccak256(solc_deployed_bytecode), - evm_bytecode: solc_bytecode.to_vec(), - evm_deployed_bytecode: solc_deployed_bytecode.to_vec(), - }); + dual_compiled_contracts.insert( + contract_info, + DualCompiledContract { + zk_bytecode_hash: H256::from_str(hash).unwrap(), + zk_deployed_bytecode: bytecode_vec, + zk_factory_deps: factory_deps_vec, + evm_bytecode_hash: keccak256(solc_deployed_bytecode), + evm_bytecode: solc_bytecode.to_vec(), + evm_deployed_bytecode: solc_deployed_bytecode.to_vec(), + }, + ); } else { - tracing::error!("matching solc artifact not found for {contract_file:?}"); + tracing::error!("matching solc artifact not found for {contract_info:?}"); } } } @@ -199,18 +188,29 @@ impl DualCompiledContracts { } /// Finds a contract matching the ZK deployed bytecode - pub fn find_by_zk_deployed_bytecode(&self, bytecode: &[u8]) -> Option<&DualCompiledContract> { - self.contracts.iter().find(|contract| bytecode.starts_with(&contract.zk_deployed_bytecode)) + pub fn find_by_zk_deployed_bytecode( + &self, + bytecode: &[u8], + ) -> Option<(&ContractInfo, &DualCompiledContract)> { + self.contracts + .iter() + .find(|(_, contract)| bytecode.starts_with(&contract.zk_deployed_bytecode)) } /// Finds a contract matching the EVM bytecode - pub fn find_by_evm_bytecode(&self, bytecode: &[u8]) -> Option<&DualCompiledContract> { - self.contracts.iter().find(|contract| bytecode.starts_with(&contract.evm_bytecode)) + pub fn find_by_evm_bytecode( + &self, + bytecode: &[u8], + ) -> Option<(&ContractInfo, &DualCompiledContract)> { + self.contracts.iter().find(|(_, contract)| bytecode.starts_with(&contract.evm_bytecode)) } /// Finds a contract matching the ZK bytecode hash - pub fn find_by_zk_bytecode_hash(&self, code_hash: H256) -> Option<&DualCompiledContract> { - self.contracts.iter().find(|contract| code_hash == contract.zk_bytecode_hash) + pub fn find_by_zk_bytecode_hash( + &self, + code_hash: H256, + ) -> Option<(&ContractInfo, &DualCompiledContract)> { + self.contracts.iter().find(|(_, contract)| code_hash == contract.zk_bytecode_hash) } /// Find a contract matching the given bytecode, whether it's EVM or ZK. @@ -224,15 +224,26 @@ impl DualCompiledContracts { let zk = self.find_by_zk_deployed_bytecode(init_code).map(|evm| (ContractType::ZK, evm)); match (&evm, &zk) { - (Some((_, evm)), Some((_, zk))) => { + (Some((_, (evm_info, evm))), Some((_, (zk_info, zk)))) => { if zk.zk_deployed_bytecode.len() >= evm.evm_bytecode.len() { - Some(FindBytecodeResult { r#type: ContractType::ZK, contract: zk, init_code }) + Some(FindBytecodeResult { + r#type: ContractType::ZK, + contract: zk, + init_code, + info: zk_info, + }) } else { - Some(FindBytecodeResult { r#type: ContractType::EVM, contract: zk, init_code }) + Some(FindBytecodeResult { + r#type: ContractType::EVM, + contract: zk, + init_code, + info: evm_info, + }) } } - _ => evm.or(zk).map(|(r#type, contract)| FindBytecodeResult { + _ => evm.or(zk).map(|(r#type, (info, contract))| FindBytecodeResult { r#type, + info, contract, init_code, }), @@ -251,9 +262,9 @@ impl DualCompiledContracts { while let Some(dep) = queue.pop_front() { // try to insert in the list of visited, if it's already present, skip if visited.insert(dep) { - if let Some(contract) = self.find_by_zk_deployed_bytecode(dep) { + if let Some((info, contract)) = self.find_by_zk_deployed_bytecode(dep) { debug!( - name = contract.name, + name = info.name, deps = contract.zk_factory_deps.len(), "new factory dependency" ); @@ -287,13 +298,15 @@ impl DualCompiledContracts { } /// Returns an iterator over all `[DualCompiledContract]`s in the collection - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.contracts.iter() } /// Adds a new `[DualCompiledContract]` to the collection - pub fn push(&mut self, contract: DualCompiledContract) { - self.contracts.push(contract); + /// + /// Will replace any contract with matching `info` + pub fn insert(&mut self, info: ContractInfo, contract: DualCompiledContract) { + self.contracts.insert(info, contract); } /// Retrieves the length of the collection. @@ -307,10 +320,8 @@ impl DualCompiledContracts { } /// Extend the inner set of contracts with the given iterator - pub fn extend(&mut self, iter: impl IntoIterator) { + pub fn extend(&mut self, iter: impl IntoIterator) { self.contracts.extend(iter); - self.contracts.sort_by(|a, b| a.name.cmp(&b.name)); - self.contracts.dedup_by(|a, b| a.name == b.name); } /// Populate the target's factory deps based on the new list @@ -322,7 +333,7 @@ impl DualCompiledContracts { let deps_bytecodes = factory_deps .into_iter() .flat_map(|hash| self.find_by_zk_bytecode_hash(hash)) - .map(|contract| contract.zk_deployed_bytecode.clone()); + .map(|(_, contract)| contract.zk_deployed_bytecode.clone()); target.zk_factory_deps.extend(deps_bytecodes); target From f8ca8065ca7410349e65603fbc91fbbbdbeca8e2 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Thu, 16 Jan 2025 20:13:36 +0100 Subject: [PATCH 25/61] feat(link:zk): recursive factory deps libs lookup fix(link:zk): consistent artifact id prefixes chore: formatting refactor(link:zk): improve link targets filtering --- crates/linking/src/zksync.rs | 82 +++++++++++++------ crates/script/src/build.rs | 73 +++++++---------- crates/strategy/zksync/src/executor/runner.rs | 33 +++----- .../src/compilers/artifact_output/zk.rs | 13 +++ .../compilers/src/dual_compiled_contracts.rs | 3 +- 5 files changed, 117 insertions(+), 87 deletions(-) diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index 9a9afe7c9..41b2242af 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -49,34 +49,70 @@ pub struct ZkLinker<'a> { } impl<'a> ZkLinker<'a> { + fn zk_artifacts(&'a self) -> impl Iterator + 'a { + self.compiler_output + .artifact_ids() + .map(|(id, v)| (id.with_stripped_file_prefixes(&self.linker.root), v)) + } + + /// Construct a new `ZkLinker` pub fn new( root: impl Into, contracts: ArtifactContracts>, compiler: ZkSolcCompiler, compiler_output: &'a ProjectCompileOutput, ) -> Self { + // TODO(zk): check prefix `root` is stripped from `compiler_output` Self { linker: Linker::new(root, contracts), compiler, compiler_output } } - fn zk_collect_factory_deps(&self, target: &ZkContractArtifact) -> Vec { - let mut contracts = - self.compiler_output.clone().with_stripped_file_prefixes(self.linker.root.as_ref()); - - let parse_factory_dep = |dep: &str| { - let mut split = dep.split(':'); - let path = split.next().expect("malformed factory dep path"); - let name = split.next().expect("malformed factory dep name"); + /// Collect the factory dependencies of the `target` artifact + /// + /// Will call itself recursively for nested dependencies + fn zk_collect_factory_deps( + &'a self, + target: &'a ArtifactId, + factory_deps: &mut BTreeSet<&'a ArtifactId>, + ) -> Result<(), LinkerError> { + let (_, artifact) = self + .zk_artifacts() + .find(|(id, _)| id.source == target.source && id.name == target.name) + .ok_or(LinkerError::MissingTargetArtifact)?; - (path.to_string(), name.to_string()) - }; + let already_linked = artifact + .factory_dependencies + .as_ref() + .iter() + .map(|map| map.values().into_iter()) + .flatten() + .collect::>(); - target + let deps_of_target = artifact .factory_dependencies_unlinked .iter() .flatten() - .map(|dep| parse_factory_dep(dep)) - .filter_map(|(path, name)| contracts.remove(Path::new(&path), &name)) - .collect() + // remove already linked deps + .filter(|dep| !already_linked.contains(dep)) + .map(|dep| { + let mut split = dep.split(':'); + let path = split.next().expect("malformed factory dep path"); + let name = split.next().expect("malformed factory dep name"); + + (path.to_string(), name.to_string()) + }); + + for (file, name) in deps_of_target { + let id = self + .linker + .find_artifact_id_by_library_path(&file, &name, None) + .ok_or_else(|| LinkerError::MissingLibraryArtifact { file, name })?; + + if factory_deps.insert(id) { + self.zk_collect_factory_deps(id, factory_deps)?; + } + } + + Ok(()) } /// Performs DFS on the graph of link references, and populates `deps` with all found libraries, @@ -87,12 +123,8 @@ impl<'a> ZkLinker<'a> { deps: &mut BTreeSet<&'a ArtifactId>, ) -> Result<(), LinkerError> { let (_, artifact) = self - .compiler_output - .artifact_ids() - .find(|(id, _)| { - let id = id.clone().with_stripped_file_prefixes(self.linker.root.as_ref()); - id.source == target.source && id.name == target.name - }) + .zk_artifacts() + .find(|(id, _)| id.source == target.source && id.name == target.name) .ok_or(LinkerError::MissingTargetArtifact)?; let mut references = BTreeMap::new(); @@ -100,8 +132,13 @@ impl<'a> ZkLinker<'a> { references.extend(bytecode.link_references()); } - let factory_deps = self.zk_collect_factory_deps(artifact); - for fdep in factory_deps { + // find all nested factory deps's link references + let mut factory_deps = BTreeSet::new(); + self.zk_collect_factory_deps(target, &mut factory_deps)?; + + for (_, fdep) in factory_deps.into_iter().filter_map(|target| { + self.zk_artifacts().find(|(id, _)| id.source == target.source && id.name == target.name) + }) { if let Some(bytecode) = &fdep.bytecode { references.extend(bytecode.link_references()); } @@ -246,7 +283,6 @@ impl<'a> ZkLinker<'a> { // NOTE(zk): doesn't really matter since we use the EVM // bytecode to determine what EraVM bytecode to deploy - dbg!(&file, &name, &address); libs_to_deploy.push(code.clone()); libraries.libs.entry(file).or_default().insert(name, address.to_checksum(None)); diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 5628c8574..8c3d4bce9 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -53,22 +53,18 @@ impl BuildData { let version = zksolc.version().context("trying to determine zksolc version")?; if version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { - eyre::bail!("deploy-time linking not supported. minimum: {}, given: {}", DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, &version); + eyre::bail!( + "deploy-time linking not supported. minimum: {}, given: {}", + DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, + &version + ); } let Some(input) = self.zk_output.as_ref() else { eyre::bail!("unable to link zk artifacts if no zk compilation output is provided") }; - Ok(ZkLinker::new( - self.project_root.clone(), - input - .artifact_ids() - .map(|(id, v)| (id.with_stripped_file_prefixes(self.project_root.as_ref()), v)) - .collect(), - zksolc, - input, - )) + Ok(ZkLinker::new(self.project_root.clone(), input.artifact_ids().map(|(id, v)| (id.with_stripped_file_prefixes(&self.project_root), v)).collect(), zksolc, input)) } /// Will attempt linking via `zksolc` @@ -85,15 +81,17 @@ impl BuildData { &mut self, script_config: &ScriptConfig, known_libraries: Libraries, - evm_linked_artifacts: ArtifactContracts, + evm_linked_contracts: ArtifactContracts, ) -> Result { if !script_config.config.zksync.should_compile() { - return Ok(evm_linked_artifacts); + return Ok(evm_linked_contracts); } let Some(input) = self.zk_output.as_ref() else { eyre::bail!("unable to link zk artifacts if no zk compilation output is provided"); }; + let input = input.clone().with_stripped_file_prefixes(&self.project_root); + let mut dual_compiled_contracts = self.dual_compiled_contracts.take().unwrap_or_default(); let linker = self.get_zk_linker(script_config)?; @@ -105,7 +103,7 @@ impl BuildData { .linker .contracts .keys() - .find(|id| id.source == target.source && id.name == target.name) + .find(|id|id.source == target.source && id.name == target.name) else { eyre::bail!("unable to find zk target artifact for linking"); }; @@ -149,20 +147,14 @@ impl BuildData { let linked_contracts = linker .zk_get_linked_artifacts( - linker - .linker - .contracts - .iter() - // we add this filter to avoid linking contracts that don't need linking - .filter(|(_, art)| { - // NOTE(zk): no need to check `deployed_bytecode` - // as those `link_references` would be the same as `bytecode` - art.bytecode - .as_ref() - .map(|bc| !bc.link_references.is_empty()) - .unwrap_or_default() - }) - .map(|(id, _)| id), + input + .artifact_ids() + .filter(|(_, artifact)| !artifact.is_unlinked()) + // we can't use the `id` directly here + // becuase we expect an iterator of references + .map(|(id, _)| { + linker.linker.contracts.get_key_value(&id).expect("id to be present").0 + }), &libraries, ) .context("retrieving all fully linked contracts")?; @@ -170,23 +162,17 @@ impl BuildData { let newly_linked_dual_compiled_contracts = linked_contracts .iter() .flat_map(|(needle, zk)| { - evm_linked_artifacts + evm_linked_contracts .iter() .find(|(id, _)| id.source == needle.source && id.name == needle.name) .map(|(_, evm)| (needle, zk, evm)) }) .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) .map(|(id, linked_zk, evm)| { - let (_, unlinked_zk_artifact) = - input - .artifact_ids() - .find(|(contract_id, _)| { - contract_id - .clone() - .with_stripped_file_prefixes(self.project_root.as_ref()) == - id.clone() - }) - .expect("unable to find original (pre-linking) artifact"); + let (_, unlinked_zk_artifact) = input + .artifact_ids() + .find(|(contract_id, _)| contract_id == id) + .expect("unable to find original (pre-linking) artifact"); let zk_bytecode = linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); let zk_hash = hash_bytecode(&zk_bytecode); @@ -233,12 +219,12 @@ impl BuildData { .artifact_ids() .map(|(id, v)| { ( - id.with_stripped_file_prefixes(self.project_root.as_ref()), + id, CompactContractBytecode::from(CompactContractBytecodeCow::from(v)), ) }) .chain(linked_contracts) - .chain(evm_linked_artifacts) + .chain(evm_linked_contracts) .collect(); Ok(contracts) @@ -436,10 +422,13 @@ impl PreprocessedState { let zk_compiler = ProjectCompiler::new().files(sources_to_compile); - zk_output = Some(zk_compiler.zksync_compile(&zk_project)?); + zk_output = Some( + zk_compiler + .zksync_compile(&zk_project)? + ); Some(DualCompiledContracts::new( &output, - &zk_output.clone().unwrap(), + zk_output.as_ref().unwrap(), &project.paths, &zk_project.paths, )) diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 3a39a7db0..79ad7e982 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -135,9 +135,10 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { let Some(input) = ctx.compilation_output.as_ref() else { return Err(LinkerError::MissingTargetArtifact); }; + let input = input.clone().with_stripped_file_prefixes(root); let contracts: ArtifactContracts> = - input.artifact_ids().map(|(id, v)| (id.with_stripped_file_prefixes(root), v)).collect(); + input.artifact_ids().collect(); let Ok(zksolc) = foundry_config::zksync::config_zksolc_compiler(config) else { tracing::error!("unable to determine zksolc compiler to be used for linking"); @@ -159,7 +160,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { } })?; - let linker = ZkLinker::new(root, contracts.clone(), zksolc, input); + let linker = ZkLinker::new(root, contracts.clone(), zksolc, &input); let zk_linker_error_to_linker = |zk_error| match zk_error { ZkLinkerError::Inner(err) => err, @@ -181,26 +182,20 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { // NOTE(zk): match with EVM nonces as we will be doing a duplex deployment for // the libs 0, - linker.linker.contracts.keys(), + linker.linker.contracts.keys(), // link everything ) .map_err(zk_linker_error_to_linker)?; let linked_contracts = linker .zk_get_linked_artifacts( - linker - .linker - .contracts - .iter() - // we add this filter to avoid linking contracts that don't need linking - .filter(|(_, art)| { - // NOTE(zk): no need to check `deployed_bytecode` - // as those `link_references` would be the same as `bytecode` - art.bytecode - .as_ref() - .map(|bc| !bc.link_references.is_empty()) - .unwrap_or_default() - }) - .map(|(id, _)| id), + input + .artifact_ids() + .filter(|(_, artifact)| !artifact.is_unlinked()) + // we can't use the `id` directly here + // becuase we expect an iterator of references + .map(|(id, _)| { + linker.linker.contracts.get_key_value(&id).expect("id to be present").0 + }), &libraries, ) .map_err(zk_linker_error_to_linker)?; @@ -218,9 +213,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { .map(|(id, linked_zk, evm)| { let (_, unlinked_zk_artifact) = input .artifact_ids() - .find(|(contract_id, _)| { - contract_id.clone().with_stripped_file_prefixes(root) == id.clone() - }) + .find(|(contract_id, _)| contract_id == id) .expect("unable to find original (pre-linking) artifact"); let zk_bytecode = linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); diff --git a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs index 99793e9c3..0e18c60f6 100644 --- a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs +++ b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs @@ -66,6 +66,19 @@ pub struct ZkContractArtifact { } impl ZkContractArtifact { + /// Returns true if contract is not linked + pub fn is_unlinked(&self) -> bool { + self.hash.is_none() || + !self.missing_libraries().map_or(true, Vec::is_empty) || + self.factory_dependencies_unlinked + .as_ref() + .and_then(|unlinked| { + self.factory_dependencies.as_ref().map(|linked| unlinked.len() - linked.len()) + }) + .unwrap_or_default() > + 0 + } + /// Get contract missing libraries pub fn missing_libraries(&self) -> Option<&Vec> { self.bytecode.as_ref().map(|bc| &bc.missing_libraries) diff --git a/crates/zksync/compilers/src/dual_compiled_contracts.rs b/crates/zksync/compilers/src/dual_compiled_contracts.rs index 9023ad19d..71438984c 100644 --- a/crates/zksync/compilers/src/dual_compiled_contracts.rs +++ b/crates/zksync/compilers/src/dual_compiled_contracts.rs @@ -5,10 +5,9 @@ use std::{ str::FromStr, }; -use foundry_compilers::ConfigurableArtifacts; use foundry_compilers::{ info::ContractInfo, solc::SolcLanguage, Artifact, ArtifactId, ArtifactOutput, - ProjectCompileOutput, ProjectPathsConfig, + ConfigurableArtifacts, ProjectCompileOutput, ProjectPathsConfig, }; use alloy_primitives::{keccak256, B256}; From 8a944b52bf8ed619c67a0b4ca2b6da5b93dafd6d Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Thu, 16 Jan 2025 20:46:57 +0100 Subject: [PATCH 26/61] fix(link:zk): invert bool --- crates/script/src/build.rs | 2 +- crates/strategy/zksync/src/executor/runner.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 8c3d4bce9..ba7ba9d16 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -149,7 +149,7 @@ impl BuildData { .zk_get_linked_artifacts( input .artifact_ids() - .filter(|(_, artifact)| !artifact.is_unlinked()) + .filter(|(_, artifact)| artifact.is_unlinked()) // we can't use the `id` directly here // becuase we expect an iterator of references .map(|(id, _)| { diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 79ad7e982..c8fcf378a 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -190,7 +190,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { .zk_get_linked_artifacts( input .artifact_ids() - .filter(|(_, artifact)| !artifact.is_unlinked()) + .filter(|(_, artifact)| artifact.is_unlinked()) // we can't use the `id` directly here // becuase we expect an iterator of references .map(|(id, _)| { From 2d1a6aaef012d41e33b22b387bd57d275e47b870 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Fri, 17 Jan 2025 09:56:31 +0100 Subject: [PATCH 27/61] fix(artifacts:zk): `is_unlinked` logic fix(link:zk): don't preemptively strip file prefixes fix(link:zk): better factory dep detection chore: formatting --- crates/linking/src/zksync.rs | 51 ++++++++++++++----- crates/script/src/build.rs | 22 +++----- crates/strategy/zksync/src/executor/runner.rs | 3 +- .../compilers/src/artifacts/contract.rs | 18 +++---- .../src/compilers/artifact_output/zk.rs | 18 +++---- 5 files changed, 64 insertions(+), 48 deletions(-) diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index 41b2242af..ae1986ef4 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -50,9 +50,7 @@ pub struct ZkLinker<'a> { impl<'a> ZkLinker<'a> { fn zk_artifacts(&'a self) -> impl Iterator + 'a { - self.compiler_output - .artifact_ids() - .map(|(id, v)| (id.with_stripped_file_prefixes(&self.linker.root), v)) + self.compiler_output.artifact_ids() } /// Construct a new `ZkLinker` @@ -390,19 +388,39 @@ impl<'a> ZkLinker<'a> { targets: impl IntoIterator, libraries: &Libraries, ) -> Result { + let mut contracts = self + .linker + .contracts + .clone() + .into_iter() + // we strip these here because the file references are also relative + // and the linker wouldn't be able to properly detect matching factory deps + // (libraries are given separately and alredy stripped) + .map(|(id, v)| (id.with_stripped_file_prefixes(&self.linker.root), v)) + .collect(); let mut targets = targets.into_iter().cloned().collect::>(); - let mut contracts = self.linker.contracts.clone(); let mut linked_artifacts = vec![]; - // TODO(zk): determine if this loop is still needed like this // explanation below while let Some(id) = targets.pop_front() { - match Self::zk_link(&contracts, &id, libraries, &self.compiler) { + if linked_artifacts.iter().any(|(linked, _)| linked == &id) { + // skip already linked + continue; + } + + match Self::zk_link( + &contracts, + // we strip here _only_ so that the target matches what's in `contracts` + // but we want to return the full id in the `linked_artifacts` + &id.clone().with_stripped_file_prefixes(&self.linker.root), + libraries, + &self.compiler, + ) { Ok(linked) => { - // persist linked contract for successive iterations *contracts.entry(id.clone()).or_default() = linked.clone(); - linked_artifacts.push((id.clone(), CompactContractBytecode::from(linked))); + // persist linked contract for successive iterations + linked_artifacts.push((id, CompactContractBytecode::from(linked))); } // contract was ignored, no need to add it to the list of linked contracts Err(ZkLinkerError::MissingFactoryDeps(fdeps)) => { @@ -412,20 +430,29 @@ impl<'a> ZkLinker<'a> { // and instead `id` remains unlinked // TODO(zk): might be unnecessary, observed when paths were wrong let mut ids = fdeps - .into_iter() + .iter() .flat_map(|fdep| { contracts.iter().find(|(id, _)| { - id.source.as_path() == Path::new(fdep.filename.as_str()) - && id.name == fdep.library + // strip here to match against the fdep which is stripped + let id = + (*id).clone().with_stripped_file_prefixes(&self.linker.root); + id.source.as_path() == Path::new(fdep.filename.as_str()) && + id.name == fdep.library }) }) + // we want to keep the non-stripped .map(|(id, _)| id.clone()) .peekable(); // if we have no dep ids then we avoid // queueing our own id to avoid infinite loop // TODO(zk): find a better way to avoid issues later - if ids.peek().is_some() { + if let Some(sample_dep) = ids.peek() { + // ensure that the sample_dep is in `contracts` + if contracts.get(sample_dep).is_none() { + return Err(ZkLinkerError::MissingFactoryDeps(fdeps)); + } + targets.extend(ids); // queue factory deps for linking targets.push_back(id); // reque original target } diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index ba7ba9d16..bfdf721f4 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -64,7 +64,7 @@ impl BuildData { eyre::bail!("unable to link zk artifacts if no zk compilation output is provided") }; - Ok(ZkLinker::new(self.project_root.clone(), input.artifact_ids().map(|(id, v)| (id.with_stripped_file_prefixes(&self.project_root), v)).collect(), zksolc, input)) + Ok(ZkLinker::new(self.project_root.clone(), input.artifact_ids().collect(), zksolc, input)) } /// Will attempt linking via `zksolc` @@ -90,20 +90,18 @@ impl BuildData { let Some(input) = self.zk_output.as_ref() else { eyre::bail!("unable to link zk artifacts if no zk compilation output is provided"); }; - let input = input.clone().with_stripped_file_prefixes(&self.project_root); - + let mut dual_compiled_contracts = self.dual_compiled_contracts.take().unwrap_or_default(); let linker = self.get_zk_linker(script_config)?; // NOTE(zk): translate solc ArtifactId to zksolc otherwise // we won't be able to find it in the zksolc output - let target = self.target.clone().with_stripped_file_prefixes(self.project_root.as_ref()); let Some(target) = linker .linker .contracts .keys() - .find(|id|id.source == target.source && id.name == target.name) + .find(|id| id.source == self.target.source && id.name == self.target.name) else { eyre::bail!("unable to find zk target artifact for linking"); }; @@ -201,7 +199,7 @@ impl BuildData { unlinked_zk_artifact.factory_dependencies.iter().flatten().map( |(hash, _)| { H256::from_slice( - alloy_primitives::hex::decode(dbg!(hash)) + alloy_primitives::hex::decode(hash) .expect("malformed factory dep hash") .as_slice(), ) @@ -217,12 +215,7 @@ impl BuildData { // base zksolc contracts + newly linked + evm contracts let contracts = input .artifact_ids() - .map(|(id, v)| { - ( - id, - CompactContractBytecode::from(CompactContractBytecodeCow::from(v)), - ) - }) + .map(|(id, v)| (id, CompactContractBytecode::from(CompactContractBytecodeCow::from(v)))) .chain(linked_contracts) .chain(evm_linked_contracts) .collect(); @@ -422,10 +415,7 @@ impl PreprocessedState { let zk_compiler = ProjectCompiler::new().files(sources_to_compile); - zk_output = Some( - zk_compiler - .zksync_compile(&zk_project)? - ); + zk_output = Some(zk_compiler.zksync_compile(&zk_project)?); Some(DualCompiledContracts::new( &output, zk_output.as_ref().unwrap(), diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index c8fcf378a..140b981fe 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -135,7 +135,6 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { let Some(input) = ctx.compilation_output.as_ref() else { return Err(LinkerError::MissingTargetArtifact); }; - let input = input.clone().with_stripped_file_prefixes(root); let contracts: ArtifactContracts> = input.artifact_ids().collect(); @@ -243,7 +242,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { unlinked_zk_artifact.factory_dependencies.iter().flatten().map( |(hash, _)| { H256::from_slice( - alloy_primitives::hex::decode(dbg!(hash)) + alloy_primitives::hex::decode(hash) .expect("malformed factory dep hash") .as_slice(), ) diff --git a/crates/zksync/compilers/src/artifacts/contract.rs b/crates/zksync/compilers/src/artifacts/contract.rs index 37672baca..3055d43e2 100644 --- a/crates/zksync/compilers/src/artifacts/contract.rs +++ b/crates/zksync/compilers/src/artifacts/contract.rs @@ -64,15 +64,15 @@ fn storage_layout_is_empty(storage_layout: &StorageLayout) -> bool { impl Contract { /// Returns true if contract is not linked pub fn is_unlinked(&self) -> bool { - self.hash.is_none() || - !self.missing_libraries.is_empty() || - self.factory_dependencies_unlinked - .as_ref() - .and_then(|unlinked| { - self.factory_dependencies.as_ref().map(|linked| unlinked.len() - linked.len()) - }) - .unwrap_or_default() > - 0 + let unlinked_fdeps = self + .factory_dependencies_unlinked + .as_ref() + .map(|unlinked| unlinked.len()) + .unwrap_or_default(); + let linked_fdeps = + self.factory_dependencies.as_ref().map(|linked| linked.len()).unwrap_or_default(); + + !self.missing_libraries.is_empty() || unlinked_fdeps - linked_fdeps > 0 } /// takes missing libraries output and transforms into link references diff --git a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs index 0e18c60f6..736610c81 100644 --- a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs +++ b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs @@ -68,15 +68,15 @@ pub struct ZkContractArtifact { impl ZkContractArtifact { /// Returns true if contract is not linked pub fn is_unlinked(&self) -> bool { - self.hash.is_none() || - !self.missing_libraries().map_or(true, Vec::is_empty) || - self.factory_dependencies_unlinked - .as_ref() - .and_then(|unlinked| { - self.factory_dependencies.as_ref().map(|linked| unlinked.len() - linked.len()) - }) - .unwrap_or_default() > - 0 + let unlinked_fdeps = self + .factory_dependencies_unlinked + .as_ref() + .map(|unlinked| unlinked.len()) + .unwrap_or_default(); + let linked_fdeps = + self.factory_dependencies.as_ref().map(|linked| linked.len()).unwrap_or_default(); + + !self.missing_libraries().map_or(true, Vec::is_empty) || unlinked_fdeps - linked_fdeps > 0 } /// Get contract missing libraries From 32e29136b0e270c85f3e9476cc1b135d67f22042 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Fri, 17 Jan 2025 19:58:07 +0100 Subject: [PATCH 28/61] feat(strategy): `deploy_library` CREATE2 mode feat(strategy): return "broadcastable" tx feat(link:zk): populate newly linked factory deps feat(script): use `deploy_library` --- crates/evm/evm/src/executors/mod.rs | 18 ++- crates/evm/evm/src/executors/strategy.rs | 81 +++++++++-- crates/forge/src/runner.rs | 13 +- crates/script/src/build.rs | 49 ++++--- crates/script/src/runner.rs | 81 +++++------ crates/strategy/zksync/src/executor/runner.rs | 132 ++++++++++++------ .../compilers/src/dual_compiled_contracts.rs | 64 ++++++++- 7 files changed, 307 insertions(+), 131 deletions(-) diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 0a6db6f5b..66eaedcb1 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -16,6 +16,7 @@ use alloy_primitives::{ Address, Bytes, Log, U256, }; use alloy_sol_types::{sol, SolCall}; +use foundry_common::TransactionMaybeSigned; use foundry_evm_core::{ backend::{Backend, BackendError, BackendResult, CowBackend, DatabaseExt, GLOBAL_FAIL_SLOT}, constants::{ @@ -40,7 +41,7 @@ use std::{ borrow::Cow, time::{Duration, Instant}, }; -use strategy::ExecutorStrategy; +use strategy::{DeployLibKind, ExecutorStrategy}; mod builder; pub use builder::ExecutorBuilder; @@ -305,16 +306,19 @@ impl Executor { /// Deploys a library contract and commits the new state to the underlying database. /// - /// Executes a CREATE transaction with the contract `code` and persistent database state - /// modifications. + /// Executes a `deploy_kind` transaction with the provided parameters + /// and persistent database state modifications. + /// + /// Will return a list of deployment results and transaction requests + /// Will also ensure nonce is increased for the sender pub fn deploy_library( &mut self, from: Address, - code: Bytes, + kind: DeployLibKind, value: U256, rd: Option<&RevertDecoder>, - ) -> Result, EvmError> { - self.strategy.runner.deploy_library(self, from, code, value, rd) + ) -> Result, EvmError> { + self.strategy.runner.deploy_library(self, from, kind, value, rd) } /// Deploys a contract using the given `env` and commits the new state to the underlying @@ -686,7 +690,7 @@ impl Executor { /// /// If using a backend with cheatcodes, `tx.gas_price` and `block.number` will be overwritten by /// the cheatcode state in between calls. - pub fn build_test_env( + fn build_test_env( &self, caller: Address, transact_to: TxKind, diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index 0d4ab07af..8bf1dbaba 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -1,19 +1,19 @@ use std::{any::Any, borrow::Borrow, collections::BTreeMap, fmt::Debug, path::Path}; use alloy_json_abi::JsonAbi; -use alloy_primitives::{Address, Bytes, U256}; +use alloy_primitives::{Address, Bytes, TxKind, B256, U256}; use alloy_serde::OtherFields; -use eyre::Result; +use eyre::{Context, Result}; use foundry_cheatcodes::strategy::{ CheatcodeInspectorStrategy, EvmCheatcodeInspectorStrategyRunner, }; -use foundry_common::{ContractsByArtifact, TestFunctionExt}; +use foundry_common::{ContractsByArtifact, TestFunctionExt, TransactionMaybeSigned}; use foundry_compilers::{ artifacts::Libraries, contracts::ArtifactContracts, Artifact, ArtifactId, ProjectCompileOutput, }; use foundry_config::Config; use foundry_evm_core::{ - backend::{strategy::BackendStrategy, Backend, BackendResult, CowBackend}, + backend::{strategy::BackendStrategy, Backend, BackendResult, CowBackend, DatabaseExt}, decode::RevertDecoder, }; use foundry_linking::{Linker, LinkerError}; @@ -22,7 +22,7 @@ use foundry_zksync_compilers::{ dual_compiled_contracts::DualCompiledContracts, }; use revm::{ - primitives::{Env, EnvWithHandlerCfg, ResultAndState}, + primitives::{Env, EnvWithHandlerCfg, Output, ResultAndState}, DatabaseRef, }; @@ -70,6 +70,16 @@ pub struct LinkOutput { pub libraries: Libraries, } +/// Type of library deployment +#[derive(Debug, Clone)] +pub enum DeployLibKind { + /// CREATE(bytecode) + Create(Bytes), + + /// CREATE2(salt, bytecode) + Create2(B256, Bytes), +} + impl ExecutorStrategy { pub fn new_evm() -> Self { Self { runner: &EvmExecutorStrategyRunner, context: Box::new(()) } @@ -111,10 +121,10 @@ pub trait ExecutorStrategyRunner: Debug + Send + Sync + ExecutorStrategyExt { &self, executor: &mut Executor, from: Address, - code: Bytes, + input: DeployLibKind, value: U256, rd: Option<&RevertDecoder>, - ) -> Result, EvmError>; + ) -> Result, EvmError>; /// Execute a transaction and *WITHOUT* applying state changes. fn call( @@ -256,8 +266,8 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { let Some(abi) = &contract.abi else { continue }; // if it's a test, link it and add to deployable contracts - if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && - abi.functions().any(|func| func.name.is_any_test()) + if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) + && abi.functions().any(|func| func.name.is_any_test()) { let Some(bytecode) = contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty()) @@ -286,11 +296,58 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { &self, executor: &mut Executor, from: Address, - code: Bytes, + kind: DeployLibKind, value: U256, rd: Option<&RevertDecoder>, - ) -> Result, EvmError> { - executor.deploy(from, code, value, rd).map(|dr| vec![dr]) + ) -> Result, EvmError> { + let nonce = self.get_nonce(executor, from).context("retrieving sender nonce")?; + + match kind { + DeployLibKind::Create(code) => { + executor.deploy(from, code.clone(), value, rd).map(|dr| { + let mut request = TransactionMaybeSigned::new(Default::default()); + let unsigned = request.as_unsigned_mut().unwrap(); + unsigned.from = Some(from); + unsigned.input = code.into(); + unsigned.nonce = Some(nonce); + + vec![(dr, request)] + }) + } + DeployLibKind::Create2(salt, code) => { + let create2_deployer = executor.create2_deployer(); + let calldata: Bytes = [salt.as_ref(), code.as_ref()].concat().into(); + let result = + executor.transact_raw(from, create2_deployer, calldata.clone(), value)?; + let result = result.into_result(rd)?; + + let Some(Output::Create(_, Some(address))) = result.out else { + return Err(eyre::eyre!( + "Deployment succeeded, but no address was returned: {result:#?}" + ) + .into()); + }; + + // also mark this library as persistent, this will ensure that the state of the + // library is persistent across fork swaps in forking mode + executor.backend_mut().add_persistent_account(address); + debug!(%address, "deployed contract with create2"); + + let mut request = TransactionMaybeSigned::new(Default::default()); + let unsigned = request.as_unsigned_mut().unwrap(); + unsigned.from = Some(from); + unsigned.input = calldata.into(); + unsigned.nonce = Some(nonce); + unsigned.to = Some(TxKind::Call(create2_deployer)); + + // manually increase nonce when performing CALLs + executor + .set_nonce(from, nonce + 1) + .context("increasing nonce after CREATE2 deployment")?; + + Ok(vec![(DeployResult { raw: result, address }, request)]) + } + } } fn call( diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 093b0fe09..552b8fa30 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -21,6 +21,7 @@ use foundry_evm::{ invariant::{ check_sequence, replay_error, replay_run, InvariantExecutor, InvariantFuzzError, }, + strategy::DeployLibKind, CallResult, EvmError, Executor, ITest, RawCallResult, }, fuzz::{ @@ -126,7 +127,7 @@ impl<'a> ContractRunner<'a> { for code in self.mcr.libs_to_deploy.iter() { let deploy_result = self.executor.deploy_library( LIBRARY_DEPLOYER, - code.clone(), + DeployLibKind::Create(code.clone()), U256::ZERO, Some(&self.mcr.revert_decoder), ); @@ -136,7 +137,9 @@ impl<'a> ContractRunner<'a> { Ok(deployments) => deployments.into_iter().map(Ok).collect(), }; - for deploy_result in deployments { + for deploy_result in + deployments.into_iter().map(|result| result.map(|(deployment, _)| deployment)) + { // Record deployed library address. if let Ok(deployed) = &deploy_result { result.deployed_libs.push(deployed.address); @@ -314,7 +317,7 @@ impl<'a> ContractRunner<'a> { [("setUp()".to_string(), TestResult::fail("multiple setUp functions".to_string()))] .into(), warnings, - ) + ); } // Check if `afterInvariant` function with valid signature declared. @@ -330,7 +333,7 @@ impl<'a> ContractRunner<'a> { )] .into(), warnings, - ) + ); } let call_after_invariant = after_invariant_fns.first().is_some_and(|after_invariant_fn| { let match_sig = after_invariant_fn.name == "afterInvariant"; @@ -364,7 +367,7 @@ impl<'a> ContractRunner<'a> { start.elapsed(), [("setUp()".to_string(), TestResult::setup_result(setup))].into(), warnings, - ) + ); } // Filter out functions sequentially since it's very fast and there is no need to do it diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index bfdf721f4..5e1e3952a 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -26,7 +26,7 @@ use foundry_zksync_compilers::{ dual_compiled_contracts::{DualCompiledContract, DualCompiledContracts}, }; use foundry_zksync_core::{hash_bytecode, DEFAULT_CREATE2_DEPLOYER_ZKSYNC, H256}; -use std::{path::PathBuf, str::FromStr, sync::Arc}; +use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::Arc}; /// Container for the compiled contracts. #[derive(Debug)] @@ -171,6 +171,7 @@ impl BuildData { .artifact_ids() .find(|(contract_id, _)| contract_id == id) .expect("unable to find original (pre-linking) artifact"); + let zk_bytecode = linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); let zk_hash = hash_bytecode(&zk_bytecode); @@ -182,7 +183,7 @@ impl BuildData { let contract = DualCompiledContract { zk_bytecode_hash: zk_hash, zk_deployed_bytecode: zk_bytecode.to_vec(), - // TODO(zk): retrieve unlinked factory deps (1.5.9) + // rest of factory deps is populated later zk_factory_deps: vec![zk_bytecode.to_vec()], evm_bytecode_hash: B256::from_slice(&keccak256(evm.as_ref())[..]), // TODO(zk): determine if this is ok, as it's @@ -192,24 +193,40 @@ impl BuildData { }; ( - contract_info, - // populate factory deps that were already linked - dual_compiled_contracts.extend_factory_deps_by_hash( - contract, - unlinked_zk_artifact.factory_dependencies.iter().flatten().map( - |(hash, _)| { - H256::from_slice( - alloy_primitives::hex::decode(hash) - .expect("malformed factory dep hash") - .as_slice(), - ) - }, - ), + (contract_info.clone(), contract), + ( + contract_info, + unlinked_zk_artifact + .factory_dependencies_unlinked + .clone() + .unwrap_or_default(), ), ) }); - dual_compiled_contracts.extend(newly_linked_dual_compiled_contracts.collect::>()); + let (new_contracts, new_contracts_deps): (Vec<_>, HashMap<_, _>) = + newly_linked_dual_compiled_contracts.unzip(); + dual_compiled_contracts.extend(new_contracts); + + // now that we have an updated list of DualCompiledContracts + // retrieve all the factory deps for a given contracts and store them + new_contracts_deps.into_iter().for_each(|(info, deps)| { + deps.into_iter().for_each(|dep| { + let mut split = dep.split(':'); + let path = split.next().expect("malformed factory dep path"); + let name = split.next().expect("malformed factory dep name"); + + let bytecode = dual_compiled_contracts + .find(Some(path), Some(name)) + .next() + .expect("unknown factory dep") + .zk_deployed_bytecode + .clone(); + + dual_compiled_contracts.insert_factory_deps(&info, Some(bytecode)); + }); + }); + self.dual_compiled_contracts.replace(dual_compiled_contracts); // base zksolc contracts + newly linked + evm contracts diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index 6fd579f10..37aed16c1 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -1,15 +1,16 @@ use super::ScriptResult; use crate::build::ScriptPredeployLibraries; use alloy_eips::eip7702::SignedAuthorization; -use alloy_primitives::{Address, Bytes, TxKind, U256}; -use alloy_rpc_types::TransactionRequest; +use alloy_primitives::{Address, Bytes, U256}; use alloy_serde::OtherFields; use eyre::Result; use foundry_cheatcodes::BroadcastableTransaction; use foundry_config::Config; use foundry_evm::{ constants::CALLER, - executors::{DeployResult, EvmError, ExecutionErr, Executor, RawCallResult}, + executors::{ + strategy::DeployLibKind, DeployResult, EvmError, ExecutionErr, Executor, RawCallResult, + }, opts::EvmOpts, revm::interpreter::{return_ok, InstructionResult}, traces::{TraceKind, Traces}, @@ -62,26 +63,26 @@ impl ScriptRunner { // Deploy libraries match libraries { ScriptPredeployLibraries::Default(libraries) => libraries.iter().for_each(|code| { - let result = self + let results = self .executor - .deploy(self.evm_opts.sender, code.clone(), U256::ZERO, None) - .expect("couldn't deploy library") - .raw; + .deploy_library( + self.evm_opts.sender, + DeployLibKind::Create(code.clone()), + U256::ZERO, + None, + ) + .expect("couldn't deploy library"); + + for (result, transaction) in results { + if let Some(deploy_traces) = result.raw.traces { + traces.push((TraceKind::Deployment, deploy_traces)); + } - if let Some(deploy_traces) = result.traces { - traces.push((TraceKind::Deployment, deploy_traces)); + library_transactions.push_back(BroadcastableTransaction { + rpc: self.evm_opts.fork_url.clone(), + transaction, + }) } - - library_transactions.push_back(BroadcastableTransaction { - rpc: self.evm_opts.fork_url.clone(), - transaction: TransactionRequest { - from: Some(self.evm_opts.sender), - input: code.clone().into(), - nonce: Some(sender_nonce + library_transactions.len() as u64), - ..Default::default() - } - .into(), - }) }), ScriptPredeployLibraries::Create2(libraries, salt) => { let create2_deployer = self.executor.create2_deployer(); @@ -91,40 +92,28 @@ impl ScriptRunner { if !self.executor.is_empty_code(address)? { continue; } - let calldata = [salt.as_ref(), library.as_ref()].concat(); - let result = self + + let results = self .executor - .transact_raw( + .deploy_library( self.evm_opts.sender, - create2_deployer, - calldata.clone().into(), + DeployLibKind::Create2(salt.clone(), library.clone()), U256::from(0), + None, ) .expect("couldn't deploy library"); - if let Some(deploy_traces) = result.traces { - traces.push((TraceKind::Deployment, deploy_traces)); - } - - library_transactions.push_back(BroadcastableTransaction { - rpc: self.evm_opts.fork_url.clone(), - transaction: TransactionRequest { - from: Some(self.evm_opts.sender), - input: calldata.into(), - nonce: Some(sender_nonce + library_transactions.len() as u64), - to: Some(TxKind::Call(create2_deployer)), - ..Default::default() + for (result, transaction) in results { + if let Some(deploy_traces) = result.raw.traces { + traces.push((TraceKind::Deployment, deploy_traces)); } - .into(), - }); - } - // Sender nonce is not incremented when performing CALLs. We need to manually - // increase it. - self.executor.set_nonce( - self.evm_opts.sender, - sender_nonce + library_transactions.len() as u64, - )?; + library_transactions.push_back(BroadcastableTransaction { + rpc: self.evm_opts.fork_url.clone(), + transaction, + }); + } + } } }; diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 140b981fe..cae28b1de 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -1,30 +1,35 @@ -use std::path::Path; +use std::{collections::HashMap, path::Path}; use alloy_primitives::{keccak256, Address, Bytes, TxKind, B256, U256}; use alloy_rpc_types::serde_helpers::OtherFields; -use alloy_zksync::provider::{zksync_provider, ZksyncProvider}; +use alloy_zksync::{ + contracts::l2::contract_deployer::CONTRACT_DEPLOYER_ADDRESS, + provider::{zksync_provider, ZksyncProvider}, +}; use eyre::Result; use foundry_linking::{ LinkerError, ZkLinker, ZkLinkerError, DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, }; use revm::{ - primitives::{CreateScheme, Env, EnvWithHandlerCfg, ResultAndState}, + primitives::{CreateScheme, Env, EnvWithHandlerCfg, Output, ResultAndState}, Database, }; +use tracing::debug; use zksync_types::H256; -use foundry_common::ContractsByArtifact; +use foundry_common::{ContractsByArtifact, TransactionMaybeSigned}; use foundry_compilers::{ artifacts::CompactContractBytecodeCow, contracts::ArtifactContracts, info::ContractInfo, Artifact, ProjectCompileOutput, }; use foundry_config::Config; use foundry_evm::{ - backend::{Backend, BackendResult, CowBackend}, + backend::{Backend, BackendResult, CowBackend, DatabaseExt}, + constants::DEFAULT_CREATE2_DEPLOYER, decode::RevertDecoder, executors::{ strategy::{ - EvmExecutorStrategyRunner, ExecutorStrategyContext, ExecutorStrategyExt, + DeployLibKind, EvmExecutorStrategyRunner, ExecutorStrategyContext, ExecutorStrategyExt, ExecutorStrategyRunner, LinkOutput, }, DeployResult, EvmError, Executor, @@ -35,7 +40,10 @@ use foundry_zksync_compilers::{ compilers::{artifact_output::zk::ZkArtifactOutput, zksolc::ZkSolcCompiler}, dual_compiled_contracts::{DualCompiledContract, DualCompiledContracts}, }; -use foundry_zksync_core::{encode_create_params, hash_bytecode, vm::ZkEnv, ZkTransactionMetadata}; +use foundry_zksync_core::{ + encode_create_params, hash_bytecode, vm::ZkEnv, ZkTransactionMetadata, + DEFAULT_CREATE2_DEPLOYER_ZKSYNC, +}; use crate::{ backend::{ZksyncBackendStrategyBuilder, ZksyncInspectContext}, @@ -225,7 +233,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { let contract = DualCompiledContract { zk_bytecode_hash: zk_hash, zk_deployed_bytecode: zk_bytecode.to_vec(), - // TODO(zk): retrieve unlinked factory deps (1.5.9) + // rest of factory deps is populated later zk_factory_deps: vec![zk_bytecode.to_vec()], evm_bytecode_hash: B256::from_slice(&keccak256(evm.as_ref())[..]), // TODO(zk): determine if this is ok, as it's @@ -235,25 +243,40 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { }; ( - contract_info, - // populate factory deps that were already linked - ctx.dual_compiled_contracts.extend_factory_deps_by_hash( - contract, - unlinked_zk_artifact.factory_dependencies.iter().flatten().map( - |(hash, _)| { - H256::from_slice( - alloy_primitives::hex::decode(hash) - .expect("malformed factory dep hash") - .as_slice(), - ) - }, - ), + (contract_info.clone(), contract), + ( + contract_info, + unlinked_zk_artifact + .factory_dependencies_unlinked + .clone() + .unwrap_or_default(), ), ) }); - ctx.dual_compiled_contracts - .extend(newly_linked_dual_compiled_contracts.collect::>()); + let (new_contracts, new_contracts_deps): (Vec<_>, HashMap<_, _>) = + newly_linked_dual_compiled_contracts.unzip(); + ctx.dual_compiled_contracts.extend(new_contracts); + + // now that we have an updated list of DualCompiledContracts + // retrieve all the factory deps for a given contracts and store them + new_contracts_deps.into_iter().for_each(|(info, deps)| { + deps.into_iter().for_each(|dep| { + let mut split = dep.split(':'); + let path = split.next().expect("malformed factory dep path"); + let name = split.next().expect("malformed factory dep name"); + + let bytecode = ctx + .dual_compiled_contracts + .find(Some(path), Some(name)) + .next() + .expect("unknown factory dep") + .zk_deployed_bytecode + .clone(); + + ctx.dual_compiled_contracts.insert_factory_deps(&info, Some(bytecode)); + }); + }); let linked_contracts: ArtifactContracts = contracts .into_iter() @@ -280,10 +303,10 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { &self, executor: &mut Executor, from: Address, - code: Bytes, + kind: DeployLibKind, value: U256, rd: Option<&RevertDecoder>, - ) -> Result, EvmError> { + ) -> Result, EvmError> { // sync deployer account info let nonce = EvmExecutorStrategyRunner.get_nonce(executor, from).expect("deployer to exist"); let balance = @@ -294,12 +317,23 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { tracing::debug!(?nonce, ?balance, sender = ?from, "deploying lib in EraVM"); let mut evm_deployment = - EvmExecutorStrategyRunner.deploy_library(executor, from, code.clone(), value, rd)?; + EvmExecutorStrategyRunner.deploy_library(executor, from, kind.clone(), value, rd)?; let ctx = get_context(executor.strategy.context.as_mut()); + let (code, create_scheme, to) = match kind { + DeployLibKind::Create(bytes) => { + (bytes, CreateScheme::Create, CONTRACT_DEPLOYER_ADDRESS) + } + DeployLibKind::Create2(salt, bytes) => ( + bytes, + CreateScheme::Create2 { salt: salt.into() }, + DEFAULT_CREATE2_DEPLOYER_ZKSYNC, + ), + }; + // lookup dual compiled contract based on EVM bytecode - let Some((_, dual_contract)) = + let Some((dual_contract_info, dual_contract)) = ctx.dual_compiled_contracts.find_by_evm_bytecode(code.as_ref()) else { // we don't know what the equivalent zk contract would be @@ -307,33 +341,45 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { }; // no need for constructor args as it's a lib - let create_params = - encode_create_params(&CreateScheme::Create, dual_contract.zk_bytecode_hash, vec![]); + let create_params: Bytes = + encode_create_params(&create_scheme, dual_contract.zk_bytecode_hash, vec![]).into(); // populate ctx.transaction_context with factory deps // we also populate the ctx so the deployment is executed // entirely in EraVM let factory_deps = ctx.dual_compiled_contracts.fetch_all_factory_deps(dual_contract); + tracing::debug!(n_fdeps = factory_deps.len()); // persist existing paymaster data (TODO(zk): is this needed?) let paymaster_data = ctx.transaction_context.take().and_then(|metadata| metadata.paymaster_data); ctx.transaction_context = Some(ZkTransactionMetadata { factory_deps, paymaster_data }); - // eravm_env: call to ContractDeployer w/ properly encoded calldata - let env = executor.build_test_env( - from, - // foundry_zksync_core::vm::runner::transact takes care of using the ContractDeployer - // address - TxKind::Create, - create_params.into(), - value, - ); - - executor.deploy_with_env(env, rd).map(move |dr| { - evm_deployment.push(dr); - evm_deployment - }) + let result = executor.transact_raw(from, to, create_params.clone(), value)?; + let result = result.into_result(rd)?; + + let Some(Output::Create(_, Some(address))) = result.out else { + return Err(eyre::eyre!( + "Deployment succeeded, but no address was returned: {result:#?}" + ) + .into()); + }; + + // also mark this library as persistent, this will ensure that the state of the library is + // persistent across fork swaps in forking mode + executor.backend_mut().add_persistent_account(address); + debug!(%address, "deployed contract with create2"); + + let mut request = TransactionMaybeSigned::new(Default::default()); + let unsigned = request.as_unsigned_mut().unwrap(); + unsigned.from = Some(from); + unsigned.input = create_params.into(); + unsigned.nonce = Some(nonce); + // we use the deployer here for consistency with linking + unsigned.to = Some(TxKind::Call(to)); + + evm_deployment.push((DeployResult { raw: result, address }, request)); + Ok(evm_deployment) } fn new_backend_strategy(&self) -> foundry_evm_core::backend::strategy::BackendStrategy { diff --git a/crates/zksync/compilers/src/dual_compiled_contracts.rs b/crates/zksync/compilers/src/dual_compiled_contracts.rs index 71438984c..3568de375 100644 --- a/crates/zksync/compilers/src/dual_compiled_contracts.rs +++ b/crates/zksync/compilers/src/dual_compiled_contracts.rs @@ -6,8 +6,8 @@ use std::{ }; use foundry_compilers::{ - info::ContractInfo, solc::SolcLanguage, Artifact, ArtifactId, ArtifactOutput, - ConfigurableArtifacts, ProjectCompileOutput, ProjectPathsConfig, + info::ContractInfo, solc::SolcLanguage, Artifact, ArtifactId, ProjectCompileOutput, + ProjectPathsConfig, }; use alloy_primitives::{keccak256, B256}; @@ -308,6 +308,51 @@ impl DualCompiledContracts { self.contracts.insert(info, contract); } + /// Attempt reading an existing `[DualCompiledContract]` + pub fn get(&self, info: &ContractInfo) -> Option<&DualCompiledContract> { + self.contracts.get(info) + } + + /// Search for matching contracts in the collection + /// + /// Contracts are ordered in descending best-fit order + pub fn find<'a: 'b, 'b>( + &'a self, + path: Option<&'b str>, + name: Option<&'b str>, + ) -> impl Iterator + 'b { + let full_matches = self.contracts.iter().filter(move |(info, _)| { + // if user provides a path we should check that it matches + // we check using `ends_with` to account for prefixes + path.map_or(true, |needle| + info.path.as_ref() + .map_or(true, + |contract_path| contract_path.ends_with(needle))) + // if user provides a name we should check that it matches + && name.map_or(true, |name| name == info.name.as_str()) + }); + + let path_matches = self.contracts.iter().filter(move |(info, _)| { + // if a path is provided, check that it matches + // if no path is provided, don't match it + path.map_or(false, |needle| { + info.path.as_ref().map_or(false, |contract_path| contract_path.ends_with(needle)) + }) + }); + + let name_matches = self.contracts.iter().filter(move |(info, _)| { + // if name is provided, check that it matches + // if no name is provided, don't match it + name.map(|name| name == info.name.as_str()).unwrap_or(false) + }); + + full_matches + .chain(path_matches) + .chain(name_matches) + .map(|(_, contract)| contract) + .into_iter() + } + /// Retrieves the length of the collection. pub fn len(&self) -> usize { self.contracts.len() @@ -337,4 +382,19 @@ impl DualCompiledContracts { target.zk_factory_deps.extend(deps_bytecodes); target } + + /// Populate the target's factory deps based on the new list + /// + /// Will return `None` if no matching `target` exists + /// Will not override existing factory deps + pub fn insert_factory_deps( + &mut self, + target: &ContractInfo, + factory_deps: impl IntoIterator>, + ) -> Option<&DualCompiledContract> { + self.contracts.get_mut(target).map(|contract| { + contract.zk_factory_deps.extend(factory_deps); + &*contract + }) + } } From b313febc434491023bdbe87b26dbcaae87dcded2 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Fri, 17 Jan 2025 20:07:39 +0100 Subject: [PATCH 29/61] fix(zk:transact): detect direct deployments --- crates/zksync/core/src/vm/runner.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/zksync/core/src/vm/runner.rs b/crates/zksync/core/src/vm/runner.rs index 647d0d9d9..f927229af 100644 --- a/crates/zksync/core/src/vm/runner.rs +++ b/crates/zksync/core/src/vm/runner.rs @@ -9,7 +9,7 @@ use tracing::{debug, info}; use zksync_basic_types::H256; use zksync_types::{ ethabi, fee::Fee, l2::L2Tx, transaction_request::PaymasterParams, CONTRACT_DEPLOYER_ADDRESS, - U256, + CREATE2_FACTORY_ADDRESS, U256, }; use std::{cmp::min, fmt::Debug}; @@ -51,7 +51,10 @@ where let caller = env.tx.caller; let nonce = ZKVMData::new(&mut ecx).get_tx_nonce(caller); let (transact_to, is_create) = match env.tx.transact_to { - TransactTo::Call(to) => (to.to_h160(), false), + TransactTo::Call(to) => { + let to = to.to_h160(); + (to, to == CONTRACT_DEPLOYER_ADDRESS || to == CREATE2_FACTORY_ADDRESS) + } TransactTo::Create => (CONTRACT_DEPLOYER_ADDRESS, true), }; @@ -316,7 +319,7 @@ fn get_historical_block_hashes(ecx: &mut EvmContext) -> HashMa let (block_number, overflow) = ecx.env.block.number.overflowing_sub(alloy_primitives::U256::from(i)); if overflow { - break + break; } match ecx.block_hash(block_number.to_u256().as_u64()) { Ok(block_hash) => { From 956383362b699573679f971ba3d2d813cdafa905 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Fri, 17 Jan 2025 20:25:30 +0100 Subject: [PATCH 30/61] feat(script:zk): match CREATE/CREATE2 with EVM --- crates/script/src/build.rs | 22 ++++++------------- crates/strategy/zksync/src/executor/runner.rs | 2 +- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 5e1e3952a..415b8360c 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -82,6 +82,7 @@ impl BuildData { script_config: &ScriptConfig, known_libraries: Libraries, evm_linked_contracts: ArtifactContracts, + use_create2: bool, ) -> Result { if !script_config.config.zksync.should_compile() { return Ok(evm_linked_contracts); @@ -107,17 +108,7 @@ impl BuildData { }; let create2_deployer = DEFAULT_CREATE2_DEPLOYER_ZKSYNC; - let can_use_create2 = if let Some(fork_url) = &script_config.evm_opts.fork_url { - let provider = try_get_http_provider(fork_url)?; - let deployer_code = provider.get_code_at(create2_deployer).await?; - - !deployer_code.is_empty() - } else { - // If --fork-url is not provided, we are just simulating the script. - true - }; - - let maybe_create2_link_output = can_use_create2 + let maybe_create2_link_output = use_create2 .then(|| { linker .zk_link_with_create2( @@ -271,13 +262,14 @@ impl BuildData { }) .flatten(); - let (libraries, predeploy_libs) = if let Some(output) = maybe_create2_link_output { + let (libraries, predeploy_libs, uses_create2) = if let Some(output) = maybe_create2_link_output { ( output.libraries, ScriptPredeployLibraries::Create2( output.libs_to_deploy, script_config.config.create2_library_salt, ), + true ) } else { let output = self.get_linker().link_with_nonce_or_address( @@ -287,14 +279,14 @@ impl BuildData { [&self.target], )?; - (output.libraries, ScriptPredeployLibraries::Default(output.libs_to_deploy)) + (output.libraries, ScriptPredeployLibraries::Default(output.libs_to_deploy), false) }; let known_contracts = self .get_linker() .get_linked_artifacts(&libraries) .context("retrieving fully linked artifacts")?; - let known_contracts = self.zk_link(script_config, known_libraries, known_contracts).await?; + let known_contracts = self.zk_link(script_config, known_libraries, known_contracts, uses_create2).await?; LinkedBuildData::new( libraries, @@ -313,7 +305,7 @@ impl BuildData { ) -> Result { let known_contracts = self.get_linker().get_linked_artifacts(&libraries)?; let known_contracts = - self.zk_link(script_config, libraries.clone(), known_contracts).await?; + self.zk_link(script_config, libraries.clone(), known_contracts, false).await?; LinkedBuildData::new( libraries, diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index cae28b1de..22c56988b 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -333,7 +333,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { }; // lookup dual compiled contract based on EVM bytecode - let Some((dual_contract_info, dual_contract)) = + let Some((_, dual_contract)) = ctx.dual_compiled_contracts.find_by_evm_bytecode(code.as_ref()) else { // we don't know what the equivalent zk contract would be From 7e2a2d839fa553116668a7544a1963f55c8a8002 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 20 Jan 2025 12:39:03 +0100 Subject: [PATCH 31/61] fix(zk:libs): encode extra metadata --- crates/strategy/zksync/src/executor/runner.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 22c56988b..44f753e57 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -42,7 +42,7 @@ use foundry_zksync_compilers::{ }; use foundry_zksync_core::{ encode_create_params, hash_bytecode, vm::ZkEnv, ZkTransactionMetadata, - DEFAULT_CREATE2_DEPLOYER_ZKSYNC, + DEFAULT_CREATE2_DEPLOYER_ZKSYNC, ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY, }; use crate::{ @@ -353,7 +353,8 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { // persist existing paymaster data (TODO(zk): is this needed?) let paymaster_data = ctx.transaction_context.take().and_then(|metadata| metadata.paymaster_data); - ctx.transaction_context = Some(ZkTransactionMetadata { factory_deps, paymaster_data }); + let metadata = ZkTransactionMetadata { factory_deps, paymaster_data }; + ctx.transaction_context = Some(metadata.clone()); let result = executor.transact_raw(from, to, create_params.clone(), value)?; let result = result.into_result(rd)?; @@ -377,6 +378,10 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { unsigned.nonce = Some(nonce); // we use the deployer here for consistency with linking unsigned.to = Some(TxKind::Call(to)); + unsigned.other.insert( + ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY.to_string(), + serde_json::to_value(metadata).expect("failed encoding json"), + ); evm_deployment.push((DeployResult { raw: result, address }, request)); Ok(evm_deployment) From 3a2513418e5c2093cefada1702e3685e46a646c2 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 20 Jan 2025 20:23:38 +0100 Subject: [PATCH 32/61] refactor(executors): `DeployLibResult` fix(lib:zk): remove EVM lib deployments from broadcastable txs --- crates/evm/evm/src/executors/mod.rs | 4 +- crates/evm/evm/src/executors/strategy.rs | 20 +++++++-- crates/forge/src/runner.rs | 4 +- crates/forge/tests/it/test_helpers.rs | 6 ++- crates/forge/tests/it/zk/linking.rs | 44 +++++++++---------- crates/script/src/runner.rs | 27 +++++++----- crates/strategy/zksync/src/executor/runner.rs | 15 +++++-- 7 files changed, 73 insertions(+), 47 deletions(-) diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 66eaedcb1..0d3447d0d 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -41,7 +41,7 @@ use std::{ borrow::Cow, time::{Duration, Instant}, }; -use strategy::{DeployLibKind, ExecutorStrategy}; +use strategy::{DeployLibKind, DeployLibResult, ExecutorStrategy}; mod builder; pub use builder::ExecutorBuilder; @@ -317,7 +317,7 @@ impl Executor { kind: DeployLibKind, value: U256, rd: Option<&RevertDecoder>, - ) -> Result, EvmError> { + ) -> Result, EvmError> { self.strategy.runner.deploy_library(self, from, kind, value, rd) } diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index 8bf1dbaba..a7af2ea79 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -80,6 +80,15 @@ pub enum DeployLibKind { Create2(B256, Bytes), } +/// Represents the result of a library deployment +#[derive(Debug)] +pub struct DeployLibResult { + /// Result of the deployment + pub result: DeployResult, + /// Equivalent transaction to deploy the given library + pub tx: Option, +} + impl ExecutorStrategy { pub fn new_evm() -> Self { Self { runner: &EvmExecutorStrategyRunner, context: Box::new(()) } @@ -124,7 +133,7 @@ pub trait ExecutorStrategyRunner: Debug + Send + Sync + ExecutorStrategyExt { input: DeployLibKind, value: U256, rd: Option<&RevertDecoder>, - ) -> Result, EvmError>; + ) -> Result, EvmError>; /// Execute a transaction and *WITHOUT* applying state changes. fn call( @@ -299,7 +308,7 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { kind: DeployLibKind, value: U256, rd: Option<&RevertDecoder>, - ) -> Result, EvmError> { + ) -> Result, EvmError> { let nonce = self.get_nonce(executor, from).context("retrieving sender nonce")?; match kind { @@ -311,7 +320,7 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { unsigned.input = code.into(); unsigned.nonce = Some(nonce); - vec![(dr, request)] + vec![DeployLibResult { result: dr, tx: Some(request) }] }) } DeployLibKind::Create2(salt, code) => { @@ -345,7 +354,10 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { .set_nonce(from, nonce + 1) .context("increasing nonce after CREATE2 deployment")?; - Ok(vec![(DeployResult { raw: result, address }, request)]) + Ok(vec![DeployLibResult { + result: DeployResult { raw: result, address }, + tx: Some(request), + }]) } } } diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 552b8fa30..6b43f0f73 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -21,7 +21,7 @@ use foundry_evm::{ invariant::{ check_sequence, replay_error, replay_run, InvariantExecutor, InvariantFuzzError, }, - strategy::DeployLibKind, + strategy::{DeployLibKind, DeployLibResult}, CallResult, EvmError, Executor, ITest, RawCallResult, }, fuzz::{ @@ -138,7 +138,7 @@ impl<'a> ContractRunner<'a> { }; for deploy_result in - deployments.into_iter().map(|result| result.map(|(deployment, _)| deployment)) + deployments.into_iter().map(|result| result.map(|deployment| deployment.result)) { // Record deployed library address. if let Ok(deployed) = &deploy_result { diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index 13896b180..2ea898ab5 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -612,8 +612,12 @@ pub async fn run_zk_script_test( let content = foundry_common::fs::read_to_string(run_latest).unwrap(); let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + let txs = + json["transactions"].as_array().expect("broadcastable txs"); + tracing::debug!(?txs, "executed script"); + assert_eq!( - json["transactions"].as_array().expect("broadcastable txs").len(), + txs.len(), expected_broadcastable_txs ); cmd.forge_fuse(); diff --git a/crates/forge/tests/it/zk/linking.rs b/crates/forge/tests/it/zk/linking.rs index 4fddd0695..ff644aded 100644 --- a/crates/forge/tests/it/zk/linking.rs +++ b/crates/forge/tests/it/zk/linking.rs @@ -1,5 +1,5 @@ use forge::revm::primitives::SpecId; -use foundry_test_utils::{forgetest_async, util, Filter, TestProject}; +use foundry_test_utils::{forgetest_async, util, Filter, TestCommand, TestProject}; use foundry_zksync_compilers::compilers::zksolc::settings::ZkSolcError; use semver::Version; @@ -20,7 +20,7 @@ forgetest_async!( #[should_panic = "no bytecode for contract; is it abstract or unlinked?"] script_zk_fails_indirect_reference_to_unlinked, |prj, cmd| { - setup_libs_prj(&mut prj, Some(Version::new(1, 5, 9))); + setup_libs_prj(&mut prj, &mut cmd, Some(Version::new(1, 5, 9))); run_zk_script_test( prj.root(), &mut cmd, @@ -34,29 +34,27 @@ forgetest_async!( } ); -forgetest_async!( - script_zk_deploy_time_linking, - |prj, cmd| { - setup_libs_prj(&mut prj, Some(Version::new(1, 5, 9))); - run_zk_script_test( - prj.root(), - &mut cmd, - "./script/Libraries.s.sol", - "DeployTimeLinking", - None, - 1, - Some(&["-vvvvv"]), - ) - .await; - } -); +forgetest_async!(script_zk_deploy_time_linking, |prj, cmd| { + setup_libs_prj(&mut prj, &mut cmd, Some(Version::new(1, 5, 9))); + run_zk_script_test( + prj.root(), + &mut cmd, + "./script/Libraries.s.sol", + "DeployTimeLinking", + None, + // lib `Foo` + `UsesFoo` deployment + 2, + Some(&["-vvvvv", "--broadcast"]), + ) + .await; +}); forgetest_async!( #[ignore] #[should_panic = "deploy-time linking not supported"] script_zk_deploy_time_linking_fails_older_version, |prj, cmd| { - setup_libs_prj(&mut prj, Some(Version::new(1, 5, 8))); + setup_libs_prj(&mut prj, &mut cmd, Some(Version::new(1, 5, 8))); run_zk_script_test( prj.root(), &mut cmd, @@ -74,7 +72,7 @@ forgetest_async!( #[should_panic = "Dynamic linking not supported"] create_zk_using_unlinked_fails, |prj, cmd| { - setup_libs_prj(&mut prj, None); + setup_libs_prj(&mut prj, &mut cmd, None); // we don't really connect to the rpc because // we expect to fail before that point @@ -90,13 +88,13 @@ forgetest_async!( } ); -fn setup_libs_prj(prj: &mut TestProject, zksolc: Option) { +fn setup_libs_prj(prj: &mut TestProject, cmd: &mut TestCommand, zksolc: Option) { util::initialize(prj.root()); let zksolc = zksolc.unwrap_or_else(|| Version::new(1, 5, 9)); // force zksolc 1.5.9 - let mut config = prj.config_from_output::<_, &str>([]); - if zksolc >= Version::new(1,5,9) { + let mut config = cmd.config(); + if zksolc >= Version::new(1, 5, 9) { config.zksync.suppressed_errors.insert(ZkSolcError::AssemblyCreate); } config.zksync.zksolc.replace(foundry_config::SolcReq::Version(zksolc)); diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index 37aed16c1..c69dc205c 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -9,7 +9,8 @@ use foundry_config::Config; use foundry_evm::{ constants::CALLER, executors::{ - strategy::DeployLibKind, DeployResult, EvmError, ExecutionErr, Executor, RawCallResult, + strategy::{DeployLibKind, DeployLibResult}, + DeployResult, EvmError, ExecutionErr, Executor, RawCallResult, }, opts::EvmOpts, revm::interpreter::{return_ok, InstructionResult}, @@ -73,15 +74,17 @@ impl ScriptRunner { ) .expect("couldn't deploy library"); - for (result, transaction) in results { + for DeployLibResult { result, tx } in results { if let Some(deploy_traces) = result.raw.traces { traces.push((TraceKind::Deployment, deploy_traces)); } - library_transactions.push_back(BroadcastableTransaction { - rpc: self.evm_opts.fork_url.clone(), - transaction, - }) + if let Some(transaction) = tx { + library_transactions.push_back(BroadcastableTransaction { + rpc: self.evm_opts.fork_url.clone(), + transaction, + }); + } } }), ScriptPredeployLibraries::Create2(libraries, salt) => { @@ -103,15 +106,17 @@ impl ScriptRunner { ) .expect("couldn't deploy library"); - for (result, transaction) in results { + for DeployLibResult { result, tx } in results { if let Some(deploy_traces) = result.raw.traces { traces.push((TraceKind::Deployment, deploy_traces)); } - library_transactions.push_back(BroadcastableTransaction { - rpc: self.evm_opts.fork_url.clone(), - transaction, - }); + if let Some(transaction) = tx { + library_transactions.push_back(BroadcastableTransaction { + rpc: self.evm_opts.fork_url.clone(), + transaction, + }); + } } } } diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 44f753e57..d9a1f479e 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -29,8 +29,8 @@ use foundry_evm::{ decode::RevertDecoder, executors::{ strategy::{ - DeployLibKind, EvmExecutorStrategyRunner, ExecutorStrategyContext, ExecutorStrategyExt, - ExecutorStrategyRunner, LinkOutput, + DeployLibKind, DeployLibResult, EvmExecutorStrategyRunner, ExecutorStrategyContext, + ExecutorStrategyExt, ExecutorStrategyRunner, LinkOutput, }, DeployResult, EvmError, Executor, }, @@ -306,7 +306,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { kind: DeployLibKind, value: U256, rd: Option<&RevertDecoder>, - ) -> Result, EvmError> { + ) -> Result, EvmError> { // sync deployer account info let nonce = EvmExecutorStrategyRunner.get_nonce(executor, from).expect("deployer to exist"); let balance = @@ -383,7 +383,14 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { serde_json::to_value(metadata).expect("failed encoding json"), ); - evm_deployment.push((DeployResult { raw: result, address }, request)); + // ignore all EVM broadcastables + evm_deployment.iter_mut().for_each(|result| { + result.tx.take(); + }); + evm_deployment.push(DeployLibResult { + result: DeployResult { raw: result, address }, + tx: Some(request), + }); Ok(evm_deployment) } From 3c3916747128374c340ccda69619d136abddd324 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 20 Jan 2025 20:44:40 +0100 Subject: [PATCH 33/61] fix(artifact:zk): avoid underflow refactor(artifact:zk): `all_factory_deps` method chore: formatting --- crates/linking/src/zksync.rs | 8 ++++---- crates/script/src/build.rs | 15 +++++---------- crates/strategy/zksync/src/executor/runner.rs | 14 ++++---------- .../src/compilers/artifact_output/zk.rs | 18 +++++++++++++++--- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index ae1986ef4..635c24d44 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -85,7 +85,7 @@ impl<'a> ZkLinker<'a> { .flatten() .collect::>(); - let deps_of_target = artifact + let unlinked_deps_of_target = artifact .factory_dependencies_unlinked .iter() .flatten() @@ -99,7 +99,7 @@ impl<'a> ZkLinker<'a> { (path.to_string(), name.to_string()) }); - for (file, name) in deps_of_target { + for (file, name) in unlinked_deps_of_target { let id = self .linker .find_artifact_id_by_library_path(&file, &name, None) @@ -436,8 +436,8 @@ impl<'a> ZkLinker<'a> { // strip here to match against the fdep which is stripped let id = (*id).clone().with_stripped_file_prefixes(&self.linker.root); - id.source.as_path() == Path::new(fdep.filename.as_str()) && - id.name == fdep.library + id.source.as_path() == Path::new(fdep.filename.as_str()) + && id.name == fdep.library }) }) // we want to keep the non-stripped diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 415b8360c..8ac8e0590 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -183,16 +183,11 @@ impl BuildData { evm_bytecode: evm.to_vec(), }; - ( - (contract_info.clone(), contract), - ( - contract_info, - unlinked_zk_artifact - .factory_dependencies_unlinked - .clone() - .unwrap_or_default(), - ), - ) + + let mut factory_deps = unlinked_zk_artifact.all_factory_deps().collect::>(); + factory_deps.dedup(); + + ((contract_info.clone(), contract), (contract_info, factory_deps)) }); let (new_contracts, new_contracts_deps): (Vec<_>, HashMap<_, _>) = diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index d9a1f479e..7c4544d8f 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -242,16 +242,10 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { evm_bytecode: evm.to_vec(), }; - ( - (contract_info.clone(), contract), - ( - contract_info, - unlinked_zk_artifact - .factory_dependencies_unlinked - .clone() - .unwrap_or_default(), - ), - ) + let mut factory_deps = unlinked_zk_artifact.all_factory_deps().collect::>(); + factory_deps.dedup(); + + ((contract_info.clone(), contract), (contract_info, factory_deps)) }); let (new_contracts, new_contracts_deps): (Vec<_>, HashMap<_, _>) = diff --git a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs index 736610c81..473496b4b 100644 --- a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs +++ b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs @@ -68,17 +68,29 @@ pub struct ZkContractArtifact { impl ZkContractArtifact { /// Returns true if contract is not linked pub fn is_unlinked(&self) -> bool { + let linked_fdeps = + self.factory_dependencies.as_ref().map(|linked| linked.len()).unwrap_or_default(); let unlinked_fdeps = self .factory_dependencies_unlinked .as_ref() .map(|unlinked| unlinked.len()) - .unwrap_or_default(); - let linked_fdeps = - self.factory_dependencies.as_ref().map(|linked| linked.len()).unwrap_or_default(); + // default to the same number as linked_deps if this one is missing + // since it would mean there are no deps that were unlinked + // so we want the check later to return false + .unwrap_or(linked_fdeps); !self.missing_libraries().map_or(true, Vec::is_empty) || unlinked_fdeps - linked_fdeps > 0 } + /// Returns a list of _all_ factory deps, by : + /// + /// Will return unlinked as well as linked factory deps (might contain duplicates) + pub fn all_factory_deps(&self) -> impl Iterator { + let linked = self.factory_dependencies.iter().flatten().map(|(_, dep)| dep); + let unlinked = self.factory_dependencies_unlinked.iter().flatten(); + linked.chain(unlinked) + } + /// Get contract missing libraries pub fn missing_libraries(&self) -> Option<&Vec> { self.bytecode.as_ref().map(|bc| &bc.missing_libraries) From 7738ed9ed5e7196d50e027ae21677d9f02d4e7c7 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 20 Jan 2025 21:53:08 +0100 Subject: [PATCH 34/61] fix(test:zk): proper stripping during link feat(link:script): register target & deps --- crates/forge/tests/it/zk/linking.rs | 11 +++++-- crates/linking/src/zksync.rs | 20 +++++++------ crates/script/src/build.rs | 14 ++++----- crates/strategy/zksync/src/executor/runner.rs | 29 ++++++++----------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/crates/forge/tests/it/zk/linking.rs b/crates/forge/tests/it/zk/linking.rs index ff644aded..1fb8f284a 100644 --- a/crates/forge/tests/it/zk/linking.rs +++ b/crates/forge/tests/it/zk/linking.rs @@ -8,6 +8,8 @@ use crate::{ test_helpers::{deploy_zk_contract, run_zk_script_test, TEST_DATA_DEFAULT}, }; +const ZKSOLC_MIN_LINKING_VERSION: Version = Version::new(1, 5, 9); + #[tokio::test(flavor = "multi_thread")] async fn test_zk_deploy_time_linking() { let runner = TEST_DATA_DEFAULT.runner_zksync(); @@ -20,7 +22,7 @@ forgetest_async!( #[should_panic = "no bytecode for contract; is it abstract or unlinked?"] script_zk_fails_indirect_reference_to_unlinked, |prj, cmd| { - setup_libs_prj(&mut prj, &mut cmd, Some(Version::new(1, 5, 9))); + setup_libs_prj(&mut prj, &mut cmd, Some(ZKSOLC_MIN_LINKING_VERSION)); run_zk_script_test( prj.root(), &mut cmd, @@ -35,7 +37,7 @@ forgetest_async!( ); forgetest_async!(script_zk_deploy_time_linking, |prj, cmd| { - setup_libs_prj(&mut prj, &mut cmd, Some(Version::new(1, 5, 9))); + setup_libs_prj(&mut prj, &mut cmd, Some(ZKSOLC_MIN_LINKING_VERSION)); run_zk_script_test( prj.root(), &mut cmd, @@ -54,7 +56,10 @@ forgetest_async!( #[should_panic = "deploy-time linking not supported"] script_zk_deploy_time_linking_fails_older_version, |prj, cmd| { - setup_libs_prj(&mut prj, &mut cmd, Some(Version::new(1, 5, 8))); + let mut version = ZKSOLC_MIN_LINKING_VERSION; + version.patch -= 1; + + setup_libs_prj(&mut prj, &mut cmd, Some(version)); run_zk_script_test( prj.root(), &mut cmd, diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index 635c24d44..bee00713a 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -115,10 +115,11 @@ impl<'a> ZkLinker<'a> { /// Performs DFS on the graph of link references, and populates `deps` with all found libraries, /// including ones of factory deps. - fn zk_collect_dependencies( + pub fn zk_collect_dependencies( &'a self, target: &'a ArtifactId, - deps: &mut BTreeSet<&'a ArtifactId>, + libraries: &mut BTreeSet<&'a ArtifactId>, + factory_deps: Option<&mut BTreeSet<&'a ArtifactId>>, ) -> Result<(), LinkerError> { let (_, artifact) = self .zk_artifacts() @@ -131,10 +132,11 @@ impl<'a> ZkLinker<'a> { } // find all nested factory deps's link references - let mut factory_deps = BTreeSet::new(); - self.zk_collect_factory_deps(target, &mut factory_deps)?; + let mut fdeps_default = BTreeSet::new(); + let factory_deps = factory_deps.unwrap_or(&mut fdeps_default); + self.zk_collect_factory_deps(target, factory_deps)?; - for (_, fdep) in factory_deps.into_iter().filter_map(|target| { + for (_, fdep) in factory_deps.iter().filter_map(|target| { self.zk_artifacts().find(|(id, _)| id.source == target.source && id.name == target.name) }) { if let Some(bytecode) = &fdep.bytecode { @@ -151,8 +153,8 @@ impl<'a> ZkLinker<'a> { file: file.to_string(), name: contract.to_string(), })?; - if deps.insert(id) { - self.zk_collect_dependencies(id, deps)?; + if libraries.insert(id) { + self.zk_collect_dependencies(id, libraries, Some(factory_deps))?; } } } @@ -182,7 +184,7 @@ impl<'a> ZkLinker<'a> { let mut needed_libraries = BTreeSet::new(); for target in targets { - self.zk_collect_dependencies(target, &mut needed_libraries)?; + self.zk_collect_dependencies(target, &mut needed_libraries, None)?; } let mut libs_to_deploy = Vec::new(); @@ -225,7 +227,7 @@ impl<'a> ZkLinker<'a> { let mut contracts = self.linker.contracts.clone(); let mut needed_libraries = BTreeSet::new(); - self.zk_collect_dependencies(target, &mut needed_libraries)?; + self.zk_collect_dependencies(target, &mut needed_libraries, None)?; let attempt_link = |contracts: &mut ArtifactContracts>, id, diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 8ac8e0590..237d0ffb2 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -134,16 +134,14 @@ impl BuildData { output.libraries }; + let mut factory_deps = Default::default(); + let mut libs = Default::default(); + linker.zk_collect_dependencies(target, &mut libs, Some(&mut factory_deps)).expect("able to enumerate all factory deps"); + let linked_contracts = linker .zk_get_linked_artifacts( - input - .artifact_ids() - .filter(|(_, artifact)| artifact.is_unlinked()) - // we can't use the `id` directly here - // becuase we expect an iterator of references - .map(|(id, _)| { - linker.linker.contracts.get_key_value(&id).expect("id to be present").0 - }), + // only retrieve target and its deps + factory_deps.into_iter().chain(libs.into_iter()).chain([target]), &libraries, ) .context("retrieving all fully linked contracts")?; diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 7c4544d8f..7ecb3338a 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -144,6 +144,9 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { return Err(LinkerError::MissingTargetArtifact); }; + // we don't strip here unlinke upstream due to + // `input` being used later during linking + // and that is unstripped let contracts: ArtifactContracts> = input.artifact_ids().collect(); @@ -194,33 +197,25 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { .map_err(zk_linker_error_to_linker)?; let linked_contracts = linker - .zk_get_linked_artifacts( - input - .artifact_ids() - .filter(|(_, artifact)| artifact.is_unlinked()) - // we can't use the `id` directly here - // becuase we expect an iterator of references - .map(|(id, _)| { - linker.linker.contracts.get_key_value(&id).expect("id to be present").0 - }), - &libraries, - ) + .zk_get_linked_artifacts(linker.linker.contracts.keys(), &libraries) .map_err(zk_linker_error_to_linker)?; let newly_linked_dual_compiled_contracts = linked_contracts .iter() .flat_map(|(needle, zk)| { + // match EVM linking's prefix stripping + let stripped = needle.clone().with_stripped_file_prefixes(&root); evm_link .linked_contracts .iter() - .find(|(id, _)| id.source == needle.source && id.name == needle.name) - .map(|(_, evm)| (needle, zk, evm)) + .find(|(id, _)| id.source == stripped.source && id.name == stripped.name) + .map(|(_, evm)| (needle, stripped, zk, evm)) }) - .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) - .map(|(id, linked_zk, evm)| { + .filter(|(_, _, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) + .map(|(unstripped_id, id, linked_zk, evm)| { let (_, unlinked_zk_artifact) = input .artifact_ids() - .find(|(contract_id, _)| contract_id == id) + .find(|(contract_id, _)| contract_id == unstripped_id) .expect("unable to find original (pre-linking) artifact"); let zk_bytecode = linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); @@ -363,7 +358,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { // also mark this library as persistent, this will ensure that the state of the library is // persistent across fork swaps in forking mode executor.backend_mut().add_persistent_account(address); - debug!(%address, "deployed contract with create2"); + debug!(%address, "deployed contract"); let mut request = TransactionMaybeSigned::new(Default::default()); let unsigned = request.as_unsigned_mut().unwrap(); From 5e04426df46f3dd1a873d019620a35915cb4618b Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 21 Jan 2025 19:50:42 +0100 Subject: [PATCH 35/61] chore: fix spelling & docs fix(test:zk): default to zksolc 1.5.9 --- crates/forge/tests/it/test_helpers.rs | 2 +- crates/forge/tests/it/zk/linking.rs | 4 ++++ crates/linking/src/zksync.rs | 5 ++--- crates/zksync/compilers/src/compilers/zksolc/mod.rs | 2 +- crates/zksync/compilers/src/compilers/zksolc/settings.rs | 2 ++ 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index 2ea898ab5..c85da3a51 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -201,7 +201,7 @@ impl ForgeTestProfile { zk_config.zksync.startup = true; zk_config.zksync.fallback_oz = true; zk_config.zksync.optimizer_mode = '3'; - zk_config.zksync.zksolc = Some(foundry_config::SolcReq::Version(Version::new(1, 5, 8))); + zk_config.zksync.zksolc = Some(foundry_config::SolcReq::Version(Version::new(1, 5, 9))); zk_config.fuzz.no_zksync_reserved_addresses = true; zk_config.invariant.depth = 15; diff --git a/crates/forge/tests/it/zk/linking.rs b/crates/forge/tests/it/zk/linking.rs index 1fb8f284a..87ff9d3ac 100644 --- a/crates/forge/tests/it/zk/linking.rs +++ b/crates/forge/tests/it/zk/linking.rs @@ -18,6 +18,10 @@ async fn test_zk_deploy_time_linking() { TestConfig::with_filter(runner, filter).spec_id(SpecId::SHANGHAI).run().await; } +// TODO(zk): add equivalent test for `GetCodeUnlinked` +// would probably need to split in separate file (and skip other file) +// as tests look for _all_ lib deps and deploy them for every test + forgetest_async!( #[should_panic = "no bytecode for contract; is it abstract or unlinked?"] script_zk_fails_indirect_reference_to_unlinked, diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index bee00713a..790e51306 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -60,7 +60,6 @@ impl<'a> ZkLinker<'a> { compiler: ZkSolcCompiler, compiler_output: &'a ProjectCompileOutput, ) -> Self { - // TODO(zk): check prefix `root` is stripped from `compiler_output` Self { linker: Linker::new(root, contracts), compiler, compiler_output } } @@ -397,7 +396,7 @@ impl<'a> ZkLinker<'a> { .into_iter() // we strip these here because the file references are also relative // and the linker wouldn't be able to properly detect matching factory deps - // (libraries are given separately and alredy stripped) + // (libraries are given separately and already stripped) .map(|(id, v)| (id.with_stripped_file_prefixes(&self.linker.root), v)) .collect(); let mut targets = targets.into_iter().cloned().collect::>(); @@ -456,7 +455,7 @@ impl<'a> ZkLinker<'a> { } targets.extend(ids); // queue factory deps for linking - targets.push_back(id); // reque original target + targets.push_back(id); // requeue original target } } Err(err) => return Err(err), diff --git a/crates/zksync/compilers/src/compilers/zksolc/mod.rs b/crates/zksync/compilers/src/compilers/zksolc/mod.rs index c48bce056..e80863574 100644 --- a/crates/zksync/compilers/src/compilers/zksolc/mod.rs +++ b/crates/zksync/compilers/src/compilers/zksolc/mod.rs @@ -40,7 +40,7 @@ pub const ZKSOLC: &str = "zksolc"; /// ZKsync solc release used for all ZKsync solc versions pub const ZKSYNC_SOLC_RELEASE: Version = Version::new(1, 0, 1); /// Default zksolc version -pub const ZKSOLC_VERSION: Version = Version::new(1, 5, 8); +pub const ZKSOLC_VERSION: Version = Version::new(1, 5, 9); #[cfg(test)] macro_rules! take_solc_installer_lock { diff --git a/crates/zksync/compilers/src/compilers/zksolc/settings.rs b/crates/zksync/compilers/src/compilers/zksolc/settings.rs index 5815c71cb..9eaba1410 100644 --- a/crates/zksync/compilers/src/compilers/zksolc/settings.rs +++ b/crates/zksync/compilers/src/compilers/zksolc/settings.rs @@ -54,6 +54,8 @@ pub enum ZkSolcError { /// `sendtransfer` error: Using `send()` or `transfer()` methods on `address payable` instead /// of `call()`. SendTransfer, + /// `assemblycreate` error: using 'create'/'create2' in an assembly block, + /// probably by providing bytecode and expecting an EVM-like behavior. AssemblyCreate, } From 9154294cc00837ffe350ab93d7648a36d3a97842 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 21 Jan 2025 20:05:46 +0100 Subject: [PATCH 36/61] fix(test:script): avoid expecting create2 output fix(script): avoid unpacking create2 result fix(script): avoid marking create2 result address as persistent --- crates/evm/evm/src/executors/strategy.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index a7af2ea79..48ad4bd86 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -325,21 +325,17 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { } DeployLibKind::Create2(salt, code) => { let create2_deployer = executor.create2_deployer(); + let calldata: Bytes = [salt.as_ref(), code.as_ref()].concat().into(); let result = executor.transact_raw(from, create2_deployer, calldata.clone(), value)?; let result = result.into_result(rd)?; - let Some(Output::Create(_, Some(address))) = result.out else { - return Err(eyre::eyre!( - "Deployment succeeded, but no address was returned: {result:#?}" - ) - .into()); - }; - - // also mark this library as persistent, this will ensure that the state of the - // library is persistent across fork swaps in forking mode - executor.backend_mut().add_persistent_account(address); + let address = result + .out + .as_ref() + .and_then(|out| out.address().cloned()) + .unwrap_or_else(|| create2_deployer.create2_from_code(salt, code.as_ref())); debug!(%address, "deployed contract with create2"); let mut request = TransactionMaybeSigned::new(Default::default()); From 2872620d1f66e84d87f96b5e924b617bb1dfce2d Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 21 Jan 2025 20:13:57 +0100 Subject: [PATCH 37/61] chore: clippy --- crates/evm/evm/src/executors/mod.rs | 1 - crates/evm/evm/src/executors/strategy.rs | 4 ++-- crates/forge/src/runner.rs | 2 +- crates/linking/src/zksync.rs | 7 +++---- crates/script/src/build.rs | 2 +- crates/script/src/runner.rs | 2 +- crates/strategy/zksync/src/executor/runner.rs | 6 ++---- crates/zksync/compilers/src/dual_compiled_contracts.rs | 1 - crates/zksync/core/src/lib.rs | 2 +- 9 files changed, 11 insertions(+), 16 deletions(-) diff --git a/crates/evm/evm/src/executors/mod.rs b/crates/evm/evm/src/executors/mod.rs index 0d3447d0d..d34603737 100644 --- a/crates/evm/evm/src/executors/mod.rs +++ b/crates/evm/evm/src/executors/mod.rs @@ -16,7 +16,6 @@ use alloy_primitives::{ Address, Bytes, Log, U256, }; use alloy_sol_types::{sol, SolCall}; -use foundry_common::TransactionMaybeSigned; use foundry_evm_core::{ backend::{Backend, BackendError, BackendResult, CowBackend, DatabaseExt, GLOBAL_FAIL_SLOT}, constants::{ diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index 48ad4bd86..bd854d291 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -13,7 +13,7 @@ use foundry_compilers::{ }; use foundry_config::Config; use foundry_evm_core::{ - backend::{strategy::BackendStrategy, Backend, BackendResult, CowBackend, DatabaseExt}, + backend::{strategy::BackendStrategy, Backend, BackendResult, CowBackend}, decode::RevertDecoder, }; use foundry_linking::{Linker, LinkerError}; @@ -22,7 +22,7 @@ use foundry_zksync_compilers::{ dual_compiled_contracts::DualCompiledContracts, }; use revm::{ - primitives::{Env, EnvWithHandlerCfg, Output, ResultAndState}, + primitives::{Env, EnvWithHandlerCfg, ResultAndState}, DatabaseRef, }; diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 6b43f0f73..cd40ce7df 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -21,7 +21,7 @@ use foundry_evm::{ invariant::{ check_sequence, replay_error, replay_run, InvariantExecutor, InvariantFuzzError, }, - strategy::{DeployLibKind, DeployLibResult}, + strategy::DeployLibKind, CallResult, EvmError, Executor, ITest, RawCallResult, }, fuzz::{ diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index 790e51306..7425e787f 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -80,8 +80,7 @@ impl<'a> ZkLinker<'a> { .factory_dependencies .as_ref() .iter() - .map(|map| map.values().into_iter()) - .flatten() + .flat_map(|map| map.values()) .collect::>(); let unlinked_deps_of_target = artifact @@ -102,7 +101,7 @@ impl<'a> ZkLinker<'a> { let id = self .linker .find_artifact_id_by_library_path(&file, &name, None) - .ok_or_else(|| LinkerError::MissingLibraryArtifact { file, name })?; + .ok_or(LinkerError::MissingLibraryArtifact { file, name })?; if factory_deps.insert(id) { self.zk_collect_factory_deps(id, factory_deps)?; @@ -234,7 +233,7 @@ impl<'a> ZkLinker<'a> { zksolc| { let original = contracts.get(id).expect("library present in list of contracts"); // Link library with provided libs and extract bytecode object (possibly unlinked). - match Self::zk_link(&contracts, id, libraries, zksolc) { + match Self::zk_link(contracts, id, libraries, zksolc) { Ok(linked) => { // persist linked contract for successive iterations *contracts.entry(id.clone()).or_default() = linked.clone(); diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 237d0ffb2..9851dbc71 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -25,7 +25,7 @@ use foundry_zksync_compilers::{ compilers::{artifact_output::zk::ZkArtifactOutput, zksolc::ZkSolcCompiler}, dual_compiled_contracts::{DualCompiledContract, DualCompiledContracts}, }; -use foundry_zksync_core::{hash_bytecode, DEFAULT_CREATE2_DEPLOYER_ZKSYNC, H256}; +use foundry_zksync_core::{hash_bytecode, DEFAULT_CREATE2_DEPLOYER_ZKSYNC}; use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::Arc}; /// Container for the compiled contracts. diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index c69dc205c..2bd512a87 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -100,7 +100,7 @@ impl ScriptRunner { .executor .deploy_library( self.evm_opts.sender, - DeployLibKind::Create2(salt.clone(), library.clone()), + DeployLibKind::Create2(*salt, library.clone()), U256::from(0), None, ) diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 7ecb3338a..46495f410 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -15,7 +15,6 @@ use revm::{ Database, }; use tracing::debug; -use zksync_types::H256; use foundry_common::{ContractsByArtifact, TransactionMaybeSigned}; use foundry_compilers::{ @@ -25,7 +24,6 @@ use foundry_compilers::{ use foundry_config::Config; use foundry_evm::{ backend::{Backend, BackendResult, CowBackend, DatabaseExt}, - constants::DEFAULT_CREATE2_DEPLOYER, decode::RevertDecoder, executors::{ strategy::{ @@ -170,7 +168,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { } })?; - let linker = ZkLinker::new(root, contracts.clone(), zksolc, &input); + let linker = ZkLinker::new(root, contracts.clone(), zksolc, input); let zk_linker_error_to_linker = |zk_error| match zk_error { ZkLinkerError::Inner(err) => err, @@ -204,7 +202,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { .iter() .flat_map(|(needle, zk)| { // match EVM linking's prefix stripping - let stripped = needle.clone().with_stripped_file_prefixes(&root); + let stripped = needle.clone().with_stripped_file_prefixes(root); evm_link .linked_contracts .iter() diff --git a/crates/zksync/compilers/src/dual_compiled_contracts.rs b/crates/zksync/compilers/src/dual_compiled_contracts.rs index 3568de375..ff2bf08db 100644 --- a/crates/zksync/compilers/src/dual_compiled_contracts.rs +++ b/crates/zksync/compilers/src/dual_compiled_contracts.rs @@ -350,7 +350,6 @@ impl DualCompiledContracts { .chain(path_matches) .chain(name_matches) .map(|(_, contract)| contract) - .into_iter() } /// Retrieves the length of the collection. diff --git a/crates/zksync/core/src/lib.rs b/crates/zksync/core/src/lib.rs index e7ffbd89c..f6215b021 100644 --- a/crates/zksync/core/src/lib.rs +++ b/crates/zksync/core/src/lib.rs @@ -211,7 +211,7 @@ pub fn compute_create2_address( salt: B256, constructor_input: &[u8], ) -> Address { - const CREATE2_PREFIX: &'static [u8] = b"zksyncCreate2"; + const CREATE2_PREFIX: &[u8] = b"zksyncCreate2"; let prefix = keccak256(CREATE2_PREFIX); let sender = sender.to_h256(); let constructor_input_hash = keccak256(constructor_input); From 16ffced30584a2ae1ae56631c461812da00f159a Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 21 Jan 2025 20:14:34 +0100 Subject: [PATCH 38/61] chore: formatting --- crates/evm/evm/src/executors/strategy.rs | 4 +- crates/forge/tests/it/test_helpers.rs | 8 +--- crates/linking/src/zksync.rs | 4 +- crates/script/src/build.rs | 45 ++++++++++--------- .../compilers/src/dual_compiled_contracts.rs | 5 +-- 5 files changed, 31 insertions(+), 35 deletions(-) diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index bd854d291..8b9564b54 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -275,8 +275,8 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { let Some(abi) = &contract.abi else { continue }; // if it's a test, link it and add to deployable contracts - if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) - && abi.functions().any(|func| func.name.is_any_test()) + if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && + abi.functions().any(|func| func.name.is_any_test()) { let Some(bytecode) = contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty()) diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index c85da3a51..0bca7f6b9 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -612,14 +612,10 @@ pub async fn run_zk_script_test( let content = foundry_common::fs::read_to_string(run_latest).unwrap(); let json: serde_json::Value = serde_json::from_str(&content).unwrap(); - let txs = - json["transactions"].as_array().expect("broadcastable txs"); + let txs = json["transactions"].as_array().expect("broadcastable txs"); tracing::debug!(?txs, "executed script"); - assert_eq!( - txs.len(), - expected_broadcastable_txs - ); + assert_eq!(txs.len(), expected_broadcastable_txs); cmd.forge_fuse(); } diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index 7425e787f..ff992745e 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -436,8 +436,8 @@ impl<'a> ZkLinker<'a> { // strip here to match against the fdep which is stripped let id = (*id).clone().with_stripped_file_prefixes(&self.linker.root); - id.source.as_path() == Path::new(fdep.filename.as_str()) - && id.name == fdep.library + id.source.as_path() == Path::new(fdep.filename.as_str()) && + id.name == fdep.library }) }) // we want to keep the non-stripped diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 9851dbc71..7f09169f4 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -136,7 +136,9 @@ impl BuildData { let mut factory_deps = Default::default(); let mut libs = Default::default(); - linker.zk_collect_dependencies(target, &mut libs, Some(&mut factory_deps)).expect("able to enumerate all factory deps"); + linker + .zk_collect_dependencies(target, &mut libs, Some(&mut factory_deps)) + .expect("able to enumerate all factory deps"); let linked_contracts = linker .zk_get_linked_artifacts( @@ -181,7 +183,6 @@ impl BuildData { evm_bytecode: evm.to_vec(), }; - let mut factory_deps = unlinked_zk_artifact.all_factory_deps().collect::>(); factory_deps.dedup(); @@ -255,31 +256,33 @@ impl BuildData { }) .flatten(); - let (libraries, predeploy_libs, uses_create2) = if let Some(output) = maybe_create2_link_output { - ( - output.libraries, - ScriptPredeployLibraries::Create2( - output.libs_to_deploy, - script_config.config.create2_library_salt, - ), - true - ) - } else { - let output = self.get_linker().link_with_nonce_or_address( - known_libraries.clone(), - script_config.evm_opts.sender, - script_config.sender_nonce, - [&self.target], - )?; + let (libraries, predeploy_libs, uses_create2) = + if let Some(output) = maybe_create2_link_output { + ( + output.libraries, + ScriptPredeployLibraries::Create2( + output.libs_to_deploy, + script_config.config.create2_library_salt, + ), + true, + ) + } else { + let output = self.get_linker().link_with_nonce_or_address( + known_libraries.clone(), + script_config.evm_opts.sender, + script_config.sender_nonce, + [&self.target], + )?; - (output.libraries, ScriptPredeployLibraries::Default(output.libs_to_deploy), false) - }; + (output.libraries, ScriptPredeployLibraries::Default(output.libs_to_deploy), false) + }; let known_contracts = self .get_linker() .get_linked_artifacts(&libraries) .context("retrieving fully linked artifacts")?; - let known_contracts = self.zk_link(script_config, known_libraries, known_contracts, uses_create2).await?; + let known_contracts = + self.zk_link(script_config, known_libraries, known_contracts, uses_create2).await?; LinkedBuildData::new( libraries, diff --git a/crates/zksync/compilers/src/dual_compiled_contracts.rs b/crates/zksync/compilers/src/dual_compiled_contracts.rs index ff2bf08db..1fe883ccf 100644 --- a/crates/zksync/compilers/src/dual_compiled_contracts.rs +++ b/crates/zksync/compilers/src/dual_compiled_contracts.rs @@ -346,10 +346,7 @@ impl DualCompiledContracts { name.map(|name| name == info.name.as_str()).unwrap_or(false) }); - full_matches - .chain(path_matches) - .chain(name_matches) - .map(|(_, contract)| contract) + full_matches.chain(path_matches).chain(name_matches).map(|(_, contract)| contract) } /// Retrieves the length of the collection. From bdce3c3893f9250ca88070ee261698dfed67562a Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 21 Jan 2025 20:16:20 +0100 Subject: [PATCH 39/61] chore: codespell --- crates/zksync/core/src/vm/farcall.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zksync/core/src/vm/farcall.rs b/crates/zksync/core/src/vm/farcall.rs index d19c977fb..5cfd93139 100644 --- a/crates/zksync/core/src/vm/farcall.rs +++ b/crates/zksync/core/src/vm/farcall.rs @@ -173,7 +173,7 @@ impl FarCallHandler { PrimitiveValue { value: return_fat_ptr.to_u256(), is_pointer: false }; // Just rewriting `code_page` is very error-prone, since the same memory page would be - // re-used for decommitments. We'll use a different approach: + // reused for decommitments. We'll use a different approach: // - Set `previous_code_word` to the value that we want // - Set `previous_code_memory_page` to the current code page + `previous_super_pc` to 0 // (as it corresponds From 72ca9529bc3156bcf75e0f2a68e0828e3bd8de50 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 21 Jan 2025 20:26:58 +0100 Subject: [PATCH 40/61] fix(artifacts:zk): `is_unlinked` underflow --- crates/zksync/compilers/src/artifacts/contract.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/zksync/compilers/src/artifacts/contract.rs b/crates/zksync/compilers/src/artifacts/contract.rs index 3055d43e2..f4fb5c1e6 100644 --- a/crates/zksync/compilers/src/artifacts/contract.rs +++ b/crates/zksync/compilers/src/artifacts/contract.rs @@ -64,13 +64,16 @@ fn storage_layout_is_empty(storage_layout: &StorageLayout) -> bool { impl Contract { /// Returns true if contract is not linked pub fn is_unlinked(&self) -> bool { + let linked_fdeps = + self.factory_dependencies.as_ref().map(|linked| linked.len()).unwrap_or_default(); let unlinked_fdeps = self .factory_dependencies_unlinked .as_ref() .map(|unlinked| unlinked.len()) - .unwrap_or_default(); - let linked_fdeps = - self.factory_dependencies.as_ref().map(|linked| linked.len()).unwrap_or_default(); + // default to the same number as linked_deps if this one is missing + // since it would mean there are no deps that were unlinked + // so we want the check later to return false + .unwrap_or(linked_fdeps); !self.missing_libraries.is_empty() || unlinked_fdeps - linked_fdeps > 0 } From 7000890df3e9e137b61c7e0106e78667bb87429f Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 21 Jan 2025 20:44:09 +0100 Subject: [PATCH 41/61] feat(compiler:zk): `objectFormat` --- .../compilers/src/artifacts/contract.rs | 60 +++++++++++++++---- .../src/compilers/artifact_output/zk.rs | 22 ++----- .../compilers/artifact_output/zk/bytecode.rs | 21 +++++-- 3 files changed, 69 insertions(+), 34 deletions(-) diff --git a/crates/zksync/compilers/src/artifacts/contract.rs b/crates/zksync/compilers/src/artifacts/contract.rs index f4fb5c1e6..2f2a41ea1 100644 --- a/crates/zksync/compilers/src/artifacts/contract.rs +++ b/crates/zksync/compilers/src/artifacts/contract.rs @@ -11,6 +11,44 @@ use std::{ collections::{BTreeMap, HashSet}, }; +/// zksolc: Binary object format. +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(try_from = "String", into = "String")] +pub enum ObjectFormat { + /// Linked + #[default] + Raw, + /// Unlinked + Elf, +} + +impl From for String { + fn from(val: ObjectFormat) -> Self { + match val { + ObjectFormat::Raw => "raw", + ObjectFormat::Elf => "elf", + } + .to_string() + } +} + +impl TryFrom for ObjectFormat { + type Error = String; + fn try_from(s: String) -> Result { + match s.as_str() { + "raw" => Ok(Self::Raw), + "elf" => Ok(Self::Elf), + s => Err(format!("Unknown zksolc object format: {s}")), + } + } +} + +impl ObjectFormat { + pub fn is_unlinked(&self) -> bool { + matches!(self, Self::Elf) + } +} + /// Represents a compiled solidity contract #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] @@ -55,6 +93,15 @@ pub struct Contract { /// The contract's unlinked libraries #[serde(default)] pub missing_libraries: Vec, + + /// zksolc's binary object format + /// + /// Tells whether the bytecode has been linked. + /// + /// Introduced in 1.5.8, beforehand we can assume the bytecode + /// was always fully linked + #[serde(default)] + pub object_format: ObjectFormat, } fn storage_layout_is_empty(storage_layout: &StorageLayout) -> bool { @@ -64,18 +111,7 @@ fn storage_layout_is_empty(storage_layout: &StorageLayout) -> bool { impl Contract { /// Returns true if contract is not linked pub fn is_unlinked(&self) -> bool { - let linked_fdeps = - self.factory_dependencies.as_ref().map(|linked| linked.len()).unwrap_or_default(); - let unlinked_fdeps = self - .factory_dependencies_unlinked - .as_ref() - .map(|unlinked| unlinked.len()) - // default to the same number as linked_deps if this one is missing - // since it would mean there are no deps that were unlinked - // so we want the check later to return false - .unwrap_or(linked_fdeps); - - !self.missing_libraries.is_empty() || unlinked_fdeps - linked_fdeps > 0 + self.object_format.is_unlinked() } /// takes missing libraries output and transforms into link references diff --git a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs index 473496b4b..9e9635a98 100644 --- a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs +++ b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs @@ -68,18 +68,7 @@ pub struct ZkContractArtifact { impl ZkContractArtifact { /// Returns true if contract is not linked pub fn is_unlinked(&self) -> bool { - let linked_fdeps = - self.factory_dependencies.as_ref().map(|linked| linked.len()).unwrap_or_default(); - let unlinked_fdeps = self - .factory_dependencies_unlinked - .as_ref() - .map(|unlinked| unlinked.len()) - // default to the same number as linked_deps if this one is missing - // since it would mean there are no deps that were unlinked - // so we want the check later to return false - .unwrap_or(linked_fdeps); - - !self.missing_libraries().map_or(true, Vec::is_empty) || unlinked_fdeps - linked_fdeps > 0 + self.bytecode.as_ref().map(|bc| bc.is_unlinked()).unwrap_or(false) } /// Returns a list of _all_ factory deps, by : @@ -154,7 +143,6 @@ impl ArtifactOutput for ZkArtifactOutput { contract: Self::CompilerContract, source_file: Option<&SourceFile>, ) -> Self::Artifact { - let is_unlinked = contract.is_unlinked(); let Contract { abi, metadata, @@ -168,14 +156,16 @@ impl ArtifactOutput for ZkArtifactOutput { factory_dependencies, factory_dependencies_unlinked, missing_libraries, + object_format, } = contract; let (bytecode, assembly) = eravm - .map(|eravm| (eravm.bytecode(is_unlinked), eravm.assembly)) + .map(|eravm| (eravm.bytecode(object_format.is_unlinked()), eravm.assembly)) .or_else(|| evm.map(|evm| (evm.bytecode.map(|bc| bc.object), evm.assembly))) .unwrap_or_else(|| (None, None)); - let bytecode = bytecode - .map(|object| ZkArtifactBytecode::with_object(object, is_unlinked, missing_libraries)); + let bytecode = bytecode.map(|object| { + ZkArtifactBytecode::with_object(object, object_format, missing_libraries) + }); ZkContractArtifact { abi, diff --git a/crates/zksync/compilers/src/compilers/artifact_output/zk/bytecode.rs b/crates/zksync/compilers/src/compilers/artifact_output/zk/bytecode.rs index 48088aa45..a9e3987ef 100644 --- a/crates/zksync/compilers/src/compilers/artifact_output/zk/bytecode.rs +++ b/crates/zksync/compilers/src/compilers/artifact_output/zk/bytecode.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use crate::artifacts::contract::Contract; +use crate::artifacts::contract::{Contract, ObjectFormat}; use alloy_primitives::Bytes; use foundry_compilers_artifacts_solc::{ BytecodeObject, CompactBytecode, CompactDeployedBytecode, Offsets, @@ -23,7 +23,7 @@ where pub struct ZkArtifactBytecode { #[serde(serialize_with = "serialize_bytes_without_prefix")] object: Bytes, - is_unlinked: bool, + object_format: ObjectFormat, /// Bytecode missing libraries #[serde(default)] @@ -34,7 +34,7 @@ impl ZkArtifactBytecode { /// Get Bytecode from parts pub fn with_object( object: BytecodeObject, - is_unlinked: bool, + object_format: ObjectFormat, missing_libraries: Vec, ) -> Self { let object = match object { @@ -43,7 +43,12 @@ impl ZkArtifactBytecode { alloy_primitives::hex::decode(s).expect("valid bytecode").into() } }; - Self { object, is_unlinked, missing_libraries } + Self { object, object_format, missing_libraries } + } + + /// Returns `true` if the bytecode is unlinked + pub fn is_unlinked(&self) -> bool { + self.object_format.is_unlinked() } /// Get link references @@ -53,7 +58,7 @@ impl ZkArtifactBytecode { /// Get bytecode object pub fn object(&self) -> BytecodeObject { - if self.is_unlinked { + if self.object_format.is_unlinked() { // convert to unlinked let encoded = alloy_primitives::hex::encode(&self.object); BytecodeObject::Unlinked(encoded) @@ -86,7 +91,11 @@ mod tests { #[test] fn serialized_bytecode_is_not_prefixed() { let object = Bytes::from(vec![0xDEu8, 0xAD, 0xBE, 0xEF]); - let sample = ZkArtifactBytecode { object, is_unlinked: false, missing_libraries: vec![] }; + let sample = ZkArtifactBytecode { + object, + object_format: ObjectFormat::Raw, + missing_libraries: vec![], + }; let json_str = serde_json::to_string(&sample).expect("able to serialize artifact bytecode as json"); From e8c9ba0cf87a85b39219eb1ddf8a37c2a966c656 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 21 Jan 2025 20:55:25 +0100 Subject: [PATCH 42/61] fix(script:link:zk): skip version check if no libs --- crates/script/src/build.rs | 42 +++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 7f09169f4..64db75824 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -50,21 +50,31 @@ impl BuildData { fn get_zk_linker(&self, script_config: &ScriptConfig) -> Result> { let zksolc = foundry_config::zksync::config_zksolc_compiler(&script_config.config) .context("retrieving zksolc compiler to be used for linking")?; - let version = zksolc.version().context("trying to determine zksolc version")?; - if version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { - eyre::bail!( - "deploy-time linking not supported. minimum: {}, given: {}", - DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, - &version - ); - } let Some(input) = self.zk_output.as_ref() else { eyre::bail!("unable to link zk artifacts if no zk compilation output is provided") }; - Ok(ZkLinker::new(self.project_root.clone(), input.artifact_ids().collect(), zksolc, input)) + let linker = + ZkLinker::new(self.project_root.clone(), input.artifact_ids().collect(), zksolc, input); + + let mut libs = Default::default(); + linker.zk_collect_dependencies(&self.target, &mut libs, None)?; + + // if there are no no libs, no linking will happen + // so we can skip version check + if libs.len() != 0 { + if version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { + eyre::bail!( + "deploy-time linking not supported. minimum: {}, given: {}", + DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, + &version + ); + } + } + + Ok(linker) } /// Will attempt linking via `zksolc` @@ -94,18 +104,18 @@ impl BuildData { let mut dual_compiled_contracts = self.dual_compiled_contracts.take().unwrap_or_default(); - let linker = self.get_zk_linker(script_config)?; - // NOTE(zk): translate solc ArtifactId to zksolc otherwise // we won't be able to find it in the zksolc output - let Some(target) = linker - .linker - .contracts - .keys() + let Some(target) = input + .artifact_ids() + .map(|(id, _)| id) .find(|id| id.source == self.target.source && id.name == self.target.name) else { eyre::bail!("unable to find zk target artifact for linking"); }; + let target = ⌖ + + let linker = self.get_zk_linker(script_config)?; let create2_deployer = DEFAULT_CREATE2_DEPLOYER_ZKSYNC; let maybe_create2_link_output = use_create2 @@ -138,7 +148,7 @@ impl BuildData { let mut libs = Default::default(); linker .zk_collect_dependencies(target, &mut libs, Some(&mut factory_deps)) - .expect("able to enumerate all factory deps"); + .expect("able to enumerate all deps"); let linked_contracts = linker .zk_get_linked_artifacts( From 219e62f5a2e6dbb76d173914cfdfc4b9e0b35a84 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 21 Jan 2025 20:59:44 +0100 Subject: [PATCH 43/61] fix(test:link:zk): avoid version check w/o libs --- crates/strategy/zksync/src/executor/runner.rs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 46495f410..c2058ba64 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -153,20 +153,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { // TODO(zk): better error return Err(LinkerError::CyclicDependency); }; - - // TODO(zk): better error - zksolc.version().map_err(|_| LinkerError::CyclicDependency).and_then(|version| { - if version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { - tracing::error!( - %version, - minimum_version = %DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, - "deploy-time linking not supported" - ); - Err(LinkerError::CyclicDependency) - } else { - Ok(()) - } - })?; + let version = zksolc.version().map_err(|_| LinkerError::CyclicDependency)?; let linker = ZkLinker::new(root, contracts.clone(), zksolc, input); @@ -194,6 +181,20 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { ) .map_err(zk_linker_error_to_linker)?; + // if we have no libraries then no linking will happen + // so we can skip the version check + if !libraries.is_empty() { + // TODO(zk): better error + if version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { + tracing::error!( + %version, + minimum_version = %DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, + "deploy-time linking not supported" + ); + return Err(LinkerError::CyclicDependency.into()); + } + } + let linked_contracts = linker .zk_get_linked_artifacts(linker.linker.contracts.keys(), &libraries) .map_err(zk_linker_error_to_linker)?; From 0331cd8d6cff7882466b89f802a5b85c533aa81b Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 21 Jan 2025 21:03:54 +0100 Subject: [PATCH 44/61] chore: clippy --- crates/script/src/build.rs | 14 ++++++-------- crates/strategy/zksync/src/executor/runner.rs | 2 +- crates/zksync/compilers/src/artifacts/contract.rs | 1 + 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 64db75824..9eb3ffaf7 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -64,14 +64,12 @@ impl BuildData { // if there are no no libs, no linking will happen // so we can skip version check - if libs.len() != 0 { - if version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { - eyre::bail!( - "deploy-time linking not supported. minimum: {}, given: {}", - DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, - &version - ); - } + if !libs.is_empty() && version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { + eyre::bail!( + "deploy-time linking not supported. minimum: {}, given: {}", + DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, + &version + ); } Ok(linker) diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index c2058ba64..10cc5c2de 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -191,7 +191,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { minimum_version = %DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, "deploy-time linking not supported" ); - return Err(LinkerError::CyclicDependency.into()); + return Err(LinkerError::CyclicDependency); } } diff --git a/crates/zksync/compilers/src/artifacts/contract.rs b/crates/zksync/compilers/src/artifacts/contract.rs index 2f2a41ea1..c68d46fc3 100644 --- a/crates/zksync/compilers/src/artifacts/contract.rs +++ b/crates/zksync/compilers/src/artifacts/contract.rs @@ -44,6 +44,7 @@ impl TryFrom for ObjectFormat { } impl ObjectFormat { + /// Returns true if the object format is considered `unlinked` pub fn is_unlinked(&self) -> bool { matches!(self, Self::Elf) } From 0314731c3a6949418ded8d515c2a44b53efcaa54 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Tue, 21 Jan 2025 21:15:31 +0100 Subject: [PATCH 45/61] fix(compiler:zk): optional object_format --- crates/zksync/compilers/src/artifacts/contract.rs | 4 ++-- crates/zksync/compilers/src/compilers/artifact_output/zk.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/zksync/compilers/src/artifacts/contract.rs b/crates/zksync/compilers/src/artifacts/contract.rs index c68d46fc3..82998b476 100644 --- a/crates/zksync/compilers/src/artifacts/contract.rs +++ b/crates/zksync/compilers/src/artifacts/contract.rs @@ -102,7 +102,7 @@ pub struct Contract { /// Introduced in 1.5.8, beforehand we can assume the bytecode /// was always fully linked #[serde(default)] - pub object_format: ObjectFormat, + pub object_format: Option, } fn storage_layout_is_empty(storage_layout: &StorageLayout) -> bool { @@ -112,7 +112,7 @@ fn storage_layout_is_empty(storage_layout: &StorageLayout) -> bool { impl Contract { /// Returns true if contract is not linked pub fn is_unlinked(&self) -> bool { - self.object_format.is_unlinked() + self.object_format.as_ref().map(|format| format.is_unlinked()).unwrap_or_default() } /// takes missing libraries output and transforms into link references diff --git a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs index 9e9635a98..2f98b112c 100644 --- a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs +++ b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs @@ -158,6 +158,7 @@ impl ArtifactOutput for ZkArtifactOutput { missing_libraries, object_format, } = contract; + let object_format = object_format.unwrap_or_default(); let (bytecode, assembly) = eravm .map(|eravm| (eravm.bytecode(object_format.is_unlinked()), eravm.assembly)) From b29c5f68473257aff923cd46a1a2e53134fd0546 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Wed, 22 Jan 2025 16:15:12 +0100 Subject: [PATCH 46/61] fix(link:zk): ignore target version for lookup --- crates/linking/src/zksync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/linking/src/zksync.rs b/crates/linking/src/zksync.rs index ff992745e..5afa522e1 100644 --- a/crates/linking/src/zksync.rs +++ b/crates/linking/src/zksync.rs @@ -146,7 +146,7 @@ impl<'a> ZkLinker<'a> { for contract in libs.keys() { let id = self .linker - .find_artifact_id_by_library_path(file, contract, Some(&target.version)) + .find_artifact_id_by_library_path(file, contract, None) .ok_or_else(|| LinkerError::MissingLibraryArtifact { file: file.to_string(), name: contract.to_string(), From d6b9bc3a2169f4ddfedb724169ca0e4f5d74da1b Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Wed, 22 Jan 2025 20:26:52 +0100 Subject: [PATCH 47/61] fix(link:zk): proper EVM deployed_bc/bc --- crates/script/src/build.rs | 9 +++++---- crates/strategy/zksync/src/executor/runner.rs | 10 ++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 9eb3ffaf7..7f08ee63e 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -174,7 +174,8 @@ impl BuildData { let zk_bytecode = linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); let zk_hash = hash_bytecode(&zk_bytecode); - let evm = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); + let evm_deployed = evm.get_deployed_bytecode_bytes().expect("no EVM deployed bytecode (or unlinked)"); + let evm_bytecode = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); let contract_info = ContractInfo { name: id.name.clone(), path: Some(id.source.to_string_lossy().into_owned()), @@ -184,11 +185,11 @@ impl BuildData { zk_deployed_bytecode: zk_bytecode.to_vec(), // rest of factory deps is populated later zk_factory_deps: vec![zk_bytecode.to_vec()], - evm_bytecode_hash: B256::from_slice(&keccak256(evm.as_ref())[..]), + evm_bytecode_hash: B256::from_slice(&keccak256(evm_deployed.as_ref())[..]), // TODO(zk): determine if this is ok, as it's // not really used in dual compiled contracts - evm_deployed_bytecode: evm.to_vec(), - evm_bytecode: evm.to_vec(), + evm_deployed_bytecode: evm_deployed.to_vec(), + evm_bytecode: evm_bytecode.to_vec(), }; let mut factory_deps = unlinked_zk_artifact.all_factory_deps().collect::>(); diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 10cc5c2de..2a3a64d57 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -219,7 +219,9 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { let zk_bytecode = linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); let zk_hash = hash_bytecode(&zk_bytecode); - let evm = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); + let evm_deployed = + evm.get_deployed_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); + let evm_bytecode = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); let contract_info = ContractInfo { name: id.name.clone(), path: Some(id.source.to_string_lossy().into_owned()), @@ -229,11 +231,11 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { zk_deployed_bytecode: zk_bytecode.to_vec(), // rest of factory deps is populated later zk_factory_deps: vec![zk_bytecode.to_vec()], - evm_bytecode_hash: B256::from_slice(&keccak256(evm.as_ref())[..]), + evm_bytecode_hash: B256::from_slice(&keccak256(evm_deployed.as_ref())[..]), // TODO(zk): determine if this is ok, as it's // not really used in dual compiled contracts - evm_deployed_bytecode: evm.to_vec(), - evm_bytecode: evm.to_vec(), + evm_deployed_bytecode: evm_deployed.to_vec(), + evm_bytecode: evm_bytecode.to_vec(), }; let mut factory_deps = unlinked_zk_artifact.all_factory_deps().collect::>(); From 17fd4b40f4569b80c26cb824aa9bc7c7fa794964 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Wed, 22 Jan 2025 20:51:05 +0100 Subject: [PATCH 48/61] chore: fmt --- crates/script/src/build.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 7f08ee63e..01dd61f8b 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -174,7 +174,9 @@ impl BuildData { let zk_bytecode = linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); let zk_hash = hash_bytecode(&zk_bytecode); - let evm_deployed = evm.get_deployed_bytecode_bytes().expect("no EVM deployed bytecode (or unlinked)"); + let evm_deployed = evm + .get_deployed_bytecode_bytes() + .expect("no EVM deployed bytecode (or unlinked)"); let evm_bytecode = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); let contract_info = ContractInfo { name: id.name.clone(), From 02e18e61e208e29513359f6db0563e63805443a8 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Thu, 23 Jan 2025 11:28:28 +0100 Subject: [PATCH 49/61] chore: fmt --- crates/forge/src/multi_runner.rs | 4 ++-- crates/forge/tests/it/test_helpers.rs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index e603a3ad3..09c931c1a 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -487,8 +487,8 @@ impl MultiContractRunnerBuilder { evm_opts: EvmOpts, mut strategy: ExecutorStrategy, ) -> Result { - if let Some(zk) = zk_output { - strategy.runner.zksync_set_compilation_output(strategy.context.as_mut(), zk); + if let Some(zk_output) = zk_output { + strategy.runner.zksync_set_compilation_output(strategy.context.as_mut(), zk_output); } let LinkOutput { diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index 0bca7f6b9..9ba80f5c2 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -612,10 +612,11 @@ pub async fn run_zk_script_test( let content = foundry_common::fs::read_to_string(run_latest).unwrap(); let json: serde_json::Value = serde_json::from_str(&content).unwrap(); - let txs = json["transactions"].as_array().expect("broadcastable txs"); - tracing::debug!(?txs, "executed script"); - assert_eq!(txs.len(), expected_broadcastable_txs); + assert_eq!( + json["transactions"].as_array().expect("broadcastable txs").len(), + expected_broadcastable_txs + ); cmd.forge_fuse(); } From 8aa4479461fbe8fe04599ba8d4243d3a798cb101 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Thu, 23 Jan 2025 16:52:56 +0100 Subject: [PATCH 50/61] test(zk): `DualCompiledContracts::find` units --- crates/script/src/build.rs | 1 + crates/strategy/zksync/src/executor/runner.rs | 1 + .../compilers/src/dual_compiled_contracts.rs | 201 ++++++++++++++++-- 3 files changed, 183 insertions(+), 20 deletions(-) diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 01dd61f8b..05824d34b 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -216,6 +216,7 @@ impl BuildData { .find(Some(path), Some(name)) .next() .expect("unknown factory dep") + .1 .zk_deployed_bytecode .clone(); diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 2a3a64d57..6efb0046f 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -261,6 +261,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { .find(Some(path), Some(name)) .next() .expect("unknown factory dep") + .1 .zk_deployed_bytecode .clone(); diff --git a/crates/zksync/compilers/src/dual_compiled_contracts.rs b/crates/zksync/compilers/src/dual_compiled_contracts.rs index 1fe883ccf..6be9338ec 100644 --- a/crates/zksync/compilers/src/dual_compiled_contracts.rs +++ b/crates/zksync/compilers/src/dual_compiled_contracts.rs @@ -42,6 +42,17 @@ pub struct DualCompiledContract { pub evm_bytecode: Vec, } +/// Indicates the type of match from a `find` search +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FindMatchType { + /// The result matched both path and name + FullMatch, + /// The result only matched the path + Path, + /// The result only matched the name + Name, +} + /// Couple contract type with contract and init code pub struct FindBytecodeResult<'a> { r#type: ContractType, @@ -320,33 +331,47 @@ impl DualCompiledContracts { &'a self, path: Option<&'b str>, name: Option<&'b str>, - ) -> impl Iterator + 'b { - let full_matches = self.contracts.iter().filter(move |(info, _)| { - // if user provides a path we should check that it matches - // we check using `ends_with` to account for prefixes - path.map_or(true, |needle| + ) -> impl Iterator + 'b { + let full_matches = self + .contracts + .iter() + .filter(move |(info, _)| { + // if user provides a path we should check that it matches + // we check using `ends_with` to account for prefixes + path.map_or(false, |needle| info.path.as_ref() - .map_or(true, + .map_or(false, |contract_path| contract_path.ends_with(needle))) // if user provides a name we should check that it matches - && name.map_or(true, |name| name == info.name.as_str()) - }); + && name.map_or(false, |name| name == info.name.as_str()) + }) + .map(|(_, contract)| (FindMatchType::FullMatch, contract)); - let path_matches = self.contracts.iter().filter(move |(info, _)| { - // if a path is provided, check that it matches - // if no path is provided, don't match it - path.map_or(false, |needle| { - info.path.as_ref().map_or(false, |contract_path| contract_path.ends_with(needle)) + let path_matches = self + .contracts + .iter() + .filter(move |(info, _)| { + // if a path is provided, check that it matches + // if no path is provided, don't match it + path.map_or(false, |needle| { + info.path + .as_ref() + .map_or(false, |contract_path| contract_path.ends_with(needle)) + }) }) - }); + .map(|(_, contract)| (FindMatchType::Path, contract)); - let name_matches = self.contracts.iter().filter(move |(info, _)| { - // if name is provided, check that it matches - // if no name is provided, don't match it - name.map(|name| name == info.name.as_str()).unwrap_or(false) - }); + let name_matches = self + .contracts + .iter() + .filter(move |(info, _)| { + // if name is provided, check that it matches + // if no name is provided, don't match it + name.map(|name| name == info.name.as_str()).unwrap_or(false) + }) + .map(|(_, contract)| (FindMatchType::Name, contract)); - full_matches.chain(path_matches).chain(name_matches).map(|(_, contract)| contract) + full_matches.chain(path_matches).chain(name_matches) } /// Retrieves the length of the collection. @@ -394,3 +419,139 @@ impl DualCompiledContracts { }) } } + +#[cfg(test)] +mod tests { + use alloy_primitives::Bytes; + use zksync_types::bytecode::BytecodeHash; + + use super::*; + + fn find_sample() -> DualCompiledContracts { + let evm_empty_bytes = Bytes::from_static(&[0]).to_vec(); + let zk_empty_bytes = vec![0u8; 32]; + + let zk_bytecode_hash = BytecodeHash::for_bytecode(&zk_empty_bytes).value(); + + let sample_contract = DualCompiledContract { + zk_bytecode_hash, + zk_deployed_bytecode: zk_empty_bytes, + zk_factory_deps: Default::default(), + evm_bytecode_hash: B256::from_slice(&keccak256(&evm_empty_bytes)[..]), + evm_deployed_bytecode: evm_empty_bytes.clone(), + evm_bytecode: evm_empty_bytes, + }; + + let infos = [ + ContractInfo::new("src/Foo.sol:Foo"), + ContractInfo::new("src/Foo.sol:DoubleFoo"), + ContractInfo::new("test/Foo.t.sol:FooTest"), + ContractInfo::new("Bar"), + ContractInfo::new("BarScript"), + ContractInfo::new("script/Qux.sol:Foo"), + ContractInfo::new("script/Qux.sol:QuxScript"), + ]; + + let contracts = infos.into_iter().map(|info| (info, sample_contract.clone())); + DualCompiledContracts { + contracts: contracts.collect(), + zk_artifact_path: PathBuf::from("zkout"), + evm_artifact_path: PathBuf::from("out"), + } + } + + #[track_caller] + fn assert_find_results<'a>( + results: impl Iterator, + assertions: Vec, + ) { + let results = results.collect::>(); + + let num_assertions = assertions.len(); + let num_results = results.len(); + assert!( + num_assertions == num_results, + "unexpected number of results! Expected: {num_assertions}, got: {num_results}" + ); + + for (i, (assertion, (result, _))) in assertions.into_iter().zip(results).enumerate() { + assert!( + assertion == result, + "assertion failed for match #{i}! Expected: {assertion:?}, got: {result:?}" + ); + } + } + + #[test] + fn find_nothing() { + let collection = find_sample(); + + assert_find_results(collection.find(None, None), vec![]); + } + + #[test] + fn find_by_full_match() { + let collection = find_sample(); + + let foo_find_asserts = vec![ + FindMatchType::FullMatch, + FindMatchType::Path, + // DoubleFoo + FindMatchType::Path, + FindMatchType::Name, + // Qux.sol:Foo + FindMatchType::Name, + ]; + assert_find_results( + collection.find(Some("src/Foo.sol"), Some("Foo")), + foo_find_asserts.clone(), + ); + assert_find_results(collection.find(Some("Foo.sol"), Some("Foo")), foo_find_asserts); + + let foo_test_find_asserts = + vec![FindMatchType::FullMatch, FindMatchType::Path, FindMatchType::Name]; + assert_find_results( + collection.find(Some("test/Foo.t.sol"), Some("FooTest")), + foo_test_find_asserts.clone(), + ); + assert_find_results( + collection.find(Some("Foo.t.sol"), Some("FooTest")), + foo_test_find_asserts, + ); + } + + #[test] + fn find_by_path() { + let collection = find_sample(); + + let foo_find_asserts = vec![FindMatchType::Path, FindMatchType::Path]; + assert_find_results(collection.find(Some("src/Foo.sol"), None), foo_find_asserts.clone()); + assert_find_results(collection.find(Some("Foo.sol"), None), foo_find_asserts); + + assert_find_results( + collection.find(Some("test/Foo.t.sol"), None), + vec![FindMatchType::Path], + ); + assert_find_results( + collection.find(Some("Foo.t.sol"), Some("FooTester")), + vec![FindMatchType::Path], + ); + } + + #[test] + fn find_by_name() { + let collection = find_sample(); + + assert_find_results( + collection.find(None, Some("Foo")), + vec![FindMatchType::Name, FindMatchType::Name], + ); + assert_find_results(collection.find(None, Some("QuxScript")), vec![FindMatchType::Name]); + + assert_find_results(collection.find(None, Some("BarScript")), vec![FindMatchType::Name]); + assert_find_results( + collection.find(Some("Bar.s.sol"), Some("BarScript")), + vec![FindMatchType::Name], + ); + } +} From 81c6cf866089d07521a60d120cc6b3bee3318ea3 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Thu, 23 Jan 2025 17:31:33 +0100 Subject: [PATCH 51/61] fix: clippy --- .../zksync/compilers/src/dual_compiled_contracts.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/zksync/compilers/src/dual_compiled_contracts.rs b/crates/zksync/compilers/src/dual_compiled_contracts.rs index 6be9338ec..00571e58b 100644 --- a/crates/zksync/compilers/src/dual_compiled_contracts.rs +++ b/crates/zksync/compilers/src/dual_compiled_contracts.rs @@ -338,12 +338,12 @@ impl DualCompiledContracts { .filter(move |(info, _)| { // if user provides a path we should check that it matches // we check using `ends_with` to account for prefixes - path.map_or(false, |needle| + path.is_some_and(|needle| info.path.as_ref() - .map_or(false, + .is_some_and( |contract_path| contract_path.ends_with(needle))) // if user provides a name we should check that it matches - && name.map_or(false, |name| name == info.name.as_str()) + && name.is_some_and(|name| name == info.name.as_str()) }) .map(|(_, contract)| (FindMatchType::FullMatch, contract)); @@ -353,10 +353,8 @@ impl DualCompiledContracts { .filter(move |(info, _)| { // if a path is provided, check that it matches // if no path is provided, don't match it - path.map_or(false, |needle| { - info.path - .as_ref() - .map_or(false, |contract_path| contract_path.ends_with(needle)) + path.is_some_and(|needle| { + info.path.as_ref().is_some_and(|contract_path| contract_path.ends_with(needle)) }) }) .map(|(_, contract)| (FindMatchType::Path, contract)); From f8a4f9551266f4435c44650541d87f6e73478175 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Thu, 23 Jan 2025 19:30:59 +0100 Subject: [PATCH 52/61] refactor(strategy): dedicated linking module --- crates/evm/evm/src/executors/strategy.rs | 141 +-------- crates/strategy/zksync/src/executor/runner.rs | 275 +----------------- 2 files changed, 25 insertions(+), 391 deletions(-) diff --git a/crates/evm/evm/src/executors/strategy.rs b/crates/evm/evm/src/executors/strategy.rs index 8b9564b54..d29f76985 100644 --- a/crates/evm/evm/src/executors/strategy.rs +++ b/crates/evm/evm/src/executors/strategy.rs @@ -1,22 +1,18 @@ -use std::{any::Any, borrow::Borrow, collections::BTreeMap, fmt::Debug, path::Path}; +use std::{any::Any, fmt::Debug, path::Path}; -use alloy_json_abi::JsonAbi; -use alloy_primitives::{Address, Bytes, TxKind, B256, U256}; +use alloy_primitives::{Address, U256}; use alloy_serde::OtherFields; -use eyre::{Context, Result}; +use eyre::Result; use foundry_cheatcodes::strategy::{ CheatcodeInspectorStrategy, EvmCheatcodeInspectorStrategyRunner, }; -use foundry_common::{ContractsByArtifact, TestFunctionExt, TransactionMaybeSigned}; -use foundry_compilers::{ - artifacts::Libraries, contracts::ArtifactContracts, Artifact, ArtifactId, ProjectCompileOutput, -}; +use foundry_compilers::ProjectCompileOutput; use foundry_config::Config; use foundry_evm_core::{ backend::{strategy::BackendStrategy, Backend, BackendResult, CowBackend}, decode::RevertDecoder, }; -use foundry_linking::{Linker, LinkerError}; +use foundry_linking::LinkerError; use foundry_zksync_compilers::{ compilers::{artifact_output::zk::ZkArtifactOutput, zksolc::ZkSolcCompiler}, dual_compiled_contracts::DualCompiledContracts, @@ -28,7 +24,10 @@ use revm::{ use crate::inspectors::InspectorStack; -use super::{DeployResult, EvmError, Executor}; +use super::{EvmError, Executor}; + +mod libraries; +pub use libraries::*; pub trait ExecutorStrategyContext: Debug + Send + Sync + Any { /// Clone the strategy context. @@ -61,34 +60,6 @@ pub struct ExecutorStrategy { pub context: Box, } -pub struct LinkOutput { - pub deployable_contracts: BTreeMap, - pub revert_decoder: RevertDecoder, - pub linked_contracts: ArtifactContracts, - pub known_contracts: ContractsByArtifact, - pub libs_to_deploy: Vec, - pub libraries: Libraries, -} - -/// Type of library deployment -#[derive(Debug, Clone)] -pub enum DeployLibKind { - /// CREATE(bytecode) - Create(Bytes), - - /// CREATE2(salt, bytecode) - Create2(B256, Bytes), -} - -/// Represents the result of a library deployment -#[derive(Debug)] -pub struct DeployLibResult { - /// Result of the deployment - pub result: DeployResult, - /// Equivalent transaction to deploy the given library - pub tx: Option, -} - impl ExecutorStrategy { pub fn new_evm() -> Self { Self { runner: &EvmExecutorStrategyRunner, context: Box::new(()) } @@ -253,51 +224,7 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { input: &ProjectCompileOutput, deployer: Address, ) -> Result { - let contracts = - input.artifact_ids().map(|(id, v)| (id.with_stripped_file_prefixes(root), v)).collect(); - let linker = Linker::new(root, contracts); - - // Build revert decoder from ABIs of all artifacts. - let abis = linker - .contracts - .iter() - .filter_map(|(_, contract)| contract.abi.as_ref().map(|abi| abi.borrow())); - let revert_decoder = RevertDecoder::new().with_abis(abis); - - let foundry_linking::LinkOutput { libraries, libs_to_deploy } = linker - .link_with_nonce_or_address(Default::default(), deployer, 0, linker.contracts.keys())?; - - let linked_contracts = linker.get_linked_artifacts(&libraries)?; - - // Create a mapping of name => (abi, deployment code, Vec) - let mut deployable_contracts = BTreeMap::default(); - for (id, contract) in linked_contracts.iter() { - let Some(abi) = &contract.abi else { continue }; - - // if it's a test, link it and add to deployable contracts - if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && - abi.functions().any(|func| func.name.is_any_test()) - { - let Some(bytecode) = - contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty()) - else { - continue; - }; - - deployable_contracts.insert(id.clone(), (abi.clone(), bytecode)); - } - } - - let known_contracts = ContractsByArtifact::new(linked_contracts.clone()); - - Ok(LinkOutput { - deployable_contracts, - revert_decoder, - linked_contracts, - known_contracts, - libs_to_deploy, - libraries, - }) + self.link_impl(root, input, deployer) } /// Deploys a library, applying state changes @@ -309,53 +236,7 @@ impl ExecutorStrategyRunner for EvmExecutorStrategyRunner { value: U256, rd: Option<&RevertDecoder>, ) -> Result, EvmError> { - let nonce = self.get_nonce(executor, from).context("retrieving sender nonce")?; - - match kind { - DeployLibKind::Create(code) => { - executor.deploy(from, code.clone(), value, rd).map(|dr| { - let mut request = TransactionMaybeSigned::new(Default::default()); - let unsigned = request.as_unsigned_mut().unwrap(); - unsigned.from = Some(from); - unsigned.input = code.into(); - unsigned.nonce = Some(nonce); - - vec![DeployLibResult { result: dr, tx: Some(request) }] - }) - } - DeployLibKind::Create2(salt, code) => { - let create2_deployer = executor.create2_deployer(); - - let calldata: Bytes = [salt.as_ref(), code.as_ref()].concat().into(); - let result = - executor.transact_raw(from, create2_deployer, calldata.clone(), value)?; - let result = result.into_result(rd)?; - - let address = result - .out - .as_ref() - .and_then(|out| out.address().cloned()) - .unwrap_or_else(|| create2_deployer.create2_from_code(salt, code.as_ref())); - debug!(%address, "deployed contract with create2"); - - let mut request = TransactionMaybeSigned::new(Default::default()); - let unsigned = request.as_unsigned_mut().unwrap(); - unsigned.from = Some(from); - unsigned.input = calldata.into(); - unsigned.nonce = Some(nonce); - unsigned.to = Some(TxKind::Call(create2_deployer)); - - // manually increase nonce when performing CALLs - executor - .set_nonce(from, nonce + 1) - .context("increasing nonce after CREATE2 deployment")?; - - Ok(vec![DeployLibResult { - result: DeployResult { raw: result, address }, - tx: Some(request), - }]) - } - } + self.deploy_library_impl(executor, from, kind, value, rd) } fn call( diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 6efb0046f..e3fa01a25 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -1,47 +1,34 @@ -use std::{collections::HashMap, path::Path}; +use std::path::Path; -use alloy_primitives::{keccak256, Address, Bytes, TxKind, B256, U256}; +use alloy_primitives::{Address, U256}; use alloy_rpc_types::serde_helpers::OtherFields; -use alloy_zksync::{ - contracts::l2::contract_deployer::CONTRACT_DEPLOYER_ADDRESS, - provider::{zksync_provider, ZksyncProvider}, -}; +use alloy_zksync::provider::{zksync_provider, ZksyncProvider}; use eyre::Result; -use foundry_linking::{ - LinkerError, ZkLinker, ZkLinkerError, DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, -}; +use foundry_linking::LinkerError; use revm::{ - primitives::{CreateScheme, Env, EnvWithHandlerCfg, Output, ResultAndState}, + primitives::{Env, EnvWithHandlerCfg, ResultAndState}, Database, }; -use tracing::debug; -use foundry_common::{ContractsByArtifact, TransactionMaybeSigned}; -use foundry_compilers::{ - artifacts::CompactContractBytecodeCow, contracts::ArtifactContracts, info::ContractInfo, - Artifact, ProjectCompileOutput, -}; +use foundry_compilers::ProjectCompileOutput; use foundry_config::Config; use foundry_evm::{ - backend::{Backend, BackendResult, CowBackend, DatabaseExt}, + backend::{Backend, BackendResult, CowBackend}, decode::RevertDecoder, executors::{ strategy::{ DeployLibKind, DeployLibResult, EvmExecutorStrategyRunner, ExecutorStrategyContext, ExecutorStrategyExt, ExecutorStrategyRunner, LinkOutput, }, - DeployResult, EvmError, Executor, + EvmError, Executor, }, inspectors::InspectorStack, }; use foundry_zksync_compilers::{ compilers::{artifact_output::zk::ZkArtifactOutput, zksolc::ZkSolcCompiler}, - dual_compiled_contracts::{DualCompiledContract, DualCompiledContracts}, -}; -use foundry_zksync_core::{ - encode_create_params, hash_bytecode, vm::ZkEnv, ZkTransactionMetadata, - DEFAULT_CREATE2_DEPLOYER_ZKSYNC, ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY, + dual_compiled_contracts::DualCompiledContracts, }; +use foundry_zksync_core::vm::ZkEnv; use crate::{ backend::{ZksyncBackendStrategyBuilder, ZksyncInspectContext}, @@ -49,6 +36,8 @@ use crate::{ executor::{try_get_zksync_transaction_metadata, ZksyncExecutorStrategyContext}, }; +mod libraries; + /// Defines the [ExecutorStrategyRunner] strategy for ZKsync. #[derive(Debug, Default, Clone)] pub struct ZksyncExecutorStrategyRunner; @@ -135,159 +124,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { input: &ProjectCompileOutput, deployer: Address, ) -> Result { - let evm_link = EvmExecutorStrategyRunner.link(ctx, config, root, input, deployer)?; - - let ctx = get_context(ctx); - let Some(input) = ctx.compilation_output.as_ref() else { - return Err(LinkerError::MissingTargetArtifact); - }; - - // we don't strip here unlinke upstream due to - // `input` being used later during linking - // and that is unstripped - let contracts: ArtifactContracts> = - input.artifact_ids().collect(); - - let Ok(zksolc) = foundry_config::zksync::config_zksolc_compiler(config) else { - tracing::error!("unable to determine zksolc compiler to be used for linking"); - // TODO(zk): better error - return Err(LinkerError::CyclicDependency); - }; - let version = zksolc.version().map_err(|_| LinkerError::CyclicDependency)?; - - let linker = ZkLinker::new(root, contracts.clone(), zksolc, input); - - let zk_linker_error_to_linker = |zk_error| match zk_error { - ZkLinkerError::Inner(err) => err, - // TODO(zk): better error value - ZkLinkerError::MissingLibraries(libs) => LinkerError::MissingLibraryArtifact { - file: "libraries".to_string(), - name: libs.len().to_string(), - }, - ZkLinkerError::MissingFactoryDeps(libs) => LinkerError::MissingLibraryArtifact { - file: "factoryDeps".to_string(), - name: libs.len().to_string(), - }, - }; - - let foundry_linking::LinkOutput { libraries, libs_to_deploy: _ } = linker - .zk_link_with_nonce_or_address( - Default::default(), - deployer, - // NOTE(zk): match with EVM nonces as we will be doing a duplex deployment for - // the libs - 0, - linker.linker.contracts.keys(), // link everything - ) - .map_err(zk_linker_error_to_linker)?; - - // if we have no libraries then no linking will happen - // so we can skip the version check - if !libraries.is_empty() { - // TODO(zk): better error - if version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { - tracing::error!( - %version, - minimum_version = %DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, - "deploy-time linking not supported" - ); - return Err(LinkerError::CyclicDependency); - } - } - - let linked_contracts = linker - .zk_get_linked_artifacts(linker.linker.contracts.keys(), &libraries) - .map_err(zk_linker_error_to_linker)?; - - let newly_linked_dual_compiled_contracts = linked_contracts - .iter() - .flat_map(|(needle, zk)| { - // match EVM linking's prefix stripping - let stripped = needle.clone().with_stripped_file_prefixes(root); - evm_link - .linked_contracts - .iter() - .find(|(id, _)| id.source == stripped.source && id.name == stripped.name) - .map(|(_, evm)| (needle, stripped, zk, evm)) - }) - .filter(|(_, _, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) - .map(|(unstripped_id, id, linked_zk, evm)| { - let (_, unlinked_zk_artifact) = input - .artifact_ids() - .find(|(contract_id, _)| contract_id == unstripped_id) - .expect("unable to find original (pre-linking) artifact"); - let zk_bytecode = - linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); - let zk_hash = hash_bytecode(&zk_bytecode); - let evm_deployed = - evm.get_deployed_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); - let evm_bytecode = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); - let contract_info = ContractInfo { - name: id.name.clone(), - path: Some(id.source.to_string_lossy().into_owned()), - }; - let contract = DualCompiledContract { - zk_bytecode_hash: zk_hash, - zk_deployed_bytecode: zk_bytecode.to_vec(), - // rest of factory deps is populated later - zk_factory_deps: vec![zk_bytecode.to_vec()], - evm_bytecode_hash: B256::from_slice(&keccak256(evm_deployed.as_ref())[..]), - // TODO(zk): determine if this is ok, as it's - // not really used in dual compiled contracts - evm_deployed_bytecode: evm_deployed.to_vec(), - evm_bytecode: evm_bytecode.to_vec(), - }; - - let mut factory_deps = unlinked_zk_artifact.all_factory_deps().collect::>(); - factory_deps.dedup(); - - ((contract_info.clone(), contract), (contract_info, factory_deps)) - }); - - let (new_contracts, new_contracts_deps): (Vec<_>, HashMap<_, _>) = - newly_linked_dual_compiled_contracts.unzip(); - ctx.dual_compiled_contracts.extend(new_contracts); - - // now that we have an updated list of DualCompiledContracts - // retrieve all the factory deps for a given contracts and store them - new_contracts_deps.into_iter().for_each(|(info, deps)| { - deps.into_iter().for_each(|dep| { - let mut split = dep.split(':'); - let path = split.next().expect("malformed factory dep path"); - let name = split.next().expect("malformed factory dep name"); - - let bytecode = ctx - .dual_compiled_contracts - .find(Some(path), Some(name)) - .next() - .expect("unknown factory dep") - .1 - .zk_deployed_bytecode - .clone(); - - ctx.dual_compiled_contracts.insert_factory_deps(&info, Some(bytecode)); - }); - }); - - let linked_contracts: ArtifactContracts = contracts - .into_iter() - .map(|(id, art)| (id, foundry_compilers::artifacts::CompactContractBytecode::from(art))) - // Extend original zk contracts with newly linked ones - .chain(linked_contracts) - // Extend zk contracts with solc contracts as well. This is required for traces to - // accurately detect contract names deployed in EVM mode, and when using - // `vm.zkVmSkip()` cheatcode. - .chain(evm_link.linked_contracts) - .collect(); - - Ok(LinkOutput { - deployable_contracts: evm_link.deployable_contracts, - revert_decoder: evm_link.revert_decoder, - known_contracts: ContractsByArtifact::new(linked_contracts.clone()), - linked_contracts, - libs_to_deploy: evm_link.libs_to_deploy, - libraries: evm_link.libraries, - }) + self.link_impl(ctx, config, root, input, deployer) } fn deploy_library( @@ -298,91 +135,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { value: U256, rd: Option<&RevertDecoder>, ) -> Result, EvmError> { - // sync deployer account info - let nonce = EvmExecutorStrategyRunner.get_nonce(executor, from).expect("deployer to exist"); - let balance = - EvmExecutorStrategyRunner.get_balance(executor, from).expect("deployer to exist"); - - Self::set_deployment_nonce(executor, from, nonce).map_err(|err| eyre::eyre!(err))?; - self.set_balance(executor, from, balance).map_err(|err| eyre::eyre!(err))?; - tracing::debug!(?nonce, ?balance, sender = ?from, "deploying lib in EraVM"); - - let mut evm_deployment = - EvmExecutorStrategyRunner.deploy_library(executor, from, kind.clone(), value, rd)?; - - let ctx = get_context(executor.strategy.context.as_mut()); - - let (code, create_scheme, to) = match kind { - DeployLibKind::Create(bytes) => { - (bytes, CreateScheme::Create, CONTRACT_DEPLOYER_ADDRESS) - } - DeployLibKind::Create2(salt, bytes) => ( - bytes, - CreateScheme::Create2 { salt: salt.into() }, - DEFAULT_CREATE2_DEPLOYER_ZKSYNC, - ), - }; - - // lookup dual compiled contract based on EVM bytecode - let Some((_, dual_contract)) = - ctx.dual_compiled_contracts.find_by_evm_bytecode(code.as_ref()) - else { - // we don't know what the equivalent zk contract would be - return Ok(evm_deployment); - }; - - // no need for constructor args as it's a lib - let create_params: Bytes = - encode_create_params(&create_scheme, dual_contract.zk_bytecode_hash, vec![]).into(); - - // populate ctx.transaction_context with factory deps - // we also populate the ctx so the deployment is executed - // entirely in EraVM - let factory_deps = ctx.dual_compiled_contracts.fetch_all_factory_deps(dual_contract); - tracing::debug!(n_fdeps = factory_deps.len()); - - // persist existing paymaster data (TODO(zk): is this needed?) - let paymaster_data = - ctx.transaction_context.take().and_then(|metadata| metadata.paymaster_data); - let metadata = ZkTransactionMetadata { factory_deps, paymaster_data }; - ctx.transaction_context = Some(metadata.clone()); - - let result = executor.transact_raw(from, to, create_params.clone(), value)?; - let result = result.into_result(rd)?; - - let Some(Output::Create(_, Some(address))) = result.out else { - return Err(eyre::eyre!( - "Deployment succeeded, but no address was returned: {result:#?}" - ) - .into()); - }; - - // also mark this library as persistent, this will ensure that the state of the library is - // persistent across fork swaps in forking mode - executor.backend_mut().add_persistent_account(address); - debug!(%address, "deployed contract"); - - let mut request = TransactionMaybeSigned::new(Default::default()); - let unsigned = request.as_unsigned_mut().unwrap(); - unsigned.from = Some(from); - unsigned.input = create_params.into(); - unsigned.nonce = Some(nonce); - // we use the deployer here for consistency with linking - unsigned.to = Some(TxKind::Call(to)); - unsigned.other.insert( - ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY.to_string(), - serde_json::to_value(metadata).expect("failed encoding json"), - ); - - // ignore all EVM broadcastables - evm_deployment.iter_mut().for_each(|result| { - result.tx.take(); - }); - evm_deployment.push(DeployLibResult { - result: DeployResult { raw: result, address }, - tx: Some(request), - }); - Ok(evm_deployment) + self.deploy_library_impl(executor, from, kind, value, rd) } fn new_backend_strategy(&self) -> foundry_evm_core::backend::strategy::BackendStrategy { From dec820b1ac0c4b84234e75b68f7ca9f8b21e7c3a Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Fri, 24 Jan 2025 10:47:59 +0100 Subject: [PATCH 53/61] fix: forgot to commit the new files --- .../evm/src/executors/strategy/libraries.rs | 157 ++++++++++ .../zksync/src/executor/runner/libraries.rs | 295 ++++++++++++++++++ 2 files changed, 452 insertions(+) create mode 100644 crates/evm/evm/src/executors/strategy/libraries.rs create mode 100644 crates/strategy/zksync/src/executor/runner/libraries.rs diff --git a/crates/evm/evm/src/executors/strategy/libraries.rs b/crates/evm/evm/src/executors/strategy/libraries.rs new file mode 100644 index 000000000..f0d9a0c70 --- /dev/null +++ b/crates/evm/evm/src/executors/strategy/libraries.rs @@ -0,0 +1,157 @@ +//! Contains various definitions and items related to deploy-time linking + +use std::{borrow::Borrow, collections::BTreeMap, path::Path}; + +use alloy_json_abi::JsonAbi; +use alloy_primitives::{Address, Bytes, TxKind, B256, U256}; +use eyre::Context; +use foundry_common::{ContractsByArtifact, TestFunctionExt, TransactionMaybeSigned}; +use foundry_compilers::{ + artifacts::Libraries, contracts::ArtifactContracts, Artifact, ArtifactId, ProjectCompileOutput, +}; +use foundry_evm_core::decode::RevertDecoder; +use foundry_linking::{Linker, LinkerError}; + +use crate::executors::{DeployResult, EvmError, Executor}; + +use super::{EvmExecutorStrategyRunner, ExecutorStrategyRunner}; + +pub struct LinkOutput { + pub deployable_contracts: BTreeMap, + pub revert_decoder: RevertDecoder, + pub linked_contracts: ArtifactContracts, + pub known_contracts: ContractsByArtifact, + pub libs_to_deploy: Vec, + pub libraries: Libraries, +} + +/// Type of library deployment +#[derive(Debug, Clone)] +pub enum DeployLibKind { + /// CREATE(bytecode) + Create(Bytes), + + /// CREATE2(salt, bytecode) + Create2(B256, Bytes), +} + +/// Represents the result of a library deployment +#[derive(Debug)] +pub struct DeployLibResult { + /// Result of the deployment + pub result: DeployResult, + /// Equivalent transaction to deploy the given library + pub tx: Option, +} + +impl EvmExecutorStrategyRunner { + pub(super) fn link_impl( + &self, + root: &Path, + input: &ProjectCompileOutput, + deployer: Address, + ) -> Result { + let contracts = + input.artifact_ids().map(|(id, v)| (id.with_stripped_file_prefixes(root), v)).collect(); + let linker = Linker::new(root, contracts); + + // Build revert decoder from ABIs of all artifacts. + let abis = linker + .contracts + .iter() + .filter_map(|(_, contract)| contract.abi.as_ref().map(|abi| abi.borrow())); + let revert_decoder = RevertDecoder::new().with_abis(abis); + + let foundry_linking::LinkOutput { libraries, libs_to_deploy } = linker + .link_with_nonce_or_address(Default::default(), deployer, 0, linker.contracts.keys())?; + + let linked_contracts = linker.get_linked_artifacts(&libraries)?; + + // Create a mapping of name => (abi, deployment code, Vec) + let mut deployable_contracts = BTreeMap::default(); + for (id, contract) in linked_contracts.iter() { + let Some(abi) = &contract.abi else { continue }; + + // if it's a test, link it and add to deployable contracts + if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) + && abi.functions().any(|func| func.name.is_any_test()) + { + let Some(bytecode) = + contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty()) + else { + continue; + }; + + deployable_contracts.insert(id.clone(), (abi.clone(), bytecode)); + } + } + + let known_contracts = ContractsByArtifact::new(linked_contracts.clone()); + + Ok(LinkOutput { + deployable_contracts, + revert_decoder, + linked_contracts, + known_contracts, + libs_to_deploy, + libraries, + }) + } + + pub(super) fn deploy_library_impl( + &self, + executor: &mut Executor, + from: Address, + kind: DeployLibKind, + value: U256, + rd: Option<&RevertDecoder>, + ) -> Result, EvmError> { + let nonce = self.get_nonce(executor, from).context("retrieving sender nonce")?; + + match kind { + DeployLibKind::Create(code) => { + executor.deploy(from, code.clone(), value, rd).map(|dr| { + let mut request = TransactionMaybeSigned::new(Default::default()); + let unsigned = request.as_unsigned_mut().unwrap(); + unsigned.from = Some(from); + unsigned.input = code.into(); + unsigned.nonce = Some(nonce); + + vec![DeployLibResult { result: dr, tx: Some(request) }] + }) + } + DeployLibKind::Create2(salt, code) => { + let create2_deployer = executor.create2_deployer(); + + let calldata: Bytes = [salt.as_ref(), code.as_ref()].concat().into(); + let result = + executor.transact_raw(from, create2_deployer, calldata.clone(), value)?; + let result = result.into_result(rd)?; + + let address = result + .out + .as_ref() + .and_then(|out| out.address().cloned()) + .unwrap_or_else(|| create2_deployer.create2_from_code(salt, code.as_ref())); + debug!(%address, "deployed contract with create2"); + + let mut request = TransactionMaybeSigned::new(Default::default()); + let unsigned = request.as_unsigned_mut().unwrap(); + unsigned.from = Some(from); + unsigned.input = calldata.into(); + unsigned.nonce = Some(nonce); + unsigned.to = Some(TxKind::Call(create2_deployer)); + + // manually increase nonce when performing CALLs + executor + .set_nonce(from, nonce + 1) + .context("increasing nonce after CREATE2 deployment")?; + + Ok(vec![DeployLibResult { + result: DeployResult { raw: result, address }, + tx: Some(request), + }]) + } + } + } +} diff --git a/crates/strategy/zksync/src/executor/runner/libraries.rs b/crates/strategy/zksync/src/executor/runner/libraries.rs new file mode 100644 index 000000000..6ef500412 --- /dev/null +++ b/crates/strategy/zksync/src/executor/runner/libraries.rs @@ -0,0 +1,295 @@ +//! Contains various definitions and items related to deploy-time linking +//! for zksync + +use std::{collections::HashMap, path::Path}; + +use alloy_primitives::{keccak256, Address, Bytes, TxKind, B256, U256}; +use alloy_zksync::contracts::l2::contract_deployer::CONTRACT_DEPLOYER_ADDRESS; +use foundry_common::{ContractsByArtifact, TransactionMaybeSigned}; +use foundry_compilers::{ + artifacts::CompactContractBytecodeCow, contracts::ArtifactContracts, info::ContractInfo, + Artifact, ProjectCompileOutput, +}; +use foundry_config::Config; +use foundry_evm::{ + backend::DatabaseExt, + decode::RevertDecoder, + executors::{ + strategy::{ + DeployLibKind, DeployLibResult, EvmExecutorStrategyRunner, ExecutorStrategyContext, + ExecutorStrategyRunner, LinkOutput, + }, + DeployResult, EvmError, Executor, + }, +}; +use foundry_linking::{ + LinkerError, ZkLinker, ZkLinkerError, DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, +}; +use foundry_zksync_compilers::dual_compiled_contracts::DualCompiledContract; +use foundry_zksync_core::{ + encode_create_params, hash_bytecode, ZkTransactionMetadata, DEFAULT_CREATE2_DEPLOYER_ZKSYNC, + ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY, +}; +use revm::primitives::{CreateScheme, Output}; + +use super::{get_context, ZksyncExecutorStrategyRunner}; + +impl ZksyncExecutorStrategyRunner { + pub(super) fn link_impl( + &self, + ctx: &mut dyn ExecutorStrategyContext, + config: &Config, + root: &Path, + input: &ProjectCompileOutput, + deployer: Address, + ) -> Result { + let evm_link = EvmExecutorStrategyRunner.link(ctx, config, root, input, deployer)?; + + let ctx = get_context(ctx); + let Some(input) = ctx.compilation_output.as_ref() else { + return Err(LinkerError::MissingTargetArtifact); + }; + + // we don't strip here unlinke upstream due to + // `input` being used later during linking + // and that is unstripped + let contracts: ArtifactContracts> = + input.artifact_ids().collect(); + + let Ok(zksolc) = foundry_config::zksync::config_zksolc_compiler(config) else { + tracing::error!("unable to determine zksolc compiler to be used for linking"); + // TODO(zk): better error + return Err(LinkerError::CyclicDependency); + }; + let version = zksolc.version().map_err(|_| LinkerError::CyclicDependency)?; + + let linker = ZkLinker::new(root, contracts.clone(), zksolc, input); + + let zk_linker_error_to_linker = |zk_error| match zk_error { + ZkLinkerError::Inner(err) => err, + // TODO(zk): better error value + ZkLinkerError::MissingLibraries(libs) => LinkerError::MissingLibraryArtifact { + file: "libraries".to_string(), + name: libs.len().to_string(), + }, + ZkLinkerError::MissingFactoryDeps(libs) => LinkerError::MissingLibraryArtifact { + file: "factoryDeps".to_string(), + name: libs.len().to_string(), + }, + }; + + let foundry_linking::LinkOutput { libraries, libs_to_deploy: _ } = linker + .zk_link_with_nonce_or_address( + Default::default(), + deployer, + // NOTE(zk): match with EVM nonces as we will be doing a duplex deployment for + // the libs + 0, + linker.linker.contracts.keys(), // link everything + ) + .map_err(zk_linker_error_to_linker)?; + + // if we have no libraries then no linking will happen + // so we can skip the version check + if !libraries.is_empty() { + // TODO(zk): better error + if version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { + tracing::error!( + %version, + minimum_version = %DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, + "deploy-time linking not supported" + ); + return Err(LinkerError::CyclicDependency); + } + } + + let linked_contracts = linker + .zk_get_linked_artifacts(linker.linker.contracts.keys(), &libraries) + .map_err(zk_linker_error_to_linker)?; + + let newly_linked_dual_compiled_contracts = linked_contracts + .iter() + .flat_map(|(needle, zk)| { + // match EVM linking's prefix stripping + let stripped = needle.clone().with_stripped_file_prefixes(root); + evm_link + .linked_contracts + .iter() + .find(|(id, _)| id.source == stripped.source && id.name == stripped.name) + .map(|(_, evm)| (needle, stripped, zk, evm)) + }) + .filter(|(_, _, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) + .map(|(unstripped_id, id, linked_zk, evm)| { + let (_, unlinked_zk_artifact) = input + .artifact_ids() + .find(|(contract_id, _)| contract_id == unstripped_id) + .expect("unable to find original (pre-linking) artifact"); + let zk_bytecode = + linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); + let zk_hash = hash_bytecode(&zk_bytecode); + let evm_deployed = + evm.get_deployed_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); + let evm_bytecode = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); + let contract_info = ContractInfo { + name: id.name.clone(), + path: Some(id.source.to_string_lossy().into_owned()), + }; + let contract = DualCompiledContract { + zk_bytecode_hash: zk_hash, + zk_deployed_bytecode: zk_bytecode.to_vec(), + // rest of factory deps is populated later + zk_factory_deps: vec![zk_bytecode.to_vec()], + evm_bytecode_hash: B256::from_slice(&keccak256(evm_deployed.as_ref())[..]), + // TODO(zk): determine if this is ok, as it's + // not really used in dual compiled contracts + evm_deployed_bytecode: evm_deployed.to_vec(), + evm_bytecode: evm_bytecode.to_vec(), + }; + + let mut factory_deps = unlinked_zk_artifact.all_factory_deps().collect::>(); + factory_deps.dedup(); + + ((contract_info.clone(), contract), (contract_info, factory_deps)) + }); + + let (new_contracts, new_contracts_deps): (Vec<_>, HashMap<_, _>) = + newly_linked_dual_compiled_contracts.unzip(); + ctx.dual_compiled_contracts.extend(new_contracts); + + // now that we have an updated list of DualCompiledContracts + // retrieve all the factory deps for a given contracts and store them + new_contracts_deps.into_iter().for_each(|(info, deps)| { + deps.into_iter().for_each(|dep| { + let mut split = dep.split(':'); + let path = split.next().expect("malformed factory dep path"); + let name = split.next().expect("malformed factory dep name"); + + let bytecode = ctx + .dual_compiled_contracts + .find(Some(path), Some(name)) + .next() + .expect("unknown factory dep") + .1 + .zk_deployed_bytecode + .clone(); + + ctx.dual_compiled_contracts.insert_factory_deps(&info, Some(bytecode)); + }); + }); + + let linked_contracts: ArtifactContracts = contracts + .into_iter() + .map(|(id, art)| (id, foundry_compilers::artifacts::CompactContractBytecode::from(art))) + // Extend original zk contracts with newly linked ones + .chain(linked_contracts) + // Extend zk contracts with solc contracts as well. This is required for traces to + // accurately detect contract names deployed in EVM mode, and when using + // `vm.zkVmSkip()` cheatcode. + .chain(evm_link.linked_contracts) + .collect(); + + Ok(LinkOutput { + deployable_contracts: evm_link.deployable_contracts, + revert_decoder: evm_link.revert_decoder, + known_contracts: ContractsByArtifact::new(linked_contracts.clone()), + linked_contracts, + libs_to_deploy: evm_link.libs_to_deploy, + libraries: evm_link.libraries, + }) + } + + pub(super) fn deploy_library_impl( + &self, + executor: &mut Executor, + from: Address, + kind: DeployLibKind, + value: U256, + rd: Option<&RevertDecoder>, + ) -> Result, EvmError> { + // sync deployer account info + let nonce = EvmExecutorStrategyRunner.get_nonce(executor, from).expect("deployer to exist"); + let balance = + EvmExecutorStrategyRunner.get_balance(executor, from).expect("deployer to exist"); + + Self::set_deployment_nonce(executor, from, nonce).map_err(|err| eyre::eyre!(err))?; + self.set_balance(executor, from, balance).map_err(|err| eyre::eyre!(err))?; + tracing::debug!(?nonce, ?balance, sender = ?from, "deploying lib in EraVM"); + + let mut evm_deployment = + EvmExecutorStrategyRunner.deploy_library(executor, from, kind.clone(), value, rd)?; + + let ctx = get_context(executor.strategy.context.as_mut()); + + let (code, create_scheme, to) = match kind { + DeployLibKind::Create(bytes) => { + (bytes, CreateScheme::Create, CONTRACT_DEPLOYER_ADDRESS) + } + DeployLibKind::Create2(salt, bytes) => ( + bytes, + CreateScheme::Create2 { salt: salt.into() }, + DEFAULT_CREATE2_DEPLOYER_ZKSYNC, + ), + }; + + // lookup dual compiled contract based on EVM bytecode + let Some((_, dual_contract)) = + ctx.dual_compiled_contracts.find_by_evm_bytecode(code.as_ref()) + else { + // we don't know what the equivalent zk contract would be + return Ok(evm_deployment); + }; + + // no need for constructor args as it's a lib + let create_params: Bytes = + encode_create_params(&create_scheme, dual_contract.zk_bytecode_hash, vec![]).into(); + + // populate ctx.transaction_context with factory deps + // we also populate the ctx so the deployment is executed + // entirely in EraVM + let factory_deps = ctx.dual_compiled_contracts.fetch_all_factory_deps(dual_contract); + tracing::debug!(n_fdeps = factory_deps.len()); + + // persist existing paymaster data (TODO(zk): is this needed?) + let paymaster_data = + ctx.transaction_context.take().and_then(|metadata| metadata.paymaster_data); + let metadata = ZkTransactionMetadata { factory_deps, paymaster_data }; + ctx.transaction_context = Some(metadata.clone()); + + let result = executor.transact_raw(from, to, create_params.clone(), value)?; + let result = result.into_result(rd)?; + + let Some(Output::Create(_, Some(address))) = result.out else { + return Err(eyre::eyre!( + "Deployment succeeded, but no address was returned: {result:#?}" + ) + .into()); + }; + + // also mark this library as persistent, this will ensure that the state of the library is + // persistent across fork swaps in forking mode + executor.backend_mut().add_persistent_account(address); + tracing::debug!(%address, "deployed contract"); + + let mut request = TransactionMaybeSigned::new(Default::default()); + let unsigned = request.as_unsigned_mut().unwrap(); + unsigned.from = Some(from); + unsigned.input = create_params.into(); + unsigned.nonce = Some(nonce); + // we use the deployer here for consistency with linking + unsigned.to = Some(TxKind::Call(to)); + unsigned.other.insert( + ZKSYNC_TRANSACTION_OTHER_FIELDS_KEY.to_string(), + serde_json::to_value(metadata).expect("failed encoding json"), + ); + + // ignore all EVM broadcastables + evm_deployment.iter_mut().for_each(|result| { + result.tx.take(); + }); + evm_deployment.push(DeployLibResult { + result: DeployResult { raw: result, address }, + tx: Some(request), + }); + Ok(evm_deployment) + } +} From eea5fb826a3bc3d3d088121090c8b10356fbfd8f Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Fri, 24 Jan 2025 10:50:53 +0100 Subject: [PATCH 54/61] chore: fmt --- crates/evm/evm/src/executors/strategy/libraries.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/evm/evm/src/executors/strategy/libraries.rs b/crates/evm/evm/src/executors/strategy/libraries.rs index f0d9a0c70..f98ccdad1 100644 --- a/crates/evm/evm/src/executors/strategy/libraries.rs +++ b/crates/evm/evm/src/executors/strategy/libraries.rs @@ -73,8 +73,8 @@ impl EvmExecutorStrategyRunner { let Some(abi) = &contract.abi else { continue }; // if it's a test, link it and add to deployable contracts - if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) - && abi.functions().any(|func| func.name.is_any_test()) + if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && + abi.functions().any(|func| func.name.is_any_test()) { let Some(bytecode) = contract.get_bytecode_bytes().map(|b| b.into_owned()).filter(|b| !b.is_empty()) From b41be0d0815ede567ff181f7dbe8d9d03971de43 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Fri, 24 Jan 2025 14:22:36 +0100 Subject: [PATCH 55/61] fix(zk:link): new nonce types --- Cargo.lock | 70 +++++++++---------- crates/strategy/zksync/src/executor/runner.rs | 5 +- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 446ca6c57..fab72ce1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1146,7 +1146,7 @@ dependencies = [ [[package]] name = "anvil" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-chains", "alloy-consensus 0.8.0", @@ -1217,7 +1217,7 @@ dependencies = [ [[package]] name = "anvil-core" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-consensus 0.8.0", "alloy-dyn-abi", @@ -1241,7 +1241,7 @@ dependencies = [ [[package]] name = "anvil-rpc" -version = "0.0.3" +version = "0.0.4" dependencies = [ "serde", "serde_json", @@ -1249,7 +1249,7 @@ dependencies = [ [[package]] name = "anvil-server" -version = "0.0.3" +version = "0.0.4" dependencies = [ "anvil-rpc", "async-trait", @@ -2488,7 +2488,7 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cast" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-chains", "alloy-consensus 0.8.0", @@ -2604,7 +2604,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chisel" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -4239,7 +4239,7 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "forge" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-chains", "alloy-consensus 0.8.0", @@ -4338,7 +4338,7 @@ dependencies = [ [[package]] name = "forge-doc" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-primitives", "derive_more", @@ -4361,7 +4361,7 @@ dependencies = [ [[package]] name = "forge-fmt" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-primitives", "ariadne", @@ -4377,7 +4377,7 @@ dependencies = [ [[package]] name = "forge-script" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-chains", "alloy-consensus 0.8.0", @@ -4426,7 +4426,7 @@ dependencies = [ [[package]] name = "forge-script-sequence" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-network 0.8.0", "alloy-primitives", @@ -4445,7 +4445,7 @@ dependencies = [ [[package]] name = "forge-sol-macro-gen" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-json-abi", "alloy-sol-macro-expander", @@ -4461,7 +4461,7 @@ dependencies = [ [[package]] name = "forge-verify" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -4523,7 +4523,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-consensus 0.8.0", "alloy-dyn-abi", @@ -4575,7 +4575,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes-common" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-primitives", "revm", @@ -4583,7 +4583,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes-spec" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-sol-types", "foundry-macros", @@ -4594,7 +4594,7 @@ dependencies = [ [[package]] name = "foundry-cli" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-chains", "alloy-dyn-abi", @@ -4636,7 +4636,7 @@ dependencies = [ [[package]] name = "foundry-common" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-consensus 0.8.0", "alloy-contract", @@ -4688,7 +4688,7 @@ dependencies = [ [[package]] name = "foundry-common-fmt" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-consensus 0.8.0", "alloy-dyn-abi", @@ -4816,7 +4816,7 @@ dependencies = [ [[package]] name = "foundry-config" -version = "0.0.3" +version = "0.0.4" dependencies = [ "Inflector", "alloy-chains", @@ -4854,7 +4854,7 @@ dependencies = [ [[package]] name = "foundry-debugger" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-primitives", "crossterm", @@ -4872,7 +4872,7 @@ dependencies = [ [[package]] name = "foundry-evm" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -4904,7 +4904,7 @@ dependencies = [ [[package]] name = "foundry-evm-abi" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -4917,7 +4917,7 @@ dependencies = [ [[package]] name = "foundry-evm-core" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-consensus 0.8.0", "alloy-dyn-abi", @@ -4953,7 +4953,7 @@ dependencies = [ [[package]] name = "foundry-evm-coverage" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-primitives", "eyre", @@ -4968,7 +4968,7 @@ dependencies = [ [[package]] name = "foundry-evm-fuzz" -version = "0.0.3" +version = "0.0.4" dependencies = [ "ahash", "alloy-dyn-abi", @@ -4995,7 +4995,7 @@ dependencies = [ [[package]] name = "foundry-evm-traces" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -5048,7 +5048,7 @@ dependencies = [ [[package]] name = "foundry-linking" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-primitives", "foundry-compilers", @@ -5061,7 +5061,7 @@ dependencies = [ [[package]] name = "foundry-macros" -version = "0.0.3" +version = "0.0.4" dependencies = [ "proc-macro-error", "proc-macro2", @@ -5071,7 +5071,7 @@ dependencies = [ [[package]] name = "foundry-strategy-zksync" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -5100,7 +5100,7 @@ dependencies = [ [[package]] name = "foundry-test-utils" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-primitives", "alloy-provider", @@ -5128,7 +5128,7 @@ dependencies = [ [[package]] name = "foundry-wallets" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-consensus 0.8.0", "alloy-dyn-abi", @@ -5159,7 +5159,7 @@ dependencies = [ [[package]] name = "foundry-zksync-compilers" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -5185,7 +5185,7 @@ dependencies = [ [[package]] name = "foundry-zksync-core" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-network 0.8.0", "alloy-primitives", @@ -5214,7 +5214,7 @@ dependencies = [ [[package]] name = "foundry-zksync-inspectors" -version = "0.0.3" +version = "0.0.4" dependencies = [ "alloy-primitives", "foundry-evm-core", diff --git a/crates/strategy/zksync/src/executor/runner.rs b/crates/strategy/zksync/src/executor/runner.rs index 0cd75db36..ed891d13e 100644 --- a/crates/strategy/zksync/src/executor/runner.rs +++ b/crates/strategy/zksync/src/executor/runner.rs @@ -52,7 +52,8 @@ impl ZksyncExecutorStrategyRunner { // fetch the full nonce to preserve account's tx nonce let full_nonce = executor.backend.storage(address, slot)?; let full_nonce = foundry_zksync_core::state::parse_full_nonce(full_nonce); - let new_full_nonce = foundry_zksync_core::state::new_full_nonce(full_nonce.tx_nonce, nonce); + let new_full_nonce = + foundry_zksync_core::state::new_full_nonce(full_nonce.tx_nonce, nonce as u128); executor.backend.insert_account_storage(address, slot, new_full_nonce)?; Ok(()) @@ -113,7 +114,7 @@ impl ExecutorStrategyRunner for ZksyncExecutorStrategyRunner { let full_nonce = executor.backend.storage(address, slot)?; let full_nonce = foundry_zksync_core::state::parse_full_nonce(full_nonce); - Ok(full_nonce.tx_nonce) + Ok(full_nonce.tx_nonce as u64) } fn link( From 21b99205eef6941ad0703d2be13fe5bb461028bb Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 27 Jan 2025 13:45:17 +0100 Subject: [PATCH 56/61] test(zk): use default zksolc version normally --- Cargo.lock | 1 - crates/forge/tests/it/zk/linking.rs | 11 ++++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d31b59936..406961469 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5260,7 +5260,6 @@ dependencies = [ "alloy-primitives", "dirs 5.0.1", "era-solc", - "eyre", "fd-lock", "foundry-compilers", "foundry-compilers-artifacts-solc", diff --git a/crates/forge/tests/it/zk/linking.rs b/crates/forge/tests/it/zk/linking.rs index 5884bf9ea..a3b6433f8 100644 --- a/crates/forge/tests/it/zk/linking.rs +++ b/crates/forge/tests/it/zk/linking.rs @@ -26,7 +26,7 @@ forgetest_async!( #[should_panic = "no bytecode for contract; is it abstract or unlinked?"] script_zk_fails_indirect_reference_to_unlinked, |prj, cmd| { - setup_libs_prj(&mut prj, &mut cmd, Some(ZKSOLC_MIN_LINKING_VERSION)); + setup_libs_prj(&mut prj, &mut cmd, None); run_zk_script_test( prj.root(), &mut cmd, @@ -41,7 +41,7 @@ forgetest_async!( ); forgetest_async!(script_zk_deploy_time_linking, |prj, cmd| { - setup_libs_prj(&mut prj, &mut cmd, Some(ZKSOLC_MIN_LINKING_VERSION)); + setup_libs_prj(&mut prj, &mut cmd, None); run_zk_script_test( prj.root(), &mut cmd, @@ -101,13 +101,10 @@ forgetest_async!( fn setup_libs_prj(prj: &mut TestProject, cmd: &mut TestCommand, zksolc: Option) { util::initialize(prj.root()); - let zksolc = zksolc.unwrap_or_else(|| Version::new(1, 5, 9)); - // force zksolc 1.5.9 let mut config = cmd.config(); - if zksolc >= Version::new(1, 5, 9) { - config.zksync.suppressed_errors.insert(ZkSolcError::AssemblyCreate); + if let Some(zksolc) = zksolc { + config.zksync.zksolc.replace(foundry_config::SolcReq::Version(zksolc)) } - config.zksync.zksolc.replace(foundry_config::SolcReq::Version(zksolc)); prj.write_config(config); prj.add_script("Libraries.s.sol", include_str!("../../fixtures/zk/Libraries.s.sol")).unwrap(); From a48bfe59d2b28f4df988629750dc0b16436af027 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 27 Jan 2025 13:56:23 +0100 Subject: [PATCH 57/61] docs: add notes on diverging sections --- crates/script/src/build.rs | 3 +++ crates/script/src/runner.rs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 05824d34b..21ffe20a0 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -577,6 +577,9 @@ impl CompiledState { ScriptSequenceKind::Multi(_) => Libraries::default(), }; + // NOTE(zk): we added `script_config` to be able + // to retrieve the appropriate `zksolc` compiler version + // from the config to be used during linking let linked_build_data = build_data.link_with_libraries(&script_config, libraries).await?; Ok(BundledState { diff --git a/crates/script/src/runner.rs b/crates/script/src/runner.rs index 2bd512a87..53b315007 100644 --- a/crates/script/src/runner.rs +++ b/crates/script/src/runner.rs @@ -61,6 +61,11 @@ impl ScriptRunner { let mut library_transactions = VecDeque::new(); let mut traces = Traces::default(); + // NOTE(zk): below we moved the logic into the strategy + // so we can override it in the zksync strategy + // Additionally, we have a list of results to register + // both the EVM and EraVM deployment + // Deploy libraries match libraries { ScriptPredeployLibraries::Default(libraries) => libraries.iter().for_each(|code| { From fbf900b7c461ccc8570c6d8e066e1823f62e8080 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 27 Jan 2025 13:58:42 +0100 Subject: [PATCH 58/61] fix(zk): remove duplicate code from merge --- .../compilers/src/artifacts/contract.rs | 39 ------------------- .../src/compilers/artifact_output/zk.rs | 5 --- 2 files changed, 44 deletions(-) diff --git a/crates/zksync/compilers/src/artifacts/contract.rs b/crates/zksync/compilers/src/artifacts/contract.rs index a00b568d5..44296c31c 100644 --- a/crates/zksync/compilers/src/artifacts/contract.rs +++ b/crates/zksync/compilers/src/artifacts/contract.rs @@ -50,45 +50,6 @@ impl ObjectFormat { } } -/// zksolc: Binary object format. -#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(try_from = "String", into = "String")] -pub enum ObjectFormat { - /// Linked - #[default] - Raw, - /// Unlinked - Elf, -} - -impl From for String { - fn from(val: ObjectFormat) -> Self { - match val { - ObjectFormat::Raw => "raw", - ObjectFormat::Elf => "elf", - } - .to_string() - } -} - -impl TryFrom for ObjectFormat { - type Error = String; - fn try_from(s: String) -> Result { - match s.as_str() { - "raw" => Ok(Self::Raw), - "elf" => Ok(Self::Elf), - s => Err(format!("Unknown zksolc object format: {s}")), - } - } -} - -impl ObjectFormat { - /// Returns `true` if the bytecode is unlinked - pub fn is_unlinked(&self) -> bool { - matches!(self, Self::Elf) - } -} - /// Represents a compiled solidity contract #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] diff --git a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs index 6bc893257..c2e700035 100644 --- a/crates/zksync/compilers/src/compilers/artifact_output/zk.rs +++ b/crates/zksync/compilers/src/compilers/artifact_output/zk.rs @@ -66,11 +66,6 @@ pub struct ZkContractArtifact { } impl ZkContractArtifact { - /// Returns true if contract is not linked - pub fn is_unlinked(&self) -> bool { - self.bytecode.as_ref().map(|bc| bc.is_unlinked()).unwrap_or(false) - } - /// Returns a list of _all_ factory deps, by : /// /// Will return unlinked as well as linked factory deps (might contain duplicates) From 7046a3ac5cd57c9c32abafa887cf76eca0e0856a Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 27 Jan 2025 14:03:53 +0100 Subject: [PATCH 59/61] refactor(script:zk): move linking to own module --- crates/script/src/build.rs | 207 ++---------------------------- crates/script/src/build/zksync.rs | 204 +++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 199 deletions(-) create mode 100644 crates/script/src/build/zksync.rs diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 21ffe20a0..99bd8621c 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -2,7 +2,7 @@ use crate::{ broadcast::BundledState, execute::LinkedState, multi_sequence::MultiChainSequence, sequence::ScriptSequenceKind, ScriptArgs, ScriptConfig, }; -use alloy_primitives::{keccak256, Bytes, B256}; +use alloy_primitives::{Bytes, B256}; use alloy_provider::Provider; use eyre::{Context, OptionExt, Result}; use forge_script_sequence::ScriptSequence; @@ -11,22 +11,21 @@ use foundry_common::{ compile::ProjectCompiler, provider::try_get_http_provider, ContractData, ContractsByArtifact, }; use foundry_compilers::{ - artifacts::{BytecodeObject, CompactContractBytecode, CompactContractBytecodeCow, Libraries}, + artifacts::{BytecodeObject, Libraries}, compilers::{multi::MultiCompilerLanguage, Language}, - contracts::ArtifactContracts, info::ContractInfo, solc::SolcLanguage, - utils::source_files_iter, - Artifact, ArtifactId, ProjectCompileOutput, + utils::source_files_iter, ArtifactId, ProjectCompileOutput, }; use foundry_evm::traces::debug::ContractSources; -use foundry_linking::{Linker, ZkLinker, DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION}; +use foundry_linking::Linker; use foundry_zksync_compilers::{ compilers::{artifact_output::zk::ZkArtifactOutput, zksolc::ZkSolcCompiler}, - dual_compiled_contracts::{DualCompiledContract, DualCompiledContracts}, + dual_compiled_contracts::DualCompiledContracts, }; -use foundry_zksync_core::{hash_bytecode, DEFAULT_CREATE2_DEPLOYER_ZKSYNC}; -use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::Arc}; +use std::{path::PathBuf, str::FromStr, sync::Arc}; + +mod zksync; /// Container for the compiled contracts. #[derive(Debug)] @@ -47,196 +46,6 @@ impl BuildData { Linker::new(self.project_root.clone(), self.output.artifact_ids().collect()) } - fn get_zk_linker(&self, script_config: &ScriptConfig) -> Result> { - let zksolc = foundry_config::zksync::config_zksolc_compiler(&script_config.config) - .context("retrieving zksolc compiler to be used for linking")?; - let version = zksolc.version().context("trying to determine zksolc version")?; - - let Some(input) = self.zk_output.as_ref() else { - eyre::bail!("unable to link zk artifacts if no zk compilation output is provided") - }; - - let linker = - ZkLinker::new(self.project_root.clone(), input.artifact_ids().collect(), zksolc, input); - - let mut libs = Default::default(); - linker.zk_collect_dependencies(&self.target, &mut libs, None)?; - - // if there are no no libs, no linking will happen - // so we can skip version check - if !libs.is_empty() && version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { - eyre::bail!( - "deploy-time linking not supported. minimum: {}, given: {}", - DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, - &version - ); - } - - Ok(linker) - } - - /// Will attempt linking via `zksolc` - /// - /// Will attempt linking with a CREATE2 deployer if possible first, otherwise - /// just using CREATE. - /// After linking is done it will update the list of `DualCompiledContracts` with - /// the newly linked contracts (and their EVM equivalent). - /// Finally, return the list of known contracts - /// - /// If compilation for zksync is not enabled will return the - /// given EVM linked artifacts - async fn zk_link( - &mut self, - script_config: &ScriptConfig, - known_libraries: Libraries, - evm_linked_contracts: ArtifactContracts, - use_create2: bool, - ) -> Result { - if !script_config.config.zksync.should_compile() { - return Ok(evm_linked_contracts); - } - - let Some(input) = self.zk_output.as_ref() else { - eyre::bail!("unable to link zk artifacts if no zk compilation output is provided"); - }; - - let mut dual_compiled_contracts = self.dual_compiled_contracts.take().unwrap_or_default(); - - // NOTE(zk): translate solc ArtifactId to zksolc otherwise - // we won't be able to find it in the zksolc output - let Some(target) = input - .artifact_ids() - .map(|(id, _)| id) - .find(|id| id.source == self.target.source && id.name == self.target.name) - else { - eyre::bail!("unable to find zk target artifact for linking"); - }; - let target = ⌖ - - let linker = self.get_zk_linker(script_config)?; - - let create2_deployer = DEFAULT_CREATE2_DEPLOYER_ZKSYNC; - let maybe_create2_link_output = use_create2 - .then(|| { - linker - .zk_link_with_create2( - known_libraries.clone(), - create2_deployer, - script_config.config.create2_library_salt, - target, - ) - .ok() - }) - .flatten(); - - let libraries = if let Some(output) = maybe_create2_link_output { - output.libraries - } else { - let output = linker.zk_link_with_nonce_or_address( - known_libraries, - script_config.evm_opts.sender, - script_config.sender_nonce, - [target], - )?; - - output.libraries - }; - - let mut factory_deps = Default::default(); - let mut libs = Default::default(); - linker - .zk_collect_dependencies(target, &mut libs, Some(&mut factory_deps)) - .expect("able to enumerate all deps"); - - let linked_contracts = linker - .zk_get_linked_artifacts( - // only retrieve target and its deps - factory_deps.into_iter().chain(libs.into_iter()).chain([target]), - &libraries, - ) - .context("retrieving all fully linked contracts")?; - - let newly_linked_dual_compiled_contracts = linked_contracts - .iter() - .flat_map(|(needle, zk)| { - evm_linked_contracts - .iter() - .find(|(id, _)| id.source == needle.source && id.name == needle.name) - .map(|(_, evm)| (needle, zk, evm)) - }) - .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) - .map(|(id, linked_zk, evm)| { - let (_, unlinked_zk_artifact) = input - .artifact_ids() - .find(|(contract_id, _)| contract_id == id) - .expect("unable to find original (pre-linking) artifact"); - - let zk_bytecode = - linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); - let zk_hash = hash_bytecode(&zk_bytecode); - let evm_deployed = evm - .get_deployed_bytecode_bytes() - .expect("no EVM deployed bytecode (or unlinked)"); - let evm_bytecode = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); - let contract_info = ContractInfo { - name: id.name.clone(), - path: Some(id.source.to_string_lossy().into_owned()), - }; - let contract = DualCompiledContract { - zk_bytecode_hash: zk_hash, - zk_deployed_bytecode: zk_bytecode.to_vec(), - // rest of factory deps is populated later - zk_factory_deps: vec![zk_bytecode.to_vec()], - evm_bytecode_hash: B256::from_slice(&keccak256(evm_deployed.as_ref())[..]), - // TODO(zk): determine if this is ok, as it's - // not really used in dual compiled contracts - evm_deployed_bytecode: evm_deployed.to_vec(), - evm_bytecode: evm_bytecode.to_vec(), - }; - - let mut factory_deps = unlinked_zk_artifact.all_factory_deps().collect::>(); - factory_deps.dedup(); - - ((contract_info.clone(), contract), (contract_info, factory_deps)) - }); - - let (new_contracts, new_contracts_deps): (Vec<_>, HashMap<_, _>) = - newly_linked_dual_compiled_contracts.unzip(); - dual_compiled_contracts.extend(new_contracts); - - // now that we have an updated list of DualCompiledContracts - // retrieve all the factory deps for a given contracts and store them - new_contracts_deps.into_iter().for_each(|(info, deps)| { - deps.into_iter().for_each(|dep| { - let mut split = dep.split(':'); - let path = split.next().expect("malformed factory dep path"); - let name = split.next().expect("malformed factory dep name"); - - let bytecode = dual_compiled_contracts - .find(Some(path), Some(name)) - .next() - .expect("unknown factory dep") - .1 - .zk_deployed_bytecode - .clone(); - - dual_compiled_contracts.insert_factory_deps(&info, Some(bytecode)); - }); - }); - - self.dual_compiled_contracts.replace(dual_compiled_contracts); - - // base zksolc contracts + newly linked + evm contracts - let contracts = input - .artifact_ids() - .map(|(id, v)| (id, CompactContractBytecode::from(CompactContractBytecodeCow::from(v)))) - .chain(linked_contracts) - .chain(evm_linked_contracts) - .collect(); - - Ok(contracts) - } - /// Links contracts. Uses CREATE2 linking when possible, otherwise falls back to /// default linking with sender nonce and address. pub async fn link(mut self, script_config: &ScriptConfig) -> Result { diff --git a/crates/script/src/build/zksync.rs b/crates/script/src/build/zksync.rs new file mode 100644 index 000000000..176c4e26d --- /dev/null +++ b/crates/script/src/build/zksync.rs @@ -0,0 +1,204 @@ +use std::collections::HashMap; + +use alloy_primitives::{keccak256, B256}; +use foundry_compilers::{artifacts::{CompactContractBytecode, CompactContractBytecodeCow, Libraries}, contracts::ArtifactContracts, info::ContractInfo, Artifact}; +use foundry_linking::{ZkLinker, DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION}; +use eyre::{Context, Result}; +use foundry_zksync_compilers::dual_compiled_contracts::DualCompiledContract; +use foundry_zksync_core::{hash_bytecode, DEFAULT_CREATE2_DEPLOYER_ZKSYNC}; + +use crate::ScriptConfig; + +use super::BuildData; + +impl BuildData { + fn get_zk_linker(&self, script_config: &ScriptConfig) -> Result> { + let zksolc = foundry_config::zksync::config_zksolc_compiler(&script_config.config) + .context("retrieving zksolc compiler to be used for linking")?; + let version = zksolc.version().context("trying to determine zksolc version")?; + + let Some(input) = self.zk_output.as_ref() else { + eyre::bail!("unable to link zk artifacts if no zk compilation output is provided") + }; + + let linker = + ZkLinker::new(self.project_root.clone(), input.artifact_ids().collect(), zksolc, input); + + let mut libs = Default::default(); + linker.zk_collect_dependencies(&self.target, &mut libs, None)?; + + // if there are no no libs, no linking will happen + // so we can skip version check + if !libs.is_empty() && version < DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION { + eyre::bail!( + "deploy-time linking not supported. minimum: {}, given: {}", + DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION, + &version + ); + } + + Ok(linker) + } + + /// Will attempt linking via `zksolc` + /// + /// Will attempt linking with a CREATE2 deployer if possible first, otherwise + /// just using CREATE. + /// After linking is done it will update the list of `DualCompiledContracts` with + /// the newly linked contracts (and their EVM equivalent). + /// Finally, return the list of known contracts + /// + /// If compilation for zksync is not enabled will return the + /// given EVM linked artifacts + pub(super) async fn zk_link( + &mut self, + script_config: &ScriptConfig, + known_libraries: Libraries, + evm_linked_contracts: ArtifactContracts, + use_create2: bool, + ) -> Result { + if !script_config.config.zksync.should_compile() { + return Ok(evm_linked_contracts); + } + + let Some(input) = self.zk_output.as_ref() else { + eyre::bail!("unable to link zk artifacts if no zk compilation output is provided"); + }; + + let mut dual_compiled_contracts = self.dual_compiled_contracts.take().unwrap_or_default(); + + // NOTE(zk): translate solc ArtifactId to zksolc otherwise + // we won't be able to find it in the zksolc output + let Some(target) = input + .artifact_ids() + .map(|(id, _)| id) + .find(|id| id.source == self.target.source && id.name == self.target.name) + else { + eyre::bail!("unable to find zk target artifact for linking"); + }; + let target = ⌖ + + let linker = self.get_zk_linker(script_config)?; + + let create2_deployer = DEFAULT_CREATE2_DEPLOYER_ZKSYNC; + let maybe_create2_link_output = use_create2 + .then(|| { + linker + .zk_link_with_create2( + known_libraries.clone(), + create2_deployer, + script_config.config.create2_library_salt, + target, + ) + .ok() + }) + .flatten(); + + let libraries = if let Some(output) = maybe_create2_link_output { + output.libraries + } else { + let output = linker.zk_link_with_nonce_or_address( + known_libraries, + script_config.evm_opts.sender, + script_config.sender_nonce, + [target], + )?; + + output.libraries + }; + + let mut factory_deps = Default::default(); + let mut libs = Default::default(); + linker + .zk_collect_dependencies(target, &mut libs, Some(&mut factory_deps)) + .expect("able to enumerate all deps"); + + let linked_contracts = linker + .zk_get_linked_artifacts( + // only retrieve target and its deps + factory_deps.into_iter().chain(libs.into_iter()).chain([target]), + &libraries, + ) + .context("retrieving all fully linked contracts")?; + + let newly_linked_dual_compiled_contracts = linked_contracts + .iter() + .flat_map(|(needle, zk)| { + evm_linked_contracts + .iter() + .find(|(id, _)| id.source == needle.source && id.name == needle.name) + .map(|(_, evm)| (needle, zk, evm)) + }) + .filter(|(_, zk, evm)| zk.bytecode.is_some() && evm.bytecode.is_some()) + .map(|(id, linked_zk, evm)| { + let (_, unlinked_zk_artifact) = input + .artifact_ids() + .find(|(contract_id, _)| contract_id == id) + .expect("unable to find original (pre-linking) artifact"); + + let zk_bytecode = + linked_zk.get_bytecode_bytes().expect("no EraVM bytecode (or unlinked)"); + let zk_hash = hash_bytecode(&zk_bytecode); + let evm_deployed = evm + .get_deployed_bytecode_bytes() + .expect("no EVM deployed bytecode (or unlinked)"); + let evm_bytecode = evm.get_bytecode_bytes().expect("no EVM bytecode (or unlinked)"); + let contract_info = ContractInfo { + name: id.name.clone(), + path: Some(id.source.to_string_lossy().into_owned()), + }; + let contract = DualCompiledContract { + zk_bytecode_hash: zk_hash, + zk_deployed_bytecode: zk_bytecode.to_vec(), + // rest of factory deps is populated later + zk_factory_deps: vec![zk_bytecode.to_vec()], + evm_bytecode_hash: B256::from_slice(&keccak256(evm_deployed.as_ref())[..]), + // TODO(zk): determine if this is ok, as it's + // not really used in dual compiled contracts + evm_deployed_bytecode: evm_deployed.to_vec(), + evm_bytecode: evm_bytecode.to_vec(), + }; + + let mut factory_deps = unlinked_zk_artifact.all_factory_deps().collect::>(); + factory_deps.dedup(); + + ((contract_info.clone(), contract), (contract_info, factory_deps)) + }); + + let (new_contracts, new_contracts_deps): (Vec<_>, HashMap<_, _>) = + newly_linked_dual_compiled_contracts.unzip(); + dual_compiled_contracts.extend(new_contracts); + + // now that we have an updated list of DualCompiledContracts + // retrieve all the factory deps for a given contracts and store them + new_contracts_deps.into_iter().for_each(|(info, deps)| { + deps.into_iter().for_each(|dep| { + let mut split = dep.split(':'); + let path = split.next().expect("malformed factory dep path"); + let name = split.next().expect("malformed factory dep name"); + + let bytecode = dual_compiled_contracts + .find(Some(path), Some(name)) + .next() + .expect("unknown factory dep") + .1 + .zk_deployed_bytecode + .clone(); + + dual_compiled_contracts.insert_factory_deps(&info, Some(bytecode)); + }); + }); + + self.dual_compiled_contracts.replace(dual_compiled_contracts); + + // base zksolc contracts + newly linked + evm contracts + let contracts = input + .artifact_ids() + .map(|(id, v)| (id, CompactContractBytecode::from(CompactContractBytecodeCow::from(v)))) + .chain(linked_contracts) + .chain(evm_linked_contracts) + .collect(); + + Ok(contracts) + } +} From 72b7f27813b2917e91725dfb2036036b9faa56a2 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 27 Jan 2025 14:07:03 +0100 Subject: [PATCH 60/61] chore: fmt --- crates/config/src/zksync.rs | 5 +---- crates/script/src/build.rs | 3 ++- crates/script/src/build/zksync.rs | 9 +++++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/config/src/zksync.rs b/crates/config/src/zksync.rs index b155b1ac4..e5d8c1188 100644 --- a/crates/config/src/zksync.rs +++ b/crates/config/src/zksync.rs @@ -264,10 +264,7 @@ fn config_solc_compiler(config: &Config) -> Result { } SolcReq::Local(path) => { if !path.is_file() { - return Err(SolcError::msg(format!( - "`solc` {} does not exist", - path.display() - ))); + return Err(SolcError::msg(format!("`solc` {} does not exist", path.display()))); } let version = get_solc_version_info(path)?.version; Solc::new_with_version( diff --git a/crates/script/src/build.rs b/crates/script/src/build.rs index 99bd8621c..dfdde1f73 100644 --- a/crates/script/src/build.rs +++ b/crates/script/src/build.rs @@ -15,7 +15,8 @@ use foundry_compilers::{ compilers::{multi::MultiCompilerLanguage, Language}, info::ContractInfo, solc::SolcLanguage, - utils::source_files_iter, ArtifactId, ProjectCompileOutput, + utils::source_files_iter, + ArtifactId, ProjectCompileOutput, }; use foundry_evm::traces::debug::ContractSources; use foundry_linking::Linker; diff --git a/crates/script/src/build/zksync.rs b/crates/script/src/build/zksync.rs index 176c4e26d..43d5245a3 100644 --- a/crates/script/src/build/zksync.rs +++ b/crates/script/src/build/zksync.rs @@ -1,9 +1,14 @@ use std::collections::HashMap; use alloy_primitives::{keccak256, B256}; -use foundry_compilers::{artifacts::{CompactContractBytecode, CompactContractBytecodeCow, Libraries}, contracts::ArtifactContracts, info::ContractInfo, Artifact}; -use foundry_linking::{ZkLinker, DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION}; use eyre::{Context, Result}; +use foundry_compilers::{ + artifacts::{CompactContractBytecode, CompactContractBytecodeCow, Libraries}, + contracts::ArtifactContracts, + info::ContractInfo, + Artifact, +}; +use foundry_linking::{ZkLinker, DEPLOY_TIME_LINKING_ZKSOLC_MIN_VERSION}; use foundry_zksync_compilers::dual_compiled_contracts::DualCompiledContract; use foundry_zksync_core::{hash_bytecode, DEFAULT_CREATE2_DEPLOYER_ZKSYNC}; From 4b2f052b4eedd54c0aa131b97ceaf40723fb5310 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Mon, 27 Jan 2025 14:16:30 +0100 Subject: [PATCH 61/61] chore: clippy --- crates/forge/tests/it/zk/linking.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/forge/tests/it/zk/linking.rs b/crates/forge/tests/it/zk/linking.rs index a3b6433f8..40f5daff9 100644 --- a/crates/forge/tests/it/zk/linking.rs +++ b/crates/forge/tests/it/zk/linking.rs @@ -1,6 +1,5 @@ use forge::revm::primitives::SpecId; use foundry_test_utils::{forgetest_async, util, Filter, TestCommand, TestProject}; -use foundry_zksync_compilers::compilers::zksolc::settings::ZkSolcError; use semver::Version; use crate::{ @@ -103,7 +102,7 @@ fn setup_libs_prj(prj: &mut TestProject, cmd: &mut TestCommand, zksolc: Option