diff --git a/crates/artifacts/zksolc/src/bytecode.rs b/crates/artifacts/zksolc/src/bytecode.rs deleted file mode 100644 index 8f49b285..00000000 --- a/crates/artifacts/zksolc/src/bytecode.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::collections::BTreeMap; - -use foundry_compilers_artifacts_solc::{ - bytecode::{serialize_bytecode_without_prefix, BytecodeObject}, - CompactBytecode, CompactDeployedBytecode, -}; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct Bytecode { - #[serde(serialize_with = "serialize_bytecode_without_prefix")] - pub object: BytecodeObject, -} - -// NOTE: distinction between bytecode and deployed bytecode make no sense of zkEvm, but -// we implement these conversions in order to be able to use the Artifacts trait. -impl From for CompactBytecode { - fn from(bcode: Bytecode) -> Self { - Self { object: bcode.object, source_map: None, link_references: BTreeMap::default() } - } -} - -impl From for CompactDeployedBytecode { - fn from(bcode: Bytecode) -> Self { - Self { bytecode: Some(bcode.into()), immutable_references: BTreeMap::default() } - } -} diff --git a/crates/artifacts/zksolc/src/contract.rs b/crates/artifacts/zksolc/src/contract.rs index ba8c67b1..646a8048 100644 --- a/crates/artifacts/zksolc/src/contract.rs +++ b/crates/artifacts/zksolc/src/contract.rs @@ -1,9 +1,9 @@ //! Contract related types. -use crate::Evm; +use crate::{EraVM, Evm}; use alloy_json_abi::JsonAbi; use foundry_compilers_artifacts_solc::{ - CompactContractBytecode, CompactContractBytecodeCow, CompactContractRef, DevDoc, StorageLayout, - UserDoc, + Bytecode, CompactBytecode, CompactContractBytecode, CompactContractBytecodeCow, + CompactContractRef, CompactDeployedBytecode, DevDoc, Offsets, StorageLayout, UserDoc, }; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, collections::BTreeMap}; @@ -31,12 +31,63 @@ pub struct Contract { /// The contract factory dependencies. #[serde(default, skip_serializing_if = "Option::is_none")] pub factory_dependencies: Option>, - /// The contract missing libraries. + /// EraVM-related outputs #[serde(default, skip_serializing_if = "Option::is_none")] - pub missing_libraries: Option>, - /// EVM-related outputs + pub eravm: Option, + /// EVM-related outputs (deprecated) #[serde(default, skip_serializing_if = "Option::is_none")] pub evm: Option, + /// The contract's unlinked libraries + #[serde(default)] + pub missing_libraries: Vec, +} + +impl Contract { + pub fn is_unlinked(&self) -> bool { + self.hash.is_none() || !self.missing_libraries.is_empty() + } + + pub fn missing_libs_to_link_references( + missing_libraries: &[String], + ) -> BTreeMap>> { + missing_libraries + .iter() + .map(|file_and_lib| { + let mut parts = file_and_lib.split(':'); + let filename = parts.next().expect("missing library contract file (:)"); + let contract = parts.next().expect("missing library contract name (:)"); + (filename.to_owned(), contract.to_owned()) + }) + .fold(BTreeMap::default(), |mut acc, (filename, contract)| { + acc.entry(filename) + .or_default() + //empty offsets since we can't patch it anyways + .insert(contract, vec![]); + acc + }) + } + + fn link_references(&self) -> BTreeMap>> { + Self::missing_libs_to_link_references(self.missing_libraries.as_slice()) + } + + pub fn bytecode(&self) -> Option { + self.eravm + .as_ref() + .and_then(|eravm| eravm.bytecode(self.is_unlinked())) + .or_else(|| { + self.evm + .as_ref() + .and_then(|evm| evm.bytecode.as_ref()) + .map(|bytecode| &bytecode.object) + .cloned() + }) + .map(|object| { + let mut bytecode: Bytecode = object.into(); + bytecode.link_references = self.link_references(); + bytecode + }) + } } // CompactContract variants @@ -48,36 +99,36 @@ pub struct Contract { // Ideally the Artifacts trait would not be coupled to a specific Contract type impl<'a> From<&'a Contract> for CompactContractBytecodeCow<'a> { fn from(artifact: &'a Contract) -> Self { - let (bytecode, deployed_bytecode) = if let Some(ref evm) = artifact.evm { - ( - evm.bytecode.clone().map(Into::into).map(Cow::Owned), - evm.bytecode.clone().map(Into::into).map(Cow::Owned), - ) - } else { - (None, None) - }; + let bc = artifact.bytecode(); + let bytecode = bc.clone().map(|bc| CompactBytecode { + object: bc.object, + source_map: None, + link_references: bc.link_references, + }); + let deployed_bytecode = bytecode.clone().map(|bytecode| CompactDeployedBytecode { + bytecode: Some(bytecode), + immutable_references: Default::default(), + }); + CompactContractBytecodeCow { abi: artifact.abi.as_ref().map(Cow::Borrowed), - bytecode, - deployed_bytecode, + bytecode: bytecode.map(Cow::Owned), + deployed_bytecode: deployed_bytecode.map(Cow::Owned), } } } impl From for CompactContractBytecode { fn from(c: Contract) -> Self { - let bytecode = if let Some(evm) = c.evm { evm.bytecode } else { None }; - Self { - abi: c.abi.map(Into::into), - deployed_bytecode: bytecode.clone().map(|b| b.into()), - bytecode: bytecode.clone().map(|b| b.into()), - } + CompactContractBytecodeCow::from(&c).into() } } impl<'a> From<&'a Contract> for CompactContractRef<'a> { fn from(c: &'a Contract) -> Self { - let (bin, bin_runtime) = if let Some(ref evm) = c.evm { + let (bin, bin_runtime) = if let Some(ref eravm) = c.eravm { + (eravm.bytecode.as_ref(), eravm.bytecode.as_ref()) + } else if let Some(ref evm) = c.evm { (evm.bytecode.as_ref().map(|c| &c.object), evm.bytecode.as_ref().map(|c| &c.object)) } else { (None, None) diff --git a/crates/artifacts/zksolc/src/lib.rs b/crates/artifacts/zksolc/src/lib.rs index 504c2656..c36b7aba 100644 --- a/crates/artifacts/zksolc/src/lib.rs +++ b/crates/artifacts/zksolc/src/lib.rs @@ -1,5 +1,5 @@ use foundry_compilers_artifacts_solc::{ - CompactContractRef, FileToContractsMap, SourceFile, SourceFiles, + Bytecode, BytecodeObject, CompactContractRef, FileToContractsMap, SourceFile, SourceFiles, }; use semver::Version; @@ -9,12 +9,11 @@ use std::{ path::{Path, PathBuf}, }; -pub mod bytecode; pub mod contract; pub mod error; pub mod output_selection; -use self::{bytecode::Bytecode, contract::Contract, error::Error}; +use self::{contract::Contract, error::Error}; /// file -> (contract name -> Contract) pub type Contracts = FileToContractsMap; @@ -109,6 +108,36 @@ pub struct Evm { pub extra_metadata: Option, } +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct EraVM { + /// The contract EraVM assembly code. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub assembly: Option, + /// The contract bytecode. + /// Is reset by that of EraVM before yielding the compiled project artifacts. + #[serde(default, skip_serializing_if = "Option::is_none")] + bytecode: Option, +} + +impl EraVM { + pub fn bytecode(&self, should_be_unlinked: bool) -> Option { + self.bytecode.as_ref().map(|object| match (should_be_unlinked, object) { + (true, BytecodeObject::Bytecode(bc)) => { + // convert to unlinked + let encoded = alloy_primitives::hex::encode(bc); + BytecodeObject::Unlinked(encoded) + } + (false, BytecodeObject::Unlinked(bc)) => { + // convert to linked + let bytecode = alloy_primitives::hex::decode(bc).expect("valid bytecode"); + BytecodeObject::Bytecode(bytecode.into()) + } + _ => object.to_owned(), + }) + } +} + /// /// The `solc --standard-json` output contract EVM extra metadata. #[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)] diff --git a/crates/compilers/src/compilers/zksolc/mod.rs b/crates/compilers/src/compilers/zksolc/mod.rs index 7208b6f5..d1003dbc 100644 --- a/crates/compilers/src/compilers/zksolc/mod.rs +++ b/crates/compilers/src/compilers/zksolc/mod.rs @@ -302,6 +302,7 @@ impl ZkSolc { let output = std::str::from_utf8(&output).map_err(|_| SolcError::InvalidUtf8)?; let mut compiler_output: CompilerOutput = serde_json::from_str(output)?; + // Add zksync version so that there's some way to identify if zksync solc was used // by looking at build info compiler_output.zksync_solc_version = self.solc_version_info.zksync_version.clone(); @@ -728,7 +729,7 @@ mod tests { let out = zksolc().compile(&input).unwrap(); let (_, mut contracts) = out.split(); let contract = contracts.remove("LinkTest").unwrap(); - let bytecode = &contract.evm.unwrap().bytecode.unwrap().object; + let bytecode = contract.bytecode().unwrap().object; assert!(!bytecode.is_unlinked()); } @@ -741,7 +742,7 @@ mod tests { let out = zksolc().compile(&input).unwrap(); let (_, mut contracts) = out.split(); let contract = contracts.remove("LinkTest").unwrap(); - let bytecode = &contract.evm.unwrap().bytecode.unwrap().object; + let bytecode = contract.bytecode().unwrap().object; assert!(!bytecode.is_unlinked()); } } diff --git a/crates/compilers/src/zksync/artifact_output/zk.rs b/crates/compilers/src/zksync/artifact_output/zk.rs index e78674b8..273d6160 100644 --- a/crates/compilers/src/zksync/artifact_output/zk.rs +++ b/crates/compilers/src/zksync/artifact_output/zk.rs @@ -12,7 +12,7 @@ use foundry_compilers_artifacts::{ CompactBytecode, CompactContract, CompactContractBytecode, CompactContractBytecodeCow, CompactDeployedBytecode, }, - zksolc::{bytecode::Bytecode, contract::Contract, Evm}, + zksolc::contract::Contract, SolcLanguage, }; use path_slash::PathBufExt; @@ -24,19 +24,18 @@ use std::{ path::Path, }; +mod bytecode; +pub use bytecode::ZkArtifactBytecode; + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ZkContractArtifact { pub abi: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub bytecode: Option, + pub bytecode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub assembly: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub method_identifiers: Option>, - //#[serde(default, skip_serializing_if = "Vec::is_empty")] - //pub generated_sources: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] pub metadata: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub storage_layout: Option, @@ -50,13 +49,17 @@ pub struct ZkContractArtifact { pub hash: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub factory_dependencies: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub missing_libraries: Option>, /// The identifier of the source file #[serde(default, skip_serializing_if = "Option::is_none")] pub id: Option, } +impl ZkContractArtifact { + pub fn missing_libraries(&self) -> Option<&Vec> { + self.bytecode.as_ref().map(|bc| &bc.missing_libraries) + } +} + // CompactContract variants // TODO: for zkEvm, the distinction between bytecode and deployed_bytecode makes little sense, // and there some fields that the ouptut doesn't provide (e.g: source_map) @@ -92,8 +95,8 @@ impl From for CompactContract { fn from(c: ZkContractArtifact) -> Self { // TODO: c.abi might have None, we need to get this field from solc_metadata Self { - bin: c.bytecode.clone().map(|b| b.object), - bin_runtime: c.bytecode.clone().map(|b| b.object), + bin: c.bytecode.clone().map(|b| b.object()), + bin_runtime: c.bytecode.clone().map(|b| b.object()), abi: c.abi, } } @@ -132,16 +135,14 @@ impl ZkArtifactOutput { contract: Contract, source_file: Option<&SourceFile>, ) -> ZkContractArtifact { - let mut artifact_bytecode = None; - let mut artifact_method_identifiers = None; - let mut artifact_assembly = None; - + let is_unlinked = contract.is_unlinked(); let Contract { abi, metadata, userdoc, devdoc, storage_layout, + eravm, evm, ir_optimized, hash, @@ -149,29 +150,20 @@ impl ZkArtifactOutput { missing_libraries, } = contract; - if let Some(evm) = evm { - let Evm { - assembly, - bytecode, - method_identifiers, - extra_metadata: _, - legacy_assembly: _, - } = evm; - - artifact_bytecode = bytecode.map(Into::into); - artifact_method_identifiers = Some(method_identifiers); - artifact_assembly = assembly; - } + let (bytecode, assembly) = eravm + .map(|eravm| (eravm.bytecode(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)); ZkContractArtifact { abi, hash, factory_dependencies, - missing_libraries, storage_layout: Some(storage_layout), - bytecode: artifact_bytecode, - assembly: artifact_assembly, - method_identifiers: artifact_method_identifiers, + bytecode, + assembly, metadata, userdoc: Some(userdoc), devdoc: Some(devdoc), diff --git a/crates/compilers/src/zksync/artifact_output/zk/bytecode.rs b/crates/compilers/src/zksync/artifact_output/zk/bytecode.rs new file mode 100644 index 00000000..a26b94ab --- /dev/null +++ b/crates/compilers/src/zksync/artifact_output/zk/bytecode.rs @@ -0,0 +1,96 @@ +use std::collections::BTreeMap; + +use alloy_primitives::Bytes; +use foundry_compilers_artifacts::{ + zksolc::contract::Contract, BytecodeObject, CompactBytecode, CompactDeployedBytecode, Offsets, +}; +use serde::{Deserialize, Serialize}; + +/// This will serialize the bytecode data without a `0x` prefix +/// +/// Equivalent of solc artifact bytecode's +/// [`serialize_bytecode_without_prefix`](foundry_compilers_artifacts::solc::bytecode::serialize_bytecode_without_prefix) +pub fn serialize_bytes_without_prefix(code: &Bytes, s: S) -> Result +where + S: serde::Serializer, +{ + s.serialize_str(&alloy_primitives::hex::encode(code)) +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct ZkArtifactBytecode { + #[serde(serialize_with = "serialize_bytes_without_prefix")] + object: Bytes, + is_unlinked: bool, + + #[serde(default)] + pub missing_libraries: Vec, +} + +impl ZkArtifactBytecode { + pub fn with_object( + object: BytecodeObject, + is_unlinked: bool, + missing_libraries: Vec, + ) -> Self { + let object = match object { + BytecodeObject::Bytecode(bc) => bc, + BytecodeObject::Unlinked(s) => { + alloy_primitives::hex::decode(s).expect("valid bytecode").into() + } + }; + Self { object, is_unlinked, missing_libraries } + } + + pub fn link_references(&self) -> BTreeMap>> { + Contract::missing_libs_to_link_references(self.missing_libraries.as_slice()) + } + + pub fn object(&self) -> BytecodeObject { + if self.is_unlinked { + // convert to unlinked + let encoded = alloy_primitives::hex::encode(&self.object); + BytecodeObject::Unlinked(encoded) + } else { + // convert to linked + BytecodeObject::Bytecode(self.object.clone()) + } + } +} + +// NOTE: distinction between bytecode and deployed bytecode makes no sense of zkEvm, but +// we implement these conversions in order to be able to use the Artifacts trait. +impl From for CompactBytecode { + fn from(bcode: ZkArtifactBytecode) -> Self { + let link_references = bcode.link_references(); + Self { object: bcode.object(), source_map: None, link_references } + } +} + +impl From for CompactDeployedBytecode { + fn from(bcode: ZkArtifactBytecode) -> Self { + Self { bytecode: Some(bcode.into()), immutable_references: BTreeMap::default() } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + /// https://github.com/Moonsong-Labs/compilers/pull/44 + 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 json_str = + serde_json::to_string(&sample).expect("able to serialize artifact bytecode as json"); + + let deserialized: serde_json::Value = + serde_json::from_str(&json_str).expect("able to deserialize json"); + + let bytecode_str = deserialized["object"].as_str().expect(".object to be a string"); + + assert!(!bytecode_str.starts_with("0x")); + } +} diff --git a/crates/compilers/tests/zksync.rs b/crates/compilers/tests/zksync.rs index 95d3ef2f..e077bc6b 100644 --- a/crates/compilers/tests/zksync.rs +++ b/crates/compilers/tests/zksync.rs @@ -591,7 +591,7 @@ fn zksync_can_compile_yul_sample() { .clone() .unwrap(); - let yul_bytecode = simple_store_artifact.object.as_bytes().unwrap(); + let yul_bytecode = simple_store_artifact.object().into_bytes().unwrap(); assert!(!yul_bytecode.is_empty(), "SimpleStore.yul bytecode is empty"); }