Skip to content

Commit

Permalink
feat(contract-verifier): Support Vyper toolchain for EVM bytecodes (#…
Browse files Browse the repository at this point in the history
…3251)

## What ❔

- Supports the Vyper toolchain for EVM bytecodes in the contract
verifier.
- Adds some more unit tests for the verifier (e.g., testing multi-file
inputs, Yul contracts, abstract contract errors etc.).

## Why ❔

Part of preparations to support EVM bytecodes throughout the 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 12, 2024
1 parent 363b4f0 commit 75f7db9
Show file tree
Hide file tree
Showing 12 changed files with 720 additions and 132 deletions.
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

0 comments on commit 75f7db9

Please sign in to comment.