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 Vyper toolchain for EVM bytecodes #3251

Merged
merged 9 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions core/lib/contract_verifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ semver.workspace = true
[dev-dependencies]
zksync_node_test_utils.workspace = true
zksync_vm_interface.workspace = true

assert_matches.workspace = true
test-casing.workspace = true
84 changes: 70 additions & 14 deletions core/lib/contract_verifier/src/compilers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,69 @@
use std::collections::HashMap;

use anyhow::Context as _;
use serde::{Deserialize, Serialize};
use zksync_types::contract_verification_api::CompilationArtifacts;

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

mod solc;
mod vyper;
mod zksolc;
mod zkvyper;

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct StandardJson {
pub language: String,
pub sources: HashMap<String, Source>,
#[serde(default)]
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,
}

impl Default for Settings {
fn default() -> Self {
Self {
output_selection: None,
other: serde_json::json!({}),
}
}
}

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

/// Users may provide either just contract name or source file name and contract name joined with ":".
fn process_contract_name(original_name: &str, extension: &str) -> (String, String) {
if let Some((file_name, contract_name)) = original_name.rsplit_once(':') {
(file_name.to_owned(), contract_name.to_owned())
} else {
(
format!("{original_name}.{extension}"),
original_name.to_owned(),
)
}
}

/// Parsing logic shared between `solc` and `zksolc`.
fn parse_standard_json_output(
output: &serde_json::Value,
Expand All @@ -31,11 +75,16 @@ fn parse_standard_json_output(
let errors = errors.as_array().unwrap().clone();
if errors
.iter()
.any(|err| err["severity"].as_str().unwrap() == "error")
.any(|err| err["severity"].as_str() == Some("error"))
{
let error_messages = errors
.into_iter()
.map(|err| err["formattedMessage"].clone())
.filter_map(|err| {
// `formattedMessage` is an optional field
err.get("formattedMessage")
.or_else(|| err.get("message"))
.cloned()
})
.collect();
return Err(ContractVerifierError::CompilationError(
serde_json::Value::Array(error_messages),
Expand All @@ -50,28 +99,35 @@ fn parse_standard_json_output(
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 {
let Some(bytecode_str) = contract.pointer("/evm/bytecode/object") else {
return Err(ContractVerifierError::AbstractContract(contract_name));
};
let bytecode_str = bytecode_str
.as_str()
.context("unexpected `/evm/bytecode/object` value")?;
// Strip an optional `0x` prefix (output by `vyper`, but not by `solc` / `zksolc`)
let bytecode_str = bytecode_str.strip_prefix("0x").unwrap_or(bytecode_str);
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")?
let Some(bytecode_str) = contract.pointer("/evm/deployedBytecode/object") else {
return Err(ContractVerifierError::AbstractContract(contract_name));
};
let bytecode_str = bytecode_str
.as_str()
.ok_or(ContractVerifierError::AbstractContract(contract_name))?;
.context("unexpected `/evm/deployedBytecode/object` value")?;
let bytecode_str = bytecode_str.strip_prefix("0x").unwrap_or(bytecode_str);
Some(hex::decode(bytecode_str).context("invalid deployed bytecode")?)
} else {
None
};

let abi = contract["abi"].clone();
if !abi.is_array() {
let mut abi = contract["abi"].clone();
if abi.is_null() {
// ABI is undefined for Yul contracts when compiled with standalone `solc`. For uniformity with `zksolc`,
// replace it with an empty array.
abi = serde_json::json!([]);
} else if !abi.is_array() {
let err = anyhow::anyhow!(
"unexpected value for ABI: {}",
serde_json::to_string_pretty(&abi).unwrap()
Expand Down
33 changes: 2 additions & 31 deletions core/lib/contract_verifier/src/compilers/solc.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
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 super::{parse_standard_json_output, process_contract_name, Settings, Source, StandardJson};
use crate::{error::ContractVerifierError, resolver::Compiler};

// Here and below, fields are public for testing purposes.
Expand All @@ -19,24 +18,6 @@ pub(crate) struct SolcInput {
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,
Expand All @@ -50,17 +31,7 @@ impl Solc {
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 (file_name, contract_name) = process_contract_name(&req.contract_name, "sol");
let default_output_selection = serde_json::json!({
"*": {
"*": [ "abi", "evm.bytecode", "evm.deployedBytecode" ],
Expand Down
114 changes: 114 additions & 0 deletions core/lib/contract_verifier/src/compilers/vyper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use std::{collections::HashMap, mem, path::PathBuf, process::Stdio};

use anyhow::Context;
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, process_contract_name, Settings, Source, StandardJson};
use crate::{error::ContractVerifierError, resolver::Compiler};

#[derive(Debug)]
pub(crate) struct VyperInput {
pub contract_name: String,
pub file_name: String,
pub sources: HashMap<String, String>,
pub optimizer_mode: Option<String>,
}

impl VyperInput {
pub fn new(req: VerificationIncomingRequest) -> Result<Self, ContractVerifierError> {
let (file_name, contract_name) = process_contract_name(&req.contract_name, "vy");

let sources = match req.source_code_data {
SourceCodeData::VyperMultiFile(s) => s,
other => unreachable!("unexpected `SourceCodeData` variant: {other:?}"),
};
Ok(Self {
contract_name,
file_name,
sources,
optimizer_mode: if req.optimization_used {
req.optimizer_mode
} else {
// `none` mode is not the default mode (which is `gas`), so we must specify it explicitly here
Some("none".to_owned())
},
})
}

fn take_standard_json(&mut self) -> StandardJson {
let sources = mem::take(&mut self.sources);
let sources = sources
.into_iter()
.map(|(name, content)| (name, Source { content }));

StandardJson {
language: "Vyper".to_owned(),
sources: sources.collect(),
settings: Settings {
output_selection: Some(serde_json::json!({
"*": [ "abi", "evm.bytecode", "evm.deployedBytecode" ],
})),
other: serde_json::json!({
"optimize": self.optimizer_mode.as_deref(),
}),
},
}
}
}

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

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

#[async_trait]
impl Compiler<VyperInput> for Vyper {
async fn compile(
self: Box<Self>,
mut input: VyperInput,
) -> 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("cannot spawn vyper")?;
let mut stdin = child.stdin.take().unwrap();
let standard_json = input.take_standard_json();
let content = serde_json::to_vec(&standard_json)
.context("cannot encode standard JSON input for vyper")?;
stdin
.write_all(&content)
.await
.context("failed writing standard JSON to vyper stdin")?;
stdin
.flush()
.await
.context("failed flushing standard JSON to vyper")?;
drop(stdin);

let output = child.wait_with_output().await.context("vyper failed")?;
if output.status.success() {
let output =
serde_json::from_slice(&output.stdout).context("vyper output is not valid JSON")?;
parse_standard_json_output(&output, input.contract_name, input.file_name, true)
} else {
Err(ContractVerifierError::CompilerError(
"vyper",
String::from_utf8_lossy(&output.stderr).to_string(),
))
}
}
}
14 changes: 2 additions & 12 deletions core/lib/contract_verifier/src/compilers/zksolc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use zksync_types::contract_verification_api::{
CompilationArtifacts, SourceCodeData, VerificationIncomingRequest,
};

use super::{parse_standard_json_output, Source};
use super::{parse_standard_json_output, process_contract_name, Source};
use crate::{
error::ContractVerifierError,
resolver::{Compiler, CompilerPaths},
Expand Down Expand Up @@ -85,17 +85,7 @@ impl ZkSolc {
pub fn build_input(
req: VerificationIncomingRequest,
) -> Result<ZkSolcInput, 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 (file_name, contract_name) = process_contract_name(&req.contract_name, "sol");
let default_output_selection = serde_json::json!({
"*": {
"*": [ "abi" ],
Expand Down
Loading
Loading