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

feat(contract-verifier): Support Solidity contracts with EVM bytecode in contract verifier #3225

Merged
Merged
Show file tree
Hide file tree
Changes from 17 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
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/lib/contract_verifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ semver.workspace = true
[dev-dependencies]
zksync_node_test_utils.workspace = true
zksync_vm_interface.workspace = true
test-casing.workspace = true
87 changes: 87 additions & 0 deletions core/lib/contract_verifier/src/compilers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use anyhow::Context as _;
use serde::{Deserialize, Serialize};
use zksync_types::contract_verification_api::CompilationArtifacts;

pub(crate) use self::{
solc::{Solc, SolcInput},
zksolc::{ZkSolc, ZkSolcInput},
zkvyper::{ZkVyper, ZkVyperInput},
};
use crate::error::ContractVerifierError;

mod solc;
mod zksolc;
mod zkvyper;

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Source {
/// The source code file content.
pub content: String,
}

/// Parsing logic shared between `solc` and `zksolc`.
fn parse_standard_json_output(
output: &serde_json::Value,
contract_name: String,
file_name: String,
get_deployed_bytecode: bool,
) -> Result<CompilationArtifacts, ContractVerifierError> {
if let Some(errors) = output.get("errors") {
let errors = errors.as_array().unwrap().clone();
if errors
.iter()
.any(|err| err["severity"].as_str().unwrap() == "error")
{
let error_messages = errors
.into_iter()
.map(|err| err["formattedMessage"].clone())
.collect();
return Err(ContractVerifierError::CompilationError(
serde_json::Value::Array(error_messages),
));
}
}

let contracts = output["contracts"]
.get(&file_name)
.ok_or(ContractVerifierError::MissingSource(file_name))?;
let Some(contract) = contracts.get(&contract_name) else {
return Err(ContractVerifierError::MissingContract(contract_name));
};

let Some(bytecode_str) = contract
.pointer("/evm/bytecode/object")
.context("missing bytecode in solc / zksolc output")?
.as_str()
else {
return Err(ContractVerifierError::AbstractContract(contract_name));
};
let bytecode = hex::decode(bytecode_str).context("invalid bytecode")?;

let deployed_bytecode = if get_deployed_bytecode {
let bytecode_str = contract
.pointer("/evm/deployedBytecode/object")
.context("missing deployed bytecode in solc output")?
.as_str()
.ok_or(ContractVerifierError::AbstractContract(contract_name))?;
Some(hex::decode(bytecode_str).context("invalid deployed bytecode")?)
} else {
None
};

let abi = contract["abi"].clone();
if !abi.is_array() {
let err = anyhow::anyhow!(
"unexpected value for ABI: {}",
serde_json::to_string_pretty(&abi).unwrap()
);
return Err(err.into());
}

Ok(CompilationArtifacts {
bytecode,
deployed_bytecode,
abi,
})
}
168 changes: 168 additions & 0 deletions core/lib/contract_verifier/src/compilers/solc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use std::{collections::HashMap, path::PathBuf, process::Stdio};

use anyhow::Context;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
use zksync_queued_job_processor::async_trait;
use zksync_types::contract_verification_api::{
CompilationArtifacts, SourceCodeData, VerificationIncomingRequest,
};

use super::{parse_standard_json_output, Source};
use crate::{error::ContractVerifierError, resolver::Compiler};

// Here and below, fields are public for testing purposes.
#[derive(Debug)]
pub(crate) struct SolcInput {
pub standard_json: StandardJson,
pub contract_name: String,
pub file_name: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct StandardJson {
pub language: String,
pub sources: HashMap<String, Source>,
settings: Settings,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Settings {
/// The output selection filters.
output_selection: Option<serde_json::Value>,
/// Other settings (only filled when parsing `StandardJson` input from the request).
#[serde(flatten)]
other: serde_json::Value,
}

#[derive(Debug)]
pub(crate) struct Solc {
path: PathBuf,
}

impl Solc {
pub fn new(path: PathBuf) -> Self {
Self { path }
}

pub fn build_input(
req: VerificationIncomingRequest,
) -> Result<SolcInput, ContractVerifierError> {
// Users may provide either just contract name or
// source file name and contract name joined with ":".
let (file_name, contract_name) =
if let Some((file_name, contract_name)) = req.contract_name.rsplit_once(':') {
(file_name.to_string(), contract_name.to_string())
} else {
(
format!("{}.sol", req.contract_name),
req.contract_name.clone(),
)
};
let default_output_selection = serde_json::json!({
"*": {
"*": [ "abi", "evm.bytecode", "evm.deployedBytecode" ],
"": [ "abi", "evm.bytecode", "evm.deployedBytecode" ],
}
});

let standard_json = match req.source_code_data {
SourceCodeData::SolSingleFile(source_code) => {
let source = Source {
content: source_code,
};
let sources = HashMap::from([(file_name.clone(), source)]);
let settings = Settings {
output_selection: Some(default_output_selection),
other: serde_json::json!({
"optimizer": {
"enabled": req.optimization_used,
},
slowli marked this conversation as resolved.
Show resolved Hide resolved
}),
};

StandardJson {
language: "Solidity".to_owned(),
sources,
settings,
}
}
SourceCodeData::StandardJsonInput(map) => {
let mut compiler_input: StandardJson =
serde_json::from_value(serde_json::Value::Object(map))
.map_err(|_| ContractVerifierError::FailedToDeserializeInput)?;
// Set default output selection even if it is different in request.
compiler_input.settings.output_selection = Some(default_output_selection);
compiler_input
}
SourceCodeData::YulSingleFile(source_code) => {
let source = Source {
content: source_code,
};
let sources = HashMap::from([(file_name.clone(), source)]);
let settings = Settings {
output_selection: Some(default_output_selection),
other: serde_json::json!({
"optimizer": {
"enabled": req.optimization_used,
},
}),
};
StandardJson {
language: "Yul".to_owned(),
sources,
settings,
}
}
other => unreachable!("Unexpected `SourceCodeData` variant: {other:?}"),
};

Ok(SolcInput {
standard_json,
contract_name,
file_name,
})
}
}

#[async_trait]
impl Compiler<SolcInput> for Solc {
async fn compile(
self: Box<Self>,
input: SolcInput,
) -> Result<CompilationArtifacts, ContractVerifierError> {
let mut command = tokio::process::Command::new(&self.path);
let mut child = command
.arg("--standard-json")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("failed spawning solc")?;
let stdin = child.stdin.as_mut().unwrap();
let content = serde_json::to_vec(&input.standard_json)
.context("cannot encode standard JSON input for solc")?;
stdin
.write_all(&content)
.await
.context("failed writing standard JSON to solc stdin")?;
stdin
.flush()
.await
.context("failed flushing standard JSON to solc")?;

let output = child.wait_with_output().await.context("solc failed")?;
if output.status.success() {
let output = serde_json::from_slice(&output.stdout)
.context("zksolc output is not valid JSON")?;
parse_standard_json_output(&output, input.contract_name, input.file_name, true)
} else {
Err(ContractVerifierError::CompilerError(
"zksolc".to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
))
}
}
}
Loading
Loading