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 28 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
7 changes: 7 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 = { version = "0.22.1", default-features = false }
bytes = { version = "1.6.0", default-features = false }
casper-hashing = "3.0.0"
casper-types = { version = "4.0.1", features = ["std"] }
clap = { version = "~4.4", optional = true, features = ["cargo", "deprecated", "wrap_help"] }
clap_complete = { version = "<4.5.0", 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,13 +46,17 @@ reqwest = { version = "0.12.3", 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 = { version = "0.4.41", default-features = false }
thiserror = "1.0.50"
tokio = { version = "1.34.0", optional = true, features = ["macros", "rt", "sync", "time"] }
uint = "0.9.5"

[dev-dependencies]
tempfile = "3.8.1"

[build-dependencies]
vergen = { version = "7", default-features = false, features = ["git"] }

[patch.crates-io]
casper-hashing = { git = "https://github.com/casper-network/casper-node", branch = "dev" }
casper-types = { git = "https://github.com/casper-network/casper-node", branch = "dev" }
Expand Down
22 changes: 4 additions & 18 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
use std::process::Command;
gRoussac marked this conversation as resolved.
Show resolved Hide resolved

const GIT_HASH_ENV_VAR: &str = "GIT_SHA_SHORT";
use vergen::{Config, ShaKind};

fn main() {
//Build command to retrieve the short git commit hash
let git_process_output = Command::new("git")
.arg("rev-parse")
.arg("--short")
.arg("HEAD")
.output()
.expect("Failed to retrieve short git commit hash");

//Parse the raw output into a string, we still need to remove the newline character
let git_hash_raw =
String::from_utf8(git_process_output.stdout).expect("Failed to convert git hash to string");
//Remove the newline character from the short git commit hash
let git_hash = git_hash_raw.trim_end_matches('\n');

println!("cargo:rustc-env={}={}", GIT_HASH_ENV_VAR, git_hash);
let mut config = Config::default();
*config.git_mut().sha_kind_mut() = ShaKind::Short;
let _ = vergen::vergen(config);
}
20 changes: 20 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,22 @@ 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,
verification_project_path: Option<&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,
verification_project_path,
verbosity,
)
.await
.map_err(CliError::from)
}
8 changes: 8 additions & 0 deletions lib/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ pub enum Error {
/// Failed to validate response.
#[error("invalid response: {0}")]
ResponseFailedValidation(#[from] ValidateResponseError),

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

/// Failed to construct HTTP client.
#[error("failed to construct HTTP client")]
FailedToConstructHttpClient,
}

impl From<ToBytesError> for Error {
Expand Down
52 changes: 52 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,48 @@ 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,
project_path: Option<&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 project_path {
Some(path) => Path::new(path).to_path_buf(),
None => match current_dir() {
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
}
4 changes: 1 addition & 3 deletions lib/rpcs/v1_6_0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ pub(crate) mod get_block_transfers {
}

pub(crate) mod get_dictionary_item {
pub use crate::rpcs::v1_5_0::get_dictionary_item::{
DictionaryItemIdentifier, GetDictionaryItemResult,
};
pub use crate::rpcs::v1_5_0::get_dictionary_item::GetDictionaryItemResult;
pub(crate) use crate::rpcs::v1_5_0::get_dictionary_item::{
GetDictionaryItemParams, GET_DICTIONARY_ITEM_METHOD,
};
Expand Down
213 changes: 213 additions & 0 deletions lib/verification.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
use std::{cmp::min, io, 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,
};

const MAX_RETRIES: u32 = 10;
const BASE_DELAY: Duration = Duration::from_secs(3);
const MAX_DELAY: Duration = Duration::from_secs(300);

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, 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() {
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
///
/// * `key` - The key of the deployed contract.
/// * `base_url` - The base path of the verification URL.
/// * `code_archive` - Base64-encoded tar-gzipped archive of the source code.
/// * `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::FailedToConstructHttpClient);
};

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}");
}
}

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 mut retries = MAX_RETRIES;
moubctez marked this conversation as resolved.
Show resolved Hide resolved
let mut delay = BASE_DELAY;

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;
delay = min(delay * 2, MAX_DELAY);
}
}

/// 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