From 75f7db9b535b4dee4c6662be609aec996555383c Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Tue, 12 Nov 2024 12:01:28 +0200 Subject: [PATCH] feat(contract-verifier): Support Vyper toolchain for EVM bytecodes (#3251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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`. --- Cargo.lock | 1 + core/lib/contract_verifier/Cargo.toml | 2 + .../contract_verifier/src/compilers/mod.rs | 84 ++++- .../contract_verifier/src/compilers/solc.rs | 33 +- .../contract_verifier/src/compilers/vyper.rs | 114 ++++++ .../contract_verifier/src/compilers/zksolc.rs | 14 +- .../src/compilers/zkvyper.rs | 135 ++++--- core/lib/contract_verifier/src/lib.rs | 25 +- core/lib/contract_verifier/src/resolver.rs | 22 +- core/lib/contract_verifier/src/tests/mod.rs | 70 +++- core/lib/contract_verifier/src/tests/real.rs | 350 +++++++++++++++++- .../types/src/contract_verification_api.rs | 2 + 12 files changed, 720 insertions(+), 132 deletions(-) create mode 100644 core/lib/contract_verifier/src/compilers/vyper.rs diff --git a/Cargo.lock b/Cargo.lock index bdd2f84527b8..eb93300b1729 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10817,6 +10817,7 @@ name = "zksync_contract_verifier_lib" version = "0.1.0" dependencies = [ "anyhow", + "assert_matches", "chrono", "ethabi", "hex", diff --git a/core/lib/contract_verifier/Cargo.toml b/core/lib/contract_verifier/Cargo.toml index bdbfa90bf76a..6ccd6422d7da 100644 --- a/core/lib/contract_verifier/Cargo.toml +++ b/core/lib/contract_verifier/Cargo.toml @@ -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 diff --git a/core/lib/contract_verifier/src/compilers/mod.rs b/core/lib/contract_verifier/src/compilers/mod.rs index a56b4e32d1a1..c82a6575ee4c 100644 --- a/core/lib/contract_verifier/src/compilers/mod.rs +++ b/core/lib/contract_verifier/src/compilers/mod.rs @@ -1,18 +1,50 @@ +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, + #[serde(default)] + settings: Settings, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Settings { + /// The output selection filters. + output_selection: Option, + /// 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 { @@ -20,6 +52,18 @@ pub(crate) struct Source { 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, @@ -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), @@ -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() diff --git a/core/lib/contract_verifier/src/compilers/solc.rs b/core/lib/contract_verifier/src/compilers/solc.rs index bb453cb729c2..10adcad3542e 100644 --- a/core/lib/contract_verifier/src/compilers/solc.rs +++ b/core/lib/contract_verifier/src/compilers/solc.rs @@ -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. @@ -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, - settings: Settings, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct Settings { - /// The output selection filters. - output_selection: Option, - /// 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, @@ -50,17 +31,7 @@ impl Solc { pub fn build_input( req: VerificationIncomingRequest, ) -> Result { - // 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" ], diff --git a/core/lib/contract_verifier/src/compilers/vyper.rs b/core/lib/contract_verifier/src/compilers/vyper.rs new file mode 100644 index 000000000000..59b950f9f17f --- /dev/null +++ b/core/lib/contract_verifier/src/compilers/vyper.rs @@ -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, + pub optimizer_mode: Option, +} + +impl VyperInput { + pub fn new(req: VerificationIncomingRequest) -> Result { + 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 for Vyper { + async fn compile( + self: Box, + mut input: VyperInput, + ) -> Result { + 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(), + )) + } + } +} diff --git a/core/lib/contract_verifier/src/compilers/zksolc.rs b/core/lib/contract_verifier/src/compilers/zksolc.rs index 0d6b5828e31c..ff435e96aeb6 100644 --- a/core/lib/contract_verifier/src/compilers/zksolc.rs +++ b/core/lib/contract_verifier/src/compilers/zksolc.rs @@ -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}, @@ -85,17 +85,7 @@ impl ZkSolc { pub fn build_input( req: VerificationIncomingRequest, ) -> Result { - // 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" ], diff --git a/core/lib/contract_verifier/src/compilers/zkvyper.rs b/core/lib/contract_verifier/src/compilers/zkvyper.rs index b3dacce64e77..4f7c10214f8a 100644 --- a/core/lib/contract_verifier/src/compilers/zkvyper.rs +++ b/core/lib/contract_verifier/src/compilers/zkvyper.rs @@ -1,21 +1,54 @@ -use std::{collections::HashMap, fs::File, io::Write, path::Path, process::Stdio}; +use std::{ffi::OsString, path, path::Path, process::Stdio}; use anyhow::Context as _; +use tokio::{fs, io::AsyncWriteExt}; use zksync_queued_job_processor::async_trait; -use zksync_types::contract_verification_api::{ - CompilationArtifacts, SourceCodeData, VerificationIncomingRequest, -}; +use zksync_types::contract_verification_api::CompilationArtifacts; +use super::VyperInput; use crate::{ error::ContractVerifierError, resolver::{Compiler, CompilerPaths}, }; -#[derive(Debug)] -pub(crate) struct ZkVyperInput { - pub contract_name: String, - pub sources: HashMap, - pub optimizer_mode: Option, +impl VyperInput { + async fn write_files(&self, root_dir: &Path) -> anyhow::Result> { + let mut paths = Vec::with_capacity(self.sources.len()); + for (name, content) in &self.sources { + let mut name = name.clone(); + if !name.ends_with(".vy") { + name += ".vy"; + } + + let name_path = Path::new(&name); + anyhow::ensure!( + !name_path.is_absolute(), + "absolute contract filename: {name}" + ); + let normal_components = name_path + .components() + .all(|component| matches!(component, path::Component::Normal(_))); + anyhow::ensure!( + normal_components, + "contract filename contains disallowed components: {name}" + ); + + let path = root_dir.join(name_path); + if let Some(prefix) = path.parent() { + fs::create_dir_all(prefix) + .await + .with_context(|| format!("failed creating parent dir for `{name}`"))?; + } + let mut file = fs::File::create(&path) + .await + .with_context(|| format!("failed creating file for `{name}`"))?; + file.write_all(content.as_bytes()) + .await + .with_context(|| format!("failed writing to `{name}`"))?; + paths.push(path.into_os_string()); + } + Ok(paths) + } } #[derive(Debug)] @@ -28,28 +61,6 @@ impl ZkVyper { Self { paths } } - pub fn build_input( - req: VerificationIncomingRequest, - ) -> Result { - // Users may provide either just contract name or - // source file name and contract name joined with ":". - let contract_name = if let Some((_, contract_name)) = req.contract_name.rsplit_once(':') { - contract_name.to_owned() - } else { - req.contract_name.clone() - }; - - let sources = match req.source_code_data { - SourceCodeData::VyperMultiFile(s) => s, - other => unreachable!("unexpected `SourceCodeData` variant: {other:?}"), - }; - Ok(ZkVyperInput { - contract_name, - sources, - optimizer_mode: req.optimizer_mode, - }) - } - fn parse_output( output: &serde_json::Value, contract_name: String, @@ -80,10 +91,10 @@ impl ZkVyper { } #[async_trait] -impl Compiler for ZkVyper { +impl Compiler for ZkVyper { async fn compile( self: Box, - input: ZkVyperInput, + input: VyperInput, ) -> Result { let mut command = tokio::process::Command::new(&self.paths.zk); if let Some(o) = input.optimizer_mode.as_ref() { @@ -97,22 +108,15 @@ impl Compiler for ZkVyper { .stdout(Stdio::piped()) .stderr(Stdio::piped()); - let temp_dir = tempfile::tempdir().context("failed creating temporary dir")?; - for (mut name, content) in input.sources { - if !name.ends_with(".vy") { - name += ".vy"; - } - let path = temp_dir.path().join(&name); - if let Some(prefix) = path.parent() { - std::fs::create_dir_all(prefix) - .with_context(|| format!("failed creating parent dir for `{name}`"))?; - } - let mut file = File::create(&path) - .with_context(|| format!("failed creating file for `{name}`"))?; - file.write_all(content.as_bytes()) - .with_context(|| format!("failed writing to `{name}`"))?; - command.arg(path.into_os_string()); - } + let temp_dir = tokio::task::spawn_blocking(tempfile::tempdir) + .await + .context("panicked creating temporary dir")? + .context("failed creating temporary dir")?; + let file_paths = input + .write_files(temp_dir.path()) + .await + .context("failed writing Vyper files to temp dir")?; + command.args(file_paths); let child = command.spawn().context("cannot spawn zkvyper")?; let output = child.wait_with_output().await.context("zkvyper failed")?; @@ -128,3 +132,36 @@ impl Compiler for ZkVyper { } } } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + #[tokio::test] + async fn sanitizing_contract_paths() { + let mut input = VyperInput { + contract_name: "Test".to_owned(), + file_name: "test.vy".to_owned(), + sources: HashMap::from([("/etc/shadow".to_owned(), String::new())]), + optimizer_mode: None, + }; + + let temp_dir = tempfile::TempDir::new().unwrap(); + let err = input + .write_files(temp_dir.path()) + .await + .unwrap_err() + .to_string(); + assert!(err.contains("absolute"), "{err}"); + + input.sources = HashMap::from([("../../../etc/shadow".to_owned(), String::new())]); + let err = input + .write_files(temp_dir.path()) + .await + .unwrap_err() + .to_string(); + assert!(err.contains("disallowed components"), "{err}"); + } +} diff --git a/core/lib/contract_verifier/src/lib.rs b/core/lib/contract_verifier/src/lib.rs index 686bb0d7bdc3..e8bf05c72e81 100644 --- a/core/lib/contract_verifier/src/lib.rs +++ b/core/lib/contract_verifier/src/lib.rs @@ -22,7 +22,7 @@ use zksync_types::{ use zksync_utils::bytecode::{prepare_evm_bytecode, BytecodeMarker}; use crate::{ - compilers::{Solc, ZkSolc, ZkVyper}, + compilers::{Solc, VyperInput, ZkSolc}, error::ContractVerifierError, metrics::API_CONTRACT_VERIFIER_METRICS, resolver::{CompilerResolver, EnvCompilerResolver}, @@ -47,7 +47,6 @@ struct ZkCompilerVersions { #[derive(Debug)] enum VersionedCompiler { Solc(String), - #[allow(dead_code)] // TODO (EVM-864): add vyper support Vyper(String), ZkSolc(ZkCompilerVersions), ZkVyper(ZkCompilerVersions), @@ -292,7 +291,7 @@ impl ContractVerifier { ) -> Result { let zkvyper = self.compiler_resolver.resolve_zkvyper(version).await?; tracing::debug!(?zkvyper, ?version, "resolved compiler"); - let input = ZkVyper::build_input(req)?; + let input = VyperInput::new(req)?; time::timeout(self.compilation_timeout, zkvyper.compile(input)) .await .map_err(|_| ContractVerifierError::CompilationTimeout)? @@ -312,6 +311,20 @@ impl ContractVerifier { .map_err(|_| ContractVerifierError::CompilationTimeout)? } + async fn compile_vyper( + &self, + version: &str, + req: VerificationIncomingRequest, + ) -> Result { + let vyper = self.compiler_resolver.resolve_vyper(version).await?; + tracing::debug!(?vyper, ?req.compiler_versions, "resolved compiler"); + let input = VyperInput::new(req)?; + + time::timeout(self.compilation_timeout, vyper.compile(input)) + .await + .map_err(|_| ContractVerifierError::CompilationTimeout)? + } + #[tracing::instrument(level = "debug", skip_all)] async fn compile( &self, @@ -340,11 +353,7 @@ impl ContractVerifier { match &compiler { VersionedCompiler::Solc(version) => self.compile_solc(version, req).await, - VersionedCompiler::Vyper(_) => { - // TODO (EVM-864): add vyper support - let err = anyhow::anyhow!("vyper toolchain is not yet supported for EVM contracts"); - return Err(err.into()); - } + VersionedCompiler::Vyper(version) => self.compile_vyper(version, req).await, VersionedCompiler::ZkSolc(version) => self.compile_zksolc(version, req).await, VersionedCompiler::ZkVyper(version) => self.compile_zkvyper(version, req).await, } diff --git a/core/lib/contract_verifier/src/resolver.rs b/core/lib/contract_verifier/src/resolver.rs index 34a70b759797..018da12a152a 100644 --- a/core/lib/contract_verifier/src/resolver.rs +++ b/core/lib/contract_verifier/src/resolver.rs @@ -10,7 +10,7 @@ use zksync_types::contract_verification_api::CompilationArtifacts; use zksync_utils::env::Workspace; use crate::{ - compilers::{Solc, SolcInput, ZkSolc, ZkSolcInput, ZkVyper, ZkVyperInput}, + compilers::{Solc, SolcInput, Vyper, VyperInput, ZkSolc, ZkSolcInput, ZkVyper}, error::ContractVerifierError, ZkCompilerVersions, }; @@ -115,11 +115,17 @@ pub(crate) trait CompilerResolver: fmt::Debug + Send + Sync { version: &ZkCompilerVersions, ) -> Result>, ContractVerifierError>; + /// Resolves a `vyper` compiler. + async fn resolve_vyper( + &self, + version: &str, + ) -> Result>, ContractVerifierError>; + /// Resolves a `zkvyper` compiler. async fn resolve_zkvyper( &self, version: &ZkCompilerVersions, - ) -> Result>, ContractVerifierError>; + ) -> Result>, ContractVerifierError>; } /// Encapsulates a one-off compilation process. @@ -218,10 +224,20 @@ impl CompilerResolver for EnvCompilerResolver { ))) } + async fn resolve_vyper( + &self, + version: &str, + ) -> Result>, ContractVerifierError> { + let vyper_path = CompilerType::Vyper + .bin_path(&self.home_dir, version) + .await?; + Ok(Box::new(Vyper::new(vyper_path))) + } + async fn resolve_zkvyper( &self, version: &ZkCompilerVersions, - ) -> Result>, ContractVerifierError> { + ) -> Result>, ContractVerifierError> { let zkvyper_path = CompilerType::ZkVyper .bin_path(&self.home_dir, &version.zk) .await?; diff --git a/core/lib/contract_verifier/src/tests/mod.rs b/core/lib/contract_verifier/src/tests/mod.rs index 15951e578ff0..2aad39a12e0e 100644 --- a/core/lib/contract_verifier/src/tests/mod.rs +++ b/core/lib/contract_verifier/src/tests/mod.rs @@ -20,7 +20,7 @@ use zksync_vm_interface::{tracer::ValidationTraces, TransactionExecutionMetrics, use super::*; use crate::{ - compilers::{SolcInput, ZkSolcInput, ZkVyperInput}, + compilers::{SolcInput, VyperInput, ZkSolcInput}, resolver::{Compiler, SupportedCompilerVersions}, }; @@ -53,6 +53,39 @@ const COUNTER_CONTRACT_WITH_CONSTRUCTOR: &str = r#" } } "#; +const COUNTER_CONTRACT_WITH_INTERFACE: &str = r#" + interface ICounter { + function increment(uint256 x) external; + } + + contract Counter is ICounter { + uint256 value; + + function increment(uint256 x) external override { + value += x; + } + } +"#; +const COUNTER_VYPER_CONTRACT: &str = r#" +#pragma version ^0.3.10 + +value: uint256 + +@external +def increment(x: uint256): + self.value += x +"#; +const EMPTY_YUL_CONTRACT: &str = r#" +object "Empty" { + code { + mstore(0, 0) + return(0, 32) + } + object "Empty_deployed" { + code { } + } +} +"#; #[derive(Debug, Clone, Copy)] enum TestContract { @@ -122,7 +155,7 @@ async fn mock_evm_deployment( calldata.extend_from_slice(ðabi::encode(constructor_args)); let deployment = Execute { contract_address: None, - calldata, // FIXME: check + calldata, value: 0.into(), factory_deps: vec![], }; @@ -295,10 +328,17 @@ impl CompilerResolver for MockCompilerResolver { Ok(Box::new(self.clone())) } + async fn resolve_vyper( + &self, + _version: &str, + ) -> Result>, ContractVerifierError> { + unreachable!("not tested") + } + async fn resolve_zkvyper( &self, _version: &ZkCompilerVersions, - ) -> Result>, ContractVerifierError> { + ) -> Result>, ContractVerifierError> { unreachable!("not tested") } } @@ -443,10 +483,32 @@ async fn assert_request_success( .unwrap() .expect("no verification info"); assert_eq!(verification_info.artifacts.bytecode, *expected_bytecode); - assert_eq!(verification_info.artifacts.abi, counter_contract_abi()); + assert_eq!( + without_internal_types(verification_info.artifacts.abi.clone()), + without_internal_types(counter_contract_abi()) + ); verification_info } +fn without_internal_types(mut abi: serde_json::Value) -> serde_json::Value { + let items = abi.as_array_mut().unwrap(); + for item in items { + if let Some(inputs) = item.get_mut("inputs") { + let inputs = inputs.as_array_mut().unwrap(); + for input in inputs { + input.as_object_mut().unwrap().remove("internalType"); + } + } + if let Some(outputs) = item.get_mut("outputs") { + let outputs = outputs.as_array_mut().unwrap(); + for output in outputs { + output.as_object_mut().unwrap().remove("internalType"); + } + } + } + abi +} + #[test_casing(2, TestContract::ALL)] #[tokio::test] async fn verifying_evm_bytecode(contract: TestContract) { diff --git a/core/lib/contract_verifier/src/tests/real.rs b/core/lib/contract_verifier/src/tests/real.rs index a7113044b405..4282e6de4ef8 100644 --- a/core/lib/contract_verifier/src/tests/real.rs +++ b/core/lib/contract_verifier/src/tests/real.rs @@ -4,14 +4,27 @@ use std::{env, sync::Arc, time::Duration}; +use assert_matches::assert_matches; use zksync_utils::bytecode::validate_bytecode; use super::*; +#[derive(Debug, Clone, Copy)] +enum Toolchain { + Solidity, + Vyper, +} + +impl Toolchain { + const ALL: [Self; 2] = [Self::Solidity, Self::Vyper]; +} + #[derive(Debug, Clone)] struct TestCompilerVersions { solc: String, zksolc: String, + vyper: String, + zkvyper: String, } impl TestCompilerVersions { @@ -23,6 +36,8 @@ impl TestCompilerVersions { Some(Self { solc, zksolc: versions.zksolc.pop()?, + vyper: versions.vyper.pop()?, + zkvyper: versions.zkvyper.pop()?, }) } @@ -42,6 +57,23 @@ impl TestCompilerVersions { }, } } + + fn zkvyper(self) -> ZkCompilerVersions { + ZkCompilerVersions { + base: self.vyper, + zk: self.zkvyper, + } + } + + fn vyper_for_api(self, bytecode_kind: BytecodeMarker) -> CompilerVersions { + CompilerVersions::Vyper { + compiler_vyper_version: self.vyper, + compiler_zkvyper_version: match bytecode_kind { + BytecodeMarker::Evm => None, + BytecodeMarker::EraVm => Some(self.zkvyper), + }, + } + } } async fn checked_env_resolver() -> Option<(EnvCompilerResolver, TestCompilerVersions)> { @@ -76,18 +108,23 @@ macro_rules! real_resolver { }; } +#[test_casing(2, [false, true])] #[tokio::test] -async fn using_real_compiler() { +async fn using_real_zksolc(specify_contract_file: bool) { let (compiler_resolver, supported_compilers) = real_resolver!(); let compiler = compiler_resolver .resolve_zksolc(&supported_compilers.clone().zksolc()) .await .unwrap(); - let req = VerificationIncomingRequest { + let mut req = VerificationIncomingRequest { compiler_versions: supported_compilers.solc_for_api(BytecodeMarker::EraVm), ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT) }; + if specify_contract_file { + set_multi_file_solc_input(&mut req); + } + let input = ZkSolc::build_input(req).unwrap(); let output = compiler.compile(input).await.unwrap(); @@ -95,19 +132,43 @@ async fn using_real_compiler() { assert_eq!(output.abi, counter_contract_abi()); } +fn set_multi_file_solc_input(req: &mut VerificationIncomingRequest) { + let input = serde_json::json!({ + "language": "Solidity", + "sources": { + "contracts/test.sol": { + "content": COUNTER_CONTRACT, + }, + }, + "settings": { + "optimizer": { "enabled": true }, + }, + }); + let serde_json::Value::Object(input) = input else { + unreachable!(); + }; + req.source_code_data = SourceCodeData::StandardJsonInput(input); + req.contract_name = "contracts/test.sol:Counter".to_owned(); +} + +#[test_casing(2, [false, true])] #[tokio::test] -async fn using_standalone_solc() { +async fn using_standalone_solc(specify_contract_file: bool) { let (compiler_resolver, supported_compilers) = real_resolver!(); let version = &supported_compilers.solc; let compiler = compiler_resolver.resolve_solc(version).await.unwrap(); - let req = VerificationIncomingRequest { + let mut req = VerificationIncomingRequest { compiler_versions: CompilerVersions::Solc { compiler_solc_version: version.clone(), compiler_zksolc_version: None, }, ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT) }; + if specify_contract_file { + set_multi_file_solc_input(&mut req); + } + let input = Solc::build_input(req).unwrap(); let output = compiler.compile(input).await.unwrap(); @@ -115,18 +176,271 @@ async fn using_standalone_solc() { assert_eq!(output.abi, counter_contract_abi()); } -#[test_casing(2, BYTECODE_KINDS)] +#[test_casing(2, [false, true])] #[tokio::test] -async fn using_real_compiler_in_verifier(bytecode_kind: BytecodeMarker) { +async fn using_zksolc_with_abstract_contract(specify_contract_file: bool) { let (compiler_resolver, supported_compilers) = real_resolver!(); + let compiler = compiler_resolver + .resolve_zksolc(&supported_compilers.clone().zksolc()) + .await + .unwrap(); + let (source_code_data, contract_name) = if specify_contract_file { + let input = serde_json::json!({ + "language": "Solidity", + "sources": { + "contracts/test.sol": { + "content": COUNTER_CONTRACT_WITH_INTERFACE, + }, + }, + "settings": { + "optimizer": { "enabled": true }, + }, + }); + let serde_json::Value::Object(input) = input else { + unreachable!(); + }; + ( + SourceCodeData::StandardJsonInput(input), + "contracts/test.sol:ICounter", + ) + } else { + ( + SourceCodeData::SolSingleFile(COUNTER_CONTRACT_WITH_INTERFACE.to_owned()), + "ICounter", + ) + }; + let req = VerificationIncomingRequest { - compiler_versions: supported_compilers.clone().solc_for_api(bytecode_kind), - ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT) + contract_address: Address::repeat_byte(1), + compiler_versions: supported_compilers.solc_for_api(BytecodeMarker::EraVm), + optimization_used: true, + optimizer_mode: None, + constructor_arguments: Default::default(), + is_system: false, + source_code_data, + contract_name: contract_name.to_owned(), + force_evmla: false, + }; + + let input = ZkSolc::build_input(req).unwrap(); + let err = compiler.compile(input).await.unwrap_err(); + assert_matches!( + err, + ContractVerifierError::AbstractContract(name) if name == "ICounter" + ); +} + +fn test_yul_request(compiler_versions: CompilerVersions) -> VerificationIncomingRequest { + VerificationIncomingRequest { + contract_address: Default::default(), + source_code_data: SourceCodeData::YulSingleFile(EMPTY_YUL_CONTRACT.to_owned()), + contract_name: "Empty".to_owned(), + compiler_versions, + optimization_used: true, + optimizer_mode: None, + constructor_arguments: Default::default(), + is_system: false, + force_evmla: false, + } +} + +#[tokio::test] +async fn compiling_yul_with_zksolc() { + let (compiler_resolver, supported_compilers) = real_resolver!(); + + let version = supported_compilers.clone().zksolc(); + let compiler = compiler_resolver.resolve_zksolc(&version).await.unwrap(); + let req = test_yul_request(supported_compilers.solc_for_api(BytecodeMarker::EraVm)); + let input = ZkSolc::build_input(req).unwrap(); + let output = compiler.compile(input).await.unwrap(); + + assert!(!output.bytecode.is_empty()); + assert!(output.deployed_bytecode.is_none()); + assert_eq!(output.abi, serde_json::json!([])); +} + +#[tokio::test] +async fn compiling_standalone_yul() { + let (compiler_resolver, supported_compilers) = real_resolver!(); + + let version = &supported_compilers.solc; + let compiler = compiler_resolver.resolve_solc(version).await.unwrap(); + let req = test_yul_request(CompilerVersions::Solc { + compiler_solc_version: version.clone(), + compiler_zksolc_version: None, + }); + let input = Solc::build_input(req).unwrap(); + let output = compiler.compile(input).await.unwrap(); + + assert!(!output.bytecode.is_empty()); + assert_ne!(output.deployed_bytecode.unwrap(), output.bytecode); + assert_eq!(output.abi, serde_json::json!([])); +} + +fn test_vyper_request( + filename: &str, + contract_name: &str, + supported_compilers: TestCompilerVersions, + bytecode_kind: BytecodeMarker, +) -> VerificationIncomingRequest { + VerificationIncomingRequest { + contract_address: Address::repeat_byte(1), + source_code_data: SourceCodeData::VyperMultiFile(HashMap::from([( + filename.to_owned(), + COUNTER_VYPER_CONTRACT.to_owned(), + )])), + contract_name: contract_name.to_owned(), + compiler_versions: supported_compilers.vyper_for_api(bytecode_kind), + optimization_used: true, + optimizer_mode: None, + constructor_arguments: Default::default(), + is_system: false, + force_evmla: false, + } +} + +#[test_casing(2, [false, true])] +#[tokio::test] +async fn using_real_zkvyper(specify_contract_file: bool) { + let (compiler_resolver, supported_compilers) = real_resolver!(); + + let compiler = compiler_resolver + .resolve_zkvyper(&supported_compilers.clone().zkvyper()) + .await + .unwrap(); + let (filename, contract_name) = if specify_contract_file { + ("contracts/Counter.vy", "contracts/Counter.vy:Counter") + } else { + ("Counter", "Counter") + }; + let req = test_vyper_request( + filename, + contract_name, + supported_compilers, + BytecodeMarker::EraVm, + ); + let input = VyperInput::new(req).unwrap(); + let output = compiler.compile(input).await.unwrap(); + + validate_bytecode(&output.bytecode).unwrap(); + assert_eq!(output.abi, without_internal_types(counter_contract_abi())); +} + +#[test_casing(2, [false, true])] +#[tokio::test] +async fn using_standalone_vyper(specify_contract_file: bool) { + let (compiler_resolver, supported_compilers) = real_resolver!(); + + let version = &supported_compilers.vyper; + let compiler = compiler_resolver.resolve_vyper(version).await.unwrap(); + let (filename, contract_name) = if specify_contract_file { + ("contracts/Counter.vy", "contracts/Counter.vy:Counter") + } else { + ("Counter.vy", "Counter") + }; + let req = test_vyper_request( + filename, + contract_name, + supported_compilers, + BytecodeMarker::Evm, + ); + let input = VyperInput::new(req).unwrap(); + let output = compiler.compile(input).await.unwrap(); + + assert!(output.deployed_bytecode.is_some()); + assert_eq!(output.abi, without_internal_types(counter_contract_abi())); +} + +#[tokio::test] +async fn using_standalone_vyper_without_optimization() { + let (compiler_resolver, supported_compilers) = real_resolver!(); + + let version = &supported_compilers.vyper; + let compiler = compiler_resolver.resolve_vyper(version).await.unwrap(); + let mut req = test_vyper_request( + "counter.vy", + "counter", + supported_compilers, + BytecodeMarker::Evm, + ); + req.optimization_used = false; + let input = VyperInput::new(req).unwrap(); + let output = compiler.compile(input).await.unwrap(); + + assert!(output.deployed_bytecode.is_some()); + assert_eq!(output.abi, without_internal_types(counter_contract_abi())); +} + +#[tokio::test] +async fn using_standalone_vyper_with_code_size_optimization() { + let (compiler_resolver, supported_compilers) = real_resolver!(); + + let version = &supported_compilers.vyper; + let compiler = compiler_resolver.resolve_vyper(version).await.unwrap(); + let mut req = test_vyper_request( + "counter.vy", + "counter", + supported_compilers, + BytecodeMarker::Evm, + ); + req.optimization_used = true; + req.optimizer_mode = Some("codesize".to_owned()); + let input = VyperInput::new(req).unwrap(); + let output = compiler.compile(input).await.unwrap(); + + assert!(output.deployed_bytecode.is_some()); + assert_eq!(output.abi, without_internal_types(counter_contract_abi())); +} + +#[tokio::test] +async fn using_standalone_vyper_with_bogus_optimization() { + let (compiler_resolver, supported_compilers) = real_resolver!(); + + let version = &supported_compilers.vyper; + let compiler = compiler_resolver.resolve_vyper(version).await.unwrap(); + let mut req = test_vyper_request( + "counter.vy", + "counter", + supported_compilers, + BytecodeMarker::Evm, + ); + req.optimization_used = true; + req.optimizer_mode = Some("???".to_owned()); + let input = VyperInput::new(req).unwrap(); + let err = compiler.compile(input).await.unwrap_err(); + + let ContractVerifierError::CompilationError(serde_json::Value::Array(errors)) = err else { + panic!("unexpected error: {err:?}"); + }; + let has_opt_level_error = errors + .iter() + .any(|err| err.as_str().unwrap().contains("optimization level")); + assert!(has_opt_level_error, "{errors:?}"); +} + +#[test_casing(4, Product((BYTECODE_KINDS, Toolchain::ALL)))] +#[tokio::test] +async fn using_real_compiler_in_verifier(bytecode_kind: BytecodeMarker, toolchain: Toolchain) { + let (compiler_resolver, supported_compilers) = real_resolver!(); + + let req = match toolchain { + Toolchain::Solidity => VerificationIncomingRequest { + compiler_versions: supported_compilers.clone().solc_for_api(bytecode_kind), + ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT) + }, + Toolchain::Vyper => VerificationIncomingRequest { + compiler_versions: supported_compilers.clone().vyper_for_api(bytecode_kind), + source_code_data: SourceCodeData::VyperMultiFile(HashMap::from([( + "Counter.vy".to_owned(), + COUNTER_VYPER_CONTRACT.to_owned(), + )])), + ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT) + }, }; let address = Address::repeat_byte(1); - let output = match bytecode_kind { - BytecodeMarker::EraVm => { + let output = match (bytecode_kind, toolchain) { + (BytecodeMarker::EraVm, Toolchain::Solidity) => { let compiler = compiler_resolver .resolve_zksolc(&supported_compilers.zksolc()) .await @@ -134,12 +448,26 @@ async fn using_real_compiler_in_verifier(bytecode_kind: BytecodeMarker) { let input = ZkSolc::build_input(req.clone()).unwrap(); compiler.compile(input).await.unwrap() } - BytecodeMarker::Evm => { + (BytecodeMarker::Evm, Toolchain::Solidity) => { let solc_version = &supported_compilers.solc; let compiler = compiler_resolver.resolve_solc(solc_version).await.unwrap(); let input = Solc::build_input(req.clone()).unwrap(); compiler.compile(input).await.unwrap() } + (_, Toolchain::Vyper) => { + let compiler = match bytecode_kind { + BytecodeMarker::EraVm => compiler_resolver + .resolve_zkvyper(&supported_compilers.zkvyper()) + .await + .unwrap(), + BytecodeMarker::Evm => compiler_resolver + .resolve_vyper(&supported_compilers.vyper) + .await + .unwrap(), + }; + let input = VyperInput::new(req.clone()).unwrap(); + compiler.compile(input).await.unwrap() + } }; let pool = ConnectionPool::test_pool().await; diff --git a/core/lib/types/src/contract_verification_api.rs b/core/lib/types/src/contract_verification_api.rs index 21e511549beb..cca5ae5a83a0 100644 --- a/core/lib/types/src/contract_verification_api.rs +++ b/core/lib/types/src/contract_verification_api.rs @@ -137,6 +137,8 @@ pub struct VerificationIncomingRequest { #[serde(flatten)] pub compiler_versions: CompilerVersions, pub optimization_used: bool, + /// Optimization mode used for the contract. Semantics depends on the compiler used; e.g., for `vyper`, + /// allowed values are `gas` (default), `codesize` or `none`. pub optimizer_mode: Option, #[serde(default)] pub constructor_arguments: Bytes,