diff --git a/Cargo.lock b/Cargo.lock index 39a8a80..20507b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3963,6 +3963,7 @@ dependencies = [ name = "revive-dt-compiler" version = "0.1.0" dependencies = [ + "alloy-primitives", "anyhow", "revive-common", "revive-dt-config", diff --git a/crates/compiler/Cargo.toml b/crates/compiler/Cargo.toml index da48b78..05b02d4 100644 --- a/crates/compiler/Cargo.toml +++ b/crates/compiler/Cargo.toml @@ -9,11 +9,13 @@ repository.workspace = true rust-version.workspace = true [dependencies] -anyhow = { workspace = true } revive-solc-json-interface = { workspace = true } revive-dt-config = { workspace = true } revive-dt-solc-binaries = { workspace = true } revive-common = { workspace = true } + +alloy-primitives = { workspace = true } +anyhow = { workspace = true } semver = { workspace = true } serde_json = { workspace = true } tracing = { workspace = true } diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 3c7e173..63e496e 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -9,6 +9,7 @@ use std::{ path::{Path, PathBuf}, }; +use alloy_primitives::Address; use revive_dt_config::Arguments; use revive_common::EVMVersion; @@ -158,6 +159,26 @@ where self } + pub fn with_library( + mut self, + scope: impl AsRef, + library_ident: impl AsRef, + library_address: Address, + ) -> Self { + self.input + .settings + .libraries + .get_or_insert_with(Default::default) + .entry(scope.as_ref().display().to_string()) + .or_default() + .insert( + library_ident.as_ref().to_owned(), + library_address.to_string(), + ); + + self + } + pub fn try_build(self, solc_path: PathBuf) -> anyhow::Result> { T::new(solc_path).build(CompilerInput { extra_options: self.extra_options, diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index 5267c52..978f37a 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -26,8 +26,8 @@ use revive_dt_common::iterators::FilesWithExtensionIterator; use revive_dt_compiler::{Compiler, SolidityCompiler}; use revive_dt_config::Arguments; use revive_dt_format::case::CaseIdx; -use revive_dt_format::input::{Calldata, Expected, ExpectedOutput, Method}; -use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdentifier}; +use revive_dt_format::input::{Calldata, EtherValue, Expected, ExpectedOutput, Method}; +use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdent}; use revive_dt_format::{input::Input, metadata::Metadata, mode::SolcMode}; use revive_dt_node::Node; use revive_dt_node_interaction::EthereumNode; @@ -57,6 +57,12 @@ pub struct State<'a, T: Platform> { /// files. deployed_contracts: HashMap>, + /// This is a map of the deployed libraries. + /// + /// This map is not per case, but rather, per metadata file. This means that we do not redeploy + /// the libraries with each case. + deployed_libraries: HashMap, + phantom: PhantomData, } @@ -70,6 +76,7 @@ where span, contracts: Default::default(), deployed_contracts: Default::default(), + deployed_libraries: Default::default(), phantom: Default::default(), } } @@ -96,10 +103,28 @@ where let compiler = Compiler::::new() .allow_path(metadata.directory()?) .solc_optimizer(mode.solc_optimize()); - - let compiler = FilesWithExtensionIterator::new(metadata.directory()?) + let mut compiler = FilesWithExtensionIterator::new(metadata.directory()?) .with_allowed_extension("sol") .try_fold(compiler, |compiler, path| compiler.with_source(&path))?; + for (library_instance, (library_address, _)) in self.deployed_libraries.iter() { + let library_ident = &metadata + .contracts + .as_ref() + .and_then(|contracts| contracts.get(library_instance)) + .expect("Impossible for library to not be found in contracts") + .contract_ident; + + // Note the following: we need to tell solc which files require the libraries to be + // linked into them. We do not have access to this information and therefore we choose + // an easier, yet more compute intensive route, of telling solc that all of the files + // need to link the library and it will only perform the linking for the files that do + // actually need the library. + compiler = FilesWithExtensionIterator::new(metadata.directory()?) + .with_allowed_extension("sol") + .fold(compiler, |compiler, path| { + compiler.with_library(&path, library_ident.as_str(), *library_address) + }); + } let mut task = CompilationTask { json_input: compiler.input(), @@ -141,6 +166,34 @@ where } } + pub fn build_and_publish_libraries( + &mut self, + metadata: &Metadata, + mode: &SolcMode, + node: &T::Blockchain, + ) -> anyhow::Result<()> { + self.build_contracts(mode, metadata)?; + + for library_instance in metadata + .libraries + .iter() + .flatten() + .flat_map(|(_, map)| map.values()) + { + self.get_or_deploy_contract_instance( + library_instance, + metadata, + None, + Input::default_caller(), + None, + None, + node, + )?; + } + + Ok(()) + } + pub fn handle_input( &mut self, metadata: &Metadata, @@ -173,12 +226,7 @@ where let mut instances_we_must_deploy = IndexMap::::new(); for instance in input.find_all_contract_instances().into_iter() { - if !self - .deployed_contracts - .entry(case_idx) - .or_default() - .contains_key(&instance) - { + if !self.deployed_contracts(case_idx).contains_key(&instance) { instances_we_must_deploy.entry(instance).or_insert(false); } } @@ -194,124 +242,22 @@ where let mut receipts = HashMap::new(); for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() { - // What we have at this moment is just a contract instance which is kind of like a variable - // name for an actual underlying contract. So, we need to resolve this instance to the info - // of the contract that it belongs to. - let Some(ContractPathAndIdentifier { - contract_source_path, - contract_ident, - }) = metadata.contract_sources()?.remove(&instance) - else { - tracing::error!("Contract source not found for instance"); - anyhow::bail!("Contract source not found for instance {:?}", instance) - }; + let calldata = deploy_with_constructor_arguments.then_some(&input.calldata); + let value = deploy_with_constructor_arguments + .then_some(input.value) + .flatten(); - let compiled_contract = self.contracts.iter().find_map(|output| { - output - .contracts - .as_ref()? - .get(&contract_source_path.display().to_string()) - .and_then(|source_file_contracts| { - source_file_contracts.get(contract_ident.as_ref()) - }) - }); - let Some(code) = compiled_contract - .and_then(|contract| contract.evm.as_ref().and_then(|evm| evm.bytecode.as_ref())) - else { - tracing::error!( - contract_source_path = contract_source_path.display().to_string(), - contract_ident = contract_ident.as_ref(), - "Failed to find bytecode for contract" - ); - anyhow::bail!("Failed to find bytecode for contract {:?}", instance) - }; - - // TODO: When we want to do linking it would be best to do it at this stage here. We have - // the context from the metadata files and therefore know what needs to be linked and in - // what order it needs to happen. - - let mut code = match alloy::hex::decode(&code.object) { - Ok(code) => code, - Err(error) => { - tracing::error!( - ?error, - contract_source_path = contract_source_path.display().to_string(), - contract_ident = contract_ident.as_ref(), - "Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking" - ); - anyhow::bail!("Failed to hex-decode the byte code {}", error) - } - }; - - if deploy_with_constructor_arguments { - let encoded_input = input - .encoded_input(self.deployed_contracts.entry(case_idx).or_default(), node)?; - code.extend(encoded_input.to_vec()); + if let (_, _, Some(receipt)) = self.get_or_deploy_contract_instance( + &instance, + metadata, + case_idx, + input.caller, + calldata, + value, + node, + )? { + receipts.insert(instance.clone(), receipt); } - - let tx = { - let tx = TransactionRequest::default().from(input.caller); - let tx = match input.value { - Some(ref value) if deploy_with_constructor_arguments => { - tx.value(value.into_inner()) - } - _ => tx, - }; - TransactionBuilder::::with_deploy_code(tx, code) - }; - - let receipt = match node.execute_transaction(tx) { - Ok(receipt) => receipt, - Err(error) => { - tracing::error!( - node = std::any::type_name::(), - ?error, - "Contract deployment transaction failed." - ); - return Err(error); - } - }; - - let Some(address) = receipt.contract_address else { - tracing::error!("Contract deployment transaction didn't return an address"); - anyhow::bail!("Contract deployment didn't return an address"); - }; - tracing::info!( - instance_name = ?instance, - instance_address = ?address, - "Deployed contract" - ); - - let Some(Value::String(metadata)) = - compiled_contract.and_then(|contract| contract.metadata.as_ref()) - else { - tracing::error!("Contract does not have a metadata field"); - anyhow::bail!("Contract does not have a metadata field"); - }; - - let Ok(metadata) = serde_json::from_str::(metadata) else { - tracing::error!(%metadata, "Failed to parse solc metadata into a structured value"); - anyhow::bail!("Failed to parse solc metadata into a structured value {metadata}"); - }; - - let Some(abi) = metadata.get("output").and_then(|value| value.get("abi")) else { - tracing::error!(%metadata, "Failed to access the .output.abi field of the solc metadata"); - anyhow::bail!( - "Failed to access the .output.abi field of the solc metadata {metadata}" - ); - }; - - let Ok(abi) = serde_json::from_value::(abi.clone()) else { - tracing::error!(%metadata, "Failed to deserialize ABI into a structured format"); - anyhow::bail!("Failed to deserialize ABI into a structured format {metadata}"); - }; - - self.deployed_contracts - .entry(case_idx) - .or_default() - .insert(instance.clone(), (address, abi)); - - receipts.insert(instance.clone(), receipt); } Ok(receipts) @@ -332,9 +278,7 @@ where .remove(&input.instance) .context("Failed to find deployment receipt"), Method::Fallback | Method::FunctionName(_) => { - let tx = match input - .legacy_transaction(self.deployed_contracts.entry(case_idx).or_default(), node) - { + let tx = match input.legacy_transaction(self.deployed_contracts(case_idx), node) { Ok(tx) => { tracing::debug!("Legacy transaction data: {tx:#?}"); tx @@ -442,7 +386,7 @@ where // Additionally, what happens if the compiler filter doesn't match? Do we consider that the // transaction should succeed? Do we just ignore the expectation? - let deployed_contracts = self.deployed_contracts.entry(case_idx).or_default(); + let deployed_contracts = self.deployed_contracts(case_idx); let chain_state_provider = node; // Handling the receipt state assertion. @@ -567,6 +511,162 @@ where Ok((execution_receipt, trace, diff)) } + + fn deployed_contracts( + &mut self, + case_idx: impl Into>, + ) -> &mut HashMap { + match case_idx.into() { + Some(case_idx) => self + .deployed_contracts + .entry(case_idx) + .or_insert_with(|| self.deployed_libraries.clone()), + None => &mut self.deployed_libraries, + } + } + + /// Gets the information of a deployed contract or library from the state. If it's found to not + /// be deployed then it will be deployed. + /// + /// If a [`CaseIdx`] is not specified then this contact instance address will be stored in the + /// cross-case deployed contracts address mapping. + #[allow(clippy::too_many_arguments)] + pub fn get_or_deploy_contract_instance( + &mut self, + contract_instance: &ContractInstance, + metadata: &Metadata, + case_idx: impl Into>, + deployer: Address, + calldata: Option<&Calldata>, + value: Option, + node: &T::Blockchain, + ) -> anyhow::Result<(Address, JsonAbi, Option)> { + let case_idx = case_idx.into(); + + if let Some((address, abi)) = self.deployed_libraries.get(contract_instance) { + return Ok((*address, abi.clone(), None)); + } + if let Some(case_idx) = case_idx { + if let Some((address, abi)) = self + .deployed_contracts + .get(&case_idx) + .and_then(|contracts| contracts.get(contract_instance)) + { + return Ok((*address, abi.clone(), None)); + } + } + + let Some(ContractPathAndIdent { + contract_source_path, + contract_ident, + }) = metadata.contract_sources()?.remove(contract_instance) + else { + tracing::error!("Contract source not found for instance"); + anyhow::bail!( + "Contract source not found for instance {:?}", + contract_instance + ) + }; + + let compiled_contract = self.contracts.iter().rev().find_map(|output| { + output + .contracts + .as_ref()? + .get(&contract_source_path.display().to_string()) + .and_then(|source_file_contracts| { + source_file_contracts.get(contract_ident.as_ref()) + }) + }); + let Some(code) = compiled_contract + .and_then(|contract| contract.evm.as_ref().and_then(|evm| evm.bytecode.as_ref())) + else { + tracing::error!( + contract_source_path = contract_source_path.display().to_string(), + contract_ident = contract_ident.as_ref(), + "Failed to find bytecode for contract" + ); + anyhow::bail!( + "Failed to find bytecode for contract {:?}", + contract_instance + ) + }; + + let mut code = match alloy::hex::decode(&code.object) { + Ok(code) => code, + Err(error) => { + tracing::error!( + ?error, + contract_source_path = contract_source_path.display().to_string(), + contract_ident = contract_ident.as_ref(), + "Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking" + ); + anyhow::bail!("Failed to hex-decode the byte code {}", error) + } + }; + + let Some(Value::String(metadata)) = + compiled_contract.and_then(|contract| contract.metadata.as_ref()) + else { + tracing::error!("Contract does not have a metadata field"); + anyhow::bail!("Contract does not have a metadata field"); + }; + + let Ok(metadata) = serde_json::from_str::(metadata) else { + tracing::error!(%metadata, "Failed to parse solc metadata into a structured value"); + anyhow::bail!("Failed to parse solc metadata into a structured value {metadata}"); + }; + + let Some(abi) = metadata.get("output").and_then(|value| value.get("abi")) else { + tracing::error!(%metadata, "Failed to access the .output.abi field of the solc metadata"); + anyhow::bail!("Failed to access the .output.abi field of the solc metadata {metadata}"); + }; + + let Ok(abi) = serde_json::from_value::(abi.clone()) else { + tracing::error!(%metadata, "Failed to deserialize ABI into a structured format"); + anyhow::bail!("Failed to deserialize ABI into a structured format {metadata}"); + }; + + if let Some(calldata) = calldata { + let calldata = calldata.calldata(self.deployed_contracts(case_idx), node)?; + code.extend(calldata); + } + + let tx = { + let tx = TransactionRequest::default().from(deployer); + let tx = match value { + Some(ref value) => tx.value(value.into_inner()), + _ => tx, + }; + TransactionBuilder::::with_deploy_code(tx, code) + }; + + let receipt = match node.execute_transaction(tx) { + Ok(receipt) => receipt, + Err(error) => { + tracing::error!( + node = std::any::type_name::(), + ?error, + "Contract deployment transaction failed." + ); + return Err(error); + } + }; + + let Some(address) = receipt.contract_address else { + tracing::error!("Contract deployment transaction didn't return an address"); + anyhow::bail!("Contract deployment didn't return an address"); + }; + tracing::info!( + instance_name = ?contract_instance, + instance_address = ?address, + "Deployed contract" + ); + + self.deployed_contracts(case_idx) + .insert(contract_instance.clone(), (address, abi.clone())); + + Ok((address, abi, Some(receipt))) + } } pub struct Driver<'a, Leader: Platform, Follower: Platform> { @@ -673,6 +773,42 @@ where let mut leader_state = State::::new(self.config, span); let mut follower_state = State::::new(self.config, span); + // Note: we are currently forced to do two compilation passes due to linking. In the + // first compilation pass we compile the libraries and publish them to the chain. In the + // second compilation pass we compile the contracts with the library addresses so that + // they're linked at compile-time. + let build_result = tracing::info_span!("Building and publishing libraries") + .in_scope(|| { + match leader_state.build_and_publish_libraries(self.metadata, &mode, self.leader_node) { + Ok(_) => { + tracing::debug!(target = ?Target::Leader, "Library building succeeded"); + execution_result.add_successful_build(Target::Leader, mode.clone()); + }, + Err(error) => { + tracing::error!(target = ?Target::Leader, ?error, "Library building failed"); + execution_result.add_failed_build(Target::Leader, mode.clone(), error); + return Err(()); + } + } + match follower_state.build_and_publish_libraries(self.metadata, &mode, self.follower_node) { + Ok(_) => { + tracing::debug!(target = ?Target::Follower, "Library building succeeded"); + execution_result.add_successful_build(Target::Follower, mode.clone()); + }, + Err(error) => { + tracing::error!(target = ?Target::Follower, ?error, "Library building failed"); + execution_result.add_failed_build(Target::Follower, mode.clone(), error); + return Err(()); + } + } + Ok(()) + }); + if build_result.is_err() { + // Note: We skip to the next solc mode as there's nothing that we can do at this + // point, the building has failed. We do NOT bail out of the execution as a whole. + continue; + } + // We build the contracts. If building the contracts for the metadata file fails then we // have no other option but to keep note of this error and move on to the next solc mode // and NOT just bail out of the execution as a whole. @@ -709,7 +845,6 @@ where // For cases if one of the inputs fail then we move on to the next case and we do NOT // bail out of the whole thing. - 'case_loop: for (case_idx, case) in self.metadata.cases.iter().enumerate() { let tracing_span = tracing::info_span!( "Handling case", diff --git a/crates/format/src/metadata.rs b/crates/format/src/metadata.rs index 9329002..47f938c 100644 --- a/crates/format/src/metadata.rs +++ b/crates/format/src/metadata.rs @@ -47,9 +47,9 @@ impl Deref for MetadataFile { pub struct Metadata { pub targets: Option>, pub cases: Vec, - pub contracts: Option>, + pub contracts: Option>, // TODO: Convert into wrapper types for clarity. - pub libraries: Option>>, + pub libraries: Option>>, pub ignore: Option, pub modes: Option>, pub file_path: Option, @@ -86,7 +86,7 @@ impl Metadata { /// Returns the contract sources with canonicalized paths for the files pub fn contract_sources( &self, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let directory = self.directory()?; let mut sources = BTreeMap::new(); let Some(contracts) = &self.contracts else { @@ -95,7 +95,7 @@ impl Metadata { for ( alias, - ContractPathAndIdentifier { + ContractPathAndIdent { contract_source_path, contract_ident, }, @@ -107,7 +107,7 @@ impl Metadata { sources.insert( alias, - ContractPathAndIdentifier { + ContractPathAndIdent { contract_source_path: absolute_path, contract_ident, }, @@ -194,7 +194,7 @@ impl Metadata { metadata.contracts = Some( [( ContractInstance::new("test"), - ContractPathAndIdentifier { + ContractPathAndIdent { contract_source_path: path.to_path_buf(), contract_ident: ContractIdent::new("Test"), }, @@ -245,7 +245,7 @@ define_wrapper_type!( /// ``` #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(try_from = "String", into = "String")] -pub struct ContractPathAndIdentifier { +pub struct ContractPathAndIdent { /// The path of the contract source code relative to the directory containing the metadata file. pub contract_source_path: PathBuf, @@ -253,7 +253,7 @@ pub struct ContractPathAndIdentifier { pub contract_ident: ContractIdent, } -impl Display for ContractPathAndIdentifier { +impl Display for ContractPathAndIdent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, @@ -264,7 +264,7 @@ impl Display for ContractPathAndIdentifier { } } -impl FromStr for ContractPathAndIdentifier { +impl FromStr for ContractPathAndIdent { type Err = anyhow::Error; fn from_str(s: &str) -> Result { @@ -300,7 +300,7 @@ impl FromStr for ContractPathAndIdentifier { } } -impl TryFrom for ContractPathAndIdentifier { +impl TryFrom for ContractPathAndIdent { type Error = anyhow::Error; fn try_from(value: String) -> Result { @@ -308,8 +308,8 @@ impl TryFrom for ContractPathAndIdentifier { } } -impl From for String { - fn from(value: ContractPathAndIdentifier) -> Self { +impl From for String { + fn from(value: ContractPathAndIdent) -> Self { value.to_string() } } @@ -324,7 +324,7 @@ mod test { let string = "ERC20/ERC20.sol:ERC20"; // Act - let identifier = ContractPathAndIdentifier::from_str(string); + let identifier = ContractPathAndIdent::from_str(string); // Assert let identifier = identifier.expect("Failed to parse");