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

feat: add contract verification #291

Draft
wants to merge 9 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions api/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fmt = "0.1.0"
thiserror = "1.0.50"
chrono = "0.4.31"
prometheus = "0.13.4"
reqwest = { version = "0.11", features = ["json"] }
reqwest = { version = "0.11", features = ["json", "multipart"] }
lazy_static = "1.4.0"
tokio = { version = "1.0", features = ["full"] }
semver = "1.0"
8 changes: 0 additions & 8 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,6 @@ WORKDIR /app
COPY . .

# Compile necessary Rust packages
# If you have to specify a specific version of cairo
# RUN rm cairo -rf
# RUN git clone https://github.com/starkware-libs/cairo.git
# RUN cd cairo; git checkout v1.0.0-alpha.6
# Or if you chose to copy the whole repo
# RUN git submodule update --init

#RUN cd cairo; cargo build --bin starknet-compile; cargo build --bin starknet-sierra-compile
RUN cd cairo_compilers; chmod +x build.sh; ./build.sh
RUN cargo build --release

Expand Down
4 changes: 4 additions & 0 deletions api/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ pub enum NetworkError {
FailedToGetClientIp,
#[error("Too many requests")]
TooManyRequests,
#[error("Failed to verify contract: {0}")]
VerificationFailed(String),
#[error("Failed to get verification status: {0}")]
VerificationStatusFailed(String),
}

impl From<NetworkError> for ApiError {
Expand Down
4 changes: 2 additions & 2 deletions api/src/handlers/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use tracing::{info, instrument};

use crate::errors::{ApiError, ExecutionError, FileError, Result, SystemError};
use crate::handlers::compile::{default_scarb_toml, scarb_toml_with_version};
use crate::metrics::{Metrics, COMPILATION_LABEL_VALUE};
use crate::metrics::{Metrics, COMPILATION_LABEL_VALUE, VERIFY_LABEL_VALUE};

use super::scarb_version::do_scarb_version;
use super::types::{BaseRequest, CompilationRequest, FileContentMap, Successful};
Expand Down Expand Up @@ -128,7 +128,7 @@ pub async fn dispatch_command(command: ApiCommand, metrics: &Metrics) -> Result<
{
Ok(result) => Ok(ApiCommandResult::Compile(result)),
Err(e) => Err(e),
},
}
}
}

Expand Down
156 changes: 156 additions & 0 deletions api/src/handlers/verify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use crate::errors::{NetworkError, Result};
use crate::handlers::process::{do_process_command, fetch_process_result};
use crate::handlers::types::VerifyResponseGetter;
use crate::handlers::types::{ApiCommand, IntoTypedResponse};
use crate::handlers::utils::{init_directories, AutoCleanUp};
use crate::metrics::Metrics;
use crate::rate_limiter::RateLimited;
use crate::worker::WorkerEngine;
use reqwest::multipart;
use rocket::serde::json::Json;
use rocket::{tokio, State};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use tracing::{debug, error, info, instrument};

use super::types::{ApiResponse, VerifyRequest, VerifyResponse};

const VOYAGER_API_BASE: &str = "https://api.voyager.online/beta";
const VERIFICATION_POLL_INTERVAL: Duration = Duration::from_secs(5);
const VERIFICATION_TIMEOUT: Duration = Duration::from_secs(300);

#[derive(Debug, Deserialize)]
struct VoyagerVerifyResponse {
job_id: String,
}

#[derive(Debug, Deserialize)]
struct VoyagerVerifyStatus {
status: i32,
status_description: String,
}

#[instrument(skip(request_json, _rate_limited, engine))]
#[post("/verify-async", data = "<request_json>")]
pub async fn verify_async(
request_json: Json<VerifyRequest>,
_rate_limited: RateLimited,
engine: &State<WorkerEngine>,
) -> ApiResponse<String> {
info!("/verify");
do_process_command(
ApiCommand::Verify {
verify_request: request_json.0,
},
engine,
)
}

#[instrument(skip(engine))]
#[get("/verify-async/<process_id>")]
pub async fn get_verify_result(process_id: &str, engine: &State<WorkerEngine>) -> VerifyResponse {
info!("/verify-result/{:?}", process_id);
fetch_process_result::<VerifyResponseGetter>(process_id, engine)
.map(|result| result.0)
.unwrap_or_else(|err| err.into_typed())
}

pub async fn do_verify(
verify_request: VerifyRequest,
_metrics: &Metrics,
) -> Result<VerifyResponse> {
debug!("verify_request: {:?}", verify_request);

// Create temporary directories
let temp_dir = init_directories(&verify_request.base_request)
.await
.map_err(|e| {
error!("Failed to initialize directories: {:?}", e);
e
})?;

let auto_clean_up = AutoCleanUp {
dirs: vec![&temp_dir],
};

// Find main contract file
let contract_file = verify_request
.base_request
.files
.iter()
.find(|f| f.file_name.ends_with(".cairo"))
.ok_or_else(|| NetworkError::VerificationFailed("No Cairo contract file found".to_string()))?;

// Create multipart form
let mut form = multipart::Form::new()
.text("name", verify_request.contract_name.clone())
.text("classHash", verify_request.contract_address.clone())
.text("license", "MIT".to_string())
.text("compilerVersion", "2.6.4".to_string()); // TODO: Make this configurable

// Add files to form
for file in &verify_request.base_request.files {
let part = multipart::Part::text(file.file_content.clone())
.file_name(file.file_name.clone());
form = form.part(file.file_name.clone(), part);
}

let client = reqwest::Client::new();

// Start verification
let verify_response = client
.post(&format!("{}/verification/send", VOYAGER_API_BASE))
.multipart(form)
.send()
.await
.map_err(|e| NetworkError::VerificationFailed(e.to_string()))?
.json::<VoyagerVerifyResponse>()
.await
.map_err(|e| NetworkError::VerificationFailed(e.to_string()))?;

let job_id = verify_response.job_id;
debug!("Verification job started with ID: {}", job_id);

// Poll for verification status
let start_time = std::time::Instant::now();
loop {
if start_time.elapsed() > VERIFICATION_TIMEOUT {
auto_clean_up.clean_up().await;
return Ok(ApiResponse::ok(())
.with_status("Timeout".to_string())
.with_code(408)
.with_message("Verification timed out after 5 minutes".to_string()));
}

let status = client
.get(&format!("{}/class-verify/job/{}", VOYAGER_API_BASE, job_id))
.send()
.await
.map_err(|e| NetworkError::VerificationStatusFailed(e.to_string()))?
.json::<VoyagerVerifyStatus>()
.await
.map_err(|e| NetworkError::VerificationStatusFailed(e.to_string()))?;

match status.status {
0 => {
tokio::time::sleep(VERIFICATION_POLL_INTERVAL).await;
continue;
}
1 => {
auto_clean_up.clean_up().await;
return Ok(ApiResponse::ok(())
.with_status("Success".to_string())
.with_code(200)
.with_message("Contract verified successfully".to_string()));
}
_ => {
auto_clean_up.clean_up().await;
return Ok(ApiResponse::ok(())
.with_status("Failed".to_string())
.with_code(400)
.with_message(format!("Verification failed: {}", status.status_description)));
}
}
}
}
2 changes: 1 addition & 1 deletion api/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use tracing::instrument;

const NAMESPACE: &str = "starknet_api";
pub(crate) const COMPILATION_LABEL_VALUE: &str = "compilation";

pub(crate) const VERIFY_LABEL_VALUE: &str = "verify";
// Action - compile/verify(once supported)
#[derive(Clone, Debug)]
pub struct Metrics {
Expand Down
5 changes: 5 additions & 0 deletions plugin/src/atoms/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { atom } from "jotai";

export type VerifyStatus = "loading" | "success" | "error" | "";

export const verifyStatusAtom = atom<VerifyStatus>("");
2 changes: 1 addition & 1 deletion plugin/src/components/ManualAccount/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { accountAtom, networkAtom, selectedAccountAtom } from "../../atoms/manua
import { envAtom } from "../../atoms/environment";
import useAccount from "../../hooks/useAccount";
import useProvider from "../../hooks/useProvider";
import useRemixClient from "../../hooks/useRemixClient";
import { useRemixClient } from "../../hooks/useRemixClient";
import { getProvider } from "../../utils/misc";
import { declTxHashAtom, deployTxHashAtom } from "../../atoms/deployment";
import { invokeTxHashAtom } from "../../atoms/interaction";
Expand Down
Loading
Loading