From 83d18301e6b60d9c7de8abc61beb8f233de7c0d4 Mon Sep 17 00:00:00 2001 From: Noah Gundotra Date: Tue, 17 Dec 2024 15:07:34 -0500 Subject: [PATCH 1/6] refactor & add export-pda-tx to handle exporting of the pda creation tx --- Cargo.lock | 35 +++- Cargo.toml | 4 + src/main.rs | 419 ++++++++++++++++++++++++++++++------------ src/solana_program.rs | 53 ++++-- 4 files changed, 372 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f4bc06..01d4ad8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -416,6 +416,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.5.3" @@ -635,6 +641,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.12.0" @@ -3364,7 +3379,7 @@ dependencies = [ "Inflector", "base64 0.21.7", "bincode", - "bs58", + "bs58 0.4.0", "bv", "lazy_static", "serde", @@ -3489,7 +3504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bfcde2fc6946c99c7e3400fadd04d1628d675bfd66cb34d461c0f3224bd27d1" dependencies = [ "block-buffer 0.10.4", - "bs58", + "bs58 0.4.0", "bv", "either", "generic-array", @@ -3623,7 +3638,7 @@ dependencies = [ "borsh 0.10.4", "borsh 0.9.3", "borsh 1.5.0", - "bs58", + "bs58 0.4.0", "bv", "bytemuck", "cc", @@ -3779,7 +3794,7 @@ dependencies = [ "async-trait", "base64 0.21.7", "bincode", - "bs58", + "bs58 0.4.0", "indicatif", "log", "reqwest", @@ -3803,7 +3818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617df2c53f948c821cefca6824e376aac04ff0d844bb27f4d3ada9e211bcffe7" dependencies = [ "base64 0.21.7", - "bs58", + "bs58 0.4.0", "jsonrpc-core", "reqwest", "semver", @@ -3842,7 +3857,7 @@ dependencies = [ "bincode", "bitflags 2.6.0", "borsh 1.5.0", - "bs58", + "bs58 0.4.0", "bytemuck", "byteorder", "chrono", @@ -3892,7 +3907,7 @@ version = "1.18.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a8613ca80150f7e277e773620ba65d2c5fcc3a08eb8026627d601421ab43aef" dependencies = [ - "bs58", + "bs58 0.4.0", "proc-macro2", "quote", "rustversion", @@ -3987,7 +4002,7 @@ dependencies = [ "base64 0.21.7", "bincode", "borsh 0.10.4", - "bs58", + "bs58 0.4.0", "lazy_static", "log", "serde", @@ -4022,7 +4037,10 @@ name = "solana-verify" version = "0.3.1" dependencies = [ "anyhow", + "base64 0.22.1", + "bincode", "borsh 1.5.0", + "bs58 0.5.1", "cargo-lock", "cargo_toml", "clap 2.34.0", @@ -4042,6 +4060,7 @@ dependencies = [ "solana-cli-config", "solana-client", "solana-sdk", + "solana-transaction-status", "tokio", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index c5ab689..c2cd603 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,10 @@ solana-client = "=1.18.23" solana-sdk = "=1.18.23" tokio = { version = "1.29.1", features = ["full"] } solana-account-decoder = "1.18.23" +bincode = "1.3.3" +bs58 = "0.5.1" +base64 = "0.22.1" +solana-transaction-status = "=1.18.23" [dependencies.uuid] version = "1.2.2" diff --git a/src/main.rs b/src/main.rs index 0564465..fc229fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,28 @@ use anyhow::anyhow; -use api::{get_remote_job, get_remote_status, send_job_with_uploader_to_remote}; +use api::{ + get_last_deployed_slot, get_remote_job, get_remote_status, send_job_with_uploader_to_remote, +}; +use base64::{prelude::BASE64_STANDARD, Engine}; +use bincode::serialize; use cargo_lock::Lockfile; use cargo_toml::Manifest; -use clap::{App, AppSettings, Arg, SubCommand}; +use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; +use serde::Serialize; use signal_hook::{ consts::{SIGINT, SIGTERM}, iterator::Signals, }; use solana_cli_config::{Config, CONFIG_FILE}; -use solana_client::rpc_client::RpcClient; -use solana_program::{get_all_pdas_available, get_program_pda, resolve_rpc_url, OtterBuildParams}; +use solana_client::{rpc_client::RpcClient, rpc_config::EncodingConfig}; +use solana_program::{ + compose_transaction, find_build_params_pda, get_all_pdas_available, get_program_pda, + resolve_rpc_url, InputParams, OtterBuildParams, OtterVerifyInstructions, +}; use solana_sdk::{ bpf_loader_upgradeable::{self, UpgradeableLoaderState}, pubkey::Pubkey, }; +use solana_transaction_status::UiTransactionEncoding; use std::{ io::Read, path::PathBuf, @@ -23,6 +32,7 @@ use std::{ Arc, }, }; +use tokio::join; use uuid::Uuid; pub mod api; #[rustfmt::skip] @@ -233,6 +243,51 @@ async fn main() -> anyhow::Result<()> { .long("skip-build") .help("Skip building and verification, only upload the PDA") .takes_value(false))) + .subcommand(SubCommand::with_name("export-pda-tx") + .about("Export the transaction as base58 for use with Squads") + .arg(Arg::with_name("uploader") + .long("uploader") + .takes_value(true) + .required(true) + .help("Specifies an address to use for uploading the program verification args (should be the program authority)")) + .arg(Arg::with_name("encoding") + .long("encoding") + .takes_value(true) + .default_value("base58") + .possible_values(&["base58", "base64"]) + .help("The encoding to use for the transaction")) + .arg(Arg::with_name("mount-path") + .long("mount-path") + .takes_value(true) + .default_value("") + .help("Relative path to the root directory or the source code repository from which to build the program")) + .arg(Arg::with_name("repo-url") + .required(true) + .help("The HTTPS URL of the repo to clone")) + .arg(Arg::with_name("commit-hash") + .long("commit-hash") + .takes_value(true) + .help("Commit hash to checkout. Required to know the correct program snapshot. Will fallback to HEAD if not provided")) + .arg(Arg::with_name("program-id") + .long("program-id") + .required(true) + .takes_value(true) + .help("The Program ID of the program to verify")) + .arg(Arg::with_name("base-image") + .short("b") + .long("base-image") + .takes_value(true) + .help("Optionally specify a custom base docker image to use for building")) + .arg(Arg::with_name("library-name") + .long("library-name") + .takes_value(true) + .help("Specify the name of the library to build and verify")) + .arg(Arg::with_name("bpf") + .long("bpf") + .help("If the program requires cargo build-bpf (instead of cargo build-sbf), set this flag")) + .arg(Arg::with_name("current-dir") + .long("current-dir") + .help("Verify in current directory"))) .subcommand(SubCommand::with_name("close") .about("Close the otter-verify PDA account associated with the given program ID") .arg(Arg::with_name("program-id") @@ -240,6 +295,10 @@ async fn main() -> anyhow::Result<()> { .required(true) .takes_value(true) .help("The address of the program to close the PDA"))) + .arg(Arg::with_name("export") + .long("export") + .required(false) + .help("Print the transaction as base58 for use with Squads")) .subcommand(SubCommand::with_name("list-program-pdas") .about("List all the PDA information associated with a program ID. Requires custom RPC endpoint") .arg(Arg::with_name("program-id") @@ -372,19 +431,7 @@ async fn main() -> anyhow::Result<()> { .map(|s| s.to_string()) .collect(); - let commit_hash = sub_m - .value_of("commit-hash") - .map(String::from) - .or_else(|| { - get_commit_hash_from_remote(&repo_url).ok() // Dynamically determine commit hash from remote - }) - .ok_or_else(|| { - anyhow::anyhow!( - "Commit hash must be provided or inferred from the remote repository" - ) - })?; - - println!("Commit hash from remote: {}", commit_hash); + let commit_hash = get_commit_hash(sub_m, &repo_url)?; println!("Skipping prompt: {}", skip_prompt); verify_from_repo( @@ -423,6 +470,96 @@ async fn main() -> anyhow::Result<()> { ) .await } + ("export-pda-tx", Some(sub_m)) => { + let uploader = sub_m.value_of("uploader").unwrap(); + 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(); + let program_id = sub_m.value_of("program-id").unwrap(); + let base_image = sub_m.value_of("base-image").map(|s| s.to_string()); + let library_name = sub_m.value_of("library-name").map(|s| s.to_string()); + let bpf_flag = sub_m.is_present("bpf"); + let encoding = sub_m.value_of("encoding").unwrap(); + + let encoding: UiTransactionEncoding = match encoding { + "base58" => UiTransactionEncoding::Base58, + "base64" => UiTransactionEncoding::Base64, + _ => { + return Err(anyhow!("Unsupported encoding: {}", encoding)); + } + }; + + let compute_unit_price = matches + .value_of("compute-unit-price") + .unwrap() + .parse::() + .unwrap_or(100000); + + let commit_hash = get_commit_hash(sub_m, &repo_url)?; + let cargo_args: Vec = sub_m + .values_of("cargo-args") + .unwrap_or_default() + .map(|s| s.to_string()) + .collect(); + + let connection = resolve_rpc_url(matches.value_of("url").map(|s| s.to_string()))?; + println!("Using connection url: {}", connection.url()); + + let last_deployed_slot = get_last_deployed_slot(&connection, &program_id.to_string()) + .await + .map_err(|err| anyhow!("Unable to get last deployed slot: {}", err))?; + + let (temp_root_path, verify_dir) = clone_repo_and_checkout( + &repo_url, + true, + &get_basename(&repo_url)?, + Some(commit_hash.clone()), + &mut temp_dir, + )?; + + let input_params = InputParams { + version: env!("CARGO_PKG_VERSION").to_string(), + git_url: repo_url, + commit: commit_hash.clone(), + args: build_args( + &mount_path, + library_name, + &temp_root_path, + base_image, + bpf_flag, + cargo_args, + )? + .0, + deployed_slot: last_deployed_slot, + }; + + std::process::Command::new("rm") + .args(["-rf", &verify_dir]) + .output()?; + + let (pda, _) = + find_build_params_pda(&Pubkey::try_from(program_id)?, &Pubkey::try_from(uploader)?); + + let tx = compose_transaction( + &input_params, + Pubkey::try_from(uploader)?, + pda, + Pubkey::try_from(program_id)?, + OtterVerifyInstructions::Initialize, + compute_unit_price, + ); + + // serialize the transaction to base58 + match encoding { + UiTransactionEncoding::Base58 => { + println!("{}", bs58::encode(serialize(&tx)?).into_string()); + } + UiTransactionEncoding::Base64 => { + println!("{}", BASE64_STANDARD.encode(serialize(&tx)?)); + } + _ => unreachable!(), + } + Ok(()) + } ("list-program-pdas", Some(sub_m)) => { let program_id = sub_m.value_of("program-id").unwrap(); list_program_pdas(Pubkey::try_from(program_id)?, &connection).await @@ -935,97 +1072,23 @@ pub fn verify_from_image( Ok(()) } -#[allow(clippy::too_many_arguments)] -pub async fn verify_from_repo( - remote: bool, - relative_mount_path: String, - connection: &RpcClient, - repo_url: String, - commit_hash: Option, - program_id: Pubkey, - base_image: Option, +fn build_args( + relative_mount_path: &str, library_name_opt: Option, + verify_tmp_root_path: &str, + base_image: Option, bpf_flag: bool, cargo_args: Vec, - 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<()> { - // Set skip_build to true if remote is true - skip_build |= remote; - - // Create a Vec to store solana-verify args - let mut args: Vec<&str> = Vec::new(); - - // Get source code from repo_url - let base_name = std::process::Command::new("basename") - .arg(&repo_url) - .output() - .map_err(|e| anyhow!("Failed to get basename of repo_url: {:?}", e)) - .and_then(|output| parse_output(output.stdout))?; - - let uuid = Uuid::new_v4().to_string(); - - // Create a temporary directory to clone the repo into - let verify_dir = if current_dir { - format!( - "{}/.{}", - std::env::current_dir()? - .as_os_str() - .to_str() - .ok_or_else(|| anyhow::Error::msg("Invalid path string"))?, - uuid.clone() - ) - } else { - format!("/tmp/solana-verify/{}", uuid) - }; - - temp_dir_opt.replace(verify_dir.clone()); - - 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") - .args(["-C", &verify_tmp_root_path]) - .args(["checkout", commit_hash]) - .output() - .map_err(|e| anyhow!("Failed to checkout commit hash: {:?}", e)); - if result.is_ok() { - println!("Checked out commit hash: {}", commit_hash); - } else { - std::process::Command::new("rm") - .args(["-rf", verify_dir.as_str()]) - .output()?; - Err(anyhow!("Encountered error in git setup: {:?}", result))?; - } - } - - check_signal(container_id_opt, temp_dir_opt); - +) -> anyhow::Result<(Vec, String, String)> { + let mut args: Vec = Vec::new(); if !relative_mount_path.is_empty() { - args.push("--mount-path"); - args.push(&relative_mount_path); + args.push("--mount-path".to_string()); + args.push(relative_mount_path.to_string()); } // Get the absolute build path to the solana program directory to build inside docker let mount_path = PathBuf::from(verify_tmp_root_path.clone()).join(&relative_mount_path); - println!("Build path: {:?}", mount_path); - args.push("--library-name"); + args.push("--library-name".to_string()); let library_name = match library_name_opt.clone() { Some(p) => p, None => { @@ -1067,28 +1130,143 @@ pub async fn verify_from_repo( })? } }; - args.push(&library_name); - println!("Verifying program: {}", library_name); + args.push(library_name.clone()); if let Some(base_image) = &base_image { - args.push("--base-image"); - args.push(base_image); + args.push("--base-image".to_string()); + args.push(base_image.clone()); } if bpf_flag { - args.push("--bpf"); + args.push("--bpf".to_string()); } if !cargo_args.is_empty() { - args.push("--"); + args.push("--".to_string()); for arg in &cargo_args { - args.push(arg); + args.push(arg.clone()); } } + Ok((args, mount_path.to_str().unwrap().to_string(), library_name)) +} + +fn clone_repo_and_checkout( + repo_url: &str, + current_dir: bool, + base_name: &str, + commit_hash: Option, + temp_dir_opt: &mut Option, +) -> anyhow::Result<(String, String)> { + let uuid = Uuid::new_v4().to_string(); + + // Create a temporary directory to clone the repo into + let verify_dir = if current_dir { + format!( + "{}/.{}", + std::env::current_dir()? + .as_os_str() + .to_str() + .ok_or_else(|| anyhow::Error::msg("Invalid path string"))?, + uuid.clone() + ) + } else { + format!("/tmp/solana-verify/{}", uuid) + }; + + temp_dir_opt.replace(verify_dir.clone()); + + let verify_tmp_root_path = format!("{}/{}", verify_dir, base_name); + println!("Cloning repo into: {}", verify_tmp_root_path); + + std::process::Command::new("git") + .args(["clone", &repo_url, &verify_tmp_root_path]) + .stdout(Stdio::inherit()) + .output()?; + + if let Some(commit_hash) = commit_hash.as_ref() { + let result = std::process::Command::new("git") + .args(["-C", &verify_tmp_root_path]) + .args(["checkout", commit_hash]) + .output() + .map_err(|e| anyhow!("Failed to checkout commit hash: {:?}", e)); + if result.is_ok() { + println!("Checked out commit hash: {}", commit_hash); + } else { + std::process::Command::new("rm") + .args(["-rf", verify_dir.as_str()]) + .output()?; + Err(anyhow!("Encountered error in git setup: {:?}", result))?; + } + } + + Ok((verify_tmp_root_path, verify_dir)) +} + +fn get_basename(repo_url: &str) -> anyhow::Result { + let base_name = std::process::Command::new("basename") + .arg(&repo_url) + .output() + .map_err(|e| anyhow!("Failed to get basename of repo_url: {:?}", e)) + .and_then(|output| parse_output(output.stdout))?; + Ok(base_name) +} + +#[allow(clippy::too_many_arguments)] +pub async fn verify_from_repo( + remote: bool, + relative_mount_path: String, + connection: &RpcClient, + repo_url: String, + commit_hash: Option, + program_id: Pubkey, + base_image: Option, + library_name_opt: Option, + bpf_flag: bool, + cargo_args: Vec, + 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<()> { + // Set skip_build to true if remote is true + skip_build |= remote; + + // Get source code from repo_url + let base_name = get_basename(&repo_url)?; + + check_signal(container_id_opt, temp_dir_opt); + + let (verify_tmp_root_path, verify_dir) = clone_repo_and_checkout( + &repo_url, + current_dir, + &base_name, + commit_hash.clone(), + temp_dir_opt, + )?; + + check_signal(container_id_opt, temp_dir_opt); + + let (args, mount_path, library_name) = build_args( + &relative_mount_path, + library_name_opt.clone(), + &verify_tmp_root_path, + base_image.clone(), + bpf_flag, + cargo_args.clone(), + )?; + println!("Build path: {:?}", mount_path); + println!("Verifying program: {}", library_name); + + check_signal(container_id_opt, temp_dir_opt); + let result: Result<(String, String), anyhow::Error> = if !skip_build { build_and_verify_repo( - mount_path.to_str().unwrap().to_string(), + mount_path, base_image.clone(), bpf_flag, library_name.clone(), @@ -1102,11 +1280,9 @@ pub async fn verify_from_repo( }; // Cleanup no matter the result - if !skip_build { - std::process::Command::new("rm") - .args(["-rf", &verify_dir]) - .output()?; - } + std::process::Command::new("rm") + .args(["-rf", &verify_dir]) + .output()?; // Handle the result match result { @@ -1126,7 +1302,7 @@ pub async fn verify_from_repo( upload_program( repo_url.clone(), &commit_hash.clone(), - args.iter().map(|&s| s.into()).collect(), + args.iter().map(|s| s.to_string()).collect(), program_id, connection, skip_prompt, @@ -1285,3 +1461,18 @@ pub async fn print_program_pda( print_build_params(&pda, &build_params); Ok(()) } + +pub fn get_commit_hash(sub_m: &ArgMatches, repo_url: &str) -> anyhow::Result { + let commit_hash = sub_m + .value_of("commit-hash") + .map(String::from) + .or_else(|| { + get_commit_hash_from_remote(&repo_url).ok() // Dynamically determine commit hash from remote + }) + .ok_or_else(|| { + anyhow::anyhow!("Commit hash must be provided or inferred from the remote repository") + })?; + + println!("Commit hash from remote: {}", commit_hash); + Ok(commit_hash) +} diff --git a/src/solana_program.rs b/src/solana_program.rs index 76e52f8..77ea7a9 100644 --- a/src/solana_program.rs +++ b/src/solana_program.rs @@ -12,7 +12,8 @@ use std::{ use borsh::{to_vec, BorshDeserialize, BorshSerialize}; use solana_sdk::{ - compute_budget::ComputeBudgetInstruction, 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; @@ -108,24 +109,14 @@ fn get_user_config() -> anyhow::Result<(Keypair, RpcClient)> { Ok((signer, rpc_client)) } -fn process_otter_verify_ixs( +pub fn compose_transaction( params: &InputParams, + signer_pubkey: Pubkey, pda_account: Pubkey, program_address: Pubkey, 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 { - get_keypair_from_path(&path_to_keypair)? - } else { - user_config.0 - }; - let signer_pubkey = signer.pubkey(); - let connection = rpc_client; - +) -> Transaction { let ix_data = if instruction != OtterVerifyInstructions::Close { create_ix_data(params, &instruction) } else { @@ -150,13 +141,41 @@ fn process_otter_verify_ixs( 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); + 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); + Transaction::new_unsigned(message) +} + +fn process_otter_verify_ixs( + params: &InputParams, + pda_account: Pubkey, + program_address: Pubkey, + 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 { + get_keypair_from_path(&path_to_keypair)? + } else { + user_config.0 + }; + let connection = rpc_client; + + let mut tx = compose_transaction( + params, + signer.pubkey(), + pda_account, + program_address, + instruction, + compute_unit_price, + ); tx.sign(&[&signer], connection.get_latest_blockhash()?); @@ -282,7 +301,7 @@ pub async fn upload_program( Ok(()) } -fn find_build_params_pda(program_id: &Pubkey, signer: &Pubkey) -> (Pubkey, u8) { +pub fn find_build_params_pda(program_id: &Pubkey, signer: &Pubkey) -> (Pubkey, u8) { let seeds: &[&[u8]; 3] = &[b"otter_verify", &signer.to_bytes(), &program_id.to_bytes()]; Pubkey::find_program_address(seeds, &OTTER_VERIFY_PROGRAM_ID) } From 37373da0d73d4ee3fc299fe18c4ec6de3836a12a Mon Sep 17 00:00:00 2001 From: Noah Gundotra Date: Tue, 17 Dec 2024 16:24:58 -0500 Subject: [PATCH 2/6] refactor into its own function --- src/main.rs | 140 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 53 deletions(-) diff --git a/src/main.rs b/src/main.rs index fc229fe..ec136e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -504,61 +504,22 @@ async fn main() -> anyhow::Result<()> { let connection = resolve_rpc_url(matches.value_of("url").map(|s| s.to_string()))?; println!("Using connection url: {}", connection.url()); - let last_deployed_slot = get_last_deployed_slot(&connection, &program_id.to_string()) - .await - .map_err(|err| anyhow!("Unable to get last deployed slot: {}", err))?; - - let (temp_root_path, verify_dir) = clone_repo_and_checkout( - &repo_url, - true, - &get_basename(&repo_url)?, - Some(commit_hash.clone()), - &mut temp_dir, - )?; - - let input_params = InputParams { - version: env!("CARGO_PKG_VERSION").to_string(), - git_url: repo_url, - commit: commit_hash.clone(), - args: build_args( - &mount_path, - library_name, - &temp_root_path, - base_image, - bpf_flag, - cargo_args, - )? - .0, - deployed_slot: last_deployed_slot, - }; - - std::process::Command::new("rm") - .args(["-rf", &verify_dir]) - .output()?; - - let (pda, _) = - find_build_params_pda(&Pubkey::try_from(program_id)?, &Pubkey::try_from(uploader)?); - - let tx = compose_transaction( - &input_params, - Pubkey::try_from(uploader)?, - pda, + export_pda_tx( + &connection, Pubkey::try_from(program_id)?, - OtterVerifyInstructions::Initialize, + Pubkey::try_from(uploader)?, + repo_url, + commit_hash, + mount_path, + library_name, + base_image, + bpf_flag, + &mut temp_dir, + encoding, + cargo_args, compute_unit_price, - ); - - // serialize the transaction to base58 - match encoding { - UiTransactionEncoding::Base58 => { - println!("{}", bs58::encode(serialize(&tx)?).into_string()); - } - UiTransactionEncoding::Base64 => { - println!("{}", BASE64_STANDARD.encode(serialize(&tx)?)); - } - _ => unreachable!(), - } - Ok(()) + ) + .await } ("list-program-pdas", Some(sub_m)) => { let program_id = sub_m.value_of("program-id").unwrap(); @@ -1476,3 +1437,76 @@ pub fn get_commit_hash(sub_m: &ArgMatches, repo_url: &str) -> anyhow::Result, + base_image: Option, + bpf_flag: bool, + temp_dir: &mut Option, + encoding: UiTransactionEncoding, + cargo_args: Vec, + compute_unit_price: u64, +) -> anyhow::Result<()> { + let last_deployed_slot = get_last_deployed_slot(&connection, &program_id.to_string()) + .await + .map_err(|err| anyhow!("Unable to get last deployed slot: {}", err))?; + + let (temp_root_path, verify_dir) = clone_repo_and_checkout( + &repo_url, + true, + &get_basename(&repo_url)?, + Some(commit_hash.clone()), + temp_dir, + )?; + + let input_params = InputParams { + version: env!("CARGO_PKG_VERSION").to_string(), + git_url: repo_url, + commit: commit_hash.clone(), + args: build_args( + &mount_path, + library_name.clone(), + &temp_root_path, + base_image.clone(), + bpf_flag.clone(), + cargo_args, + )? + .0, + deployed_slot: last_deployed_slot, + }; + + std::process::Command::new("rm") + .args(["-rf", &verify_dir]) + .output()?; + + let (pda, _) = + find_build_params_pda(&Pubkey::try_from(program_id)?, &Pubkey::try_from(uploader)?); + + let tx = compose_transaction( + &input_params, + Pubkey::try_from(uploader)?, + pda, + Pubkey::try_from(program_id)?, + OtterVerifyInstructions::Initialize, + compute_unit_price, + ); + + // serialize the transaction to base58 + match encoding { + UiTransactionEncoding::Base58 => { + println!("{}", bs58::encode(serialize(&tx)?).into_string()); + } + UiTransactionEncoding::Base64 => { + println!("{}", BASE64_STANDARD.encode(serialize(&tx)?)); + } + _ => unreachable!(), + } + + Ok(()) +} From c74d6dd941f78ef88e7ab7491f88ca8a4f02bd6b Mon Sep 17 00:00:00 2001 From: Noah Gundotra Date: Tue, 17 Dec 2024 16:29:22 -0500 Subject: [PATCH 3/6] switch update/initialize ix based on whether or not pda exists --- src/main.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index ec136e4..4c7d4b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1488,12 +1488,26 @@ async fn export_pda_tx( let (pda, _) = find_build_params_pda(&Pubkey::try_from(program_id)?, &Pubkey::try_from(uploader)?); + // check if account already exists + let instruction = match connection.get_account(&pda) { + Ok(account_info) => { + if account_info.data.len() > 0 { + println!("PDA already exists, creating update transaction"); + OtterVerifyInstructions::Update + } else { + println!("PDA does not exist, creating initialize transaction"); + OtterVerifyInstructions::Initialize + } + } + Err(_) => OtterVerifyInstructions::Initialize, + }; + let tx = compose_transaction( &input_params, Pubkey::try_from(uploader)?, pda, Pubkey::try_from(program_id)?, - OtterVerifyInstructions::Initialize, + instruction, compute_unit_price, ); From 80ba682e1653beae396ffe89572b063ee5600466 Mon Sep 17 00:00:00 2001 From: Noah Gundotra Date: Wed, 18 Dec 2024 09:52:00 -0500 Subject: [PATCH 4/6] remove dead code --- src/main.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4c7d4b5..a209acf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,13 +7,12 @@ use bincode::serialize; use cargo_lock::Lockfile; use cargo_toml::Manifest; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; -use serde::Serialize; use signal_hook::{ consts::{SIGINT, SIGTERM}, iterator::Signals, }; use solana_cli_config::{Config, CONFIG_FILE}; -use solana_client::{rpc_client::RpcClient, rpc_config::EncodingConfig}; +use solana_client::rpc_client::RpcClient; use solana_program::{ compose_transaction, find_build_params_pda, get_all_pdas_available, get_program_pda, resolve_rpc_url, InputParams, OtterBuildParams, OtterVerifyInstructions, @@ -32,7 +31,6 @@ use std::{ Arc, }, }; -use tokio::join; use uuid::Uuid; pub mod api; #[rustfmt::skip] @@ -1047,7 +1045,7 @@ fn build_args( args.push(relative_mount_path.to_string()); } // Get the absolute build path to the solana program directory to build inside docker - let mount_path = PathBuf::from(verify_tmp_root_path.clone()).join(&relative_mount_path); + let mount_path = PathBuf::from(verify_tmp_root_path).join(&relative_mount_path); args.push("--library-name".to_string()); let library_name = match library_name_opt.clone() { From 6734e000aec86ac63868dcfe9b5efafc8779279b Mon Sep 17 00:00:00 2001 From: Noah Gundotra Date: Wed, 18 Dec 2024 11:13:30 -0500 Subject: [PATCH 5/6] update prompt and function name --- src/main.rs | 16 ++++++++-------- src/solana_program.rs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index a209acf..47e9ef0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,10 +13,6 @@ use signal_hook::{ }; use solana_cli_config::{Config, CONFIG_FILE}; use solana_client::rpc_client::RpcClient; -use solana_program::{ - compose_transaction, find_build_params_pda, get_all_pdas_available, get_program_pda, - resolve_rpc_url, InputParams, OtterBuildParams, OtterVerifyInstructions, -}; use solana_sdk::{ bpf_loader_upgradeable::{self, UpgradeableLoaderState}, pubkey::Pubkey, @@ -43,7 +39,11 @@ mod test; use crate::{ api::send_job_to_remote, - solana_program::{process_close, upload_program}, + solana_program::{ + compose_transaction, find_build_params_pda, get_all_pdas_available, get_program_pda, + process_close, resolve_rpc_url, upload_program_verification_data, InputParams, + OtterBuildParams, OtterVerifyInstructions, + }, }; const MAINNET_GENESIS_HASH: &str = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d"; @@ -227,7 +227,7 @@ async fn main() -> anyhow::Result<()> { .arg(Arg::with_name("skip-prompt") .short("y") .long("skip-prompt") - .help("Skip the prompt to upload a new program")) + .help("Skip the prompt to write verify data on chain without user confirmation")) .arg(Arg::with_name("keypair") .short("k") .long("keypair") @@ -1253,12 +1253,12 @@ pub async fn verify_from_repo( if skip_build || build_hash == program_hash { if skip_build { - println!("Skipping local build and uploading program"); + println!("Skipping local build and writing verify data on chain"); } else { println!("Program hash matches ✅"); } - upload_program( + upload_program_verification_data( repo_url.clone(), &commit_hash.clone(), args.iter().map(|s| s.to_string()).collect(), diff --git a/src/solana_program.rs b/src/solana_program.rs index 77ea7a9..6e4f97d 100644 --- a/src/solana_program.rs +++ b/src/solana_program.rs @@ -208,7 +208,7 @@ pub fn resolve_rpc_url(url: Option) -> anyhow::Result { Ok(connection) } -pub async fn upload_program( +pub async fn upload_program_verification_data( git_url: String, commit: &Option, args: Vec, From 88e22316808e6203a408bc653c02ef8e98c4598c Mon Sep 17 00:00:00 2001 From: Noah Gundotra Date: Wed, 18 Dec 2024 11:16:55 -0500 Subject: [PATCH 6/6] cargo clippy fix --- src/api/client.rs | 12 ++++-------- src/main.rs | 26 +++++++++++++------------- src/solana_program.rs | 9 +++++---- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/api/client.rs b/src/api/client.rs index 0ef73c1..4207d20 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -5,7 +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::sync::atomic::Ordering; use std::thread; use std::time::{Duration, Instant}; @@ -190,7 +190,7 @@ pub async fn handle_submission_response( break; // Exit the loop and continue with normal error handling } - let status = check_job_status(&client, &request_id).await?; + let status = check_job_status(client, &request_id).await?; match status.status { JobStatus::InProgress => { if SIGNAL_RECEIVED.load(Ordering::Relaxed) { @@ -266,7 +266,7 @@ pub async fn handle_submission_response( async fn check_job_status(client: &Client, request_id: &str) -> anyhow::Result { // Get /job/:id let response = client - .get(&format!("{}/job/{}", REMOTE_SERVER_URL, request_id)) + .get(format!("{}/job/{}", REMOTE_SERVER_URL, request_id)) .send() .await .unwrap(); @@ -309,11 +309,7 @@ pub async fn get_remote_status(program_id: Pubkey) -> anyhow::Result<()> { .build()?; let response = client - .get(format!( - "{}/status-all/{}", - REMOTE_SERVER_URL, - program_id.to_string() - )) + .get(format!("{}/status-all/{}", REMOTE_SERVER_URL, program_id,)) .send() .await?; diff --git a/src/main.rs b/src/main.rs index 47e9ef0..3840bcf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1045,7 +1045,7 @@ fn build_args( args.push(relative_mount_path.to_string()); } // Get the absolute build path to the solana program directory to build inside docker - let mount_path = PathBuf::from(verify_tmp_root_path).join(&relative_mount_path); + let mount_path = PathBuf::from(verify_tmp_root_path).join(relative_mount_path); args.push("--library-name".to_string()); let library_name = match library_name_opt.clone() { @@ -1139,7 +1139,7 @@ fn clone_repo_and_checkout( println!("Cloning repo into: {}", verify_tmp_root_path); std::process::Command::new("git") - .args(["clone", &repo_url, &verify_tmp_root_path]) + .args(["clone", repo_url, &verify_tmp_root_path]) .stdout(Stdio::inherit()) .output()?; @@ -1164,7 +1164,7 @@ fn clone_repo_and_checkout( fn get_basename(repo_url: &str) -> anyhow::Result { let base_name = std::process::Command::new("basename") - .arg(&repo_url) + .arg(repo_url) .output() .map_err(|e| anyhow!("Failed to get basename of repo_url: {:?}", e)) .and_then(|output| parse_output(output.stdout))?; @@ -1404,7 +1404,7 @@ pub fn print_build_params(pubkey: &Pubkey, build_params: &OtterBuildParams) { } pub async fn list_program_pdas(program_id: Pubkey, client: &RpcClient) -> anyhow::Result<()> { - let pdas = get_all_pdas_available(&client, &program_id).await?; + let pdas = get_all_pdas_available(client, &program_id).await?; for (pda, build_params) in pdas { print_build_params(&pda, &build_params); } @@ -1416,7 +1416,7 @@ pub async fn print_program_pda( signer: Option, client: &RpcClient, ) -> anyhow::Result<()> { - let (pda, build_params) = get_program_pda(&client, &program_id, signer).await?; + let (pda, build_params) = get_program_pda(client, &program_id, signer).await?; print_build_params(&pda, &build_params); Ok(()) } @@ -1426,7 +1426,7 @@ pub fn get_commit_hash(sub_m: &ArgMatches, repo_url: &str) -> anyhow::Result anyhow::Result, compute_unit_price: u64, ) -> anyhow::Result<()> { - let last_deployed_slot = get_last_deployed_slot(&connection, &program_id.to_string()) + let last_deployed_slot = get_last_deployed_slot(connection, &program_id.to_string()) .await .map_err(|err| anyhow!("Unable to get last deployed slot: {}", err))?; @@ -1472,7 +1473,7 @@ async fn export_pda_tx( library_name.clone(), &temp_root_path, base_image.clone(), - bpf_flag.clone(), + bpf_flag, cargo_args, )? .0, @@ -1483,13 +1484,12 @@ async fn export_pda_tx( .args(["-rf", &verify_dir]) .output()?; - let (pda, _) = - find_build_params_pda(&Pubkey::try_from(program_id)?, &Pubkey::try_from(uploader)?); + let (pda, _) = find_build_params_pda(&program_id, &uploader); // check if account already exists let instruction = match connection.get_account(&pda) { Ok(account_info) => { - if account_info.data.len() > 0 { + if !account_info.data.is_empty() { println!("PDA already exists, creating update transaction"); OtterVerifyInstructions::Update } else { @@ -1502,9 +1502,9 @@ async fn export_pda_tx( let tx = compose_transaction( &input_params, - Pubkey::try_from(uploader)?, + uploader, pda, - Pubkey::try_from(program_id)?, + program_id, instruction, compute_unit_price, ); diff --git a/src/solana_program.rs b/src/solana_program.rs index 6e4f97d..3aa12f9 100644 --- a/src/solana_program.rs +++ b/src/solana_program.rs @@ -93,7 +93,7 @@ fn create_ix_data(params: &InputParams, ix: &OtterVerifyInstructions) -> Vec } fn get_keypair_from_path(path: &str) -> anyhow::Result { - solana_clap_utils::keypair::keypair_from_path(&Default::default(), &path, "keypair", false) + solana_clap_utils::keypair::keypair_from_path(&Default::default(), path, "keypair", false) .map_err(|err| anyhow!("Unable to get signer from path: {}", err)) } @@ -208,6 +208,7 @@ pub fn resolve_rpc_url(url: Option) -> anyhow::Result { Ok(connection) } +#[allow(clippy::too_many_arguments)] pub async fn upload_program_verification_data( git_url: String, commit: &Option, @@ -228,7 +229,7 @@ pub async fn upload_program_verification_data( let cli_config = get_user_config()?; let signer_pubkey: Pubkey = if let Some(ref path_to_keypair) = path_to_keypair { - get_keypair_from_path(&path_to_keypair)?.pubkey() + get_keypair_from_path(path_to_keypair)?.pubkey() } else { cli_config.0.pubkey() }; @@ -236,7 +237,7 @@ pub async fn upload_program_verification_data( // let rpc_url = connection.url(); println!("Using connection url: {}", connection.url()); - let last_deployed_slot = get_last_deployed_slot(&connection, &program_address.to_string()) + let last_deployed_slot = get_last_deployed_slot(connection, &program_address.to_string()) .await .map_err(|err| anyhow!("Unable to get last deployed slot: {}", err))?; @@ -315,7 +316,7 @@ pub async fn process_close( let signer = user_config.0; let signer_pubkey = signer.pubkey(); - let last_deployed_slot = get_last_deployed_slot(&connection, &program_address.to_string()) + let last_deployed_slot = get_last_deployed_slot(connection, &program_address.to_string()) .await .map_err(|err| anyhow!("Unable to get last deployed slot: {}", err))?;