From 719455cf42114ce40f3826a3b69e0eafd9b0a538 Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Mon, 4 Nov 2024 10:03:16 +0200 Subject: [PATCH] refactor(contract-verifier): Brush up contract verifier (#3189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ Brushes up contract verifier code. ## Why ❔ Would make it easier to support EVM emulation in the contract verifier. ## 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`. --- .github/workflows/ci-core-reusable.yml | 4 + Cargo.lock | 9 +- Cargo.toml | 1 - core/bin/contract-verifier/src/main.rs | 104 +-- .../config/src/configs/contract_verifier.rs | 7 - core/lib/config/src/testonly.rs | 3 - core/lib/contract_verifier/Cargo.toml | 6 +- core/lib/contract_verifier/src/error.rs | 12 +- core/lib/contract_verifier/src/lib.rs | 633 +++++++++--------- core/lib/contract_verifier/src/metrics.rs | 1 + core/lib/contract_verifier/src/resolver.rs | 220 ++++++ core/lib/contract_verifier/src/tests/mod.rs | 395 +++++++++++ core/lib/contract_verifier/src/tests/real.rs | 141 ++++ .../lib/contract_verifier/src/zksolc_utils.rs | 203 ++++-- .../contract_verifier/src/zkvyper_utils.rs | 90 ++- core/lib/env_config/src/contract_verifier.rs | 7 - .../protobuf_config/src/contract_verifier.rs | 10 - .../src/proto/config/contract_verifier.proto | 7 +- core/lib/prover_interface/Cargo.toml | 2 +- core/lib/prover_interface/src/inputs.rs | 2 +- core/node/consensus/src/storage/testonly.rs | 3 +- .../state_keeper/src/executor/tests/tester.rs | 5 +- core/node/test_utils/Cargo.toml | 3 +- core/node/test_utils/src/lib.rs | 33 +- core/tests/ts-integration/src/env.ts | 2 +- etc/env/base/contract_verifier.toml | 3 - etc/env/file_based/general.yaml | 3 - prover/Cargo.lock | 2 +- .../zkstack/src/commands/explorer/init.rs | 9 +- 29 files changed, 1344 insertions(+), 576 deletions(-) create mode 100644 core/lib/contract_verifier/src/resolver.rs create mode 100644 core/lib/contract_verifier/src/tests/mod.rs create mode 100644 core/lib/contract_verifier/src/tests/real.rs diff --git a/.github/workflows/ci-core-reusable.yml b/.github/workflows/ci-core-reusable.yml index c245e7341d0..da3e2d5abb5 100644 --- a/.github/workflows/ci-core-reusable.yml +++ b/.github/workflows/ci-core-reusable.yml @@ -34,6 +34,7 @@ jobs: echo "SCCACHE_GCS_SERVICE_ACCOUNT=gha-ci-runners@matterlabs-infra.iam.gserviceaccount.com" >> .env echo "SCCACHE_GCS_RW_MODE=READ_WRITE" >> .env echo "RUSTC_WRAPPER=sccache" >> .env + echo RUN_CONTRACT_VERIFICATION_TEST=true >> .env # TODO: Remove when we after upgrade of hardhat-plugins - name: pre-download compilers @@ -73,6 +74,9 @@ jobs: - name: Contracts unit tests run: ci_run yarn l1-contracts test + - name: Download compilers for contract verifier tests + run: ci_run zkstack contract-verifier init --zksolc-version=v1.5.3 --zkvyper-version=v1.5.4 --solc-version=0.8.26 --vyper-version=v0.3.10 --era-vm-solc-version=0.8.26-1.0.1 --only --chain era + - name: Rust unit tests run: | ci_run zkstack dev test rust diff --git a/Cargo.lock b/Cargo.lock index 8af4e90323c..158ed796f8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10828,7 +10828,6 @@ dependencies = [ "chrono", "ethabi", "hex", - "lazy_static", "regex", "semver 1.0.23", "serde", @@ -10838,12 +10837,13 @@ dependencies = [ "tokio", "tracing", "vise", - "zksync_config", "zksync_contracts", "zksync_dal", + "zksync_node_test_utils", "zksync_queued_job_processor", "zksync_types", "zksync_utils", + "zksync_vm_interface", ] [[package]] @@ -11733,11 +11733,10 @@ dependencies = [ "zksync_contracts", "zksync_dal", "zksync_merkle_tree", - "zksync_multivm", - "zksync_node_genesis", "zksync_system_constants", "zksync_types", "zksync_utils", + "zksync_vm_interface", ] [[package]] @@ -11874,9 +11873,9 @@ dependencies = [ "serde_with", "strum", "tokio", - "zksync_multivm", "zksync_object_store", "zksync_types", + "zksync_vm_interface", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cdbc0d107f1..1faac5e1641 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,7 +135,6 @@ hyper = "1.3" insta = "1.29.0" itertools = "0.10" jsonrpsee = { version = "0.23", default-features = false } -lazy_static = "1.4" leb128 = "0.2.5" lru = { version = "0.12.1", default-features = false } mini-moka = "0.10.0" diff --git a/core/bin/contract-verifier/src/main.rs b/core/bin/contract-verifier/src/main.rs index a8162de13e9..6929f8bfe04 100644 --- a/core/bin/contract-verifier/src/main.rs +++ b/core/bin/contract-verifier/src/main.rs @@ -7,105 +7,11 @@ use tokio::sync::watch; use zksync_config::configs::PrometheusConfig; use zksync_contract_verifier_lib::ContractVerifier; use zksync_core_leftovers::temp_config_store::{load_database_secrets, load_general_config}; -use zksync_dal::{ConnectionPool, Core, CoreDal}; +use zksync_dal::{ConnectionPool, Core}; use zksync_queued_job_processor::JobProcessor; -use zksync_utils::{env::Workspace, wait_for_tasks::ManagedTasks}; +use zksync_utils::wait_for_tasks::ManagedTasks; use zksync_vlog::prometheus::PrometheusExporterConfig; -async fn update_compiler_versions(connection_pool: &ConnectionPool) { - let mut storage = connection_pool.connection().await.unwrap(); - let mut transaction = storage.start_transaction().await.unwrap(); - - let zksync_home = Workspace::locate().core(); - - let zksolc_path = zksync_home.join("etc/zksolc-bin/"); - let zksolc_versions: Vec = std::fs::read_dir(zksolc_path) - .unwrap() - .filter_map(|file| { - let file = file.unwrap(); - let Ok(file_type) = file.file_type() else { - return None; - }; - if file_type.is_dir() { - file.file_name().into_string().ok() - } else { - None - } - }) - .collect(); - transaction - .contract_verification_dal() - .set_zksolc_versions(zksolc_versions) - .await - .unwrap(); - - let solc_path = zksync_home.join("etc/solc-bin/"); - let solc_versions: Vec = std::fs::read_dir(solc_path) - .unwrap() - .filter_map(|file| { - let file = file.unwrap(); - let Ok(file_type) = file.file_type() else { - return None; - }; - if file_type.is_dir() { - file.file_name().into_string().ok() - } else { - None - } - }) - .collect(); - transaction - .contract_verification_dal() - .set_solc_versions(solc_versions) - .await - .unwrap(); - - let zkvyper_path = zksync_home.join("etc/zkvyper-bin/"); - let zkvyper_versions: Vec = std::fs::read_dir(zkvyper_path) - .unwrap() - .filter_map(|file| { - let file = file.unwrap(); - let Ok(file_type) = file.file_type() else { - return None; - }; - if file_type.is_dir() { - file.file_name().into_string().ok() - } else { - None - } - }) - .collect(); - transaction - .contract_verification_dal() - .set_zkvyper_versions(zkvyper_versions) - .await - .unwrap(); - - let vyper_path = zksync_home.join("etc/vyper-bin/"); - let vyper_versions: Vec = std::fs::read_dir(vyper_path) - .unwrap() - .filter_map(|file| { - let file = file.unwrap(); - let Ok(file_type) = file.file_type() else { - return None; - }; - if file_type.is_dir() { - file.file_name().into_string().ok() - } else { - None - } - }) - .collect(); - - transaction - .contract_verification_dal() - .set_vyper_versions(vyper_versions) - .await - .unwrap(); - - transaction.commit().await.unwrap(); -} - #[derive(StructOpt)] #[structopt(name = "ZKsync contract code verifier", author = "Matter Labs")] struct Opt { @@ -160,9 +66,9 @@ async fn main() -> anyhow::Result<()> { .expect("Error setting Ctrl+C handler"); } - update_compiler_versions(&pool).await; - - let contract_verifier = ContractVerifier::new(verifier_config, pool); + let contract_verifier = ContractVerifier::new(verifier_config.compilation_timeout(), pool) + .await + .context("failed initializing contract verifier")?; let tasks = vec![ // TODO PLA-335: Leftovers after the prover DB split. // The prover connection pool is not used by the contract verifier, but we need to pass it diff --git a/core/lib/config/src/configs/contract_verifier.rs b/core/lib/config/src/configs/contract_verifier.rs index 0016e1255de..1dac0b17227 100644 --- a/core/lib/config/src/configs/contract_verifier.rs +++ b/core/lib/config/src/configs/contract_verifier.rs @@ -9,13 +9,9 @@ use serde::Deserialize; pub struct ContractVerifierConfig { /// Max time of a single compilation (in s). pub compilation_timeout: u64, - /// Interval between polling db for verification requests (in ms). - pub polling_interval: Option, /// Port to which the Prometheus exporter server is listening. pub prometheus_port: u16, - pub threads_per_server: Option, pub port: u16, - pub url: String, } impl ContractVerifierConfig { @@ -23,9 +19,6 @@ impl ContractVerifierConfig { Duration::from_secs(self.compilation_timeout) } - pub fn polling_interval(&self) -> Duration { - Duration::from_millis(self.polling_interval.unwrap_or(1000)) - } pub fn bind_addr(&self) -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), self.port) } diff --git a/core/lib/config/src/testonly.rs b/core/lib/config/src/testonly.rs index 72df871d7ce..93d502cc4e8 100644 --- a/core/lib/config/src/testonly.rs +++ b/core/lib/config/src/testonly.rs @@ -241,11 +241,8 @@ impl Distribution for EncodeDist { fn sample(&self, rng: &mut R) -> configs::ContractVerifierConfig { configs::ContractVerifierConfig { compilation_timeout: self.sample(rng), - polling_interval: self.sample(rng), prometheus_port: self.sample(rng), - threads_per_server: self.sample(rng), port: self.sample(rng), - url: self.sample(rng), } } } diff --git a/core/lib/contract_verifier/Cargo.toml b/core/lib/contract_verifier/Cargo.toml index 580982c9a70..3b2ab33294e 100644 --- a/core/lib/contract_verifier/Cargo.toml +++ b/core/lib/contract_verifier/Cargo.toml @@ -13,7 +13,6 @@ categories.workspace = true [dependencies] zksync_types.workspace = true zksync_dal.workspace = true -zksync_config.workspace = true zksync_contracts.workspace = true zksync_queued_job_processor.workspace = true zksync_utils.workspace = true @@ -27,8 +26,11 @@ ethabi.workspace = true vise.workspace = true hex.workspace = true serde = { workspace = true, features = ["derive"] } -lazy_static.workspace = true tempfile.workspace = true regex.workspace = true tracing.workspace = true semver.workspace = true + +[dev-dependencies] +zksync_node_test_utils.workspace = true +zksync_vm_interface.workspace = true diff --git a/core/lib/contract_verifier/src/error.rs b/core/lib/contract_verifier/src/error.rs index c66756d1f12..e55d2499c93 100644 --- a/core/lib/contract_verifier/src/error.rs +++ b/core/lib/contract_verifier/src/error.rs @@ -1,7 +1,9 @@ -#[derive(Debug, Clone, thiserror::Error)] +use zksync_dal::DalError; + +#[derive(Debug, thiserror::Error)] pub enum ContractVerifierError { #[error("Internal error")] - InternalError, + Internal(#[from] anyhow::Error), #[error("Deployed bytecode is not equal to generated one from given source")] BytecodeMismatch, #[error("Constructor arguments are not correct")] @@ -23,3 +25,9 @@ pub enum ContractVerifierError { #[error("Failed to deserialize standard JSON input")] FailedToDeserializeInput, } + +impl From for ContractVerifierError { + fn from(err: DalError) -> Self { + Self::Internal(err.generalize()) + } +} diff --git a/core/lib/contract_verifier/src/lib.rs b/core/lib/contract_verifier/src/lib.rs index c8d9b89d834..f5face9f8a5 100644 --- a/core/lib/contract_verifier/src/lib.rs +++ b/core/lib/contract_verifier/src/lib.rs @@ -1,91 +1,172 @@ +//! Contract verifier able to verify contracts created with `zksolc` or `zkvyper` toolchains. + use std::{ collections::HashMap, - path::{Path, PathBuf}, + fmt, + sync::Arc, time::{Duration, Instant}, }; use anyhow::Context as _; use chrono::Utc; use ethabi::{Contract, Token}; -use lazy_static::lazy_static; -use regex::Regex; use tokio::time; -use zksync_config::ContractVerifierConfig; -use zksync_dal::{Connection, ConnectionPool, Core, CoreDal}; +use zksync_dal::{ConnectionPool, Core, CoreDal}; use zksync_queued_job_processor::{async_trait, JobProcessor}; use zksync_types::{ contract_verification_api::{ CompilationArtifacts, CompilerType, DeployContractCalldata, SourceCodeData, - VerificationInfo, VerificationRequest, + VerificationIncomingRequest, VerificationInfo, VerificationRequest, }, Address, }; -use zksync_utils::env::Workspace; use crate::{ error::ContractVerifierError, metrics::API_CONTRACT_VERIFIER_METRICS, - zksolc_utils::{Optimizer, Settings, Source, StandardJson, ZkSolc, ZkSolcInput, ZkSolcOutput}, - zkvyper_utils::{ZkVyper, ZkVyperInput}, + resolver::{CompilerResolver, EnvCompilerResolver}, + zksolc_utils::{Optimizer, Settings, Source, StandardJson, ZkSolcInput}, + zkvyper_utils::ZkVyperInput, }; pub mod error; mod metrics; +mod resolver; +#[cfg(test)] +mod tests; mod zksolc_utils; mod zkvyper_utils; -lazy_static! { - static ref DEPLOYER_CONTRACT: Contract = zksync_contracts::deployer_contract(); -} - -fn home_path() -> PathBuf { - Workspace::locate().core() -} - -#[derive(Debug)] enum ConstructorArgs { Check(Vec), Ignore, } -#[derive(Debug)] +impl fmt::Debug for ConstructorArgs { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Check(args) => write!(formatter, "0x{}", hex::encode(args)), + Self::Ignore => formatter.write_str("(ignored)"), + } + } +} + +#[derive(Debug, Clone)] pub struct ContractVerifier { - config: ContractVerifierConfig, + compilation_timeout: Duration, + contract_deployer: Contract, connection_pool: ConnectionPool, + compiler_resolver: Arc, } impl ContractVerifier { - pub fn new(config: ContractVerifierConfig, connection_pool: ConnectionPool) -> Self { - Self { - config, + /// Creates a new verifier instance. + pub async fn new( + compilation_timeout: Duration, + connection_pool: ConnectionPool, + ) -> anyhow::Result { + Self::with_resolver( + compilation_timeout, connection_pool, + Arc::::default(), + ) + .await + } + + async fn with_resolver( + compilation_timeout: Duration, + connection_pool: ConnectionPool, + compiler_resolver: Arc, + ) -> anyhow::Result { + let this = Self { + compilation_timeout, + contract_deployer: zksync_contracts::deployer_contract(), + connection_pool, + compiler_resolver, + }; + this.sync_compiler_versions().await?; + Ok(this) + } + + /// Synchronizes compiler versions. + #[tracing::instrument(level = "debug", skip_all)] + async fn sync_compiler_versions(&self) -> anyhow::Result<()> { + let supported_versions = self + .compiler_resolver + .supported_versions() + .await + .context("cannot get supported compilers")?; + if supported_versions.lacks_any_compiler() { + tracing::warn!( + ?supported_versions, + "contract verifier lacks support of at least one compiler entirely; it may be incorrectly set up" + ); } + tracing::info!( + ?supported_versions, + "persisting supported compiler versions" + ); + + let mut storage = self + .connection_pool + .connection_tagged("contract_verifier") + .await?; + let mut transaction = storage.start_transaction().await?; + transaction + .contract_verification_dal() + .set_zksolc_versions(supported_versions.zksolc) + .await?; + transaction + .contract_verification_dal() + .set_solc_versions(supported_versions.solc) + .await?; + transaction + .contract_verification_dal() + .set_zkvyper_versions(supported_versions.zkvyper) + .await?; + transaction + .contract_verification_dal() + .set_vyper_versions(supported_versions.vyper) + .await?; + transaction.commit().await?; + Ok(()) } + #[tracing::instrument( + level = "debug", + skip_all, + err, + fields(id = request.id, addr = ?request.req.contract_address) + )] async fn verify( - storage: &mut Connection<'_, Core>, + &self, mut request: VerificationRequest, - config: ContractVerifierConfig, ) -> Result { - let artifacts = Self::compile(request.clone(), config).await?; + let artifacts = self.compile(request.req.clone()).await?; // Bytecode should be present because it is checked when accepting request. + let mut storage = self + .connection_pool + .connection_tagged("contract_verifier") + .await?; let (deployed_bytecode, creation_tx_calldata) = storage .contract_verification_dal() - .get_contract_info_for_verification(request.req.contract_address).await - .unwrap() - .ok_or_else(|| { - tracing::warn!("Contract is missing in DB for already accepted verification request. Contract address: {:#?}", request.req.contract_address); - ContractVerifierError::InternalError + .get_contract_info_for_verification(request.req.contract_address) + .await? + .with_context(|| { + format!( + "Contract is missing in DB for already accepted verification request. Contract address: {:#?}", + request.req.contract_address + ) })?; - let constructor_args = Self::decode_constructor_arguments_from_calldata( - creation_tx_calldata, - request.req.contract_address, - ); + drop(storage); + + let constructor_args = + self.decode_constructor_args(creation_tx_calldata, request.req.contract_address)?; if artifacts.bytecode != deployed_bytecode { tracing::info!( - "Bytecode mismatch req {}, deployed: 0x{}, compiled 0x{}", + "Bytecode mismatch req {}, deployed: 0x{}, compiled: 0x{}", request.id, hex::encode(deployed_bytecode), hex::encode(artifacts.bytecode) @@ -95,7 +176,13 @@ impl ContractVerifier { match constructor_args { ConstructorArgs::Check(args) => { - if request.req.constructor_arguments.0 != args { + let provided_constructor_args = &request.req.constructor_arguments.0; + if *provided_constructor_args != args { + tracing::trace!( + "Constructor args mismatch, deployed: 0x{}, provided in request: 0x{}", + hex::encode(&args), + hex::encode(provided_constructor_args) + ); return Err(ContractVerifierError::IncorrectConstructorArguments); } } @@ -104,226 +191,94 @@ impl ContractVerifier { } } + let verified_at = Utc::now(); + tracing::trace!(%verified_at, "verified request"); Ok(VerificationInfo { request, artifacts, - verified_at: Utc::now(), + verified_at, }) } async fn compile_zksolc( - request: VerificationRequest, - config: ContractVerifierConfig, + &self, + 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)) = request.req.contract_name.rsplit_once(':') { - (file_name.to_string(), contract_name.to_string()) - } else { - ( - format!("{}.sol", request.req.contract_name), - request.req.contract_name.clone(), - ) - }; - let input = Self::build_zksolc_input(request.clone(), file_name.clone())?; - - let zksolc_path = Path::new(&home_path()) - .join("etc") - .join("zksolc-bin") - .join(request.req.compiler_versions.zk_compiler_version()) - .join("zksolc"); - if !zksolc_path.exists() { - return Err(ContractVerifierError::UnknownCompilerVersion( - "zksolc".to_string(), - request.req.compiler_versions.zk_compiler_version(), - )); - } - - let solc_path = Path::new(&home_path()) - .join("etc") - .join("solc-bin") - .join(request.req.compiler_versions.compiler_version()) - .join("solc"); - if !solc_path.exists() { - return Err(ContractVerifierError::UnknownCompilerVersion( - "solc".to_string(), - request.req.compiler_versions.compiler_version(), - )); - } - - let zksolc = ZkSolc::new( - zksolc_path, - solc_path, - request.req.compiler_versions.zk_compiler_version(), - ); - - let output = time::timeout(config.compilation_timeout(), zksolc.async_compile(input)) + let zksolc = self + .compiler_resolver + .resolve_solc(&req.compiler_versions) + .await?; + tracing::debug!(?zksolc, ?req.compiler_versions, "resolved compiler"); + let input = Self::build_zksolc_input(req)?; + + time::timeout(self.compilation_timeout, zksolc.compile(input)) .await - .map_err(|_| ContractVerifierError::CompilationTimeout)??; - - match output { - ZkSolcOutput::StandardJson(output) => { - 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.as_str()) - .cloned() - .ok_or(ContractVerifierError::MissingSource(file_name))?; - let contract = contracts - .get(&contract_name) - .cloned() - .ok_or(ContractVerifierError::MissingContract(contract_name))?; - let bytecode_str = contract["evm"]["bytecode"]["object"].as_str().ok_or( - ContractVerifierError::AbstractContract(request.req.contract_name), - )?; - let bytecode = hex::decode(bytecode_str).unwrap(); - let abi = contract["abi"].clone(); - if !abi.is_array() { - tracing::error!( - "zksolc returned unexpected value for ABI: {}", - serde_json::to_string_pretty(&abi).unwrap() - ); - return Err(ContractVerifierError::InternalError); - } - - Ok(CompilationArtifacts { bytecode, abi }) - } - ZkSolcOutput::YulSingleFile(output) => { - let re = Regex::new(r"Contract `.*` bytecode: 0x([\da-f]+)").unwrap(); - let cap = re.captures(&output).unwrap(); - let bytecode_str = cap.get(1).unwrap().as_str(); - let bytecode = hex::decode(bytecode_str).unwrap(); - Ok(CompilationArtifacts { - bytecode, - abi: serde_json::Value::Array(Vec::new()), - }) - } - } + .map_err(|_| ContractVerifierError::CompilationTimeout)? } async fn compile_zkvyper( - request: VerificationRequest, - config: ContractVerifierConfig, + &self, + 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((_file_name, contract_name)) = request.req.contract_name.rsplit_once(':') { - contract_name.to_string() - } else { - request.req.contract_name.clone() - }; - let input = Self::build_zkvyper_input(request.clone())?; - - let zkvyper_path = Path::new(&home_path()) - .join("etc") - .join("zkvyper-bin") - .join(request.req.compiler_versions.zk_compiler_version()) - .join("zkvyper"); - if !zkvyper_path.exists() { - return Err(ContractVerifierError::UnknownCompilerVersion( - "zkvyper".to_string(), - request.req.compiler_versions.zk_compiler_version(), - )); - } - - let vyper_path = Path::new(&home_path()) - .join("etc") - .join("vyper-bin") - .join(request.req.compiler_versions.compiler_version()) - .join("vyper"); - if !vyper_path.exists() { - return Err(ContractVerifierError::UnknownCompilerVersion( - "vyper".to_string(), - request.req.compiler_versions.compiler_version(), - )); - } - - let zkvyper = ZkVyper::new(zkvyper_path, vyper_path); - - let output = time::timeout(config.compilation_timeout(), zkvyper.async_compile(input)) + let zkvyper = self + .compiler_resolver + .resolve_vyper(&req.compiler_versions) + .await?; + tracing::debug!(?zkvyper, ?req.compiler_versions, "resolved compiler"); + let input = Self::build_zkvyper_input(req)?; + time::timeout(self.compilation_timeout, zkvyper.compile(input)) .await - .map_err(|_| ContractVerifierError::CompilationTimeout)??; - - let file_name = format!("{contract_name}.vy"); - let object = output - .as_object() - .cloned() - .ok_or(ContractVerifierError::InternalError)?; - for (path, artifact) in object { - let path = Path::new(&path); - if path.file_name().unwrap().to_str().unwrap() == file_name { - let bytecode_str = artifact["bytecode"] - .as_str() - .ok_or(ContractVerifierError::InternalError)?; - let bytecode_without_prefix = - bytecode_str.strip_prefix("0x").unwrap_or(bytecode_str); - let bytecode = hex::decode(bytecode_without_prefix).unwrap(); - return Ok(CompilationArtifacts { - abi: artifact["abi"].clone(), - bytecode, - }); - } - } - - Err(ContractVerifierError::MissingContract(contract_name)) + .map_err(|_| ContractVerifierError::CompilationTimeout)? } - pub async fn compile( - request: VerificationRequest, - config: ContractVerifierConfig, + #[tracing::instrument(level = "debug", skip_all)] + async fn compile( + &self, + req: VerificationIncomingRequest, ) -> Result { - match request.req.source_code_data.compiler_type() { - CompilerType::Solc => Self::compile_zksolc(request, config).await, - CompilerType::Vyper => Self::compile_zkvyper(request, config).await, + match req.source_code_data.compiler_type() { + CompilerType::Solc => self.compile_zksolc(req).await, + CompilerType::Vyper => self.compile_zkvyper(req).await, } } fn build_zksolc_input( - request: VerificationRequest, - file_name: String, + req: VerificationIncomingRequest, ) -> Result { - let default_output_selection = serde_json::json!( - { - "*": { - "*": [ "abi" ], - "": [ "abi" ] - } + // 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" ], + "": [ "abi" ] } - ); + }); - match request.req.source_code_data { + match req.source_code_data { SourceCodeData::SolSingleFile(source_code) => { let source = Source { content: source_code, }; - let sources: HashMap = - vec![(file_name, source)].into_iter().collect(); + let sources = HashMap::from([(file_name.clone(), source)]); let optimizer = Optimizer { - enabled: request.req.optimization_used, - mode: request.req.optimizer_mode.and_then(|s| s.chars().next()), + enabled: req.optimization_used, + mode: req.optimizer_mode.and_then(|s| s.chars().next()), }; let optimizer_value = serde_json::to_value(optimizer).unwrap(); let settings = Settings { output_selection: Some(default_output_selection), - is_system: request.req.is_system, - force_evmla: request.req.force_evmla, + is_system: req.is_system, + force_evmla: req.force_evmla, other: serde_json::Value::Object( vec![("optimizer".to_string(), optimizer_value)] .into_iter() @@ -331,11 +286,15 @@ impl ContractVerifier { ), }; - Ok(ZkSolcInput::StandardJson(StandardJson { - language: "Solidity".to_string(), - sources, - settings, - })) + Ok(ZkSolcInput::StandardJson { + input: StandardJson { + language: "Solidity".to_string(), + sources, + settings, + }, + contract_name, + file_name, + }) } SourceCodeData::StandardJsonInput(map) => { let mut compiler_input: StandardJson = @@ -343,121 +302,184 @@ impl ContractVerifier { .map_err(|_| ContractVerifierError::FailedToDeserializeInput)?; // Set default output selection even if it is different in request. compiler_input.settings.output_selection = Some(default_output_selection); - Ok(ZkSolcInput::StandardJson(compiler_input)) + Ok(ZkSolcInput::StandardJson { + input: compiler_input, + contract_name, + file_name, + }) } SourceCodeData::YulSingleFile(source_code) => Ok(ZkSolcInput::YulSingleFile { source_code, - is_system: request.req.is_system, + is_system: req.is_system, }), - _ => panic!("Unexpected SourceCode variant"), + other => unreachable!("Unexpected `SourceCodeData` variant: {other:?}"), } } fn build_zkvyper_input( - request: VerificationRequest, + req: VerificationIncomingRequest, ) -> Result { - let sources = match request.req.source_code_data { + // 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, - _ => panic!("Unexpected SourceCode variant"), + other => unreachable!("unexpected `SourceCodeData` variant: {other:?}"), }; Ok(ZkVyperInput { + contract_name, sources, - optimizer_mode: request.req.optimizer_mode, + optimizer_mode: req.optimizer_mode, }) } - fn decode_constructor_arguments_from_calldata( + /// All returned errors are internal. + #[tracing::instrument(level = "trace", skip_all, ret, err)] + fn decode_constructor_args( + &self, calldata: DeployContractCalldata, contract_address_to_verify: Address, - ) -> ConstructorArgs { + ) -> anyhow::Result { match calldata { DeployContractCalldata::Deploy(calldata) => { - let create = DEPLOYER_CONTRACT.function("create").unwrap(); - let create2 = DEPLOYER_CONTRACT.function("create2").unwrap(); - - let create_acc = DEPLOYER_CONTRACT.function("createAccount").unwrap(); - let create2_acc = DEPLOYER_CONTRACT.function("create2Account").unwrap(); - - let force_deploy = DEPLOYER_CONTRACT + anyhow::ensure!( + calldata.len() >= 4, + "calldata doesn't include Solidity function selector" + ); + + let contract_deployer = &self.contract_deployer; + let create = contract_deployer + .function("create") + .context("no `create` in contract deployer ABI")?; + let create2 = contract_deployer + .function("create2") + .context("no `create2` in contract deployer ABI")?; + let create_acc = contract_deployer + .function("createAccount") + .context("no `createAccount` in contract deployer ABI")?; + let create2_acc = contract_deployer + .function("create2Account") + .context("no `create2Account` in contract deployer ABI")?; + let force_deploy = contract_deployer .function("forceDeployOnAddresses") - .unwrap(); + .context("no `forceDeployOnAddresses` in contract deployer ABI")?; + + let (selector, token_data) = calldata.split_at(4); // It's assumed that `create` and `create2` methods have the same parameters // and the same for `createAccount` and `create2Account`. - match &calldata[0..4] { + Ok(match selector { selector if selector == create.short_signature() || selector == create2.short_signature() => { let tokens = create - .decode_input(&calldata[4..]) - .expect("Failed to decode input"); + .decode_input(token_data) + .context("failed to decode `create` / `create2` input")?; // Constructor arguments are in the third parameter. - ConstructorArgs::Check(tokens[2].clone().into_bytes().expect( - "The third parameter of `create/create2` should be of type `bytes`", - )) + ConstructorArgs::Check(tokens[2].clone().into_bytes().context( + "third parameter of `create/create2` should be of type `bytes`", + )?) } selector if selector == create_acc.short_signature() || selector == create2_acc.short_signature() => { let tokens = create - .decode_input(&calldata[4..]) - .expect("Failed to decode input"); + .decode_input(token_data) + .context("failed to decode `createAccount` / `create2Account` input")?; // Constructor arguments are in the third parameter. - ConstructorArgs::Check( - tokens[2].clone().into_bytes().expect( - "The third parameter of `createAccount/create2Account` should be of type `bytes`", - ), - ) + ConstructorArgs::Check(tokens[2].clone().into_bytes().context( + "third parameter of `createAccount/create2Account` should be of type `bytes`", + )?) } selector if selector == force_deploy.short_signature() => { - let tokens = force_deploy - .decode_input(&calldata[4..]) - .expect("Failed to decode input"); - let deployments = tokens[0].clone().into_array().unwrap(); - for deployment in deployments { - match deployment { - Token::Tuple(tokens) => { - let address = tokens[1].clone().into_address().unwrap(); - if address == contract_address_to_verify { - let call_constructor = - tokens[2].clone().into_bool().unwrap(); - return if call_constructor { - let input = tokens[4].clone().into_bytes().unwrap(); - ConstructorArgs::Check(input) - } else { - ConstructorArgs::Ignore - }; - } - } - _ => panic!("Expected `deployment` to be a tuple"), - } - } - panic!("Couldn't find force deployment for given address"); + Self::decode_force_deployment( + token_data, + force_deploy, + contract_address_to_verify, + ) + .context("failed decoding force deployment")? } _ => ConstructorArgs::Ignore, + }) + } + DeployContractCalldata::Ignore => Ok(ConstructorArgs::Ignore), + } + } + + fn decode_force_deployment( + token_data: &[u8], + force_deploy: ðabi::Function, + contract_address_to_verify: Address, + ) -> anyhow::Result { + let tokens = force_deploy + .decode_input(token_data) + .context("failed to decode `forceDeployOnAddresses` input")?; + let deployments = tokens[0] + .clone() + .into_array() + .context("first parameter of `forceDeployOnAddresses` is not an array")?; + for deployment in deployments { + match deployment { + Token::Tuple(tokens) => { + let address = tokens[1] + .clone() + .into_address() + .context("unexpected `address`")?; + if address == contract_address_to_verify { + let call_constructor = tokens[2] + .clone() + .into_bool() + .context("unexpected `call_constructor`")?; + return Ok(if call_constructor { + let input = tokens[4] + .clone() + .into_bytes() + .context("unexpected constructor input")?; + ConstructorArgs::Check(input) + } else { + ConstructorArgs::Ignore + }); + } } + _ => anyhow::bail!("expected `deployment` to be a tuple"), } - DeployContractCalldata::Ignore => ConstructorArgs::Ignore, } + anyhow::bail!("couldn't find force deployment for address {contract_address_to_verify:?}"); } + #[tracing::instrument(level = "debug", skip_all, err, fields(id = request_id))] async fn process_result( - storage: &mut Connection<'_, Core>, + &self, request_id: usize, verification_result: Result, - ) { + ) -> anyhow::Result<()> { + let mut storage = self + .connection_pool + .connection_tagged("contract_verifier") + .await?; match verification_result { Ok(info) => { storage .contract_verification_dal() .save_verification_info(info) - .await - .unwrap(); - tracing::info!("Successfully processed request with id = {}", request_id); + .await?; + tracing::info!("Successfully processed request with id = {request_id}"); } Err(error) => { - let error_message = error.to_string(); + let error_message = match &error { + ContractVerifierError::Internal(err) => { + // Do not expose the error externally, but log it. + tracing::warn!(request_id, "internal error processing request: {err}"); + "internal error".to_owned() + } + _ => error.to_string(), + }; let compilation_errors = match error { ContractVerifierError::CompilationError(compilation_errors) => { compilation_errors @@ -467,11 +489,11 @@ impl ContractVerifier { storage .contract_verification_dal() .save_verification_error(request_id, error_message, compilation_errors, None) - .await - .unwrap(); - tracing::info!("Request with id = {} was failed", request_id); + .await?; + tracing::info!("Request with id = {request_id} was failed"); } } + Ok(()) } } @@ -485,25 +507,29 @@ impl JobProcessor for ContractVerifier { const BACKOFF_MULTIPLIER: u64 = 1; async fn get_next_job(&self) -> anyhow::Result> { - let mut connection = self.connection_pool.connection().await.unwrap(); - - // Time overhead for all operations except for compilation. + /// Time overhead for all operations except for compilation. const TIME_OVERHEAD: Duration = Duration::from_secs(10); + let mut connection = self + .connection_pool + .connection_tagged("contract_verifier") + .await?; // Considering that jobs that reach compilation timeout will be executed in // `compilation_timeout` + `non_compilation_time_overhead` (which is significantly less than `compilation_timeout`), // we re-pick up jobs that are being executed for a bit more than `compilation_timeout`. let job = connection .contract_verification_dal() - .get_next_queued_verification_request(self.config.compilation_timeout() + TIME_OVERHEAD) - .await - .context("get_next_queued_verification_request()")?; - + .get_next_queued_verification_request(self.compilation_timeout + TIME_OVERHEAD) + .await?; Ok(job.map(|job| (job.id, job))) } async fn save_failure(&self, job_id: usize, _started_at: Instant, error: String) { - let mut connection = self.connection_pool.connection().await.unwrap(); + let mut connection = self + .connection_pool + .connection_tagged("contract_verifier") + .await + .unwrap(); connection .contract_verification_dal() @@ -524,16 +550,13 @@ impl JobProcessor for ContractVerifier { job: VerificationRequest, started_at: Instant, ) -> tokio::task::JoinHandle> { - let connection_pool = self.connection_pool.clone(); - let config = self.config.clone(); + let this = self.clone(); tokio::task::spawn(async move { tracing::info!("Started to process request with id = {}", job.id); - let mut connection = connection_pool.connection().await.unwrap(); - let job_id = job.id; - let verification_result = Self::verify(&mut connection, job, config).await; - Self::process_result(&mut connection, job_id, verification_result).await; + let verification_result = this.verify(job).await; + this.process_result(job_id, verification_result).await?; API_CONTRACT_VERIFIER_METRICS .request_processing_time diff --git a/core/lib/contract_verifier/src/metrics.rs b/core/lib/contract_verifier/src/metrics.rs index fd98f51cd56..1c6796cd7f3 100644 --- a/core/lib/contract_verifier/src/metrics.rs +++ b/core/lib/contract_verifier/src/metrics.rs @@ -5,6 +5,7 @@ use vise::{Buckets, Histogram, Metrics}; #[derive(Debug, Metrics)] #[metrics(prefix = "api_contract_verifier")] pub(crate) struct ApiContractVerifierMetrics { + /// Latency of processing a single request. #[metrics(buckets = Buckets::LATENCIES)] pub request_processing_time: Histogram, } diff --git a/core/lib/contract_verifier/src/resolver.rs b/core/lib/contract_verifier/src/resolver.rs new file mode 100644 index 00000000000..5e772900669 --- /dev/null +++ b/core/lib/contract_verifier/src/resolver.rs @@ -0,0 +1,220 @@ +use std::{fmt, path::PathBuf}; + +use anyhow::Context as _; +use tokio::fs; +use zksync_queued_job_processor::async_trait; +use zksync_types::contract_verification_api::{CompilationArtifacts, CompilerVersions}; +use zksync_utils::env::Workspace; + +use crate::{ + error::ContractVerifierError, + zksolc_utils::{ZkSolc, ZkSolcInput}, + zkvyper_utils::{ZkVyper, ZkVyperInput}, +}; + +/// Compiler versions supported by a [`CompilerResolver`]. +#[derive(Debug)] +pub(crate) struct SupportedCompilerVersions { + pub solc: Vec, + pub zksolc: Vec, + pub vyper: Vec, + pub zkvyper: Vec, +} + +impl SupportedCompilerVersions { + pub fn lacks_any_compiler(&self) -> bool { + self.solc.is_empty() + || self.zksolc.is_empty() + || self.vyper.is_empty() + || self.zkvyper.is_empty() + } +} + +#[derive(Debug, Clone)] +pub(crate) struct CompilerPaths { + /// Path to the base (non-zk) compiler. + pub base: PathBuf, + /// Path to the zk compiler. + pub zk: PathBuf, +} + +/// Encapsulates compiler paths resolution. +#[async_trait] +pub(crate) trait CompilerResolver: fmt::Debug + Send + Sync { + /// Returns compiler versions supported by this resolver. + /// + /// # Errors + /// + /// Returned errors are assumed to be fatal. + async fn supported_versions(&self) -> anyhow::Result; + + /// Resolves a `zksolc` compiler. + async fn resolve_solc( + &self, + versions: &CompilerVersions, + ) -> Result>, ContractVerifierError>; + + /// Resolves a `zkvyper` compiler. + async fn resolve_vyper( + &self, + versions: &CompilerVersions, + ) -> Result>, ContractVerifierError>; +} + +/// Encapsulates a one-off compilation process. +#[async_trait] +pub(crate) trait Compiler: Send + fmt::Debug { + /// Performs compilation. + async fn compile( + self: Box, + input: In, + ) -> Result; +} + +/// Default [`CompilerResolver`] using pre-downloaded compilers in the `/etc` subdirectories (relative to the workspace). +#[derive(Debug)] +pub(crate) struct EnvCompilerResolver { + home_dir: PathBuf, +} + +impl Default for EnvCompilerResolver { + fn default() -> Self { + Self { + home_dir: Workspace::locate().core(), + } + } +} + +impl EnvCompilerResolver { + async fn read_dir(&self, dir: &str) -> anyhow::Result> { + let mut dir_entries = fs::read_dir(self.home_dir.join(dir)) + .await + .context("failed reading dir")?; + let mut versions = vec![]; + while let Some(entry) = dir_entries.next_entry().await? { + let Ok(file_type) = entry.file_type().await else { + continue; + }; + if file_type.is_dir() { + if let Ok(name) = entry.file_name().into_string() { + versions.push(name); + } + } + } + Ok(versions) + } +} + +#[async_trait] +impl CompilerResolver for EnvCompilerResolver { + async fn supported_versions(&self) -> anyhow::Result { + Ok(SupportedCompilerVersions { + solc: self + .read_dir("etc/solc-bin") + .await + .context("failed reading solc dir")?, + zksolc: self + .read_dir("etc/zksolc-bin") + .await + .context("failed reading zksolc dir")?, + vyper: self + .read_dir("etc/vyper-bin") + .await + .context("failed reading vyper dir")?, + zkvyper: self + .read_dir("etc/zkvyper-bin") + .await + .context("failed reading zkvyper dir")?, + }) + } + + async fn resolve_solc( + &self, + versions: &CompilerVersions, + ) -> Result>, ContractVerifierError> { + let zksolc_version = versions.zk_compiler_version(); + let zksolc_path = self + .home_dir + .join("etc") + .join("zksolc-bin") + .join(&zksolc_version) + .join("zksolc"); + if !fs::try_exists(&zksolc_path) + .await + .context("failed accessing zksolc")? + { + return Err(ContractVerifierError::UnknownCompilerVersion( + "zksolc".to_owned(), + zksolc_version, + )); + } + + let solc_version = versions.compiler_version(); + let solc_path = self + .home_dir + .join("etc") + .join("solc-bin") + .join(&solc_version) + .join("solc"); + if !fs::try_exists(&solc_path) + .await + .context("failed accessing solc")? + { + return Err(ContractVerifierError::UnknownCompilerVersion( + "solc".to_owned(), + solc_version, + )); + } + + let compiler_paths = CompilerPaths { + base: solc_path, + zk: zksolc_path, + }; + Ok(Box::new(ZkSolc::new(compiler_paths, zksolc_version))) + } + + async fn resolve_vyper( + &self, + versions: &CompilerVersions, + ) -> Result>, ContractVerifierError> { + let zkvyper_version = versions.zk_compiler_version(); + let zkvyper_path = self + .home_dir + .join("etc") + .join("zkvyper-bin") + .join(&zkvyper_version) + .join("zkvyper"); + if !fs::try_exists(&zkvyper_path) + .await + .context("failed accessing zkvyper")? + { + return Err(ContractVerifierError::UnknownCompilerVersion( + "zkvyper".to_owned(), + zkvyper_version, + )); + } + + let vyper_version = versions.compiler_version(); + let vyper_path = self + .home_dir + .join("etc") + .join("vyper-bin") + .join(&vyper_version) + .join("vyper"); + if !fs::try_exists(&vyper_path) + .await + .context("failed accessing vyper")? + { + return Err(ContractVerifierError::UnknownCompilerVersion( + "vyper".to_owned(), + vyper_version, + )); + } + + let compiler_paths = CompilerPaths { + base: vyper_path, + zk: zkvyper_path, + }; + Ok(Box::new(ZkVyper::new(compiler_paths))) + } +} diff --git a/core/lib/contract_verifier/src/tests/mod.rs b/core/lib/contract_verifier/src/tests/mod.rs new file mode 100644 index 00000000000..ae7b23ebab8 --- /dev/null +++ b/core/lib/contract_verifier/src/tests/mod.rs @@ -0,0 +1,395 @@ +//! Tests for the contract verifier. + +use tokio::sync::watch; +use zksync_dal::Connection; +use zksync_node_test_utils::{create_l1_batch, create_l2_block}; +use zksync_types::{ + contract_verification_api::{CompilerVersions, VerificationIncomingRequest}, + get_code_key, get_known_code_key, + l2::L2Tx, + tx::IncludedTxLocation, + Execute, L1BatchNumber, L2BlockNumber, ProtocolVersion, StorageLog, CONTRACT_DEPLOYER_ADDRESS, + H256, +}; +use zksync_utils::{address_to_h256, bytecode::hash_bytecode}; +use zksync_vm_interface::{tracer::ValidationTraces, TransactionExecutionMetrics, VmEvent}; + +use super::*; +use crate::resolver::{Compiler, SupportedCompilerVersions}; + +mod real; + +const SOLC_VERSION: &str = "0.8.27"; +const ZKSOLC_VERSION: &str = "1.5.4"; + +async fn mock_deployment(storage: &mut Connection<'_, Core>, address: Address, bytecode: Vec) { + let bytecode_hash = hash_bytecode(&bytecode); + let logs = [ + StorageLog::new_write_log(get_code_key(&address), bytecode_hash), + StorageLog::new_write_log(get_known_code_key(&bytecode_hash), H256::from_low_u64_be(1)), + ]; + storage + .storage_logs_dal() + .append_storage_logs(L2BlockNumber(0), &logs) + .await + .unwrap(); + storage + .factory_deps_dal() + .insert_factory_deps( + L2BlockNumber(0), + &HashMap::from([(bytecode_hash, bytecode.clone())]), + ) + .await + .unwrap(); + + let mut deploy_tx = L2Tx { + execute: Execute::for_deploy(H256::zero(), bytecode, &[]), + common_data: Default::default(), + received_timestamp_ms: 0, + raw_bytes: Some(vec![0; 128].into()), + }; + deploy_tx.set_input(vec![0; 128], H256::repeat_byte(0x23)); + storage + .transactions_dal() + .insert_transaction_l2( + &deploy_tx, + TransactionExecutionMetrics::default(), + ValidationTraces::default(), + ) + .await + .unwrap(); + + let deployer_address = Address::repeat_byte(0xff); + let location = IncludedTxLocation { + tx_hash: deploy_tx.hash(), + tx_index_in_l2_block: 0, + tx_initiator_address: deployer_address, + }; + let deploy_event = VmEvent { + location: (L1BatchNumber(0), 0), + address: CONTRACT_DEPLOYER_ADDRESS, + indexed_topics: vec![ + VmEvent::DEPLOY_EVENT_SIGNATURE, + address_to_h256(&deployer_address), + bytecode_hash, + address_to_h256(&address), + ], + value: vec![], + }; + storage + .events_dal() + .save_events(L2BlockNumber(0), &[(location, vec![&deploy_event])]) + .await + .unwrap(); +} + +#[derive(Clone)] +struct MockCompilerResolver { + zksolc: Arc< + dyn Fn(ZkSolcInput) -> Result + Send + Sync, + >, +} + +impl fmt::Debug for MockCompilerResolver { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MockCompilerResolver") + .finish_non_exhaustive() + } +} + +impl MockCompilerResolver { + fn new(zksolc: impl Fn(ZkSolcInput) -> CompilationArtifacts + 'static + Send + Sync) -> Self { + Self { + zksolc: Arc::new(move |input| Ok(zksolc(input))), + } + } +} + +#[async_trait] +impl Compiler for MockCompilerResolver { + async fn compile( + self: Box, + input: ZkSolcInput, + ) -> Result { + (self.zksolc)(input) + } +} + +#[async_trait] +impl CompilerResolver for MockCompilerResolver { + async fn supported_versions(&self) -> anyhow::Result { + Ok(SupportedCompilerVersions { + solc: vec![SOLC_VERSION.to_owned()], + zksolc: vec![ZKSOLC_VERSION.to_owned()], + vyper: vec![], + zkvyper: vec![], + }) + } + + async fn resolve_solc( + &self, + versions: &CompilerVersions, + ) -> Result>, ContractVerifierError> { + if versions.compiler_version() != SOLC_VERSION { + return Err(ContractVerifierError::UnknownCompilerVersion( + "solc".to_owned(), + versions.compiler_version(), + )); + } + if versions.zk_compiler_version() != ZKSOLC_VERSION { + return Err(ContractVerifierError::UnknownCompilerVersion( + "zksolc".to_owned(), + versions.zk_compiler_version(), + )); + } + Ok(Box::new(self.clone())) + } + + async fn resolve_vyper( + &self, + _versions: &CompilerVersions, + ) -> Result>, ContractVerifierError> { + unreachable!("not tested") + } +} + +fn test_request(address: Address) -> VerificationIncomingRequest { + let contract_source = r#" + contract Counter { + uint256 value; + + function increment(uint256 x) external { + value += x; + } + } + "#; + VerificationIncomingRequest { + contract_address: address, + source_code_data: SourceCodeData::SolSingleFile(contract_source.into()), + contract_name: "Counter".to_owned(), + compiler_versions: CompilerVersions::Solc { + compiler_zksolc_version: ZKSOLC_VERSION.to_owned(), + compiler_solc_version: SOLC_VERSION.to_owned(), + }, + optimization_used: true, + optimizer_mode: None, + constructor_arguments: Default::default(), + is_system: false, + force_evmla: false, + } +} + +fn counter_contract_abi() -> serde_json::Value { + serde_json::json!([{ + "inputs": [{ + "internalType": "uint256", + "name": "x", + "type": "uint256", + }], + "name": "increment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }]) +} + +async fn prepare_storage(storage: &mut Connection<'_, Core>) { + // Storage must contain at least 1 block / batch for verifier-related queries to work correctly. + storage + .protocol_versions_dal() + .save_protocol_version_with_tx(&ProtocolVersion::default()) + .await + .unwrap(); + storage + .blocks_dal() + .insert_l2_block(&create_l2_block(0)) + .await + .unwrap(); + storage + .blocks_dal() + .insert_mock_l1_batch(&create_l1_batch(0)) + .await + .unwrap(); +} + +#[tokio::test] +async fn contract_verifier_basics() { + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + let expected_bytecode = vec![0_u8; 32]; + + prepare_storage(&mut storage).await; + let address = Address::repeat_byte(1); + mock_deployment(&mut storage, address, expected_bytecode.clone()).await; + let req = test_request(address); + let request_id = storage + .contract_verification_dal() + .add_contract_verification_request(req) + .await + .unwrap(); + + let mock_resolver = MockCompilerResolver::new(|input| { + let ZkSolcInput::StandardJson { input, .. } = &input else { + panic!("unexpected input"); + }; + assert_eq!(input.language, "Solidity"); + assert_eq!(input.sources.len(), 1); + let source = input.sources.values().next().unwrap(); + assert!(source.content.contains("contract Counter"), "{source:?}"); + + CompilationArtifacts { + bytecode: vec![0; 32], + abi: counter_contract_abi(), + } + }); + let verifier = ContractVerifier::with_resolver( + Duration::from_secs(60), + pool.clone(), + Arc::new(mock_resolver), + ) + .await + .unwrap(); + + // Check that the compiler versions are synced. + let solc_versions = storage + .contract_verification_dal() + .get_solc_versions() + .await + .unwrap(); + assert_eq!(solc_versions, [SOLC_VERSION]); + let zksolc_versions = storage + .contract_verification_dal() + .get_zksolc_versions() + .await + .unwrap(); + assert_eq!(zksolc_versions, [ZKSOLC_VERSION]); + + let (_stop_sender, stop_receiver) = watch::channel(false); + verifier.run(stop_receiver, Some(1)).await.unwrap(); + + assert_request_success(&mut storage, request_id, address, &expected_bytecode).await; +} + +async fn assert_request_success( + storage: &mut Connection<'_, Core>, + request_id: usize, + address: Address, + expected_bytecode: &[u8], +) { + let status = storage + .contract_verification_dal() + .get_verification_request_status(request_id) + .await + .unwrap() + .expect("no status"); + assert_eq!(status.error, None); + assert_eq!(status.compilation_errors, None); + assert_eq!(status.status, "successful"); + + let verification_info = storage + .contract_verification_dal() + .get_contract_verification_info(address) + .await + .unwrap() + .expect("no verification info"); + assert_eq!(verification_info.artifacts.bytecode, *expected_bytecode); + assert_eq!(verification_info.artifacts.abi, counter_contract_abi()); +} + +async fn checked_env_resolver() -> Option<(EnvCompilerResolver, SupportedCompilerVersions)> { + let compiler_resolver = EnvCompilerResolver::default(); + let supported_compilers = compiler_resolver.supported_versions().await.ok()?; + if supported_compilers.zksolc.is_empty() || supported_compilers.solc.is_empty() { + return None; + } + Some((compiler_resolver, supported_compilers)) +} + +#[tokio::test] +async fn bytecode_mismatch_error() { + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + + let address = Address::repeat_byte(1); + mock_deployment(&mut storage, address, vec![0xff; 32]).await; + let req = test_request(address); + let request_id = storage + .contract_verification_dal() + .add_contract_verification_request(req) + .await + .unwrap(); + + let mock_resolver = MockCompilerResolver::new(|_| CompilationArtifacts { + bytecode: vec![0; 32], + abi: counter_contract_abi(), + }); + let verifier = ContractVerifier::with_resolver( + Duration::from_secs(60), + pool.clone(), + Arc::new(mock_resolver), + ) + .await + .unwrap(); + + let (_stop_sender, stop_receiver) = watch::channel(false); + verifier.run(stop_receiver, Some(1)).await.unwrap(); + + let status = storage + .contract_verification_dal() + .get_verification_request_status(request_id) + .await + .unwrap() + .expect("no status"); + assert_eq!(status.status, "failed"); + assert!(status.compilation_errors.is_none(), "{status:?}"); + let error = status.error.unwrap(); + assert!(error.contains("bytecode"), "{error}"); +} + +#[tokio::test] +async fn no_compiler_version() { + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + + let address = Address::repeat_byte(1); + mock_deployment(&mut storage, address, vec![0xff; 32]).await; + let req = VerificationIncomingRequest { + compiler_versions: CompilerVersions::Solc { + compiler_zksolc_version: ZKSOLC_VERSION.to_owned(), + compiler_solc_version: "1.0.0".to_owned(), // a man can dream + }, + ..test_request(address) + }; + let request_id = storage + .contract_verification_dal() + .add_contract_verification_request(req) + .await + .unwrap(); + + let mock_resolver = + MockCompilerResolver::new(|_| unreachable!("should reject unknown solc version")); + let verifier = ContractVerifier::with_resolver( + Duration::from_secs(60), + pool.clone(), + Arc::new(mock_resolver), + ) + .await + .unwrap(); + + let (_stop_sender, stop_receiver) = watch::channel(false); + verifier.run(stop_receiver, Some(1)).await.unwrap(); + + let status = storage + .contract_verification_dal() + .get_verification_request_status(request_id) + .await + .unwrap() + .expect("no status"); + assert_eq!(status.status, "failed"); + assert!(status.compilation_errors.is_none(), "{status:?}"); + let error = status.error.unwrap(); + assert!(error.contains("solc version"), "{error}"); +} diff --git a/core/lib/contract_verifier/src/tests/real.rs b/core/lib/contract_verifier/src/tests/real.rs new file mode 100644 index 00000000000..b7acc753fd6 --- /dev/null +++ b/core/lib/contract_verifier/src/tests/real.rs @@ -0,0 +1,141 @@ +//! Tests using real compiler toolchains. Should be prepared by calling `zkstack contract-verifier init` +//! with at least one `solc` and `zksolc` version. If there are no compilers, the tests will be ignored +//! unless the `RUN_CONTRACT_VERIFICATION_TEST` env var is set to `true`, in which case the tests will fail. + +use std::{env, sync::Arc, time::Duration}; + +use zksync_utils::bytecode::validate_bytecode; + +use super::*; + +fn assert_no_compilers_expected() { + assert_ne!( + env::var("RUN_CONTRACT_VERIFICATION_TEST").ok().as_deref(), + Some("true"), + "Expected pre-installed compilers since `RUN_CONTRACT_VERIFICATION_TEST=true`, but they are not installed. \ + Use `zkstack contract-verifier init` to install compilers" + ); + println!("No compilers found, skipping the test"); +} + +#[tokio::test] +async fn using_real_compiler() { + let Some((compiler_resolver, supported_compilers)) = checked_env_resolver().await else { + assert_no_compilers_expected(); + return; + }; + + let versions = CompilerVersions::Solc { + compiler_zksolc_version: supported_compilers.zksolc[0].clone(), + compiler_solc_version: supported_compilers.solc[0].clone(), + }; + let compiler = compiler_resolver.resolve_solc(&versions).await.unwrap(); + let req = VerificationIncomingRequest { + compiler_versions: versions, + ..test_request(Address::repeat_byte(1)) + }; + let input = ContractVerifier::build_zksolc_input(req).unwrap(); + let output = compiler.compile(input).await.unwrap(); + + validate_bytecode(&output.bytecode).unwrap(); + assert_eq!(output.abi, counter_contract_abi()); +} + +#[tokio::test] +async fn using_real_compiler_in_verifier() { + let Some((compiler_resolver, supported_compilers)) = checked_env_resolver().await else { + assert_no_compilers_expected(); + return; + }; + + let versions = CompilerVersions::Solc { + compiler_zksolc_version: supported_compilers.zksolc[0].clone(), + compiler_solc_version: supported_compilers.solc[0].clone(), + }; + let address = Address::repeat_byte(1); + let compiler = compiler_resolver.resolve_solc(&versions).await.unwrap(); + let req = VerificationIncomingRequest { + compiler_versions: versions, + ..test_request(Address::repeat_byte(1)) + }; + let input = ContractVerifier::build_zksolc_input(req.clone()).unwrap(); + let output = compiler.compile(input).await.unwrap(); + + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + mock_deployment(&mut storage, address, output.bytecode.clone()).await; + let request_id = storage + .contract_verification_dal() + .add_contract_verification_request(req) + .await + .unwrap(); + + let verifier = ContractVerifier::with_resolver( + Duration::from_secs(60), + pool.clone(), + Arc::new(compiler_resolver), + ) + .await + .unwrap(); + + let (_stop_sender, stop_receiver) = watch::channel(false); + verifier.run(stop_receiver, Some(1)).await.unwrap(); + + assert_request_success(&mut storage, request_id, address, &output.bytecode).await; +} + +#[tokio::test] +async fn compilation_errors() { + let Some((compiler_resolver, supported_compilers)) = checked_env_resolver().await else { + assert_no_compilers_expected(); + return; + }; + + let versions = CompilerVersions::Solc { + compiler_zksolc_version: supported_compilers.zksolc[0].clone(), + compiler_solc_version: supported_compilers.solc[0].clone(), + }; + let address = Address::repeat_byte(1); + let req = VerificationIncomingRequest { + compiler_versions: versions, + source_code_data: SourceCodeData::SolSingleFile("contract ???".to_owned()), + ..test_request(Address::repeat_byte(1)) + }; + + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + mock_deployment(&mut storage, address, vec![0; 32]).await; + + let request_id = storage + .contract_verification_dal() + .add_contract_verification_request(req) + .await + .unwrap(); + + let verifier = ContractVerifier::with_resolver( + Duration::from_secs(60), + pool.clone(), + Arc::new(compiler_resolver), + ) + .await + .unwrap(); + + let (_stop_sender, stop_receiver) = watch::channel(false); + verifier.run(stop_receiver, Some(1)).await.unwrap(); + + let status = storage + .contract_verification_dal() + .get_verification_request_status(request_id) + .await + .unwrap() + .expect("no status"); + assert_eq!(status.status, "failed"); + let compilation_errors = status.compilation_errors.unwrap(); + assert!(!compilation_errors.is_empty()); + let has_parser_error = compilation_errors + .iter() + .any(|err| err.contains("ParserError") && err.contains("Counter.sol")); + assert!(has_parser_error, "{compilation_errors:?}"); +} diff --git a/core/lib/contract_verifier/src/zksolc_utils.rs b/core/lib/contract_verifier/src/zksolc_utils.rs index 08004632bce..0a3d84ab555 100644 --- a/core/lib/contract_verifier/src/zksolc_utils.rs +++ b/core/lib/contract_verifier/src/zksolc_utils.rs @@ -1,25 +1,31 @@ -use std::{collections::HashMap, io::Write, path::PathBuf, process::Stdio}; +use std::{collections::HashMap, io::Write, process::Stdio}; +use anyhow::Context as _; +use regex::Regex; use semver::Version; use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWriteExt; +use zksync_queued_job_processor::async_trait; +use zksync_types::contract_verification_api::CompilationArtifacts; -use crate::error::ContractVerifierError; +use crate::{ + error::ContractVerifierError, + resolver::{Compiler, CompilerPaths}, +}; #[derive(Debug)] pub enum ZkSolcInput { - StandardJson(StandardJson), + StandardJson { + input: StandardJson, + contract_name: String, + file_name: String, + }, YulSingleFile { source_code: String, is_system: bool, }, } -#[derive(Debug)] -pub enum ZkSolcOutput { - StandardJson(serde_json::Value), - YulSingleFile(String), -} - #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct StandardJson { @@ -75,35 +81,106 @@ impl Default for Optimizer { } } -pub struct ZkSolc { - zksolc_path: PathBuf, - solc_path: PathBuf, +#[derive(Debug)] +pub(crate) struct ZkSolc { + paths: CompilerPaths, zksolc_version: String, } impl ZkSolc { - pub fn new( - zksolc_path: impl Into, - solc_path: impl Into, - zksolc_version: String, - ) -> Self { + pub fn new(paths: CompilerPaths, zksolc_version: String) -> Self { ZkSolc { - zksolc_path: zksolc_path.into(), - solc_path: solc_path.into(), + paths, zksolc_version, } } - pub async fn async_compile( - &self, + fn parse_standard_json_output( + output: &serde_json::Value, + contract_name: String, + file_name: String, + ) -> Result { + 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 bytecode_str = contract["evm"]["bytecode"]["object"] + .as_str() + .ok_or(ContractVerifierError::AbstractContract(contract_name))?; + let bytecode = hex::decode(bytecode_str).unwrap(); + let abi = contract["abi"].clone(); + if !abi.is_array() { + let err = anyhow::anyhow!( + "zksolc returned unexpected value for ABI: {}", + serde_json::to_string_pretty(&abi).unwrap() + ); + return Err(err.into()); + } + + Ok(CompilationArtifacts { bytecode, abi }) + } + + fn parse_single_file_yul_output( + output: &str, + ) -> Result { + let re = Regex::new(r"Contract `.*` bytecode: 0x([\da-f]+)").unwrap(); + let cap = re + .captures(output) + .context("Yul output doesn't match regex")?; + let bytecode_str = cap.get(1).context("no matches in Yul output")?.as_str(); + let bytecode = hex::decode(bytecode_str).context("invalid Yul output bytecode")?; + Ok(CompilationArtifacts { + bytecode, + abi: serde_json::Value::Array(Vec::new()), + }) + } + + fn is_post_1_5_0(&self) -> bool { + // Special case + if &self.zksolc_version == "vm-1.5.0-a167aa3" { + false + } else if let Some(version) = self.zksolc_version.strip_prefix("v") { + if let Ok(semver) = Version::parse(version) { + let target = Version::new(1, 5, 0); + semver >= target + } else { + true + } + } else { + true + } + } +} + +#[async_trait] +impl Compiler for ZkSolc { + async fn compile( + self: Box, input: ZkSolcInput, - ) -> Result { - use tokio::io::AsyncWriteExt; - let mut command = tokio::process::Command::new(&self.zksolc_path); + ) -> Result { + let mut command = tokio::process::Command::new(&self.paths.zk); command.stdout(Stdio::piped()).stderr(Stdio::piped()); match &input { - ZkSolcInput::StandardJson(input) => { + ZkSolcInput::StandardJson { input, .. } => { if !self.is_post_1_5_0() { if input.settings.is_system { command.arg("--system-mode"); @@ -113,50 +190,51 @@ impl ZkSolc { } } - command.arg("--solc").arg(self.solc_path.to_str().unwrap()); + command.arg("--solc").arg(&self.paths.base); } ZkSolcInput::YulSingleFile { is_system, .. } => { if self.is_post_1_5_0() { if *is_system { command.arg("--enable-eravm-extensions"); } else { - command.arg("--solc").arg(self.solc_path.to_str().unwrap()); + command.arg("--solc").arg(&self.paths.base); } } else { if *is_system { command.arg("--system-mode"); } - command.arg("--solc").arg(self.solc_path.to_str().unwrap()); + command.arg("--solc").arg(&self.paths.base); } } } match input { - ZkSolcInput::StandardJson(input) => { + ZkSolcInput::StandardJson { + input, + contract_name, + file_name, + } => { let mut child = command .arg("--standard-json") .stdin(Stdio::piped()) .spawn() - .map_err(|_err| ContractVerifierError::InternalError)?; + .context("failed spawning zksolc")?; let stdin = child.stdin.as_mut().unwrap(); - let content = serde_json::to_vec(&input).unwrap(); + let content = serde_json::to_vec(&input) + .context("cannot encode standard JSON input for zksolc")?; stdin .write_all(&content) .await - .map_err(|_err| ContractVerifierError::InternalError)?; + .context("failed writing standard JSON to zksolc stdin")?; stdin .flush() .await - .map_err(|_err| ContractVerifierError::InternalError)?; + .context("failed flushing standard JSON to zksolc")?; - let output = child - .wait_with_output() - .await - .map_err(|_err| ContractVerifierError::InternalError)?; + let output = child.wait_with_output().await.context("zksolc failed")?; if output.status.success() { - Ok(ZkSolcOutput::StandardJson( - serde_json::from_slice(&output.stdout) - .expect("Compiler output must be valid JSON"), - )) + let output = serde_json::from_slice(&output.stdout) + .context("zksolc output is not valid JSON")?; + Self::parse_standard_json_output(&output, contract_name, file_name) } else { Err(ContractVerifierError::CompilerError( "zksolc".to_string(), @@ -170,9 +248,9 @@ impl ZkSolc { .suffix(".yul") .rand_bytes(0) .tempfile() - .map_err(|_err| ContractVerifierError::InternalError)?; + .context("cannot create temporary Yul file")?; file.write_all(source_code.as_bytes()) - .map_err(|_err| ContractVerifierError::InternalError)?; + .context("failed writing Yul file")?; let child = command .arg(file.path().to_str().unwrap()) .arg("--optimization") @@ -180,15 +258,12 @@ impl ZkSolc { .arg("--yul") .arg("--bin") .spawn() - .map_err(|_err| ContractVerifierError::InternalError)?; - let output = child - .wait_with_output() - .await - .map_err(|_err| ContractVerifierError::InternalError)?; + .context("failed spawning zksolc")?; + let output = child.wait_with_output().await.context("zksolc failed")?; if output.status.success() { - Ok(ZkSolcOutput::YulSingleFile( - String::from_utf8(output.stdout).expect("Couldn't parse string"), - )) + let output = + String::from_utf8(output.stdout).context("zksolc output is not UTF-8")?; + Self::parse_single_file_yul_output(&output) } else { Err(ContractVerifierError::CompilerError( "zksolc".to_string(), @@ -198,32 +273,22 @@ impl ZkSolc { } } } - - pub fn is_post_1_5_0(&self) -> bool { - // Special case - if &self.zksolc_version == "vm-1.5.0-a167aa3" { - false - } else if let Some(version) = self.zksolc_version.strip_prefix("v") { - if let Ok(semver) = Version::parse(version) { - let target = Version::new(1, 5, 0); - semver >= target - } else { - true - } - } else { - true - } - } } #[cfg(test)] mod tests { - use crate::zksolc_utils::ZkSolc; + use std::path::PathBuf; + + use crate::{resolver::CompilerPaths, zksolc_utils::ZkSolc}; #[test] fn check_is_post_1_5_0() { // Special case. - let mut zksolc = ZkSolc::new(".", ".", "vm-1.5.0-a167aa3".to_string()); + let compiler_paths = CompilerPaths { + base: PathBuf::default(), + zk: PathBuf::default(), + }; + let mut zksolc = ZkSolc::new(compiler_paths, "vm-1.5.0-a167aa3".to_string()); assert!(!zksolc.is_post_1_5_0(), "vm-1.5.0-a167aa3"); zksolc.zksolc_version = "v1.5.0".to_string(); diff --git a/core/lib/contract_verifier/src/zkvyper_utils.rs b/core/lib/contract_verifier/src/zkvyper_utils.rs index c597f78d458..bc2cd7e996c 100644 --- a/core/lib/contract_verifier/src/zkvyper_utils.rs +++ b/core/lib/contract_verifier/src/zkvyper_utils.rs @@ -1,68 +1,100 @@ -use std::{collections::HashMap, fs::File, io::Write, path::PathBuf, process::Stdio}; +use std::{collections::HashMap, fs::File, io::Write, path::Path, process::Stdio}; -use crate::error::ContractVerifierError; +use anyhow::Context as _; +use zksync_queued_job_processor::async_trait; +use zksync_types::contract_verification_api::CompilationArtifacts; + +use crate::{ + error::ContractVerifierError, + resolver::{Compiler, CompilerPaths}, +}; #[derive(Debug)] -pub struct ZkVyperInput { +pub(crate) struct ZkVyperInput { + pub contract_name: String, pub sources: HashMap, pub optimizer_mode: Option, } -pub struct ZkVyper { - zkvyper_path: PathBuf, - vyper_path: PathBuf, +#[derive(Debug)] +pub(crate) struct ZkVyper { + paths: CompilerPaths, } impl ZkVyper { - pub fn new(zkvyper_path: impl Into, vyper_path: impl Into) -> Self { - ZkVyper { - zkvyper_path: zkvyper_path.into(), - vyper_path: vyper_path.into(), + pub fn new(paths: CompilerPaths) -> Self { + Self { paths } + } + + fn parse_output( + output: &serde_json::Value, + contract_name: String, + ) -> Result { + let file_name = format!("{contract_name}.vy"); + let object = output + .as_object() + .context("Vyper output is not an object")?; + for (path, artifact) in object { + let path = Path::new(&path); + if path.file_name().unwrap().to_str().unwrap() == file_name { + let bytecode_str = artifact["bytecode"] + .as_str() + .context("bytecode is not a string")?; + let bytecode_without_prefix = + bytecode_str.strip_prefix("0x").unwrap_or(bytecode_str); + let bytecode = + hex::decode(bytecode_without_prefix).context("failed decoding bytecode")?; + return Ok(CompilationArtifacts { + abi: artifact["abi"].clone(), + bytecode, + }); + } } + Err(ContractVerifierError::MissingContract(contract_name)) } +} - pub async fn async_compile( - &self, +#[async_trait] +impl Compiler for ZkVyper { + async fn compile( + self: Box, input: ZkVyperInput, - ) -> Result { - let mut command = tokio::process::Command::new(&self.zkvyper_path); + ) -> Result { + let mut command = tokio::process::Command::new(&self.paths.zk); if let Some(o) = input.optimizer_mode.as_ref() { command.arg("-O").arg(o); } command .arg("--vyper") - .arg(self.vyper_path.to_str().unwrap()) + .arg(&self.paths.base) .arg("-f") .arg("combined_json") .stdout(Stdio::piped()) .stderr(Stdio::piped()); - let temp_dir = tempfile::tempdir().map_err(|_err| ContractVerifierError::InternalError)?; + 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); + let path = temp_dir.path().join(&name); if let Some(prefix) = path.parent() { std::fs::create_dir_all(prefix) - .map_err(|_err| ContractVerifierError::InternalError)?; + .with_context(|| format!("failed creating parent dir for `{name}`"))?; } - let mut file = - File::create(&path).map_err(|_err| ContractVerifierError::InternalError)?; + let mut file = File::create(&path) + .with_context(|| format!("failed creating file for `{name}`"))?; file.write_all(content.as_bytes()) - .map_err(|_err| ContractVerifierError::InternalError)?; + .with_context(|| format!("failed writing to `{name}`"))?; command.arg(path.into_os_string()); } - let child = command - .spawn() - .map_err(|_err| ContractVerifierError::InternalError)?; - let output = child - .wait_with_output() - .await - .map_err(|_err| ContractVerifierError::InternalError)?; + let child = command.spawn().context("cannot spawn zkvyper")?; + let output = child.wait_with_output().await.context("zkvyper failed")?; if output.status.success() { - Ok(serde_json::from_slice(&output.stdout).expect("Compiler output must be valid JSON")) + let output = serde_json::from_slice(&output.stdout) + .context("zkvyper output is not valid JSON")?; + Self::parse_output(&output, input.contract_name) } else { Err(ContractVerifierError::CompilerError( "zkvyper".to_string(), diff --git a/core/lib/env_config/src/contract_verifier.rs b/core/lib/env_config/src/contract_verifier.rs index 3079a8daa9c..484e0634158 100644 --- a/core/lib/env_config/src/contract_verifier.rs +++ b/core/lib/env_config/src/contract_verifier.rs @@ -18,11 +18,8 @@ mod tests { fn expected_config() -> ContractVerifierConfig { ContractVerifierConfig { compilation_timeout: 30, - polling_interval: Some(1000), prometheus_port: 3314, - threads_per_server: Some(128), port: 3070, - url: "127.0.0.1:3070".to_string(), } } @@ -31,12 +28,8 @@ mod tests { let mut lock = MUTEX.lock(); let config = r#" CONTRACT_VERIFIER_COMPILATION_TIMEOUT=30 - CONTRACT_VERIFIER_POLLING_INTERVAL=1000 CONTRACT_VERIFIER_PROMETHEUS_PORT=3314 CONTRACT_VERIFIER_PORT=3070 - CONTRACT_VERIFIER_URL=127.0.0.1:3070 - CONTRACT_VERIFIER_THREADS_PER_SERVER=128 - "#; lock.set_env(config); diff --git a/core/lib/protobuf_config/src/contract_verifier.rs b/core/lib/protobuf_config/src/contract_verifier.rs index e0b0517ea0f..3fb7cfe0bdf 100644 --- a/core/lib/protobuf_config/src/contract_verifier.rs +++ b/core/lib/protobuf_config/src/contract_verifier.rs @@ -10,29 +10,19 @@ impl ProtoRepr for proto::ContractVerifier { Ok(Self::Type { compilation_timeout: *required(&self.compilation_timeout) .context("compilation_timeout")?, - polling_interval: self.polling_interval, prometheus_port: required(&self.prometheus_port) .and_then(|x| Ok((*x).try_into()?)) .context("prometheus_port")?, - url: required(&self.url).cloned().context("url")?, port: required(&self.port) .and_then(|x| (*x).try_into().context("overflow")) .context("port")?, - threads_per_server: self - .threads_per_server - .map(|a| a.try_into()) - .transpose() - .context("threads_per_server")?, }) } fn build(this: &Self::Type) -> Self { Self { port: Some(this.port as u32), - url: Some(this.url.clone()), compilation_timeout: Some(this.compilation_timeout), - polling_interval: this.polling_interval, - threads_per_server: this.threads_per_server.map(|a| a as u32), prometheus_port: Some(this.prometheus_port.into()), } } diff --git a/core/lib/protobuf_config/src/proto/config/contract_verifier.proto b/core/lib/protobuf_config/src/proto/config/contract_verifier.proto index 31b1d3ed2ec..8493274c691 100644 --- a/core/lib/protobuf_config/src/proto/config/contract_verifier.proto +++ b/core/lib/protobuf_config/src/proto/config/contract_verifier.proto @@ -4,9 +4,10 @@ package zksync.config.contract_verifier; message ContractVerifier{ optional uint32 port = 1; // required; u16 - optional string url = 2; // required optional uint64 compilation_timeout = 3; - optional uint64 polling_interval = 4; - optional uint32 threads_per_server = 5; optional uint32 prometheus_port = 6; + + reserved 2; reserved "url"; + reserved 4; reserved "polling_interval"; + reserved 5; reserved "threads_per_server"; } diff --git a/core/lib/prover_interface/Cargo.toml b/core/lib/prover_interface/Cargo.toml index 889b80b4fbe..50671fb3acb 100644 --- a/core/lib/prover_interface/Cargo.toml +++ b/core/lib/prover_interface/Cargo.toml @@ -11,7 +11,7 @@ keywords.workspace = true categories.workspace = true [dependencies] -zksync_multivm.workspace = true +zksync_vm_interface.workspace = true zksync_object_store.workspace = true zksync_types.workspace = true diff --git a/core/lib/prover_interface/src/inputs.rs b/core/lib/prover_interface/src/inputs.rs index cfc1d4a0d55..48a839dc921 100644 --- a/core/lib/prover_interface/src/inputs.rs +++ b/core/lib/prover_interface/src/inputs.rs @@ -2,12 +2,12 @@ use std::{collections::HashMap, convert::TryInto, fmt::Debug}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, Bytes}; -use zksync_multivm::interface::{L1BatchEnv, SystemEnv}; use zksync_object_store::{_reexports::BoxedError, serialize_using_bincode, Bucket, StoredObject}; use zksync_types::{ basic_fri_types::Eip4844Blobs, block::L2BlockExecutionData, commitment::PubdataParams, witness_block_state::WitnessStorageState, L1BatchNumber, ProtocolVersionId, H256, U256, }; +use zksync_vm_interface::{L1BatchEnv, SystemEnv}; const HASH_LEN: usize = H256::len_bytes(); diff --git a/core/node/consensus/src/storage/testonly.rs b/core/node/consensus/src/storage/testonly.rs index 2aed011d23c..0f29e246826 100644 --- a/core/node/consensus/src/storage/testonly.rs +++ b/core/node/consensus/src/storage/testonly.rs @@ -73,7 +73,8 @@ impl ConnectionPool { L1BatchNumber(23), L2BlockNumber(87), vec![], - mock_genesis_params(protocol_version), + &BaseSystemContracts::load_from_disk(), + protocol_version, )) .await } diff --git a/core/node/state_keeper/src/executor/tests/tester.rs b/core/node/state_keeper/src/executor/tests/tester.rs index a02aeb47caf..800bf398938 100644 --- a/core/node/state_keeper/src/executor/tests/tester.rs +++ b/core/node/state_keeper/src/executor/tests/tester.rs @@ -19,7 +19,7 @@ use zksync_multivm::{ utils::StorageWritesDeduplicator, vm_latest::constants::INITIAL_STORAGE_WRITE_PUBDATA_BYTES, }; -use zksync_node_genesis::{create_genesis_l1_batch, GenesisParams}; +use zksync_node_genesis::create_genesis_l1_batch; use zksync_node_test_utils::{recover, Snapshot}; use zksync_state::{OwnedStorage, ReadStorageFactory, RocksdbStorageOptions}; use zksync_test_account::{Account, DeployContractsTx, TxType}; @@ -602,7 +602,8 @@ impl StorageSnapshot { L1BatchNumber(1), self.l2_block_number, snapshot_logs, - GenesisParams::mock(), + &BASE_SYSTEM_CONTRACTS, + ProtocolVersionId::latest(), ); let mut snapshot = recover(&mut storage, snapshot).await; snapshot.l2_block_hash = self.l2_block_hash; diff --git a/core/node/test_utils/Cargo.toml b/core/node/test_utils/Cargo.toml index af60008df57..6df100c51a7 100644 --- a/core/node/test_utils/Cargo.toml +++ b/core/node/test_utils/Cargo.toml @@ -11,11 +11,10 @@ keywords.workspace = true categories.workspace = true [dependencies] -zksync_multivm.workspace = true zksync_types.workspace = true zksync_dal.workspace = true zksync_contracts.workspace = true zksync_merkle_tree.workspace = true zksync_system_constants.workspace = true +zksync_vm_interface.workspace = true zksync_utils.workspace = true -zksync_node_genesis.workspace = true diff --git a/core/node/test_utils/src/lib.rs b/core/node/test_utils/src/lib.rs index 86ce3aadd9a..2b446fff12c 100644 --- a/core/node/test_utils/src/lib.rs +++ b/core/node/test_utils/src/lib.rs @@ -2,14 +2,9 @@ use std::collections::HashMap; -use zksync_contracts::BaseSystemContractsHashes; +use zksync_contracts::{BaseSystemContracts, BaseSystemContractsHashes}; use zksync_dal::{Connection, Core, CoreDal}; use zksync_merkle_tree::{domain::ZkSyncTree, TreeInstruction}; -use zksync_multivm::{ - interface::{TransactionExecutionResult, TxExecutionStatus, VmExecutionMetrics}, - utils::get_max_gas_per_pubdata_byte, -}; -use zksync_node_genesis::GenesisParams; use zksync_system_constants::{get_intrinsic_constants, ZKPORTER_IS_AVAILABLE}; use zksync_types::{ block::{L1BatchHeader, L2BlockHeader}, @@ -27,6 +22,10 @@ use zksync_types::{ Address, K256PrivateKey, L1BatchNumber, L2BlockNumber, L2ChainId, Nonce, ProtocolVersion, ProtocolVersionId, StorageLog, H256, U256, }; +use zksync_vm_interface::{TransactionExecutionResult, TxExecutionStatus, VmExecutionMetrics}; + +/// Value for recent protocol versions. +const MAX_GAS_PER_PUBDATA_BYTE: u64 = 50_000; /// Creates an L2 block header with the specified number and deterministic contents. pub fn create_l2_block(number: u32) -> L2BlockHeader { @@ -39,7 +38,7 @@ pub fn create_l2_block(number: u32) -> L2BlockHeader { base_fee_per_gas: 100, batch_fee_input: BatchFeeInput::l1_pegged(100, 100), fee_account_address: Address::zero(), - gas_per_pubdata_limit: get_max_gas_per_pubdata_byte(ProtocolVersionId::latest().into()), + gas_per_pubdata_limit: MAX_GAS_PER_PUBDATA_BYTE, base_system_contracts_hashes: BaseSystemContractsHashes::default(), protocol_version: Some(ProtocolVersionId::latest()), virtual_blocks: 1, @@ -195,14 +194,14 @@ impl Snapshot { l1_batch: L1BatchNumber, l2_block: L2BlockNumber, storage_logs: Vec, - genesis_params: GenesisParams, + contracts: &BaseSystemContracts, + protocol_version: ProtocolVersionId, ) -> Self { - let contracts = genesis_params.base_system_contracts(); let l1_batch = L1BatchHeader::new( l1_batch, l1_batch.0.into(), contracts.hashes(), - genesis_params.minor_protocol_version(), + protocol_version, ); let l2_block = L2BlockHeader { number: l2_block, @@ -213,11 +212,9 @@ impl Snapshot { base_fee_per_gas: 100, batch_fee_input: BatchFeeInput::l1_pegged(100, 100), fee_account_address: Address::zero(), - gas_per_pubdata_limit: get_max_gas_per_pubdata_byte( - genesis_params.minor_protocol_version().into(), - ), + gas_per_pubdata_limit: MAX_GAS_PER_PUBDATA_BYTE, base_system_contracts_hashes: contracts.hashes(), - protocol_version: Some(genesis_params.minor_protocol_version()), + protocol_version: Some(protocol_version), virtual_blocks: 1, gas_limit: 0, logs_bloom: Default::default(), @@ -253,7 +250,13 @@ pub async fn prepare_recovery_snapshot( enumeration_index: i as u64 + 1, }) .collect(); - let snapshot = Snapshot::new(l1_batch, l2_block, storage_logs, GenesisParams::mock()); + let snapshot = Snapshot::new( + l1_batch, + l2_block, + storage_logs, + &BaseSystemContracts::load_from_disk(), + ProtocolVersionId::latest(), + ); recover(storage, snapshot).await } diff --git a/core/tests/ts-integration/src/env.ts b/core/tests/ts-integration/src/env.ts index b91fcd09f0a..26f99d75780 100644 --- a/core/tests/ts-integration/src/env.ts +++ b/core/tests/ts-integration/src/env.ts @@ -126,7 +126,7 @@ async function loadTestEnvironmentFromFile(fileConfig: FileConfig): Promise