From 411bcad4d3845b5baf99af183caa97e19fd2b814 Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Wed, 6 Nov 2024 18:20:48 +0200 Subject: [PATCH] Cache supported compiler versions --- .../src/api_decl.rs | 5 + .../src/api_impl.rs | 51 ++++---- .../contract_verification_server/src/cache.rs | 122 ++++++++++++++++++ .../contract_verification_server/src/lib.rs | 1 + 4 files changed, 154 insertions(+), 25 deletions(-) create mode 100644 core/node/contract_verification_server/src/cache.rs diff --git a/core/node/contract_verification_server/src/api_decl.rs b/core/node/contract_verification_server/src/api_decl.rs index 98d4e925b91f..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(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 2ac56925b508..94be65673bad 100644 --- a/core/node/contract_verification_server/src/api_impl.rs +++ b/core/node/contract_verification_server/src/api_impl.rs @@ -1,4 +1,4 @@ -use std::{iter, sync::Arc}; +use std::{collections::HashSet, iter, sync::Arc}; use anyhow::Context as _; use axum::{ @@ -21,6 +21,7 @@ use super::{api_decl::RestApi, metrics::METRICS}; #[derive(Debug)] pub(crate) enum ApiError { IncorrectCompilerVersions, + UnsupportedCompilerVersions, MissingZkCompilerVersion, BogusZkCompilerVersion, NoDeployedContract, @@ -45,6 +46,7 @@ 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", @@ -59,10 +61,13 @@ 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:#}"); @@ -111,6 +116,14 @@ impl RestApi { let method_latency = METRICS.call[&"contract_verification"].start(); 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") @@ -158,56 +171,44 @@ impl RestApi { } #[tracing::instrument(skip(self_))] - pub async fn zksolc_versions(State(self_): State>) -> ApiResult> { + 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? - .contract_verification_dal() - .get_zksolc_versions() + .supported_compilers + .get(|supported| supported.zksolc.clone()) .await?; method_latency.observe(); Ok(Json(versions)) } #[tracing::instrument(skip(self_))] - pub async fn solc_versions(State(self_): State>) -> ApiResult> { + 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? - .contract_verification_dal() - .get_solc_versions() + .supported_compilers + .get(|supported| supported.solc.clone()) .await?; method_latency.observe(); Ok(Json(versions)) } #[tracing::instrument(skip(self_))] - pub async fn zkvyper_versions(State(self_): State>) -> ApiResult> { + 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? - .contract_verification_dal() - .get_zkvyper_versions() + .supported_compilers + .get(|supported| supported.zkvyper.clone()) .await?; method_latency.observe(); Ok(Json(versions)) } #[tracing::instrument(skip(self_))] - pub async fn vyper_versions(State(self_): State>) -> ApiResult> { + 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? - .contract_verification_dal() - .get_vyper_versions() + .supported_compilers + .get(|supported| supported.vyper.clone()) .await?; method_latency.observe(); Ok(Json(versions)) 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 80c5ce3f9bda..912cec55f0b8 100644 --- a/core/node/contract_verification_server/src/lib.rs +++ b/core/node/contract_verification_server/src/lib.rs @@ -8,6 +8,7 @@ use self::api_decl::RestApi; mod api_decl; mod api_impl; +mod cache; mod metrics; #[cfg(test)] mod tests;