Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interaction with Casper smart-contract source code verification service #144

Open
wants to merge 33 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2b9d3e7
Added verify-contract command
kchudy Dec 22, 2023
b9149a9
Added verify contract to lib
kchudy Dec 22, 2023
f879378
Added function for creating archive
kchudy Jan 2, 2024
d0c6241
Added arg parsing
kchudy Jan 2, 2024
bf44ebb
Added verificator api client
kchudy Jan 2, 2024
a6e6360
Added waiting for verification to finish
kchudy Jan 3, 2024
ff261d5
Switched to deploy hash
kchudy Jan 3, 2024
e4c24de
Updated comments
kchudy Jan 3, 2024
ffb53e3
Added getting verification details
kchudy Jan 5, 2024
4d756f6
Imploved error handling
kchudy Jan 8, 2024
104d4cb
Improved logging
kchudy Jan 8, 2024
f07b7ec
Refactored verification function
kchudy Jan 8, 2024
d72f852
Initialized API client
kchudy Jan 8, 2024
4266492
Adopt new datatypes; use reqwest
moubctez Jan 22, 2024
979ed97
Cleanup
moubctez Jan 22, 2024
2392104
Fix result type
moubctez Jan 22, 2024
4f321b5
Fixes and cleanups
moubctez Jan 22, 2024
21012c4
Sync structs with verificator
moubctez Jan 23, 2024
5e43e05
More retries
moubctez Jan 24, 2024
5a3af7c
Increase number of retries
moubctez Jan 25, 2024
af1c39b
Shorten message
moubctez Jan 25, 2024
d9caa43
Resolve comments
moubctez Apr 10, 2024
3f944b1
Update src/verify_contract.rs
moubctez Apr 11, 2024
ad4bf50
cargo fmt
moubctez Apr 11, 2024
e395eae
Update vergen to v8. This resolves audit error.
moubctez Apr 19, 2024
b59f117
Merge branch 'dev' of github.com:casper-ecosystem/casper-client-rs in…
moubctez Jun 5, 2024
537347d
Resolve pull request comments
moubctez Jun 20, 2024
96c3013
Restore vergen
moubctez Jun 24, 2024
44fef2d
Use correct build.rs
moubctez Jun 24, 2024
258db32
Fix build without default features. Now tokio is required with "time"…
moubctez Jun 26, 2024
c047567
Fix wasm build
moubctez Jun 27, 2024
deaa529
Fix CI-CD tokio not optional
gRoussac Jun 27, 2024
f53e85d
Merge pull request #2 from gRoussac/contract-verification-command
moubctez Jun 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ std-fs-io = ["casper-types/std-fs-io"]
[dependencies]
async-trait = { version = "0.1.74", optional = true }
base16 = "0.2.1"
base64 = "0.21.5"
moubctez marked this conversation as resolved.
Show resolved Hide resolved
bytes = "1.5.0"
moubctez marked this conversation as resolved.
Show resolved Hide resolved
casper-hashing = "3.0.0"
casper-types = { version = "4.0.1", features = ["std"] }
clap = { version = "4.4.10", optional = true, features = ["cargo", "deprecated", "wrap_help"] }
clap_complete = { version = "4.4.4", optional = true }
flate2 = "1.0.28"
moubctez marked this conversation as resolved.
Show resolved Hide resolved
hex-buffer-serde = "0.4.0"
humantime = "2.1.0"
itertools = "0.12.0"
Expand All @@ -43,6 +46,7 @@ reqwest = { version = "0.11.22", features = ["json"] }
schemars = "=0.8.5"
serde = { version = "1.0.193", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.108", features = ["preserve_order"] }
tar = "0.4.40"
moubctez marked this conversation as resolved.
Show resolved Hide resolved
thiserror = "1.0.50"
tokio = { version = "1.34.0", optional = true, features = ["macros", "rt", "sync", "time"] }
uint = "0.9.5"
Expand Down
14 changes: 14 additions & 0 deletions lib/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ use crate::{
DictionaryItemIdentifier,
},
types::Deploy,
verification_types::VerificationDetails,
SuccessResponse,
};
#[cfg(doc)]
Expand Down Expand Up @@ -708,3 +709,16 @@ pub async fn get_era_info(
.await
.map_err(CliError::from)
}

/// Verifies the smart contract code againt the one deployed at address.
pub async fn verify_contract(
hash_str: &str,
verification_url_base_path: &str,
verbosity_level: u64,
) -> Result<VerificationDetails, CliError> {
let key = parse::key_for_query(hash_str)?;
let verbosity = parse::verbosity(verbosity_level);
crate::verify_contract(key, verification_url_base_path, verbosity)
.await
.map_err(CliError::from)
}
4 changes: 4 additions & 0 deletions lib/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ pub enum Error {
/// Failed to validate response.
#[error("invalid response: {0}")]
ResponseFailedValidation(#[from] ValidateResponseError),

/// Failed to verify contract.
#[error("contract verification failed")]
ContractVerificationFailed,
}

impl From<ToBytesError> for Error {
Expand Down
48 changes: 48 additions & 0 deletions lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ mod transfer_target;
pub mod types;
mod validation;
mod verbosity;
mod verification;
mod verification_types;

use std::env::current_dir;
#[cfg(feature = "std-fs-io")]
use std::{
fs,
Expand Down Expand Up @@ -111,6 +114,10 @@ use types::{Account, Block, StoredValue};
use types::{Deploy, DeployHash};
pub use validation::ValidateResponseError;
pub use verbosity::Verbosity;
pub use verification::{build_archive, send_verification_request};
use verification_types::VerificationDetails;

use base64::{engine::general_purpose::STANDARD, Engine};

/// Puts a [`Deploy`] to the network for execution.
///
Expand Down Expand Up @@ -554,3 +561,44 @@ pub async fn get_era_info(
.send_request(GET_ERA_INFO_METHOD, params)
.await
}

/// Verifies the smart contract code againt the one deployed at deploy hash.
pub async fn verify_contract(
key: Key,
verification_url_base_path: &str,
verbosity: Verbosity,
) -> Result<VerificationDetails, Error> {
if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
println!("Key: {key}");
println!("Verification service base path: {verification_url_base_path}",);
}

let project_path = match current_dir() {
moubctez marked this conversation as resolved.
Show resolved Hide resolved
Ok(path) => path,
Err(error) => {
eprintln!("Cannot get current directory: {error}");
return Err(Error::ContractVerificationFailed);
}
};

let archive = match build_archive(&project_path) {
Ok(archive) => {
if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
println!("Created project archive (size: {})", archive.len());
}
archive
}
Err(error) => {
eprintln!("Cannot create project archive: {error}");
return Err(Error::ContractVerificationFailed);
}
};

send_verification_request(
key,
verification_url_base_path,
STANDARD.encode(&archive),
verbosity,
)
.await
}
208 changes: 208 additions & 0 deletions lib/verification.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
use std::path::Path;

use bytes::{BufMut, Bytes, BytesMut};
use casper_types::Key;
use flate2::{write::GzEncoder, Compression};
use reqwest::{
header::{HeaderMap, HeaderValue, CONTENT_TYPE},
Client, ClientBuilder, StatusCode,
};
use tar::Builder as TarBuilder;
use tokio::time::{sleep, Duration};

use crate::{
verification_types::{
VerificationDetails, VerificationRequest, VerificationResult, VerificationStatus,
},
Error, Verbosity,
};

static GIT_DIR_NAME: &str = ".git";
static TARGET_DIR_NAME: &str = "target";

/// Builds an archive from the specified path.
///
/// This function creates a compressed tar archive from the files and directories located at the
/// specified path. It excludes the `.git` and `target` directories from the archive.
///
/// # Arguments
///
/// * `path` - The path to the directory containing the files and directories to be archived.
///
/// # Returns
///
/// The compressed tar archive as a `Bytes` object, or an `std::io::Error` if an error occurs during
/// the archiving process.
pub fn build_archive(path: &Path) -> Result<Bytes, std::io::Error> {
let buffer = BytesMut::new().writer();
let encoder = GzEncoder::new(buffer, Compression::best());
let mut archive = TarBuilder::new(encoder);

for entry in (path.read_dir()?).flatten() {
moubctez marked this conversation as resolved.
Show resolved Hide resolved
let file_name = entry.file_name();
// Skip `.git` and `target`.
if file_name == TARGET_DIR_NAME || file_name == GIT_DIR_NAME {
continue;
}
let full_path = entry.path();
if full_path.is_dir() {
archive.append_dir_all(&file_name, &full_path)?;
} else {
archive.append_path_with_name(&full_path, &file_name)?;
}
}

let encoder = archive.into_inner()?;
let buffer = encoder.finish()?;
Ok(buffer.into_inner().freeze())
}

/// Verifies the smart contract code against the one deployed at deploy hash.
///
/// Sends a verification request to the specified verification URL base path, including the deploy hash,
/// public key, and code archive.
///
/// # Arguments
///
/// * `deploy_hash` - The hash of the deployed contract.
moubctez marked this conversation as resolved.
Show resolved Hide resolved
/// * `public_key` - The public key associated with the contract.
/// * `base_url` - The base path of the verification URL.
/// * `verbosity` - The verbosity level of the verification process.
///
/// # Returns
///
/// The verification details of the contract.
pub async fn send_verification_request(
key: Key,
base_url: &str,
code_archive: String,
verbosity: Verbosity,
) -> Result<VerificationDetails, Error> {
let verification_request = VerificationRequest {
deploy_hash: key,
code_archive,
};

let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));

let Ok(http_client) = ClientBuilder::new()
.default_headers(headers)
// https://github.com/hyperium/hyper/issues/2136
.pool_max_idle_per_host(0)
.user_agent("casper-client-rs")
.build()
else {
eprintln!("Failed to build HTTP client");
return Err(Error::ContractVerificationFailed); // FIXME: different error
moubctez marked this conversation as resolved.
Show resolved Hide resolved
};

if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
println!("Sending verification request");
}

let url = base_url.to_string() + "/verification";
let response = match http_client
.post(url)
.json(&verification_request)
.send()
.await
{
Ok(response) => response,
Err(error) => {
eprintln!("Cannot send verification request: {error:?}");
return Err(Error::ContractVerificationFailed);
}
};

match response.status() {
StatusCode::OK => {
if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
println!("Sent verification request",);
}
}
status => {
eprintln!("Verification failed with status {status}");
// return Err(Error::ContractVerificationFailed);
moubctez marked this conversation as resolved.
Show resolved Hide resolved
}
}

wait_for_verification_finished(base_url, &http_client, key, verbosity).await;

if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
println!("Getting verification details...");
}

let url = base_url.to_string() + "/verification/" + &key.to_formatted_string() + "/details";
match http_client.get(url).send().await {
Ok(response) => response.json().await.map_err(|err| {
eprintln!("Failed to parse JSON {err}");
Error::ContractVerificationFailed
}),
Err(error) => {
eprintln!("Cannot get verification details: {error:?}");
Err(Error::ContractVerificationFailed)
}
}
}

/// Waits for the verification process to finish.
async fn wait_for_verification_finished(
base_url: &str,
http_client: &Client,
key: Key,
verbosity: Verbosity,
) {
let delay = Duration::from_secs(2);
let mut retries = 20_000;
while retries != 0 {
sleep(delay).await;

match get_verification_status(base_url, http_client, key).await {
Ok(status) => {
if verbosity == Verbosity::Medium || verbosity == Verbosity::High {
println!("Verification status: {status:?}");
}
if status == VerificationStatus::Verified || status == VerificationStatus::Failed {
break;
}
}
Err(error) => {
eprintln!("Cannot get verification status: {error:?}");
break;
}
};

retries -= 1;
}
moubctez marked this conversation as resolved.
Show resolved Hide resolved
}

/// Gets the verification status of the contract.
async fn get_verification_status(
base_url: &str,
http_client: &Client,
key: Key,
) -> Result<VerificationStatus, Error> {
let url = base_url.to_string() + "/verification/" + &key.to_formatted_string() + "/status";
let response = match http_client.get(url).send().await {
Ok(response) => response,
Err(error) => {
eprintln!("Failed to fetch verification status: {error:?}");
return Err(Error::ContractVerificationFailed);
}
};

match response.status() {
StatusCode::OK => {
let result: VerificationResult = response.json().await.map_err(|err| {
eprintln!("Failed to parse JSON for verification status, {err}");
Error::ContractVerificationFailed
})?;
Ok(result.status)
}
status => {
eprintln!("Verification status not found, {status}");
Err(Error::ContractVerificationFailed)
}
}
}
Loading