-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(contract-verifier): Support Solidity contracts with EVM bytecode…
… 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
Showing
24 changed files
with
1,531 additions
and
656 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
)) | ||
} | ||
} | ||
} |
Oops, something went wrong.