From f8a4f9551266f4435c44650541d87f6e73478175 Mon Sep 17 00:00:00 2001 From: Francesco Dainese Date: Thu, 23 Jan 2025 19:30:59 +0100 Subject: [PATCH] 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 8b9564b54c..d29f76985c 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 6efb0046f8..e3fa01a253 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 {