diff --git a/smart-contract-verifier/smart-contract-verifier-server/src/services/common.rs b/smart-contract-verifier/smart-contract-verifier-server/src/services/common.rs index 1b847602e..0f8cf570c 100644 --- a/smart-contract-verifier/smart-contract-verifier-server/src/services/common.rs +++ b/smart-contract-verifier/smart-contract-verifier-server/src/services/common.rs @@ -1,7 +1,9 @@ use crate::settings::{FetcherSettings, S3FetcherSettings}; use cron::Schedule; use s3::{creds::Credentials, Bucket, Region}; -use smart_contract_verifier::{Fetcher, FileValidator, ListFetcher, S3Fetcher, Version}; +use smart_contract_verifier::{ + DetailedVersion, Fetcher, FileValidator, ListFetcher, S3Fetcher, Version, +}; use std::{path::PathBuf, str::FromStr, sync::Arc}; pub async fn initialize_fetcher( @@ -70,3 +72,31 @@ fn new_bucket(settings: &S3FetcherSettings) -> anyhow::Result> { )?); Ok(bucket) } + +/// Normalizes the requested compiler version by matching it against a list of known compiler versions. +/// The function takes a [`DetailedVersion`] from the request and attempts to find a corresponding version +/// from the known list, allowing for cases where the requested commit hash is either a prefix or a longer +/// version than the known one. If a matching version is found, it is returned; otherwise, a +/// [`Status::invalid_argument`] error is returned. +pub fn normalize_request_compiler_version( + compilers: &[DetailedVersion], + request_compiler_version: &DetailedVersion, +) -> Result { + let corresponding_known_compiler_version = compilers.iter().find(|&version| { + return version.version() == request_compiler_version.version() + && version.date() == request_compiler_version.date() + && (version + .commit() + .starts_with(request_compiler_version.commit()) + || request_compiler_version + .commit() + .starts_with(version.commit())); + }); + if let Some(compiler_version) = corresponding_known_compiler_version { + Ok(compiler_version.clone()) + } else { + Err(tonic::Status::invalid_argument(format!( + "Compiler version not found: {request_compiler_version}" + ))) + } +} diff --git a/smart-contract-verifier/smart-contract-verifier-server/src/services/solidity_verifier.rs b/smart-contract-verifier/smart-contract-verifier-server/src/services/solidity_verifier.rs index 8afe0caa4..148b7bb9a 100644 --- a/smart-contract-verifier/smart-contract-verifier-server/src/services/solidity_verifier.rs +++ b/smart-contract-verifier/smart-contract-verifier-server/src/services/solidity_verifier.rs @@ -107,7 +107,14 @@ impl SolidityVerifier for SolidityVerifierService { "Request details" ); - let result = solidity::multi_part::verify(self.client.clone(), request.try_into()?).await; + let mut verification_request: solidity::multi_part::VerificationRequest = + request.try_into()?; + verification_request.compiler_version = common::normalize_request_compiler_version( + &self.client.compilers().all_versions(), + &verification_request.compiler_version, + )?; + + let result = solidity::multi_part::verify(self.client.clone(), verification_request).await; let response = if let Ok(verification_success) = result { tracing::info!(match_type=?verification_success.match_type, "Request processed successfully"); @@ -169,7 +176,7 @@ impl SolidityVerifier for SolidityVerifierService { "Request details" ); - let verification_request = { + let mut verification_request: solidity::standard_json::VerificationRequest = { let request: Result<_, StandardJsonParseError> = request.try_into(); if let Err(err) = request { match err { @@ -186,6 +193,11 @@ impl SolidityVerifier for SolidityVerifierService { } request.unwrap() }; + verification_request.compiler_version = common::normalize_request_compiler_version( + &self.client.compilers().all_versions(), + &verification_request.compiler_version, + )?; + let result = solidity::standard_json::verify(self.client.clone(), verification_request).await; diff --git a/smart-contract-verifier/smart-contract-verifier-server/src/services/vyper_verifier.rs b/smart-contract-verifier/smart-contract-verifier-server/src/services/vyper_verifier.rs index 1aada964e..ace949957 100644 --- a/smart-contract-verifier/smart-contract-verifier-server/src/services/vyper_verifier.rs +++ b/smart-contract-verifier/smart-contract-verifier-server/src/services/vyper_verifier.rs @@ -92,7 +92,14 @@ impl VyperVerifier for VyperVerifierService { "Request details" ); - let result = vyper::multi_part::verify(self.client.clone(), request.try_into()?).await; + let mut verification_request: vyper::multi_part::VerificationRequest = + request.try_into()?; + verification_request.compiler_version = common::normalize_request_compiler_version( + &self.client.compilers().all_versions(), + &verification_request.compiler_version, + )?; + + let result = vyper::multi_part::verify(self.client.clone(), verification_request).await; let response = if let Ok(verification_success) = result { tracing::info!(match_type=?verification_success.match_type, "Request processed successfully"); @@ -154,7 +161,7 @@ impl VyperVerifier for VyperVerifierService { "Request details" ); - let verification_request = { + let mut verification_request: vyper::standard_json::VerificationRequest = { let request: Result<_, StandardJsonParseError> = request.try_into(); if let Err(err) = request { match err { @@ -169,6 +176,11 @@ impl VyperVerifier for VyperVerifierService { } request.unwrap() }; + verification_request.compiler_version = common::normalize_request_compiler_version( + &self.client.compilers().all_versions(), + &verification_request.compiler_version, + )?; + let result = vyper::standard_json::verify(self.client.clone(), verification_request).await; let response = if let Ok(verification_success) = result { diff --git a/smart-contract-verifier/smart-contract-verifier-server/tests/solidity.rs b/smart-contract-verifier/smart-contract-verifier-server/tests/solidity.rs index 45067a234..2186fc574 100644 --- a/smart-contract-verifier/smart-contract-verifier-server/tests/solidity.rs +++ b/smart-contract-verifier/smart-contract-verifier-server/tests/solidity.rs @@ -56,7 +56,10 @@ async fn test_setup(test_case: &T, bytecode_type: BytecodeType) -> .await } -async fn test_success(test_case: &T, bytecode_type: BytecodeType) { +async fn get_verification_response( + test_case: &T, + bytecode_type: BytecodeType, +) -> VerifyResponse { let response = test_setup(test_case, bytecode_type).await; if !response.status().is_success() { let status = response.status(); @@ -77,6 +80,18 @@ async fn test_success(test_case: &T, bytecode_type: BytecodeType) { "Verification extra_data is absent" ); + assert!( + verification_response.source.is_some(), + "Verification source is absent" + ); + + verification_response +} + +fn validate_verification_response( + test_case: &T, + verification_response: VerifyResponse, +) { let source = verification_response .source .expect("Verification source is absent"); @@ -260,6 +275,11 @@ async fn test_success(test_case: &T, bytecode_type: BytecodeType) { } } +async fn test_success(test_case: &T, bytecode_type: BytecodeType) { + let verification_response = get_verification_response(test_case, bytecode_type).await; + validate_verification_response(test_case, verification_response); +} + async fn _test_failure( test_case: &T, bytecode_type: BytecodeType, @@ -371,4 +391,52 @@ mod success_tests { test_success(&test_case, BytecodeType::CreationInput).await; test_success(&test_case, BytecodeType::DeployedBytecode).await; } + + #[tokio::test] + async fn flattened_accepts_partially_matching_compiler_version_commit_hashes() { + // provided commit hash is a prefix of the one used in the list + let initial_test_case = solidity_types::from_file::("simple_storage"); + let test_case = { + let mut test_case = initial_test_case.clone(); + test_case.compiler_version.pop(); + test_case + }; + let verification_response = + get_verification_response(&test_case, BytecodeType::CreationInput).await; + validate_verification_response(&initial_test_case, verification_response); + + // the commit hash from the list is a prefix of the provided one + let test_case = { + let mut test_case = initial_test_case.clone(); + test_case.compiler_version.push_str("1234"); + test_case + }; + let verification_response = + get_verification_response(&test_case, BytecodeType::CreationInput).await; + validate_verification_response(&initial_test_case, verification_response); + } + + #[tokio::test] + async fn standard_json_accepts_partially_matching_compiler_version_commit_hashes() { + // provided commit hash is a prefix of the one used in the list + let initial_test_case = solidity_types::from_file::("cancun"); + let test_case = { + let mut test_case = initial_test_case.clone(); + test_case.compiler_version.pop(); + test_case + }; + let verification_response = + get_verification_response(&test_case, BytecodeType::CreationInput).await; + validate_verification_response(&initial_test_case, verification_response); + + // the commit hash from the list is a prefix of the provided one + let test_case = { + let mut test_case = initial_test_case.clone(); + test_case.compiler_version.push_str("1234"); + test_case + }; + let verification_response = + get_verification_response(&test_case, BytecodeType::CreationInput).await; + validate_verification_response(&initial_test_case, verification_response); + } } diff --git a/smart-contract-verifier/smart-contract-verifier-server/tests/vyper.rs b/smart-contract-verifier/smart-contract-verifier-server/tests/vyper.rs index ac2a58df4..85b56ef44 100644 --- a/smart-contract-verifier/smart-contract-verifier-server/tests/vyper.rs +++ b/smart-contract-verifier/smart-contract-verifier-server/tests/vyper.rs @@ -58,8 +58,8 @@ async fn test_setup(test_case: &T) -> ServiceResponse { .await } -async fn test_success(test_case: impl TestCase) { - let response = test_setup(&test_case).await; +async fn get_verification_response(test_case: &T) -> VerifyResponse { + let response = test_setup(test_case).await; if !response.status().is_success() { let status = response.status(); let body = read_body(response).await; @@ -79,9 +79,19 @@ async fn test_success(test_case: impl TestCase) { "Verification extra_data is absent" ); - let verification_result = verification_response - .source - .expect("Verification source is absent"); + assert!( + verification_response.source.is_some(), + "Verification source is absent" + ); + + verification_response +} + +fn validate_verification_response( + test_case: &T, + verification_response: VerifyResponse, +) { + let verification_result = verification_response.source.unwrap(); // Vyper always results in partial matches, as currently there is no way to // check if the source code is exact. @@ -241,6 +251,11 @@ async fn test_success(test_case: impl TestCase) { } } +async fn test_success(test_case: impl TestCase) { + let verification_response = get_verification_response(&test_case).await; + validate_verification_response(&test_case, verification_response); +} + async fn test_failure(test_case: impl TestCase, expected_message: &str) { let response = test_setup(&test_case).await; @@ -289,7 +304,10 @@ async fn test_error(test_case: impl TestCase, expected_status: StatusCode, expec } mod flattened { - use super::{test_error, test_failure, test_success, vyper_types}; + use super::{ + get_verification_response, test_error, test_failure, test_success, + validate_verification_response, vyper_types, + }; use actix_web::http::StatusCode; use vyper_types::Flattened; @@ -366,6 +384,28 @@ mod flattened { test_case.use_deployed_bytecode = true; test_success(test_case).await; } + + #[tokio::test] + async fn accepts_partially_matching_compiler_version_commit_hashes() { + let initial_test_case = vyper_types::from_file::("simple"); + // provided commit hash is a prefix of the one used in the list + let test_case = { + let mut test_case = initial_test_case.clone(); + test_case.compiler_version.pop(); + test_case + }; + let verification_response = get_verification_response(&test_case).await; + validate_verification_response(&initial_test_case, verification_response); + + // the commit hash from the list is a prefix of the provided one + let test_case = { + let mut test_case = initial_test_case.clone(); + test_case.compiler_version.push_str("1234"); + test_case + }; + let verification_response = get_verification_response(&test_case).await; + validate_verification_response(&initial_test_case, verification_response); + } } mod multi_part { @@ -402,7 +442,9 @@ mod multi_part { } mod standard_json { - use super::{test_success, vyper_types}; + use super::{ + get_verification_response, test_success, validate_verification_response, vyper_types, + }; use crate::{test_error, test_failure}; use actix_web::http::StatusCode; use vyper_types::StandardJson; @@ -472,4 +514,27 @@ mod standard_json { vyper_types::from_file::("standard_json_interfaces_in_sources"); test_success(test_case.clone()).await; } + + #[tokio::test] + async fn accepts_partially_matching_compiler_version_commit_hashes() { + let initial_test_case = + vyper_types::from_file::("standard_json_interfaces_in_sources"); + // provided commit hash is a prefix of the one used in the list + let test_case = { + let mut test_case = initial_test_case.clone(); + test_case.compiler_version.pop(); + test_case + }; + let verification_response = get_verification_response(&test_case).await; + validate_verification_response(&initial_test_case, verification_response); + + // the commit hash from the list is a prefix of the provided one + let test_case = { + let mut test_case = initial_test_case.clone(); + test_case.compiler_version.push_str("1234"); + test_case + }; + let verification_response = get_verification_response(&test_case).await; + validate_verification_response(&initial_test_case, verification_response); + } }