diff --git a/src/api/client.rs b/src/api/client.rs index 0dd1456..0ef73c1 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -5,6 +5,7 @@ use reqwest::{Client, Response}; use serde_json::json; use solana_client::rpc_client::RpcClient; use solana_sdk::pubkey::Pubkey; +use std::sync::atomic::{Ordering}; use std::thread; use std::time::{Duration, Instant}; @@ -13,6 +14,7 @@ use crate::api::models::{ VerifyResponse, }; use crate::solana_program::get_program_pda; +use crate::SIGNAL_RECEIVED; use crate::{get_genesis_hash, MAINNET_GENESIS_HASH}; // URL for the remote server @@ -27,8 +29,16 @@ fn loading_animation(receiver: Receiver) { let pb = ProgressBar::new_spinner(); pb.set_style(spinner_style); + pb.enable_steady_tick(Duration::from_millis(100)); pb.set_message("Request sent. Awaiting server response. This may take a moment... ⏳"); + loop { + // Check if interrupt signal was received + if SIGNAL_RECEIVED.load(Ordering::Relaxed) { + pb.finish_with_message("❌ Operation interrupted by user."); + break; + } + match receiver.try_recv() { Ok(result) => { if result { @@ -42,13 +52,16 @@ fn loading_animation(receiver: Receiver) { } break; } - Err(_) => { - pb.inc(1); - thread::sleep(Duration::from_millis(100)); + if SIGNAL_RECEIVED.load(Ordering::Relaxed) { + pb.finish_with_message("❌ Operation interrupted by user."); + break; + } + thread::sleep(Duration::from_millis(10)); } } } + pb.abandon(); // Ensure the progress bar is cleaned up } fn print_verification_status( @@ -163,16 +176,28 @@ pub async fn handle_submission_response( let request_id = status_response.request_id; 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 let (sender, receiver) = unbounded(); - let handle = thread::spawn(move || loading_animation(receiver)); - // Poll the server for status + loop { + // Check for interrupt signal before polling + if SIGNAL_RECEIVED.load(Ordering::Relaxed) { + let _ = sender.send(false); + handle.join().unwrap(); + break; // Exit the loop and continue with normal error handling + } + let status = check_job_status(&client, &request_id).await?; match status.status { JobStatus::InProgress => { + if SIGNAL_RECEIVED.load(Ordering::Relaxed) { + let _ = sender.send(false); + handle.join().unwrap(); + break; + } thread::sleep(Duration::from_secs(10)); } JobStatus::Completed => { @@ -197,7 +222,6 @@ pub async fn handle_submission_response( } JobStatus::Failed => { let _ = sender.send(false); - handle.join().unwrap(); let status_response: JobVerificationResponse = status.respose.unwrap(); println!("Program {} has not been verified. ❌", program_id); diff --git a/src/main.rs b/src/main.rs index 9f871ba..0564465 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ use solana_sdk::{ use std::{ io::Read, path::PathBuf, - process::{exit, Command, Stdio}, + process::{Command, Stdio}, sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -49,24 +49,58 @@ pub fn get_network(network_str: &str) -> &str { } } +// At the top level, make the signal handler accessible throughout the program +lazy_static::lazy_static! { + static ref SIGNAL_RECEIVED: Arc = Arc::new(AtomicBool::new(false)); +} + #[tokio::main] async fn main() -> anyhow::Result<()> { // Handle SIGTERM and SIGINT gracefully by stopping the docker container let mut signals = Signals::new([SIGTERM, SIGINT])?; let mut container_id: Option = None; let mut temp_dir: Option = None; - let caught_signal = Arc::new(AtomicBool::new(false)); - let caught_signal_clone = caught_signal.clone(); let handle = signals.handle(); std::thread::spawn(move || { - #[allow(clippy::never_loop)] - for _ in signals.forever() { - caught_signal_clone.store(true, Ordering::Relaxed); - break; + if signals.forever().next().is_some() { + SIGNAL_RECEIVED.store(true, Ordering::Relaxed); } }); + // Add a function to check if we should abort + let check_signal = |container_id: &mut Option, temp_dir: &mut Option| { + if SIGNAL_RECEIVED.load(Ordering::Relaxed) { + println!("\nReceived interrupt signal, cleaning up..."); + + if let Some(container_id) = container_id.take() { + if std::process::Command::new("docker") + .args(["kill", &container_id]) + .output() + .is_err() + { + println!("Failed to close docker container"); + } else { + println!("Stopped container {}", container_id) + } + } + + if let Some(temp_dir) = temp_dir.take() { + if std::process::Command::new("rm") + .args(["-rf", &temp_dir]) + .output() + .is_err() + { + println!("Failed to remove temporary directory"); + } else { + println!("Removed temporary directory {}", temp_dir); + } + } + + std::process::exit(130); + } + }; + let matches = App::new("solana-verify") .author("Ellipsis Labs ") .version(env!("CARGO_PKG_VERSION")) @@ -78,6 +112,12 @@ async fn main() -> anyhow::Result<()> { .global(true) .takes_value(true) .help("Optionally include your RPC endpoint. Defaults to Solana CLI config file")) + .arg(Arg::with_name("compute-unit-price") + .long("compute-unit-price") + .global(true) + .takes_value(true) + .default_value("100000") + .help("Priority fee in micro-lamports per compute unit")) .subcommand(SubCommand::with_name("build") .about("Deterministically build the program in a Docker container") .arg(Arg::with_name("mount-directory") @@ -188,7 +228,11 @@ async fn main() -> anyhow::Result<()> { .arg(Arg::with_name("cargo-args") .multiple(true) .last(true) - .help("Arguments to pass to the underlying `cargo build-bpf` command"))) + .help("Arguments to pass to the underlying `cargo build-bpf` command")) + .arg(Arg::with_name("skip-build") + .long("skip-build") + .help("Skip building and verification, only upload the PDA") + .takes_value(false))) .subcommand(SubCommand::with_name("close") .about("Close the otter-verify PDA account associated with the given program ID") .arg(Arg::with_name("program-id") @@ -306,6 +350,7 @@ async fn main() -> anyhow::Result<()> { Ok(()) } ("verify-from-repo", Some(sub_m)) => { + let skip_build = sub_m.is_present("skip-build"); let remote = sub_m.is_present("remote"); let mount_path = sub_m.value_of("mount-path").map(|s| s.to_string()).unwrap(); let repo_url = sub_m.value_of("repo-url").map(|s| s.to_string()).unwrap(); @@ -316,6 +361,11 @@ async fn main() -> anyhow::Result<()> { let current_dir = sub_m.is_present("current-dir"); let skip_prompt = sub_m.is_present("skip-prompt"); let path_to_keypair = sub_m.value_of("keypair").map(|s| s.to_string()); + let compute_unit_price = matches + .value_of("compute-unit-price") + .unwrap() + .parse::() + .unwrap_or(100000); let cargo_args: Vec = sub_m .values_of("cargo-args") .unwrap_or_default() @@ -351,14 +401,27 @@ async fn main() -> anyhow::Result<()> { current_dir, skip_prompt, path_to_keypair, + compute_unit_price, + skip_build, &mut container_id, &mut temp_dir, + &check_signal, ) .await } ("close", Some(sub_m)) => { let program_id = sub_m.value_of("program-id").unwrap(); - process_close(Pubkey::try_from(program_id)?, &connection).await + let compute_unit_price = matches + .value_of("compute-unit-price") + .unwrap() + .parse::() + .unwrap_or(100000); + process_close( + Pubkey::try_from(program_id)?, + &connection, + compute_unit_price, + ) + .await } ("list-program-pdas", Some(sub_m)) => { let program_id = sub_m.value_of("program-id").unwrap(); @@ -398,32 +461,6 @@ async fn main() -> anyhow::Result<()> { ), }; - if caught_signal.load(Ordering::Relaxed) || res.is_err() { - if let Some(container_id) = container_id.clone().take() { - println!("Stopping container {}", container_id); - if std::process::Command::new("docker") - .args(["kill", &container_id]) - .output() - .is_err() - { - println!("Failed to close docker container"); - } else { - println!("Stopped container {}", container_id) - } - } - if let Some(temp_dir) = temp_dir.clone().take() { - println!("Removing temporary directory {}", temp_dir); - if std::process::Command::new("rm") - .args(["-rf", &temp_dir]) - .output() - .is_err() - { - println!("Failed to remove temporary directory"); - } else { - println!("Removed temporary directory {}", temp_dir); - } - } - } handle.close(); res } @@ -533,12 +570,30 @@ pub fn get_buffer_hash(url: Option, buffer_address: Pubkey) -> anyhow::R } pub fn get_program_hash(client: &RpcClient, program_id: Pubkey) -> anyhow::Result { + // First check if the program account exists + if client.get_account(&program_id).is_err() { + return Err(anyhow!("Program {} is not deployed", program_id)); + } + let program_buffer = Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable::id()).0; - let offset = UpgradeableLoaderState::size_of_programdata_metadata(); - let account_data = client.get_account_data(&program_buffer)?[offset..].to_vec(); - let program_hash = get_binary_hash(account_data); - Ok(program_hash) + + // Then check if the program data account exists + match client.get_account_data(&program_buffer) { + Ok(data) => { + let offset = UpgradeableLoaderState::size_of_programdata_metadata(); + let account_data = data[offset..].to_vec(); + let program_hash = get_binary_hash(account_data); + Ok(program_hash) + } + Err(_) => Err(anyhow!( + "Could not find program data for {}. This could mean:\n\ + 1. The program is not deployed\n\ + 2. The program is not upgradeable\n\ + 3. The program was deployed with a different loader", + program_id + )), + } } pub fn get_genesis_hash(client: &RpcClient) -> anyhow::Result { @@ -895,114 +950,15 @@ pub async fn verify_from_repo( current_dir: bool, skip_prompt: bool, path_to_keypair: Option, + compute_unit_price: u64, + mut skip_build: bool, container_id_opt: &mut Option, temp_dir_opt: &mut Option, + check_signal: &dyn Fn(&mut Option, &mut Option), ) -> anyhow::Result<()> { - if remote { - 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.")); - } + // Set skip_build to true if remote is true + skip_build |= remote; - println!("Sending verify command to remote machine..."); - send_job_to_remote( - &repo_url, - &commit_hash, - &program_id, - &library_name_opt, - bpf_flag, - relative_mount_path.clone(), - base_image.clone(), - cargo_args.clone(), - ) - .await?; - - let mut args: Vec<&str> = Vec::new(); - if !relative_mount_path.is_empty() { - args.push("--mount-path"); - args.push(&relative_mount_path); - } - // Get the absolute build path to the solana program directory to build inside docker - let mount_path = PathBuf::from(relative_mount_path.clone()); - println!("Build path: {:?}", mount_path); - - args.push("--library-name"); - let library_name = match library_name_opt { - Some(p) => p, - None => { - std::process::Command::new("find") - .args([mount_path.to_str().unwrap(), "-name", "Cargo.toml"]) - .output() - .map_err(|e| { - anyhow::format_err!( - "Failed to find Cargo.toml files in root directory: {}", - e.to_string() - ) - }) - .and_then(|output| { - let mut options = vec![]; - for path in String::from_utf8(output.stdout)?.split("\n") { - match get_lib_name_from_cargo_toml(path) { - Ok(name) => { - options.push(name); - } - Err(_) => { - continue; - } - } - } - if options.len() != 1 { - println!( - "Found multiple possible targets in root directory: {:?}", - options - ); - println!( - "Please explicitly specify the target with the --package-name option", - ); - Err(anyhow::format_err!( - "Failed to find unique Cargo.toml file in root directory" - )) - } else { - Ok(options[0].clone()) - } - })? - } - }; - args.push(&library_name); - println!("Verifying program: {}", library_name); - - if let Some(base_image) = &base_image { - args.push("--base-image"); - args.push(base_image); - } - - if bpf_flag { - args.push("--bpf"); - } - - if !cargo_args.clone().is_empty() { - args.push("--"); - for arg in &cargo_args { - args.push(arg); - } - } - - let x = upload_program( - repo_url, - &commit_hash.clone(), - args.iter().map(|&s| s.into()).collect(), - program_id, - connection, - skip_prompt, - path_to_keypair, - ) - .await; - if x.is_err() { - println!("Error uploading program: {:?}", x); - exit(1); - } - return Ok(()); - } // Create a Vec to store solana-verify args let mut args: Vec<&str> = Vec::new(); @@ -1034,11 +990,14 @@ pub async fn verify_from_repo( let verify_tmp_root_path = format!("{}/{}", verify_dir, base_name); println!("Cloning repo into: {}", verify_tmp_root_path); + check_signal(container_id_opt, temp_dir_opt); std::process::Command::new("git") .args(["clone", &repo_url, &verify_tmp_root_path]) .stdout(Stdio::inherit()) .output()?; + check_signal(container_id_opt, temp_dir_opt); + // Checkout a specific commit hash, if provided if let Some(commit_hash) = commit_hash.as_ref() { let result = std::process::Command::new("git") @@ -1056,6 +1015,8 @@ pub async fn verify_from_repo( } } + check_signal(container_id_opt, temp_dir_opt); + if !relative_mount_path.is_empty() { args.push("--mount-path"); args.push(&relative_mount_path); @@ -1065,45 +1026,45 @@ pub async fn verify_from_repo( println!("Build path: {:?}", mount_path); args.push("--library-name"); - let library_name = match library_name_opt { + let library_name = match library_name_opt.clone() { Some(p) => p, None => { - std::process::Command::new("find") - .args([mount_path.to_str().unwrap(), "-name", "Cargo.toml"]) - .output() - .map_err(|e| { - anyhow::format_err!( - "Failed to find Cargo.toml files in root directory: {}", - e.to_string() - ) - }) - .and_then(|output| { - let mut options = vec![]; - for path in String::from_utf8(output.stdout)?.split("\n") { - match get_lib_name_from_cargo_toml(path) { - Ok(name) => { - options.push(name); - } - Err(_) => { - continue; - } + std::process::Command::new("find") + .args([mount_path.to_str().unwrap(), "-name", "Cargo.toml"]) + .output() + .map_err(|e| { + anyhow::format_err!( + "Failed to find Cargo.toml files in root directory: {}", + e.to_string() + ) + }) + .and_then(|output| { + let mut options = vec![]; + for path in String::from_utf8(output.stdout)?.split("\n") { + match get_lib_name_from_cargo_toml(path) { + Ok(name) => { + options.push(name); + } + Err(_) => { + continue; } } - if options.len() != 1 { - println!( - "Found multiple possible targets in root directory: {:?}", - options - ); - println!( - "Please explicitly specify the target with the --package-name option", - ); - Err(anyhow::format_err!( - "Failed to find unique Cargo.toml file in root directory" - )) - } else { - Ok(options[0].clone()) - } - })? + } + if options.len() != 1 { + println!( + "Found multiple possible targets in root directory: {:?}", + options + ); + println!( + "Please explicitly specify the target with the --library-name option", + ); + Err(anyhow::format_err!( + "Failed to find unique Cargo.toml file in root directory" + )) + } else { + Ok(options[0].clone()) + } + })? } }; args.push(&library_name); @@ -1125,50 +1086,85 @@ pub async fn verify_from_repo( } } - let result = build_and_verify_repo( - mount_path.to_str().unwrap().to_string(), - base_image.clone(), - bpf_flag, - library_name.clone(), - connection, - program_id, - cargo_args.clone(), - container_id_opt, - ); + let result: Result<(String, String), anyhow::Error> = if !skip_build { + build_and_verify_repo( + mount_path.to_str().unwrap().to_string(), + base_image.clone(), + bpf_flag, + library_name.clone(), + connection, + program_id, + cargo_args.clone(), + container_id_opt, + ) + } else { + Ok(("skipped".to_string(), "skipped".to_string())) + }; // Cleanup no matter the result - std::process::Command::new("rm") - .args(["-rf", &verify_dir]) - .output()?; + if !skip_build { + std::process::Command::new("rm") + .args(["-rf", &verify_dir]) + .output()?; + } - // Compare hashes or return error - if let Ok((build_hash, program_hash)) = result { - println!("Executable Program Hash from repo: {}", build_hash); - println!("On-chain Program Hash: {}", program_hash); + // Handle the result + match result { + Ok((build_hash, program_hash)) => { + if !skip_build { + println!("Executable Program Hash from repo: {}", build_hash); + println!("On-chain Program Hash: {}", program_hash); + } - if build_hash == program_hash { - println!("Program hash matches ✅"); - let x = upload_program( - repo_url, - &commit_hash.clone(), - args.iter().map(|&s| s.into()).collect(), - program_id, - connection, - skip_prompt, - path_to_keypair, - ) - .await; - if x.is_err() { - println!("Error uploading program: {:?}", x); - exit(1); + if skip_build || build_hash == program_hash { + if skip_build { + println!("Skipping local build and uploading program"); + } else { + println!("Program hash matches ✅"); + } + + upload_program( + repo_url.clone(), + &commit_hash.clone(), + args.iter().map(|&s| s.into()).collect(), + program_id, + connection, + skip_prompt, + path_to_keypair, + compute_unit_price, + ) + .await?; + + if remote { + check_signal(container_id_opt, temp_dir_opt); + 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.")); + } + + println!("Sending verify command to remote machine..."); + send_job_to_remote( + &repo_url, + &commit_hash, + &program_id, + &library_name_opt.clone(), + bpf_flag, + relative_mount_path.clone(), + base_image.clone(), + cargo_args.clone(), + ) + .await?; + } + + Ok(()) + } else { + println!("Program hashes do not match ❌"); + println!("Executable Program Hash from repo: {}", build_hash); + println!("On-chain Program Hash: {}", program_hash); + Ok(()) } - } else { - println!("Program hashes do not match ❌"); } - - Ok(()) - } else { - Err(anyhow!("Error verifying program. {:?}", result)) + Err(e) => Err(anyhow!("Error verifying program: {:?}", e)), } } diff --git a/src/solana_program.rs b/src/solana_program.rs index bfad56f..76e52f8 100644 --- a/src/solana_program.rs +++ b/src/solana_program.rs @@ -12,8 +12,7 @@ use std::{ use borsh::{to_vec, BorshDeserialize, BorshSerialize}; use solana_sdk::{ - instruction::AccountMeta, message::Message, pubkey::Pubkey, signature::Keypair, signer::Signer, - system_program, transaction::Transaction, + compute_budget::ComputeBudgetInstruction, instruction::AccountMeta, message::Message, pubkey::Pubkey, signature::Keypair, signer::Signer, system_program, transaction::Transaction }; use solana_account_decoder::UiAccountEncoding; @@ -116,6 +115,7 @@ fn process_otter_verify_ixs( instruction: OtterVerifyInstructions, rpc_client: &RpcClient, path_to_keypair: Option, + compute_unit_price: u64, ) -> anyhow::Result<()> { let user_config = get_user_config()?; let signer = if let Some(path_to_keypair) = path_to_keypair { @@ -147,7 +147,14 @@ fn process_otter_verify_ixs( &ix_data, accounts_meta_vec, ); - let message = Message::new(&[ix], Some(&signer_pubkey)); + + let message = if compute_unit_price > 0 { + // Add compute budget instruction for priority fees only if price > 0 + let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price); + Message::new(&[compute_budget_ix, ix], Some(&signer_pubkey)) + } else { + Message::new(&[ix], Some(&signer_pubkey)) + }; let mut tx = Transaction::new_unsigned(message); @@ -190,6 +197,7 @@ pub async fn upload_program( connection: &RpcClient, skip_prompt: bool, path_to_keypair: Option, + compute_unit_price: u64, ) -> anyhow::Result<()> { if skip_prompt || prompt_user_input( @@ -237,6 +245,7 @@ pub async fn upload_program( OtterVerifyInstructions::Update, connection, path_to_keypair, + compute_unit_price, )?; } else if connection.get_account(&pda_account_2).is_ok() { let wanna_create_new_pda = skip_prompt || prompt_user_input( @@ -250,6 +259,7 @@ pub async fn upload_program( OtterVerifyInstructions::Initialize, connection, path_to_keypair, + compute_unit_price, )?; } return Ok(()); @@ -262,6 +272,7 @@ pub async fn upload_program( OtterVerifyInstructions::Initialize, connection, path_to_keypair, + compute_unit_price, )?; } } else { @@ -276,7 +287,11 @@ fn find_build_params_pda(program_id: &Pubkey, signer: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(seeds, &OTTER_VERIFY_PROGRAM_ID) } -pub async fn process_close(program_address: Pubkey, connection: &RpcClient) -> anyhow::Result<()> { +pub async fn process_close( + program_address: Pubkey, + connection: &RpcClient, + compute_unit_price: u64, +) -> anyhow::Result<()> { let user_config = get_user_config()?; let signer = user_config.0; let signer_pubkey = signer.pubkey(); @@ -301,6 +316,7 @@ pub async fn process_close(program_address: Pubkey, connection: &RpcClient) -> a OtterVerifyInstructions::Close, connection, None, + compute_unit_price, )?; } else { return Err(anyhow!(