diff --git a/Cargo.lock b/Cargo.lock index c5cee452b1ae..fb5be136e10e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10784,15 +10784,18 @@ version = "0.1.0" dependencies = [ "anyhow", "axum 0.7.7", - "serde", + "http-body-util", "serde_json", + "test-casing", "tokio", + "tower 0.4.13", "tower-http", "tracing", "vise", - "zksync_config", "zksync_dal", + "zksync_node_test_utils", "zksync_types", + "zksync_utils", ] [[package]] @@ -10800,16 +10803,13 @@ name = "zksync_contract_verifier" version = "0.1.0" dependencies = [ "anyhow", - "ctrlc", - "futures 0.3.31", - "structopt", + "clap 4.5.20", "tokio", "tracing", "zksync_config", "zksync_contract_verifier_lib", "zksync_core_leftovers", "zksync_dal", - "zksync_env_config", "zksync_queued_job_processor", "zksync_utils", "zksync_vlog", diff --git a/Cargo.toml b/Cargo.toml index 87e0de13129f..e491c64605bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,6 +130,7 @@ google-cloud-storage = "0.20.0" governor = "0.4.2" hex = "0.4" http = "1.1" +http-body-util = "0.1.2" httpmock = "0.7.0" hyper = "1.3" insta = "1.29.0" diff --git a/core/bin/contract-verifier/Cargo.toml b/core/bin/contract-verifier/Cargo.toml index f088c2337e71..5e9a9efc6e7e 100644 --- a/core/bin/contract-verifier/Cargo.toml +++ b/core/bin/contract-verifier/Cargo.toml @@ -12,7 +12,6 @@ publish = false [dependencies] zksync_dal.workspace = true -zksync_env_config.workspace = true zksync_config = { workspace = true, features = ["observability_ext"] } zksync_contract_verifier_lib.workspace = true zksync_queued_job_processor.workspace = true @@ -21,8 +20,6 @@ zksync_vlog.workspace = true zksync_core_leftovers.workspace = true anyhow.workspace = true +clap = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["full"] } -futures.workspace = true -ctrlc.workspace = true -structopt.workspace = true tracing.workspace = true diff --git a/core/bin/contract-verifier/src/main.rs b/core/bin/contract-verifier/src/main.rs index 6929f8bfe04d..88f25256c40d 100644 --- a/core/bin/contract-verifier/src/main.rs +++ b/core/bin/contract-verifier/src/main.rs @@ -1,8 +1,7 @@ -use std::{cell::RefCell, time::Duration}; +use std::{path::PathBuf, time::Duration}; -use anyhow::Context; -use futures::{channel::mpsc, executor::block_on, SinkExt, StreamExt}; -use structopt::StructOpt; +use anyhow::Context as _; +use clap::Parser; use tokio::sync::watch; use zksync_config::configs::PrometheusConfig; use zksync_contract_verifier_lib::ContractVerifier; @@ -12,27 +11,31 @@ use zksync_queued_job_processor::JobProcessor; use zksync_utils::wait_for_tasks::ManagedTasks; use zksync_vlog::prometheus::PrometheusExporterConfig; -#[derive(StructOpt)] -#[structopt(name = "ZKsync contract code verifier", author = "Matter Labs")] +#[derive(Debug, Parser)] +#[command(name = "ZKsync contract code verifier", author = "Matter Labs")] struct Opt { /// Number of jobs to process. If None, runs indefinitely. - #[structopt(long)] + #[arg(long)] jobs_number: Option, /// Path to the configuration file. - #[structopt(long)] - config_path: Option, + #[arg(long)] + config_path: Option, /// Path to the secrets file. - #[structopt(long)] - secrets_path: Option, + #[arg(long)] + secrets_path: Option, } #[tokio::main] async fn main() -> anyhow::Result<()> { - let opt = Opt::from_args(); + let opt = Opt::parse(); let general_config = load_general_config(opt.config_path).context("general config")?; - let database_secrets = load_database_secrets(opt.secrets_path).context("database secrets")?; + let observability_config = general_config + .observability + .context("ObservabilityConfig")?; + let _observability_guard = observability_config.install()?; + let database_secrets = load_database_secrets(opt.secrets_path).context("database secrets")?; let verifier_config = general_config .contract_verifier .context("ContractVerifierConfig")?; @@ -46,33 +49,13 @@ async fn main() -> anyhow::Result<()> { .context("Master DB URL is absent")?, ) .build() - .await - .unwrap(); - - let observability_config = general_config - .observability - .context("ObservabilityConfig")?; - - let _observability_guard = observability_config.install()?; + .await?; let (stop_sender, stop_receiver) = watch::channel(false); - let (stop_signal_sender, mut stop_signal_receiver) = mpsc::channel(256); - { - let stop_signal_sender = RefCell::new(stop_signal_sender.clone()); - ctrlc::set_handler(move || { - let mut sender = stop_signal_sender.borrow_mut(); - block_on(sender.send(true)).expect("Ctrl+C signal send"); - }) - .expect("Error setting Ctrl+C handler"); - } - 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 - // since `JobProcessor` trait requires it. tokio::spawn(contract_verifier.run(stop_receiver.clone(), opt.jobs_number)), tokio::spawn( PrometheusExporterConfig::pull(prometheus_config.listener_port).run(stop_receiver), @@ -82,7 +65,7 @@ async fn main() -> anyhow::Result<()> { let mut tasks = ManagedTasks::new(tasks); tokio::select! { () = tasks.wait_single() => {}, - _ = stop_signal_receiver.next() => { + _ = tokio::signal::ctrl_c() => { tracing::info!("Stop signal received, shutting down"); }, }; diff --git a/core/lib/contract_verifier/src/lib.rs b/core/lib/contract_verifier/src/lib.rs index 425440fa2eb6..686bb0d7bdc3 100644 --- a/core/lib/contract_verifier/src/lib.rs +++ b/core/lib/contract_verifier/src/lib.rs @@ -14,7 +14,7 @@ use zksync_dal::{contract_verification_dal::DeployedContractData, ConnectionPool use zksync_queued_job_processor::{async_trait, JobProcessor}; use zksync_types::{ contract_verification_api::{ - CompilationArtifacts, CompilerType, VerificationIncomingRequest, VerificationInfo, + self as api, CompilationArtifacts, VerificationIncomingRequest, VerificationInfo, VerificationRequest, }, Address, CONTRACT_DEPLOYER_ADDRESS, @@ -35,6 +35,65 @@ mod resolver; #[cfg(test)] mod tests; +#[derive(Debug)] +struct ZkCompilerVersions { + /// Version of the base / non-ZK compiler. + pub base: String, + /// Version of the ZK compiler. + pub zk: String, +} + +/// Internal counterpart of `ContractVersions` from API that encompasses all supported compilation modes. +#[derive(Debug)] +enum VersionedCompiler { + Solc(String), + #[allow(dead_code)] // TODO (EVM-864): add vyper support + Vyper(String), + ZkSolc(ZkCompilerVersions), + ZkVyper(ZkCompilerVersions), +} + +impl From for VersionedCompiler { + fn from(versions: api::CompilerVersions) -> Self { + match versions { + api::CompilerVersions::Solc { + compiler_solc_version, + compiler_zksolc_version: None, + } => Self::Solc(compiler_solc_version), + + api::CompilerVersions::Solc { + compiler_solc_version, + compiler_zksolc_version: Some(zk), + } => Self::ZkSolc(ZkCompilerVersions { + base: compiler_solc_version, + zk, + }), + + api::CompilerVersions::Vyper { + compiler_vyper_version, + compiler_zkvyper_version: None, + } => Self::Vyper(compiler_vyper_version), + + api::CompilerVersions::Vyper { + compiler_vyper_version, + compiler_zkvyper_version: Some(zk), + } => Self::ZkVyper(ZkCompilerVersions { + base: compiler_vyper_version, + zk, + }), + } + } +} + +impl VersionedCompiler { + fn expected_bytecode_kind(&self) -> BytecodeMarker { + match self { + Self::Solc(_) | Self::Vyper(_) => BytecodeMarker::Evm, + Self::ZkSolc(_) | Self::ZkVyper(_) => BytecodeMarker::EraVm, + } + } +} + enum ConstructorArgs { Check(Vec), Ignore, @@ -112,19 +171,19 @@ impl ContractVerifier { let mut transaction = storage.start_transaction().await?; transaction .contract_verification_dal() - .set_zksolc_versions(supported_versions.zksolc) + .set_zksolc_versions(&supported_versions.zksolc) .await?; transaction .contract_verification_dal() - .set_solc_versions(supported_versions.solc) + .set_solc_versions(&supported_versions.solc) .await?; transaction .contract_verification_dal() - .set_zkvyper_versions(supported_versions.zkvyper) + .set_zkvyper_versions(&supported_versions.zkvyper) .await?; transaction .contract_verification_dal() - .set_vyper_versions(supported_versions.vyper) + .set_vyper_versions(&supported_versions.vyper) .await?; transaction.commit().await?; Ok(()) @@ -214,13 +273,11 @@ impl ContractVerifier { async fn compile_zksolc( &self, + version: &ZkCompilerVersions, req: VerificationIncomingRequest, ) -> Result { - let zksolc = self - .compiler_resolver - .resolve_zksolc(&req.compiler_versions) - .await?; - tracing::debug!(?zksolc, ?req.compiler_versions, "resolved compiler"); + let zksolc = self.compiler_resolver.resolve_zksolc(version).await?; + tracing::debug!(?zksolc, ?version, "resolved compiler"); let input = ZkSolc::build_input(req)?; time::timeout(self.compilation_timeout, zksolc.compile(input)) @@ -230,13 +287,11 @@ impl ContractVerifier { async fn compile_zkvyper( &self, + version: &ZkCompilerVersions, req: VerificationIncomingRequest, ) -> Result { - let zkvyper = self - .compiler_resolver - .resolve_zkvyper(&req.compiler_versions) - .await?; - tracing::debug!(?zkvyper, ?req.compiler_versions, "resolved compiler"); + let zkvyper = self.compiler_resolver.resolve_zkvyper(version).await?; + tracing::debug!(?zkvyper, ?version, "resolved compiler"); let input = ZkVyper::build_input(req)?; time::timeout(self.compilation_timeout, zkvyper.compile(input)) .await @@ -245,12 +300,10 @@ impl ContractVerifier { async fn compile_solc( &self, + version: &str, req: VerificationIncomingRequest, ) -> Result { - let solc = self - .compiler_resolver - .resolve_solc(req.compiler_versions.compiler_version()) - .await?; + let solc = self.compiler_resolver.resolve_solc(version).await?; tracing::debug!(?solc, ?req.compiler_versions, "resolved compiler"); let input = Solc::build_input(req)?; @@ -276,15 +329,24 @@ impl ContractVerifier { return Err(err.into()); } - match (bytecode_marker, compiler_type) { - (BytecodeMarker::EraVm, CompilerType::Solc) => self.compile_zksolc(req).await, - (BytecodeMarker::EraVm, CompilerType::Vyper) => self.compile_zkvyper(req).await, - (BytecodeMarker::Evm, CompilerType::Solc) => self.compile_solc(req).await, - (BytecodeMarker::Evm, CompilerType::Vyper) => { - // TODO: add vyper support + let compiler = VersionedCompiler::from(req.compiler_versions.clone()); + if compiler.expected_bytecode_kind() != bytecode_marker { + let err = anyhow::anyhow!( + "bytecode kind expected by compiler {compiler:?} differs from the actual bytecode kind \ + of the verified contract ({bytecode_marker:?})", + ); + return Err(err.into()); + } + + 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::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 347db8fff094..34a70b759797 100644 --- a/core/lib/contract_verifier/src/resolver.rs +++ b/core/lib/contract_verifier/src/resolver.rs @@ -6,12 +6,13 @@ use std::{ use anyhow::Context as _; use tokio::fs; use zksync_queued_job_processor::async_trait; -use zksync_types::contract_verification_api::{CompilationArtifacts, CompilerVersions}; +use zksync_types::contract_verification_api::CompilationArtifacts; use zksync_utils::env::Workspace; use crate::{ compilers::{Solc, SolcInput, ZkSolc, ZkSolcInput, ZkVyper, ZkVyperInput}, error::ContractVerifierError, + ZkCompilerVersions, }; #[derive(Debug, Clone, Copy)] @@ -111,13 +112,13 @@ pub(crate) trait CompilerResolver: fmt::Debug + Send + Sync { /// Resolves a `zksolc` compiler. async fn resolve_zksolc( &self, - versions: &CompilerVersions, + version: &ZkCompilerVersions, ) -> Result>, ContractVerifierError>; /// Resolves a `zkvyper` compiler. async fn resolve_zkvyper( &self, - versions: &CompilerVersions, + version: &ZkCompilerVersions, ) -> Result>, ContractVerifierError>; } @@ -198,14 +199,14 @@ impl CompilerResolver for EnvCompilerResolver { async fn resolve_zksolc( &self, - versions: &CompilerVersions, + version: &ZkCompilerVersions, ) -> Result>, ContractVerifierError> { - let zksolc_version = versions.zk_compiler_version(); + let zksolc_version = &version.zk; let zksolc_path = CompilerType::ZkSolc .bin_path(&self.home_dir, zksolc_version) .await?; let solc_path = CompilerType::Solc - .bin_path(&self.home_dir, versions.compiler_version()) + .bin_path(&self.home_dir, &version.base) .await?; let compiler_paths = CompilerPaths { base: solc_path, @@ -219,13 +220,13 @@ impl CompilerResolver for EnvCompilerResolver { async fn resolve_zkvyper( &self, - versions: &CompilerVersions, + version: &ZkCompilerVersions, ) -> Result>, ContractVerifierError> { let zkvyper_path = CompilerType::ZkVyper - .bin_path(&self.home_dir, versions.zk_compiler_version()) + .bin_path(&self.home_dir, &version.zk) .await?; let vyper_path = CompilerType::Vyper - .bin_path(&self.home_dir, versions.compiler_version()) + .bin_path(&self.home_dir, &version.base) .await?; let compiler_paths = CompilerPaths { base: vyper_path, diff --git a/core/lib/contract_verifier/src/tests/mod.rs b/core/lib/contract_verifier/src/tests/mod.rs index f05d3155a6d4..7caa5f32c991 100644 --- a/core/lib/contract_verifier/src/tests/mod.rs +++ b/core/lib/contract_verifier/src/tests/mod.rs @@ -280,18 +280,18 @@ impl CompilerResolver for MockCompilerResolver { async fn resolve_zksolc( &self, - versions: &CompilerVersions, + version: &ZkCompilerVersions, ) -> Result>, ContractVerifierError> { - if versions.compiler_version() != SOLC_VERSION { + if version.base != SOLC_VERSION { return Err(ContractVerifierError::UnknownCompilerVersion( "solc", - versions.compiler_version().to_owned(), + version.base.clone(), )); } - if versions.zk_compiler_version() != ZKSOLC_VERSION { + if version.zk != ZKSOLC_VERSION { return Err(ContractVerifierError::UnknownCompilerVersion( "zksolc", - versions.zk_compiler_version().to_owned(), + version.zk.clone(), )); } Ok(Box::new(self.clone())) @@ -299,7 +299,7 @@ impl CompilerResolver for MockCompilerResolver { async fn resolve_zkvyper( &self, - _versions: &CompilerVersions, + _version: &ZkCompilerVersions, ) -> Result>, ContractVerifierError> { unreachable!("not tested") } @@ -311,7 +311,7 @@ fn test_request(address: Address, source: &str) -> VerificationIncomingRequest { source_code_data: SourceCodeData::SolSingleFile(source.into()), contract_name: "Counter".to_owned(), compiler_versions: CompilerVersions::Solc { - compiler_zksolc_version: ZKSOLC_VERSION.to_owned(), + compiler_zksolc_version: Some(ZKSOLC_VERSION.to_owned()), compiler_solc_version: SOLC_VERSION.to_owned(), }, optimization_used: true, @@ -375,7 +375,7 @@ async fn contract_verifier_basics(contract: TestContract) { req.constructor_arguments = ethabi::encode(contract.constructor_args()).into(); let request_id = storage .contract_verification_dal() - .add_contract_verification_request(req) + .add_contract_verification_request(&req) .await .unwrap(); @@ -468,10 +468,14 @@ async fn verifying_evm_bytecode(contract: TestContract) { ) .await; let mut req = test_request(address, contract.source()); + req.compiler_versions = CompilerVersions::Solc { + compiler_solc_version: SOLC_VERSION.to_owned(), + compiler_zksolc_version: None, + }; req.constructor_arguments = ethabi::encode(contract.constructor_args()).into(); let request_id = storage .contract_verification_dal() - .add_contract_verification_request(req) + .add_contract_verification_request(&req) .await .unwrap(); @@ -513,7 +517,7 @@ async fn bytecode_mismatch_error() { let req = test_request(address, COUNTER_CONTRACT); let request_id = storage .contract_verification_dal() - .add_contract_verification_request(req) + .add_contract_verification_request(&req) .await .unwrap(); @@ -578,6 +582,13 @@ async fn args_mismatch_error(contract: TestContract, bytecode_kind: BytecodeMark } let mut req = test_request(address, contract.source()); + if matches!(bytecode_kind, BytecodeMarker::Evm) { + req.compiler_versions = CompilerVersions::Solc { + compiler_zksolc_version: None, + compiler_solc_version: SOLC_VERSION.to_owned(), + }; + } + // Intentionally encode incorrect constructor args req.constructor_arguments = match contract { TestContract::Counter => ethabi::encode(&[Token::Bool(true)]).into(), @@ -585,7 +596,7 @@ async fn args_mismatch_error(contract: TestContract, bytecode_kind: BytecodeMark }; let request_id = storage .contract_verification_dal() - .add_contract_verification_request(req) + .add_contract_verification_request(&req) .await .unwrap(); @@ -648,10 +659,14 @@ async fn creation_bytecode_mismatch() { &[], ) .await; - let req = test_request(address, COUNTER_CONTRACT); + let mut req = test_request(address, COUNTER_CONTRACT); + req.compiler_versions = CompilerVersions::Solc { + compiler_zksolc_version: None, + compiler_solc_version: SOLC_VERSION.to_owned(), + }; let request_id = storage .contract_verification_dal() - .add_contract_verification_request(req) + .add_contract_verification_request(&req) .await .unwrap(); @@ -696,14 +711,14 @@ async fn no_compiler_version() { mock_deployment(&mut storage, address, vec![0xff; 32], &[]).await; let req = VerificationIncomingRequest { compiler_versions: CompilerVersions::Solc { - compiler_zksolc_version: ZKSOLC_VERSION.to_owned(), + compiler_zksolc_version: Some(ZKSOLC_VERSION.to_owned()), compiler_solc_version: "1.0.0".to_owned(), // a man can dream }, ..test_request(address, COUNTER_CONTRACT) }; let request_id = storage .contract_verification_dal() - .add_contract_verification_request(req) + .add_contract_verification_request(&req) .await .unwrap(); diff --git a/core/lib/contract_verifier/src/tests/real.rs b/core/lib/contract_verifier/src/tests/real.rs index 5f550a5feea8..a7113044b405 100644 --- a/core/lib/contract_verifier/src/tests/real.rs +++ b/core/lib/contract_verifier/src/tests/real.rs @@ -8,7 +8,7 @@ use zksync_utils::bytecode::validate_bytecode; use super::*; -#[derive(Debug)] +#[derive(Debug, Clone)] struct TestCompilerVersions { solc: String, zksolc: String, @@ -26,10 +26,20 @@ impl TestCompilerVersions { }) } - fn for_zksolc(self) -> CompilerVersions { + fn zksolc(self) -> ZkCompilerVersions { + ZkCompilerVersions { + base: self.solc, + zk: self.zksolc, + } + } + + fn solc_for_api(self, bytecode_kind: BytecodeMarker) -> CompilerVersions { CompilerVersions::Solc { compiler_solc_version: self.solc, - compiler_zksolc_version: self.zksolc, + compiler_zksolc_version: match bytecode_kind { + BytecodeMarker::Evm => None, + BytecodeMarker::EraVm => Some(self.zksolc), + }, } } } @@ -70,10 +80,12 @@ macro_rules! real_resolver { async fn using_real_compiler() { let (compiler_resolver, supported_compilers) = real_resolver!(); - let versions = supported_compilers.for_zksolc(); - let compiler = compiler_resolver.resolve_zksolc(&versions).await.unwrap(); + let compiler = compiler_resolver + .resolve_zksolc(&supported_compilers.clone().zksolc()) + .await + .unwrap(); let req = VerificationIncomingRequest { - compiler_versions: versions, + compiler_versions: supported_compilers.solc_for_api(BytecodeMarker::EraVm), ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT) }; let input = ZkSolc::build_input(req).unwrap(); @@ -92,7 +104,7 @@ async fn using_standalone_solc() { let req = VerificationIncomingRequest { compiler_versions: CompilerVersions::Solc { compiler_solc_version: version.clone(), - compiler_zksolc_version: "1000.0.0".to_owned(), // not used + compiler_zksolc_version: None, }, ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT) }; @@ -108,23 +120,22 @@ async fn using_standalone_solc() { async fn using_real_compiler_in_verifier(bytecode_kind: BytecodeMarker) { let (compiler_resolver, supported_compilers) = real_resolver!(); - let versions = supported_compilers.for_zksolc(); let req = VerificationIncomingRequest { - compiler_versions: versions, + compiler_versions: supported_compilers.clone().solc_for_api(bytecode_kind), ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT) }; let address = Address::repeat_byte(1); let output = match bytecode_kind { BytecodeMarker::EraVm => { let compiler = compiler_resolver - .resolve_zksolc(&req.compiler_versions) + .resolve_zksolc(&supported_compilers.zksolc()) .await .unwrap(); let input = ZkSolc::build_input(req.clone()).unwrap(); compiler.compile(input).await.unwrap() } BytecodeMarker::Evm => { - let solc_version = req.compiler_versions.compiler_version(); + 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() @@ -151,7 +162,7 @@ async fn using_real_compiler_in_verifier(bytecode_kind: BytecodeMarker) { } let request_id = storage .contract_verification_dal() - .add_contract_verification_request(req) + .add_contract_verification_request(&req) .await .unwrap(); @@ -174,10 +185,9 @@ async fn using_real_compiler_in_verifier(bytecode_kind: BytecodeMarker) { async fn compilation_errors(bytecode_kind: BytecodeMarker) { let (compiler_resolver, supported_compilers) = real_resolver!(); - let versions = supported_compilers.for_zksolc(); let address = Address::repeat_byte(1); let req = VerificationIncomingRequest { - compiler_versions: versions, + compiler_versions: supported_compilers.solc_for_api(bytecode_kind), source_code_data: SourceCodeData::SolSingleFile("contract ???".to_owned()), ..test_request(Address::repeat_byte(1), COUNTER_CONTRACT) }; @@ -196,7 +206,7 @@ async fn compilation_errors(bytecode_kind: BytecodeMarker) { let request_id = storage .contract_verification_dal() - .add_contract_verification_request(req) + .add_contract_verification_request(&req) .await .unwrap(); diff --git a/core/lib/dal/.sqlx/query-2fa2ba4a62f79d780d239409d426b602aa0cf9b0c5b1ef39b7d07d6309454fcd.json b/core/lib/dal/.sqlx/query-2fa2ba4a62f79d780d239409d426b602aa0cf9b0c5b1ef39b7d07d6309454fcd.json index 1d515edba819..0db6ba6f51b6 100644 --- a/core/lib/dal/.sqlx/query-2fa2ba4a62f79d780d239409d426b602aa0cf9b0c5b1ef39b7d07d6309454fcd.json +++ b/core/lib/dal/.sqlx/query-2fa2ba4a62f79d780d239409d426b602aa0cf9b0c5b1ef39b7d07d6309454fcd.json @@ -69,7 +69,7 @@ false, false, false, - false, + true, false, false, true, diff --git a/core/lib/dal/.sqlx/query-a115f795672787fe25bfaa8fd345094de508af93f4085be7cf3b54b1e8ecdadd.json b/core/lib/dal/.sqlx/query-a115f795672787fe25bfaa8fd345094de508af93f4085be7cf3b54b1e8ecdadd.json index ebe8ce232cfb..ac7989a5be77 100644 --- a/core/lib/dal/.sqlx/query-a115f795672787fe25bfaa8fd345094de508af93f4085be7cf3b54b1e8ecdadd.json +++ b/core/lib/dal/.sqlx/query-a115f795672787fe25bfaa8fd345094de508af93f4085be7cf3b54b1e8ecdadd.json @@ -67,7 +67,7 @@ false, false, false, - false, + true, false, false, true, diff --git a/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.down.sql b/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.down.sql new file mode 100644 index 000000000000..2693a565fd02 --- /dev/null +++ b/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE contract_verification_requests + ALTER COLUMN zk_compiler_version SET NOT NULL; diff --git a/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.up.sql b/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.up.sql new file mode 100644 index 000000000000..92a689956f55 --- /dev/null +++ b/core/lib/dal/migrations/20241106093512_make_zk_compiler_version_nullable.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE contract_verification_requests + ALTER COLUMN zk_compiler_version DROP NOT NULL; diff --git a/core/lib/dal/src/contract_verification_dal.rs b/core/lib/dal/src/contract_verification_dal.rs index 1a827545ca13..93a4ce2fd35a 100644 --- a/core/lib/dal/src/contract_verification_dal.rs +++ b/core/lib/dal/src/contract_verification_dal.rs @@ -76,7 +76,7 @@ impl ContractVerificationDal<'_, '_> { pub async fn add_contract_verification_request( &mut self, - query: VerificationIncomingRequest, + query: &VerificationIncomingRequest, ) -> DalResult { sqlx::query!( r#" @@ -104,12 +104,12 @@ impl ContractVerificationDal<'_, '_> { query.contract_address.as_bytes(), // Serialization should always succeed. serde_json::to_string(&query.source_code_data).unwrap(), - query.contract_name, + &query.contract_name, query.compiler_versions.zk_compiler_version(), query.compiler_versions.compiler_version(), query.optimization_used, - query.optimizer_mode, - query.constructor_arguments.0, + query.optimizer_mode.as_deref(), + query.constructor_arguments.0.as_slice(), query.is_system, query.force_evmla, ) @@ -441,7 +441,7 @@ impl ContractVerificationDal<'_, '_> { async fn set_compiler_versions( &mut self, compiler: Compiler, - versions: Vec, + versions: &[String], ) -> DalResult<()> { let mut transaction = self.storage.start_transaction().await?; let compiler = format!("{compiler}"); @@ -472,7 +472,7 @@ impl ContractVerificationDal<'_, '_> { UNNEST($1::TEXT []) AS u (version) ON CONFLICT (version, compiler) DO NOTHING "#, - &versions, + versions, &compiler, ) .instrument("set_compiler_versions#insert") @@ -484,20 +484,20 @@ impl ContractVerificationDal<'_, '_> { transaction.commit().await } - pub async fn set_zksolc_versions(&mut self, versions: Vec) -> DalResult<()> { + pub async fn set_zksolc_versions(&mut self, versions: &[String]) -> DalResult<()> { self.set_compiler_versions(Compiler::ZkSolc, versions).await } - pub async fn set_solc_versions(&mut self, versions: Vec) -> DalResult<()> { + pub async fn set_solc_versions(&mut self, versions: &[String]) -> DalResult<()> { self.set_compiler_versions(Compiler::Solc, versions).await } - pub async fn set_zkvyper_versions(&mut self, versions: Vec) -> DalResult<()> { + pub async fn set_zkvyper_versions(&mut self, versions: &[String]) -> DalResult<()> { self.set_compiler_versions(Compiler::ZkVyper, versions) .await } - pub async fn set_vyper_versions(&mut self, versions: Vec) -> DalResult<()> { + pub async fn set_vyper_versions(&mut self, versions: &[String]) -> DalResult<()> { self.set_compiler_versions(Compiler::Vyper, versions).await } @@ -567,7 +567,9 @@ mod tests { use std::collections::HashMap; use zksync_types::{ - tx::IncludedTxLocation, Execute, L1BatchNumber, L2BlockNumber, ProtocolVersion, + contract_verification_api::{CompilerVersions, SourceCodeData}, + tx::IncludedTxLocation, + Execute, L1BatchNumber, L2BlockNumber, ProtocolVersion, }; use zksync_utils::bytecode::hash_bytecode; use zksync_vm_interface::{tracer::ValidationTraces, TransactionExecutionMetrics}; @@ -645,4 +647,66 @@ mod tests { assert_eq!(contract.contract_address, Some(CONTRACT_DEPLOYER_ADDRESS)); assert_eq!(contract.calldata.unwrap(), tx.execute.calldata); } + + async fn test_working_with_verification_requests(zksolc: Option<&str>) { + let request = VerificationIncomingRequest { + contract_address: Address::repeat_byte(11), + source_code_data: SourceCodeData::SolSingleFile("contract Test {}".to_owned()), + contract_name: "Test".to_string(), + compiler_versions: CompilerVersions::Solc { + compiler_zksolc_version: zksolc.map(str::to_owned), + compiler_solc_version: "0.8.27".to_owned(), + }, + optimization_used: true, + optimizer_mode: Some("z".to_owned()), + constructor_arguments: web3::Bytes(b"test".to_vec()), + is_system: false, + force_evmla: true, + }; + + let pool = ConnectionPool::::test_pool().await; + let mut conn = pool.connection().await.unwrap(); + let id = conn + .contract_verification_dal() + .add_contract_verification_request(&request) + .await + .unwrap(); + + let status = conn + .contract_verification_dal() + .get_verification_request_status(id) + .await + .unwrap() + .expect("request not persisted"); + assert_eq!(status.status, "queued"); + + let req = conn + .contract_verification_dal() + .get_next_queued_verification_request(Duration::from_secs(600)) + .await + .unwrap() + .expect("request not queued"); + assert_eq!(req.id, id); + assert_eq!(req.req.contract_address, request.contract_address); + assert_eq!(req.req.contract_name, request.contract_name); + assert_eq!(req.req.compiler_versions, request.compiler_versions); + assert_eq!(req.req.optimization_used, request.optimization_used); + assert_eq!(req.req.optimizer_mode, request.optimizer_mode); + assert_eq!(req.req.constructor_arguments, request.constructor_arguments); + assert_eq!(req.req.is_system, request.is_system); + assert_eq!(req.req.force_evmla, request.force_evmla); + + let maybe_req = conn + .contract_verification_dal() + .get_next_queued_verification_request(Duration::from_secs(600)) + .await + .unwrap(); + assert!(maybe_req.is_none()); + } + + #[tokio::test] + async fn working_with_verification_requests() { + test_working_with_verification_requests(None).await; + test_working_with_verification_requests(Some("1.5.7")).await; + } } diff --git a/core/lib/dal/src/models/storage_verification_request.rs b/core/lib/dal/src/models/storage_verification_request.rs index 61895fab76d3..ae4718e41290 100644 --- a/core/lib/dal/src/models/storage_verification_request.rs +++ b/core/lib/dal/src/models/storage_verification_request.rs @@ -12,7 +12,7 @@ pub struct StorageVerificationRequest { pub contract_address: Vec, pub source_code: String, pub contract_name: String, - pub zk_compiler_version: String, + pub zk_compiler_version: Option, pub compiler_version: String, pub optimization_used: bool, pub optimizer_mode: Option, diff --git a/core/lib/dal/src/storage_logs_dal.rs b/core/lib/dal/src/storage_logs_dal.rs index adad6eb7e1db..4ed3e37a9a1d 100644 --- a/core/lib/dal/src/storage_logs_dal.rs +++ b/core/lib/dal/src/storage_logs_dal.rs @@ -225,60 +225,13 @@ impl StorageLogsDal<'_, '_> { Ok(()) } - pub async fn is_contract_deployed_at_address(&mut self, address: Address) -> bool { - let hashed_key = get_code_key(&address).hashed_key(); - let row = sqlx::query!( - r#" - SELECT - COUNT(*) AS "count!" - FROM - ( - SELECT - * - FROM - storage_logs - WHERE - hashed_key = $1 - AND miniblock_number <= COALESCE( - ( - SELECT - MAX(number) - FROM - miniblocks - ), - ( - SELECT - miniblock_number - FROM - snapshot_recovery - ) - ) - ORDER BY - miniblock_number DESC, - operation_number DESC - LIMIT - 1 - ) sl - WHERE - sl.value != $2 - "#, - hashed_key.as_bytes(), - FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH.as_bytes(), - ) - .fetch_one(self.storage.conn()) - .await - .unwrap(); - - row.count > 0 - } - /// Returns addresses and the corresponding deployment L2 block numbers among the specified contract /// `addresses`. `at_l2_block` allows filtering deployment by L2 blocks. pub async fn filter_deployed_contracts( &mut self, addresses: impl Iterator, at_l2_block: Option, - ) -> DalResult> { + ) -> DalResult> { let (bytecode_hashed_keys, address_by_hashed_key): (Vec<_>, HashMap<_, _>) = addresses .map(|address| { let hashed_key = get_code_key(&address).hashed_key().0; @@ -330,12 +283,13 @@ impl StorageLogsDal<'_, '_> { .await?; let deployment_data = rows.into_iter().filter_map(|row| { - if row.value == FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH.as_bytes() { + let bytecode_hash = H256::from_slice(&row.value); + if bytecode_hash == FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH { return None; } let l2_block_number = L2BlockNumber(row.miniblock_number as u32); let address = address_by_hashed_key[row.hashed_key.as_slice()]; - Some((address, l2_block_number)) + Some((address, (l2_block_number, bytecode_hash))) }); Ok(deployment_data.collect()) } @@ -1168,8 +1122,9 @@ mod tests { async fn filtering_deployed_contracts() { let contract_address = Address::repeat_byte(1); let other_contract_address = Address::repeat_byte(23); + let bytecode_hash = H256::repeat_byte(0xff); let successful_deployment = - StorageLog::new_write_log(get_code_key(&contract_address), H256::repeat_byte(0xff)); + StorageLog::new_write_log(get_code_key(&contract_address), bytecode_hash); let failed_deployment = StorageLog::new_write_log( get_code_key(&contract_address), FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH, @@ -1233,7 +1188,7 @@ mod tests { .unwrap(); assert_eq!( deployed_map, - HashMap::from([(contract_address, L2BlockNumber(2))]) + HashMap::from([(contract_address, (L2BlockNumber(2), bytecode_hash))]) ); } @@ -1268,7 +1223,7 @@ mod tests { .unwrap(); assert_eq!( deployed_map, - HashMap::from([(contract_address, L2BlockNumber(2))]) + HashMap::from([(contract_address, (L2BlockNumber(2), bytecode_hash))]) ); for new_l2_block in [None, Some(L2BlockNumber(3))] { @@ -1283,8 +1238,8 @@ mod tests { assert_eq!( deployed_map, HashMap::from([ - (contract_address, L2BlockNumber(2)), - (other_contract_address, L2BlockNumber(3)), + (contract_address, (L2BlockNumber(2), bytecode_hash)), + (other_contract_address, (L2BlockNumber(3), bytecode_hash)), ]) ); } diff --git a/core/lib/dal/src/tokens_dal.rs b/core/lib/dal/src/tokens_dal.rs index 218e152fa82a..b5fd67fc63c8 100644 --- a/core/lib/dal/src/tokens_dal.rs +++ b/core/lib/dal/src/tokens_dal.rs @@ -98,7 +98,7 @@ impl TokensDal<'_, '_> { .filter_map(|address| { if address.is_zero() { None - } else if let Some(deployed_at) = token_deployment_data.get(&address) { + } else if let Some((deployed_at, _)) = token_deployment_data.get(&address) { (deployed_at > &block_number).then_some(address.0) } else { // Token belongs to a "pending" L2 block that's not yet fully inserted to the database. diff --git a/core/lib/types/src/contract_verification_api.rs b/core/lib/types/src/contract_verification_api.rs index fcaa1aa9a535..21e511549beb 100644 --- a/core/lib/types/src/contract_verification_api.rs +++ b/core/lib/types/src/contract_verification_api.rs @@ -152,17 +152,19 @@ pub enum CompilerType { Vyper, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum CompilerVersions { #[serde(rename_all = "camelCase")] Solc { - compiler_zksolc_version: String, // FIXME: optional? + #[serde(default, skip_serializing_if = "Option::is_none")] + compiler_zksolc_version: Option, compiler_solc_version: String, }, #[serde(rename_all = "camelCase")] Vyper { - compiler_zkvyper_version: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + compiler_zkvyper_version: Option, compiler_vyper_version: String, }, } @@ -175,16 +177,16 @@ impl CompilerVersions { } } - pub fn zk_compiler_version(&self) -> &str { + pub fn zk_compiler_version(&self) -> Option<&str> { match self { Self::Solc { compiler_zksolc_version, .. - } => compiler_zksolc_version, + } => compiler_zksolc_version.as_deref(), Self::Vyper { compiler_zkvyper_version, .. - } => compiler_zkvyper_version, + } => compiler_zkvyper_version.as_deref(), } } diff --git a/core/node/contract_verification_server/Cargo.toml b/core/node/contract_verification_server/Cargo.toml index eeb2c7828467..038347debc64 100644 --- a/core/node/contract_verification_server/Cargo.toml +++ b/core/node/contract_verification_server/Cargo.toml @@ -11,9 +11,9 @@ keywords.workspace = true categories.workspace = true [dependencies] -zksync_config.workspace = true zksync_dal.workspace = true zksync_types.workspace = true +zksync_utils.workspace = true vise.workspace = true anyhow.workspace = true @@ -21,5 +21,11 @@ axum.workspace = true tokio = { workspace = true, features = ["time"] } tower-http = { workspace = true, features = ["cors"] } tracing.workspace = true -serde.workspace = true + +[dev-dependencies] +zksync_node_test_utils.workspace = true + +http-body-util.workspace = true serde_json.workspace = true +test-casing.workspace = true +tower.workspace = true diff --git a/core/node/contract_verification_server/src/api_decl.rs b/core/node/contract_verification_server/src/api_decl.rs index 256062936d32..d451cd79add9 100644 --- a/core/node/contract_verification_server/src/api_decl.rs +++ b/core/node/contract_verification_server/src/api_decl.rs @@ -3,10 +3,13 @@ use std::sync::Arc; use tower_http::cors::CorsLayer; use zksync_dal::{ConnectionPool, Core}; +use crate::cache::SupportedCompilersCache; + #[derive(Debug, Clone)] -pub struct RestApi { - pub(super) master_connection_pool: ConnectionPool, - pub(super) replica_connection_pool: ConnectionPool, +pub(crate) struct RestApi { + pub(crate) master_connection_pool: ConnectionPool, + pub(crate) replica_connection_pool: ConnectionPool, + pub(crate) supported_compilers: Arc, } impl RestApi { @@ -14,7 +17,9 @@ impl RestApi { master_connection_pool: ConnectionPool, replica_connection_pool: ConnectionPool, ) -> Self { + let supported_compilers = SupportedCompilersCache::new(replica_connection_pool.clone()); Self { + supported_compilers: Arc::new(supported_compilers), master_connection_pool, replica_connection_pool, } diff --git a/core/node/contract_verification_server/src/api_impl.rs b/core/node/contract_verification_server/src/api_impl.rs index b8111e98a1cc..94be65673bad 100644 --- a/core/node/contract_verification_server/src/api_impl.rs +++ b/core/node/contract_verification_server/src/api_impl.rs @@ -1,195 +1,234 @@ -use std::sync::Arc; +use std::{collections::HashSet, iter, sync::Arc}; +use anyhow::Context as _; use axum::{ extract::{Path, State}, - response::Response, + http::StatusCode, + response::{IntoResponse, Response}, Json, }; -use serde::Serialize; -use zksync_dal::CoreDal; -use zksync_types::{contract_verification_api::VerificationIncomingRequest, Address}; +use zksync_dal::{CoreDal, DalError}; +use zksync_types::{ + contract_verification_api::{ + CompilerVersions, VerificationIncomingRequest, VerificationInfo, VerificationRequestStatus, + }, + Address, +}; +use zksync_utils::bytecode::BytecodeMarker; use super::{api_decl::RestApi, metrics::METRICS}; -fn ok_json(data: impl Serialize) -> Response { - Response::builder() - .status(axum::http::StatusCode::OK) - .body(serde_json::to_string(&data).expect("Failed to serialize")) - .unwrap() +#[derive(Debug)] +pub(crate) enum ApiError { + IncorrectCompilerVersions, + UnsupportedCompilerVersions, + MissingZkCompilerVersion, + BogusZkCompilerVersion, + NoDeployedContract, + RequestNotFound, + VerificationInfoNotFound, + Internal(anyhow::Error), +} + +impl From for ApiError { + fn from(err: anyhow::Error) -> Self { + Self::Internal(err) + } +} + +impl From for ApiError { + fn from(err: DalError) -> Self { + Self::Internal(err.generalize()) + } } -fn bad_request(message: &str) -> Response { - Response::builder() - .status(axum::http::StatusCode::BAD_REQUEST) - .body(message.to_string()) - .unwrap() +impl ApiError { + pub fn message(&self) -> &'static str { + match self { + Self::IncorrectCompilerVersions => "incorrect compiler versions", + Self::UnsupportedCompilerVersions => "unsupported compiler versions", + Self::MissingZkCompilerVersion => "missing zk compiler version for EraVM bytecode", + Self::BogusZkCompilerVersion => "zk compiler version specified for EVM bytecode", + Self::NoDeployedContract => "There is no deployed contract on this address", + Self::RequestNotFound => "request not found", + Self::VerificationInfoNotFound => "verification info not found for address", + Self::Internal(_) => "internal server error", + } + } } -fn not_found() -> Response { - Response::builder() - .status(axum::http::StatusCode::NOT_FOUND) - .body(String::new()) - .unwrap() +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let status_code = match &self { + Self::IncorrectCompilerVersions + | Self::UnsupportedCompilerVersions + | Self::MissingZkCompilerVersion + | Self::BogusZkCompilerVersion + | Self::NoDeployedContract => StatusCode::BAD_REQUEST, + + Self::RequestNotFound | Self::VerificationInfoNotFound => StatusCode::NOT_FOUND, + + Self::Internal(err) => { + // Do not expose the error details to the client, but log it. + tracing::warn!("Internal error: {err:#}"); + StatusCode::INTERNAL_SERVER_ERROR + } + }; + (status_code, self.message()).into_response() + } } +type ApiResult = Result, ApiError>; + impl RestApi { #[tracing::instrument(skip(query))] fn validate_contract_verification_query( query: &VerificationIncomingRequest, - ) -> Result<(), Response> { + ) -> Result<(), ApiError> { if query.source_code_data.compiler_type() != query.compiler_versions.compiler_type() { - return Err(bad_request("incorrect compiler versions")); + return Err(ApiError::IncorrectCompilerVersions); } - Ok(()) } + fn validate_compilers( + versions: &CompilerVersions, + bytecode_kind: BytecodeMarker, + ) -> Result<(), ApiError> { + match bytecode_kind { + BytecodeMarker::EraVm if versions.zk_compiler_version().is_none() => { + Err(ApiError::MissingZkCompilerVersion) + } + BytecodeMarker::Evm if versions.zk_compiler_version().is_some() => { + Err(ApiError::BogusZkCompilerVersion) + } + _ => Ok(()), + } + } + /// Add a contract verification job to the queue if the requested contract wasn't previously verified. + // FIXME: this doesn't seem to check that the contract isn't verified; should it? #[tracing::instrument(skip(self_, request))] pub async fn verification( State(self_): State>, Json(request): Json, - ) -> Response { + ) -> ApiResult { let method_latency = METRICS.call[&"contract_verification"].start(); - if let Err(res) = Self::validate_contract_verification_query(&request) { - return res; + Self::validate_contract_verification_query(&request)?; + + let is_compilation_supported = self_ + .supported_compilers + .get(|supported| supported.contain(&request.compiler_versions)) + .await?; + if !is_compilation_supported { + return Err(ApiError::UnsupportedCompilerVersions); } + let mut storage = self_ .master_connection_pool .connection_tagged("api") - .await - .unwrap(); - - if !storage + .await?; + let deployment_info = storage .storage_logs_dal() - .is_contract_deployed_at_address(request.contract_address) - .await - { - return bad_request("There is no deployed contract on this address"); - } + .filter_deployed_contracts(iter::once(request.contract_address), None) + .await?; + let &(_, bytecode_hash) = deployment_info + .get(&request.contract_address) + .ok_or(ApiError::NoDeployedContract)?; + let bytecode_marker = BytecodeMarker::new(bytecode_hash).with_context(|| { + format!( + "unknown bytecode marker for bytecode hash {bytecode_hash:?} at address {:?}", + request.contract_address + ) + })?; + Self::validate_compilers(&request.compiler_versions, bytecode_marker)?; let request_id = storage .contract_verification_dal() - .add_contract_verification_request(request) - .await - .unwrap(); - + .add_contract_verification_request(&request) + .await?; method_latency.observe(); - ok_json(request_id) + Ok(Json(request_id)) } #[tracing::instrument(skip(self_))] pub async fn verification_request_status( State(self_): State>, id: Path, - ) -> Response { + ) -> ApiResult { let method_latency = METRICS.call[&"contract_verification_request_status"].start(); let status = self_ .replica_connection_pool .connection_tagged("api") - .await - .unwrap() + .await? .contract_verification_dal() .get_verification_request_status(*id) - .await - .unwrap(); + .await? + .ok_or(ApiError::RequestNotFound)?; method_latency.observe(); - match status { - Some(status) => ok_json(status), - None => not_found(), - } + Ok(Json(status)) } #[tracing::instrument(skip(self_))] - pub async fn zksolc_versions(State(self_): State>) -> Response { + pub async fn zksolc_versions(State(self_): State>) -> ApiResult> { let method_latency = METRICS.call[&"contract_verification_zksolc_versions"].start(); let versions = self_ - .replica_connection_pool - .connection_tagged("api") - .await - .unwrap() - .contract_verification_dal() - .get_zksolc_versions() - .await - .unwrap(); - + .supported_compilers + .get(|supported| supported.zksolc.clone()) + .await?; method_latency.observe(); - ok_json(versions) + Ok(Json(versions)) } #[tracing::instrument(skip(self_))] - pub async fn solc_versions(State(self_): State>) -> Response { + pub async fn solc_versions(State(self_): State>) -> ApiResult> { let method_latency = METRICS.call[&"contract_verification_solc_versions"].start(); let versions = self_ - .replica_connection_pool - .connection_tagged("api") - .await - .unwrap() - .contract_verification_dal() - .get_solc_versions() - .await - .unwrap(); - + .supported_compilers + .get(|supported| supported.solc.clone()) + .await?; method_latency.observe(); - ok_json(versions) + Ok(Json(versions)) } #[tracing::instrument(skip(self_))] - pub async fn zkvyper_versions(State(self_): State>) -> Response { + pub async fn zkvyper_versions(State(self_): State>) -> ApiResult> { let method_latency = METRICS.call[&"contract_verification_zkvyper_versions"].start(); let versions = self_ - .replica_connection_pool - .connection_tagged("api") - .await - .unwrap() - .contract_verification_dal() - .get_zkvyper_versions() - .await - .unwrap(); - + .supported_compilers + .get(|supported| supported.zkvyper.clone()) + .await?; method_latency.observe(); - ok_json(versions) + Ok(Json(versions)) } #[tracing::instrument(skip(self_))] - pub async fn vyper_versions(State(self_): State>) -> Response { + pub async fn vyper_versions(State(self_): State>) -> ApiResult> { let method_latency = METRICS.call[&"contract_verification_vyper_versions"].start(); let versions = self_ - .replica_connection_pool - .connection_tagged("api") - .await - .unwrap() - .contract_verification_dal() - .get_vyper_versions() - .await - .unwrap(); - + .supported_compilers + .get(|supported| supported.vyper.clone()) + .await?; method_latency.observe(); - ok_json(versions) + Ok(Json(versions)) } #[tracing::instrument(skip(self_))] pub async fn verification_info( State(self_): State>, address: Path
, - ) -> Response { + ) -> ApiResult { let method_latency = METRICS.call[&"contract_verification_info"].start(); - let info = self_ .replica_connection_pool .connection_tagged("api") - .await - .unwrap() + .await? .contract_verification_dal() .get_contract_verification_info(*address) - .await - .unwrap(); - + .await? + .ok_or(ApiError::VerificationInfoNotFound)?; method_latency.observe(); - match info { - Some(info) => ok_json(info), - None => not_found(), - } + Ok(Json(info)) } } diff --git a/core/node/contract_verification_server/src/cache.rs b/core/node/contract_verification_server/src/cache.rs new file mode 100644 index 000000000000..c8e367515287 --- /dev/null +++ b/core/node/contract_verification_server/src/cache.rs @@ -0,0 +1,122 @@ +use std::{ + collections::HashSet, + time::{Duration, Instant}, +}; + +use tokio::sync::RwLock; +use zksync_dal::{Connection, ConnectionPool, Core, CoreDal, DalError}; +use zksync_types::contract_verification_api::CompilerVersions; + +/// Compiler versions supported by the contract verifier. +#[derive(Debug, Clone)] +pub(crate) struct SupportedCompilerVersions { + pub solc: HashSet, + pub zksolc: HashSet, + pub vyper: HashSet, + pub zkvyper: HashSet, +} + +impl SupportedCompilerVersions { + /// Checks whether the supported compilers include ones specified in a request. + pub fn contain(&self, versions: &CompilerVersions) -> bool { + match versions { + CompilerVersions::Solc { + compiler_solc_version, + compiler_zksolc_version, + } => { + self.solc.contains(compiler_solc_version) + && compiler_zksolc_version + .as_ref() + .map_or(true, |ver| self.zksolc.contains(ver)) + } + CompilerVersions::Vyper { + compiler_vyper_version, + compiler_zkvyper_version, + } => { + self.vyper.contains(compiler_vyper_version) + && compiler_zkvyper_version + .as_ref() + .map_or(true, |ver| self.zkvyper.contains(ver)) + } + } + } +} + +impl SupportedCompilerVersions { + async fn new(connection: &mut Connection<'_, Core>) -> Result { + let solc = connection + .contract_verification_dal() + .get_solc_versions() + .await?; + let zksolc = connection + .contract_verification_dal() + .get_zksolc_versions() + .await?; + let vyper = connection + .contract_verification_dal() + .get_vyper_versions() + .await?; + let zkvyper = connection + .contract_verification_dal() + .get_zkvyper_versions() + .await?; + Ok(Self { + solc: solc.into_iter().collect(), + zksolc: zksolc.into_iter().collect(), + vyper: vyper.into_iter().collect(), + zkvyper: zkvyper.into_iter().collect(), + }) + } +} + +/// Cache for compiler versions supported by the contract verifier. +#[derive(Debug)] +pub(crate) struct SupportedCompilersCache { + connection_pool: ConnectionPool, + inner: RwLock>, +} + +impl SupportedCompilersCache { + const CACHE_UPDATE_INTERVAL: Duration = Duration::from_secs(10); + + pub fn new(connection_pool: ConnectionPool) -> Self { + Self { + connection_pool, + inner: RwLock::new(None), + } + } + + fn get_cached( + cache: Option<&(SupportedCompilerVersions, Instant)>, + action: impl FnOnce(&SupportedCompilerVersions) -> R, + ) -> Option { + cache.and_then(|(versions, updated_at)| { + (updated_at.elapsed() <= Self::CACHE_UPDATE_INTERVAL).then(|| action(versions)) + }) + } + + pub async fn get( + &self, + action: impl Fn(&SupportedCompilerVersions) -> R, + ) -> Result { + let output = Self::get_cached(self.inner.read().await.as_ref(), &action); + if let Some(output) = output { + return Ok(output); + } + + // We don't want to hold an exclusive lock while querying Postgres. + let supported = { + let mut connection = self.connection_pool.connection_tagged("api").await?; + let mut db_transaction = connection + .transaction_builder()? + .set_readonly() + .build() + .await?; + SupportedCompilerVersions::new(&mut db_transaction).await? + }; + let output = action(&supported); + // Another task may have written to the cache already, but we should be fine with updating it again. + *self.inner.write().await = Some((supported, Instant::now())); + Ok(output) + } +} diff --git a/core/node/contract_verification_server/src/lib.rs b/core/node/contract_verification_server/src/lib.rs index eea45f8564bf..912cec55f0b8 100644 --- a/core/node/contract_verification_server/src/lib.rs +++ b/core/node/contract_verification_server/src/lib.rs @@ -1,21 +1,24 @@ +use std::net::SocketAddr; + use anyhow::Context as _; use tokio::sync::watch; -use zksync_config::ContractVerifierConfig; use zksync_dal::ConnectionPool; use self::api_decl::RestApi; mod api_decl; mod api_impl; +mod cache; mod metrics; +#[cfg(test)] +mod tests; pub async fn start_server( master_connection_pool: ConnectionPool, replica_connection_pool: ConnectionPool, - config: ContractVerifierConfig, + bind_address: SocketAddr, mut stop_receiver: watch::Receiver, ) -> anyhow::Result<()> { - let bind_address = config.bind_addr(); let api = RestApi::new(master_connection_pool, replica_connection_pool).into_router(); let listener = tokio::net::TcpListener::bind(bind_address) diff --git a/core/node/contract_verification_server/src/tests.rs b/core/node/contract_verification_server/src/tests.rs new file mode 100644 index 000000000000..b7b0d3e8efb4 --- /dev/null +++ b/core/node/contract_verification_server/src/tests.rs @@ -0,0 +1,356 @@ +//! Tests for contract verification API server. + +use std::{str, time::Duration}; + +use axum::{ + body::Body, + http::{header, Method, Request, Response, StatusCode}, +}; +use http_body_util::BodyExt as _; +use test_casing::test_casing; +use tower::ServiceExt; +use zksync_dal::{Connection, Core, CoreDal}; +use zksync_node_test_utils::create_l2_block; +use zksync_types::{ + contract_verification_api::CompilerVersions, get_code_key, Address, L2BlockNumber, + ProtocolVersion, StorageLog, +}; +use zksync_utils::bytecode::{hash_bytecode, hash_evm_bytecode, BytecodeMarker}; + +use super::*; +use crate::api_impl::ApiError; + +const SOLC_VERSION: &str = "0.8.27"; +const ZKSOLC_VERSION: &str = "1.5.6"; + +async fn prepare_storage(storage: &mut Connection<'_, Core>) { + 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 + .contract_verification_dal() + .set_solc_versions(&[SOLC_VERSION.to_owned()]) + .await + .unwrap(); + storage + .contract_verification_dal() + .set_zksolc_versions(&[ZKSOLC_VERSION.to_owned()]) + .await + .unwrap(); +} + +async fn mock_deploy_contract( + storage: &mut Connection<'_, Core>, + address: Address, + kind: BytecodeMarker, +) { + let bytecode_hash = match kind { + BytecodeMarker::EraVm => hash_bytecode(&[0; 32]), + BytecodeMarker::Evm => hash_evm_bytecode(&[0; 96]), + }; + let deploy_log = StorageLog::new_write_log(get_code_key(&address), bytecode_hash); + storage + .storage_logs_dal() + .append_storage_logs(L2BlockNumber(0), &[deploy_log]) + .await + .unwrap() +} + +fn post_request(body: &serde_json::Value) -> Request { + Request::builder() + .method(Method::POST) + .uri("/contract_verification") + .header(header::CONTENT_TYPE, "application/json") + .body(Body::from(serde_json::to_vec(body).unwrap())) + .unwrap() +} + +async fn json_response(response: Response) -> serde_json::Value { + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(header::CONTENT_TYPE).unwrap(), + "application/json" + ); + let response = response.into_body(); + let response = response.collect().await.unwrap().to_bytes(); + serde_json::from_slice(&response).unwrap() +} + +#[tokio::test] +async fn getting_compiler_versions() { + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + + let router = RestApi::new(pool.clone(), pool).into_router(); + let req = Request::builder() + .method(Method::GET) + .uri("/contract_verification/zksolc_versions") + .body(Body::empty()) + .unwrap(); + let response = router.clone().oneshot(req).await.unwrap(); + let versions = json_response(response).await; + assert_eq!(versions, serde_json::json!([ZKSOLC_VERSION])); + + let req = Request::builder() + .method(Method::GET) + .uri("/contract_verification/solc_versions") + .body(Body::empty()) + .unwrap(); + let response = router.oneshot(req).await.unwrap(); + let versions = json_response(response).await; + assert_eq!(versions, serde_json::json!([SOLC_VERSION])); +} + +#[test_casing(2, [BytecodeMarker::EraVm, BytecodeMarker::Evm])] +#[tokio::test] +async fn submitting_request(bytecode_kind: BytecodeMarker) { + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + + let address = Address::repeat_byte(0x23); + let verification_request = serde_json::json!({ + "contractAddress": address, + "sourceCode": "contract Test {}", + "contractName": "Test", + "compilerZksolcVersion": match bytecode_kind { + BytecodeMarker::EraVm => Some(ZKSOLC_VERSION), + BytecodeMarker::Evm => None, + }, + "compilerSolcVersion": SOLC_VERSION, + "optimizationUsed": true, + }); + + let router = RestApi::new(pool.clone(), pool).into_router(); + let response = router + .clone() + .oneshot(post_request(&verification_request)) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); // the address is not deployed to + let error_message = response.collect().await.unwrap().to_bytes(); + let error_message = str::from_utf8(&error_message).unwrap(); + assert_eq!(error_message, ApiError::NoDeployedContract.message()); + + mock_deploy_contract(&mut storage, address, bytecode_kind).await; + + let response = router + .clone() + .oneshot(post_request(&verification_request)) + .await + .unwrap(); + let id = json_response(response).await; + assert_eq!(id, serde_json::json!(1)); + + let request = storage + .contract_verification_dal() + .get_next_queued_verification_request(Duration::from_secs(600)) + .await + .unwrap() + .expect("request not persisted"); + assert_eq!(request.id, 1); + assert_eq!(request.req.contract_address, address); + assert_eq!( + request.req.compiler_versions, + CompilerVersions::Solc { + compiler_zksolc_version: match bytecode_kind { + BytecodeMarker::EraVm => Some(ZKSOLC_VERSION.to_owned()), + BytecodeMarker::Evm => None, + }, + compiler_solc_version: SOLC_VERSION.to_owned(), + } + ); + assert_eq!(request.req.contract_name, "Test"); + assert!(request.req.optimization_used); + + let req = Request::builder() + .method(Method::GET) + .uri("/contract_verification/1") + .body(Body::empty()) + .unwrap(); + let response = router.oneshot(req).await.unwrap(); + let request_status = json_response(response).await; + assert_eq!(request_status["status"], "in_progress"); +} + +#[test_casing(2, [BytecodeMarker::EraVm, BytecodeMarker::Evm])] +#[tokio::test] +async fn submitting_request_with_invalid_compiler_type(bytecode_kind: BytecodeMarker) { + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + + let address = Address::repeat_byte(0x23); + mock_deploy_contract(&mut storage, address, bytecode_kind).await; + + let verification_request = serde_json::json!({ + "contractAddress": address, + "sourceCode": "contract Test {}", + "contractName": "Test", + // Intentionally incorrect versions "shape" + "compilerZksolcVersion": match bytecode_kind { + BytecodeMarker::Evm => Some(ZKSOLC_VERSION), + BytecodeMarker::EraVm => None, + }, + "compilerSolcVersion": SOLC_VERSION, + "optimizationUsed": true, + }); + let router = RestApi::new(pool.clone(), pool).into_router(); + let response = router + .oneshot(post_request(&verification_request)) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let error_message = response.collect().await.unwrap().to_bytes(); + let error_message = str::from_utf8(&error_message).unwrap(); + let expected_message = match bytecode_kind { + BytecodeMarker::Evm => ApiError::BogusZkCompilerVersion.message(), + BytecodeMarker::EraVm => ApiError::MissingZkCompilerVersion.message(), + }; + assert_eq!(error_message, expected_message); +} + +#[test_casing(2, [BytecodeMarker::EraVm, BytecodeMarker::Evm])] +#[tokio::test] +async fn submitting_request_with_unsupported_solc(bytecode_kind: BytecodeMarker) { + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + + let address = Address::repeat_byte(0x23); + mock_deploy_contract(&mut storage, address, bytecode_kind).await; + + let verification_request = serde_json::json!({ + "contractAddress": address, + "sourceCode": "contract Test {}", + "contractName": "Test", + "compilerZksolcVersion": match bytecode_kind { + BytecodeMarker::Evm => None, + BytecodeMarker::EraVm => Some(ZKSOLC_VERSION), + }, + "compilerSolcVersion": "1.0.0", + "optimizationUsed": true, + }); + let router = RestApi::new(pool.clone(), pool).into_router(); + let response = router + .oneshot(post_request(&verification_request)) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let error_message = response.collect().await.unwrap().to_bytes(); + let error_message = str::from_utf8(&error_message).unwrap(); + assert_eq!( + error_message, + ApiError::UnsupportedCompilerVersions.message() + ); +} + +#[tokio::test] +async fn submitting_request_with_unsupported_zksolc() { + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + + let address = Address::repeat_byte(0x23); + mock_deploy_contract(&mut storage, address, BytecodeMarker::EraVm).await; + + let verification_request = serde_json::json!({ + "contractAddress": address, + "sourceCode": "contract Test {}", + "contractName": "Test", + "compilerZksolcVersion": "1000.0.0", + "compilerSolcVersion": SOLC_VERSION, + "optimizationUsed": true, + }); + let router = RestApi::new(pool.clone(), pool).into_router(); + let response = router + .oneshot(post_request(&verification_request)) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let error_message = response.collect().await.unwrap().to_bytes(); + let error_message = str::from_utf8(&error_message).unwrap(); + assert_eq!( + error_message, + ApiError::UnsupportedCompilerVersions.message() + ); +} + +#[tokio::test] +async fn querying_missing_request() { + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + let router = RestApi::new(pool.clone(), pool).into_router(); + + let req = Request::builder() + .method(Method::GET) + .uri("/contract_verification/1") + .body(Body::empty()) + .unwrap(); + let response = router.oneshot(req).await.unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + let error_message = response.collect().await.unwrap().to_bytes(); + let error_message = str::from_utf8(&error_message).unwrap(); + assert_eq!(error_message, ApiError::RequestNotFound.message()); +} + +#[tokio::test] +async fn querying_missing_verification_info() { + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + let router = RestApi::new(pool.clone(), pool).into_router(); + + let req = Request::builder() + .method(Method::GET) + .uri("/contract_verification/info/0x2323232323232323232323232323232323232323") + .body(Body::empty()) + .unwrap(); + let response = router.oneshot(req).await.unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + let error_message = response.collect().await.unwrap().to_bytes(); + let error_message = str::from_utf8(&error_message).unwrap(); + assert_eq!(error_message, ApiError::VerificationInfoNotFound.message()); +} + +#[tokio::test] +async fn mismatched_compiler_type() { + let pool = ConnectionPool::test_pool().await; + let mut storage = pool.connection().await.unwrap(); + prepare_storage(&mut storage).await; + let address = Address::repeat_byte(0x23); + mock_deploy_contract(&mut storage, address, BytecodeMarker::EraVm).await; + + let verification_request = serde_json::json!({ + "contractAddress": address, + "sourceCode": "contract Test {}", + "contractName": "Test", + "compilerVyperVersion": "1.0.1", + "optimizationUsed": true, + }); + + let router = RestApi::new(pool.clone(), pool).into_router(); + let response = router + .oneshot(post_request(&verification_request)) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let error_message = response.collect().await.unwrap().to_bytes(); + let error_message = str::from_utf8(&error_message).unwrap(); + assert_eq!(error_message, ApiError::IncorrectCompilerVersions.message()); +} diff --git a/core/node/node_framework/src/implementations/layers/contract_verification_api.rs b/core/node/node_framework/src/implementations/layers/contract_verification_api.rs index 3f1f76cc1c12..2ca7cc25a1fd 100644 --- a/core/node/node_framework/src/implementations/layers/contract_verification_api.rs +++ b/core/node/node_framework/src/implementations/layers/contract_verification_api.rs @@ -69,7 +69,7 @@ impl Task for ContractVerificationApiTask { zksync_contract_verification_server::start_server( self.master_pool, self.replica_pool, - self.config, + self.config.bind_addr(), stop_receiver.0, ) .await diff --git a/zkstack_cli/Cargo.lock b/zkstack_cli/Cargo.lock index 0f0657c60278..a9089719714d 100644 --- a/zkstack_cli/Cargo.lock +++ b/zkstack_cli/Cargo.lock @@ -1097,7 +1097,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", - "serde", ] [[package]] @@ -6808,11 +6807,6 @@ dependencies = [ "rand", "secrecy", "serde", - "strum", - "strum_macros", - "time", - "url", - "vise", "zksync_basic_types", "zksync_concurrency", "zksync_consensus_utils", @@ -6962,7 +6956,6 @@ dependencies = [ "secrecy", "serde_json", "serde_yaml", - "time", "tracing", "zksync_basic_types", "zksync_config",