Skip to content

Commit

Permalink
feat(contract-verifier): Support Solidity contracts with EVM bytecode…
Browse files Browse the repository at this point in the history
… in contract verifier (#3225)

## What ❔

Supports verifying Solidity contracts deployed with EVM bytecode in the
contract verifier.

## Why ❔

Part of supporting EVM emulation throughout the Era codebase.

## Checklist

- [x] PR title corresponds to the body of PR (we generate changelog
entries from PRs).
- [x] Tests for the changes have been added / updated.
- [x] Documentation comments have been added / updated.
- [x] Code has been formatted via `zkstack dev fmt` and `zkstack dev
lint`.
  • Loading branch information
slowli authored Nov 6, 2024
1 parent 474d173 commit 8a3a82c
Show file tree
Hide file tree
Showing 24 changed files with 1,531 additions and 656 deletions.
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,
},
}),
};

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(
"solc",
String::from_utf8_lossy(&output.stderr).to_string(),
))
}
}
}
Loading

0 comments on commit 8a3a82c

Please sign in to comment.