Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(zk): properly mark unlinked contract #40

Merged
merged 4 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions crates/artifacts/zksolc/src/bytecode.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,54 @@
use std::collections::BTreeMap;

use foundry_compilers_artifacts_solc::{
bytecode::{serialize_bytecode_without_prefix, BytecodeObject},
CompactBytecode, CompactDeployedBytecode,
BytecodeObject, CompactBytecode, CompactDeployedBytecode, Offsets,
};
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,

/// This value is not part of the compiler output json
///
/// It will be populated if the contract has any missing library
Karrq marked this conversation as resolved.
Show resolved Hide resolved
#[serde(default)]
pub missing_libraries: Vec<String>,
}

impl Bytecode {
fn link_references(&self) -> BTreeMap<String, BTreeMap<String, Vec<Offsets>>> {
self.missing_libraries
.iter()
.map(|file_and_lib| {
let mut parts = file_and_lib.split(':');
let filename = parts.next().expect("missing library contract file (<file>:<name>)");
let contract = parts.next().expect("missing library contract name (<file>:<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
})
}

pub(crate) fn mark_as_unlinked(&mut self) {
if let BytecodeObject::Bytecode(inner) = &self.object {
let encoded = alloy_primitives::hex::encode(inner);
self.object = BytecodeObject::Unlinked(encoded);
}
}
}

// 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<Bytecode> for CompactBytecode {
fn from(bcode: Bytecode) -> Self {
Self { object: bcode.object, source_map: None, link_references: BTreeMap::default() }
let link_references = bcode.link_references();
Self { object: bcode.object, source_map: None, link_references }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to be careful now because bytecode and deployed bytecode will NOT be the same thing as bytecode might be an unlinked and link_references has meaningful info . Maybe update the NOTE above.
Also is having this conversion the reason why you need missing_libraries in the Bytecode struct

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in EraVM they still are the same thing, not sure why the unlinked means it's different

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CompactBytecode will have the link references for the bytecode CompactDeployedBytecode will not. Previously we only cared about the bytecode field and could use both interchangebly. It may not be the case now if we used the linked_references somewhere we need CompactBytecode

}
}

Expand Down
55 changes: 43 additions & 12 deletions crates/artifacts/zksolc/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Contract related types.
use crate::Evm;
use crate::EraVM;
use alloy_json_abi::JsonAbi;
use foundry_compilers_artifacts_solc::{
CompactContractBytecode, CompactContractBytecodeCow, CompactContractRef, DevDoc, StorageLayout,
Expand All @@ -8,10 +8,30 @@ use foundry_compilers_artifacts_solc::{
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, collections::BTreeMap};

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(transparent)]
pub struct Contract(
Karrq marked this conversation as resolved.
Show resolved Hide resolved
#[serde(deserialize_with = "crate::serde_helpers::maybe_unlinked_contract")] pub RawContract,
);

impl std::ops::Deref for Contract {
type Target = RawContract;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl std::ops::DerefMut for Contract {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

/// Represents a compiled solidity contract
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Contract {
pub struct RawContract {
pub abi: Option<JsonAbi>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
Expand All @@ -31,12 +51,21 @@ pub struct Contract {
/// The contract factory dependencies.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub factory_dependencies: Option<BTreeMap<String, String>>,
/// The contract missing libraries.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub missing_libraries: Option<Vec<String>>,
/// EVM-related outputs
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evm: Option<Evm>,
pub eravm: Option<EraVM>,
}

impl RawContract {
pub fn is_unlinked(&self) -> bool {
self.hash.is_none()
|| self
.eravm
.as_ref()
.and_then(|eravm| eravm.bytecode.as_ref())
.map(|bc| !bc.missing_libraries.is_empty())
.unwrap_or_default()
}
}

// CompactContract variants
Expand All @@ -48,10 +77,10 @@ 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 {
let (bytecode, deployed_bytecode) = if let Some(ref eravm) = artifact.eravm {
(
evm.bytecode.clone().map(Into::into).map(Cow::Owned),
evm.bytecode.clone().map(Into::into).map(Cow::Owned),
eravm.bytecode.clone().map(Into::into).map(Cow::Owned),
eravm.bytecode.clone().map(Into::into).map(Cow::Owned),
)
} else {
(None, None)
Expand All @@ -66,7 +95,8 @@ impl<'a> From<&'a Contract> for CompactContractBytecodeCow<'a> {

impl From<Contract> for CompactContractBytecode {
fn from(c: Contract) -> Self {
let bytecode = if let Some(evm) = c.evm { evm.bytecode } else { None };
let c = c.0;
let bytecode = if let Some(eravm) = c.eravm { eravm.bytecode } else { None };
Self {
abi: c.abi.map(Into::into),
deployed_bytecode: bytecode.clone().map(|b| b.into()),
Expand All @@ -77,8 +107,9 @@ impl From<Contract> for CompactContractBytecode {

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 {
(evm.bytecode.as_ref().map(|c| &c.object), evm.bytecode.as_ref().map(|c| &c.object))
let c = &c.0;
let (bin, bin_runtime) = if let Some(ref eravm) = c.eravm {
(eravm.bytecode.as_ref().map(|c| &c.object), eravm.bytecode.as_ref().map(|c| &c.object))
} else {
(None, None)
};
Expand Down
17 changes: 7 additions & 10 deletions crates/artifacts/zksolc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod bytecode;
pub mod contract;
pub mod error;
pub mod output_selection;
mod serde_helpers;

use self::{bytecode::Bytecode, contract::Contract, error::Error};

Expand Down Expand Up @@ -91,22 +92,18 @@ impl CompilerOutput {

#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Evm {
pub struct EraVM {
/// The contract EraVM assembly code.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assembly: Option<String>,
/// The contract EVM legacy assembly code.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub legacy_assembly: Option<serde_json::Value>,
/// The contract bytecode.
/// Is reset by that of EraVM before yielding the compiled project artifacts.
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "serde_helpers::opt_maybe_unwrapped_bytecode"
)]
pub bytecode: Option<Bytecode>,
/// The list of function hashes
#[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
pub method_identifiers: BTreeMap<String, String>,
elfedy marked this conversation as resolved.
Show resolved Hide resolved
/// The extra EVMLA metadata.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extra_metadata: Option<ExtraMetadata>,
}

///
Expand Down
61 changes: 61 additions & 0 deletions crates/artifacts/zksolc/src/serde_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use foundry_compilers_artifacts_solc::BytecodeObject;
use serde::{Deserialize, Deserializer};

use crate::{bytecode::Bytecode, contract::RawContract};

/// Deserialize a [`RawContract`] with added `missingLibraries` field,
/// to populate [`Bytecode`]'s `missing_libraries` with it
pub fn maybe_unlinked_contract<'de, D>(deserializer: D) -> Result<RawContract, D::Error>
Karrq marked this conversation as resolved.
Show resolved Hide resolved
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawContractWithLibs {
#[serde(default)]
pub missing_libraries: Vec<String>,
#[serde(flatten)]
pub contract: RawContract,
}

let RawContractWithLibs { missing_libraries, mut contract } =
RawContractWithLibs::deserialize(deserializer)?;

if !missing_libraries.is_empty() {
if let Some(bc) = contract.eravm.as_mut().and_then(|eravm| eravm.bytecode.as_mut()) {
bc.missing_libraries = missing_libraries;
bc.mark_as_unlinked();
}
}

Ok(contract)
}

/// Deserialize a [ `Bytecode` ] by either the raw bytecode object (from the compiler output),
/// or from a wrapped bytecode object from a stored artifact
pub fn opt_maybe_unwrapped_bytecode<'de, D>(deserializer: D) -> Result<Option<Bytecode>, D::Error>
Karrq marked this conversation as resolved.
Show resolved Hide resolved
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum CompilerBytecodeOrStored {
// what we store in our artifacts
Stored(Bytecode),
// what the compiler returns
Compiler(BytecodeObject),
}

impl Into<Bytecode> for CompilerBytecodeOrStored {
fn into(self) -> Bytecode {
match self {
CompilerBytecodeOrStored::Stored(bc) => bc,
CompilerBytecodeOrStored::Compiler(object) => {
Bytecode { object, missing_libraries: Default::default() }
}
}
}
}

Ok(<Option<CompilerBytecodeOrStored>>::deserialize(deserializer)?.map(Into::into))
}
5 changes: 3 additions & 2 deletions crates/compilers/src/compilers/zksolc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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.0.eravm.unwrap().bytecode.unwrap().object;
Karrq marked this conversation as resolved.
Show resolved Hide resolved
assert!(!bytecode.is_unlinked());
}

Expand All @@ -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.0.eravm.unwrap().bytecode.unwrap().object;
Karrq marked this conversation as resolved.
Show resolved Hide resolved
assert!(!bytecode.is_unlinked());
}
}
51 changes: 19 additions & 32 deletions crates/compilers/src/zksync/artifact_output/zk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ use foundry_compilers_artifacts::{
CompactBytecode, CompactContract, CompactContractBytecode, CompactContractBytecodeCow,
CompactDeployedBytecode,
},
zksolc::{bytecode::Bytecode, contract::Contract, Evm},
zksolc::{
bytecode::Bytecode,
contract::{Contract, RawContract},
},
SolcLanguage,
};
use path_slash::PathBufExt;
Expand All @@ -32,10 +35,8 @@ pub struct ZkContractArtifact {
pub bytecode: Option<Bytecode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assembly: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method_identifiers: Option<BTreeMap<String, String>>,
//#[serde(default, skip_serializing_if = "Vec::is_empty")]
//pub generated_sources: Vec<GeneratedSource>,
// #[serde(default, skip_serializing_if = "Option::is_none")]
// pub method_identifiers: Option<BTreeMap<String, String>>,
Karrq marked this conversation as resolved.
Show resolved Hide resolved
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand All @@ -50,13 +51,17 @@ pub struct ZkContractArtifact {
pub hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub factory_dependencies: Option<BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub missing_libraries: Option<Vec<String>>,
/// The identifier of the source file
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<u32>,
}

impl ZkContractArtifact {
pub fn missing_libraries(&self) -> Option<&Vec<String>> {
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)
Expand Down Expand Up @@ -132,46 +137,28 @@ 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 Contract {
let RawContract {
abi,
metadata,
userdoc,
devdoc,
storage_layout,
evm,
eravm,
ir_optimized,
hash,
factory_dependencies,
missing_libraries,
} = contract;
} = contract.0;

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, eravm.assembly)).unwrap_or_else(|| (None, None));

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),
Expand Down
Loading