Skip to content

Commit

Permalink
Merge pull request #104 from Ellipsis-Labs/verify-with-signer
Browse files Browse the repository at this point in the history
Verify with signer
  • Loading branch information
ngundotra authored Dec 5, 2024
2 parents eaf1f69 + c6b0406 commit f0ae855
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 5 deletions.
87 changes: 83 additions & 4 deletions src/api/client.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use anyhow::anyhow;
use crossbeam_channel::{unbounded, Receiver};
use indicatif::{HumanDuration, ProgressBar, ProgressStyle};
use reqwest::Client;
use reqwest::{Client, Response};
use serde_json::json;
use solana_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
use std::thread;
use std::time::{Duration, Instant};

use crate::api::models::{
ErrorResponse, JobResponse, JobStatus, JobVerificationResponse, VerifyResponse,
ErrorResponse, JobResponse, JobStatus, JobVerificationResponse, RemoteStatusResponseWrapper,
VerifyResponse,
};
use crate::solana_program::get_program_pda;
use crate::{get_genesis_hash, MAINNET_GENESIS_HASH};

// URL for the remote server
pub const REMOTE_SERVER_URL: &str = "https://verify.osec.io";
Expand Down Expand Up @@ -108,10 +112,56 @@ pub async fn send_job_to_remote(
.send()
.await?;

handle_submission_response(&client, response, program_id).await
}

pub async fn send_job_with_uploader_to_remote(
connection: &RpcClient,
program_id: &Pubkey,
uploader: &Pubkey,
) -> anyhow::Result<()> {
// Check that PDA exists before sending job
let genesis_hash = get_genesis_hash(connection)?;
if genesis_hash != MAINNET_GENESIS_HASH {
return Err(anyhow!("Remote verification only works with mainnet. Please omit the --remote flag to verify locally."));
}
get_program_pda(connection, program_id, Some(uploader.to_string())).await?;

let client = Client::builder()
.timeout(Duration::from_secs(18000))
.build()?;

// Send the POST request
let response = client
.post(format!("{}/verify-with-signer", REMOTE_SERVER_URL))
.json(&json!({
"program_id": program_id.to_string(),
"signer": uploader.to_string(),
"repository": "",
"commit_hash": "",
}))
.send()
.await?;

handle_submission_response(&client, response, program_id).await
}

pub async fn handle_submission_response(
client: &Client,
response: Response,
program_id: &Pubkey,
) -> anyhow::Result<()> {
if response.status().is_success() {
let status_response: VerifyResponse = response.json().await?;
// First get the raw text to preserve it in case of parsing failure
let response_text = response.text().await?;
let status_response =
serde_json::from_str::<VerifyResponse>(&response_text).map_err(|e| {
eprintln!("Failed to parse response as VerifyResponse: {}", e);
eprintln!("Raw response: {}", response_text);
anyhow!("Failed to parse server response")
})?;
let request_id = status_response.request_id;
println!("Verification request sent. ✅");
println!("Verification request sent with request id: {}", request_id);
println!("Verification in progress... ⏳");
// Span new thread for polling the server for status
// Create a channel for communication between threads
Expand Down Expand Up @@ -228,3 +278,32 @@ async fn check_job_status(client: &Client, request_id: &str) -> anyhow::Result<J
))?
}
}

pub async fn get_remote_status(program_id: Pubkey) -> anyhow::Result<()> {
let client = Client::builder()
.timeout(Duration::from_secs(18000))
.build()?;

let response = client
.get(format!(
"{}/status-all/{}",
REMOTE_SERVER_URL,
program_id.to_string()
))
.send()
.await?;

let status: RemoteStatusResponseWrapper = response.json().await?;
println!("{}", status);
Ok(())
}

pub async fn get_remote_job(job_id: &str) -> anyhow::Result<()> {
let client = Client::builder()
.timeout(Duration::from_secs(18000))
.build()?;

let job = check_job_status(&client, job_id).await?;
println!("{}", job);
Ok(())
}
3 changes: 3 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@ mod client;
mod models;
mod solana;

pub use client::get_remote_job;
pub use client::get_remote_status;
pub use client::send_job_to_remote;
pub use client::send_job_with_uploader_to_remote;
pub use solana::get_last_deployed_slot;
67 changes: 67 additions & 0 deletions src/api/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ pub struct JobResponse {
pub respose: Option<JobVerificationResponse>,
}

impl std::fmt::Display for JobResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(response) = &self.respose {
writeln!(f, "{}", response)?;
} else {
writeln!(f, "Status: {:?}", self.status)?;
}
Ok(())
}
}

#[derive(Debug, Serialize, Deserialize)]
pub enum JobStatus {
#[serde(rename = "in_progress")]
Expand All @@ -54,3 +65,59 @@ pub struct JobVerificationResponse {
pub executable_hash: String,
pub repo_url: String,
}

impl std::fmt::Display for JobVerificationResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Status: {:?}", self.status)?;
writeln!(f, "Message: {}", self.message)?;
writeln!(f, "On-chain Hash: {}", self.on_chain_hash)?;
writeln!(f, "Executable Hash: {}", self.executable_hash)?;
write!(f, "Repository URL: {}", self.repo_url)
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RemoteStatusResponse {
pub signer: String,
pub is_verified: bool,
pub on_chain_hash: String,
pub executable_hash: String,
pub repo_url: String,
pub commit: String,
pub last_verified_at: String,
}

impl std::fmt::Display for RemoteStatusResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Verification Status for Signer: {}", self.signer)?;
writeln!(
f,
"Verified: {}",
if self.is_verified { "✅" } else { "❌" }
)?;
writeln!(f, "On-chain Hash: {}", self.on_chain_hash)?;
writeln!(f, "Executable Hash: {}", self.executable_hash)?;
writeln!(f, "Repository URL: {}", self.repo_url)?;
writeln!(f, "Commit: {}", self.commit)?;
write!(f, "Last Verified: {}", self.last_verified_at)
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RemoteStatusResponseWrapper(Vec<RemoteStatusResponse>);

impl std::fmt::Display for RemoteStatusResponseWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, response) in self.0.iter().enumerate() {
if i > 0 {
writeln!(f)?;
writeln!(
f,
"----------------------------------------------------------------"
)?;
}
write!(f, "{}", response)?;
}
Ok(())
}
}
52 changes: 52 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::anyhow;
use api::{get_remote_job, get_remote_status, send_job_with_uploader_to_remote};
use cargo_lock::Lockfile;
use cargo_toml::Manifest;
use clap::{App, AppSettings, Arg, SubCommand};
Expand Down Expand Up @@ -216,6 +217,35 @@ async fn main() -> anyhow::Result<()> {
.help("Signer to get the PDA for")
)
)
.subcommand(SubCommand::with_name("remote")
.about("Send a command to a remote machine")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(SubCommand::with_name("get-status")
.about("Get the verification status of a program")
.arg(Arg::with_name("program-id")
.long("program-id")
.required(true)
.takes_value(true)
.help("The program address to fetch verification status for")))

.subcommand(SubCommand::with_name("get-job")
.about("Get the status of a verification job")
.arg(Arg::with_name("job-id")
.long("job-id")
.required(true)
.takes_value(true)))
.subcommand(SubCommand::with_name("submit-job")
.about("Submit a verification job with with on-chain information")
.arg(Arg::with_name("program-id")
.long("program-id")
.required(true)
.takes_value(true))
.arg(Arg::with_name("uploader")
.long("uploader")
.required(true)
.takes_value(true)
.help("This is the address that uploaded verified build information for the program-id")))
)
.get_matches();

let connection = resolve_rpc_url(matches.value_of("url").map(|s| s.to_string()))?;
Expand Down Expand Up @@ -339,6 +369,28 @@ async fn main() -> anyhow::Result<()> {
let signer = sub_m.value_of("signer").map(|s| s.to_string());
print_program_pda(Pubkey::try_from(program_id)?, signer, &connection).await
}
("remote", Some(sub_m)) => match sub_m.subcommand() {
("get-status", Some(sub_m)) => {
let program_id = sub_m.value_of("program-id").unwrap();
get_remote_status(Pubkey::try_from(program_id)?).await
}
("get-job", Some(sub_m)) => {
let job_id = sub_m.value_of("job-id").unwrap();
get_remote_job(job_id).await
}
("submit-job", Some(sub_m)) => {
let program_id = sub_m.value_of("program-id").unwrap();
let uploader = sub_m.value_of("uploader").unwrap();

send_job_with_uploader_to_remote(
&connection,
&Pubkey::try_from(program_id)?,
&Pubkey::try_from(uploader)?,
)
.await
}
_ => unreachable!(),
},
// Handle other subcommands in a similar manner, for now let's panic
_ => panic!(
"Unknown subcommand: {:?}\nUse '--help' to see available commands",
Expand Down
6 changes: 5 additions & 1 deletion src/solana_program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,11 @@ pub async fn get_program_pda(
.map_err(|err| anyhow!("Unable to parse build params: {}", err))?,
))
} else {
Err(anyhow!("PDA not found"))
Err(anyhow!(
"PDA not found for {:?} and uploader {:?}. Make sure you've uploaded the PDA to mainnet.",
program_id,
signer_pubkey
))
}
}

Expand Down

0 comments on commit f0ae855

Please sign in to comment.