Browse Source

fix: fix AVM instability with `solana-verify` (#3867)

* style(avm): Construct `Version` instead of parsing

* fix(avm): Only install `solana-verify` for Anchor >= 0.32

* style(avm): Refactor solana-verify install

* feat(avm): Install solana-verify from binary releases where possible

* feat(avm): Only install `solana-verify` when requested

* feat(avm): Don't re-install `solana-verify` if possible

* fix(avm): Always install solana-verify 0.4.7

* refactor(cli): Refactor AVM installation to another function

* feat(cli): Install `solana-verify` only when needed

* Formatting

* chore(avm): Update `solana-verify` version to 0.4.11
Jamie Hill-Daniel 1 tháng trước cách đây
mục cha
commit
5256dc1f9e
3 tập tin đã thay đổi với 139 bổ sung34 xóa
  1. 96 22
      avm/src/lib.rs
  2. 5 1
      avm/src/main.rs
  3. 38 11
      cli/src/lib.rs

+ 96 - 22
avm/src/lib.rs

@@ -1,4 +1,4 @@
-use anyhow::{anyhow, Error, Result};
+use anyhow::{anyhow, bail, Context, Error, Result};
 use cargo_toml::Manifest;
 use chrono::{TimeZone, Utc};
 use reqwest::header::USER_AGENT;
@@ -201,7 +201,9 @@ pub fn use_version(opt_version: Option<Version>) -> Result<()> {
             .next()
             .expect("Expected input")?;
         match input.as_str() {
-            "y" | "yes" => return install_version(InstallTarget::Version(version), false, false),
+            "y" | "yes" => {
+                return install_version(InstallTarget::Version(version), false, false, false)
+            }
             _ => return Err(anyhow!("Installation rejected.")),
         };
     }
@@ -222,7 +224,7 @@ pub enum InstallTarget {
 /// Update to the latest version
 pub fn update() -> Result<()> {
     let latest_version = get_latest_version()?;
-    install_version(InstallTarget::Version(latest_version), false, false)
+    install_version(InstallTarget::Version(latest_version), false, false, false)
 }
 
 /// The commit sha provided can be shortened,
@@ -284,6 +286,7 @@ pub fn install_version(
     install_target: InstallTarget,
     force: bool,
     from_source: bool,
+    with_solana_verify: bool,
 ) -> Result<()> {
     let (version, from_source) = match &install_target {
         InstallTarget::Version(version) => (version.to_owned(), from_source),
@@ -308,7 +311,7 @@ pub fn install_version(
     }
 
     let is_commit = matches!(install_target, InstallTarget::Commit(_));
-    let is_older_than_v0_31_0 = version < Version::parse("0.31.0")?;
+    let is_older_than_v0_31_0 = version < Version::new(0, 31, 0);
     if from_source || is_commit || is_older_than_v0_31_0 {
         // Build from source using `cargo install`
         let mut args: Vec<String> = vec![
@@ -430,37 +433,108 @@ pub fn install_version(
         )?;
     }
 
+    let is_at_least_0_32 = version >= Version::new(0, 32, 0);
+    if with_solana_verify {
+        if is_at_least_0_32 {
+            if !solana_verify_installed().is_ok_and(|v| v) {
+                #[cfg(any(target_os = "linux", target_os = "macos"))]
+                install_solana_verify()?;
+                #[cfg(not(any(target_os = "linux", target_os = "macos")))]
+                install_solana_verify_from_source()?;
+                println!("solana-verify successfully installed");
+            } else {
+                println!("solana-verify already installed");
+            }
+        } else {
+            println!("Not installing solana-verify for anchor < 0.32");
+        }
+    }
+
+    // If .version file is empty or not parseable, write the newly installed version to it
+    if current_version().is_err() {
+        let mut current_version_file = fs::File::create(current_version_file_path())?;
+        current_version_file.write_all(version.to_string().as_bytes())?;
+    }
+
+    use_version(Some(version))
+}
+
+const SOLANA_VERIFY_VERSION: Version = Version::new(0, 4, 11);
+
+/// Check if `solana-verify` is both installed and >= [`SOLANA_VERIFY_VERSION`].
+fn solana_verify_installed() -> Result<bool> {
+    let bin_path = get_bin_dir_path().join("solana-verify");
+    if !bin_path.exists() {
+        return Ok(false);
+    }
+    let output = Command::new(bin_path)
+        .arg("-V")
+        .output()
+        .context("executing `solana-verify` to check version")?;
+    let stdout =
+        String::from_utf8(output.stdout).context("expected `solana-verify` to output utf8")?;
+    let Some(("solana-verify", version)) = stdout.trim().split_once(" ") else {
+        bail!("invalid `solana-verify` output: `{stdout}`");
+    };
+    if Version::parse(version).with_context(|| "parsing solana-verify version `{version}")?
+        >= SOLANA_VERIFY_VERSION
+    {
+        Ok(true)
+    } else {
+        Ok(false)
+    }
+}
+
+/// Install `solana-verify` from binary releases. Only available on Linux and Mac
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+fn install_solana_verify() -> Result<()> {
     println!("Installing solana-verify...");
-    let solana_verify_install_output = Command::new("cargo")
+    let os = std::env::consts::OS;
+    let url = format!(
+        "https://github.com/Ellipsis-Labs/solana-verifiable-build/releases/download/v{SOLANA_VERIFY_VERSION}/solana-verify-{os}"
+    );
+    let res = reqwest::blocking::get(url)?;
+    if !res.status().is_success() {
+        bail!(
+            "Failed to download `solana-verify-{os} v{SOLANA_VERIFY_VERSION} (status code: {})",
+            res.status()
+        );
+    } else {
+        let bin_path = get_bin_dir_path().join("solana-verify");
+        fs::write(&bin_path, res.bytes()?)?;
+        #[cfg(unix)]
+        fs::set_permissions(
+            bin_path,
+            <fs::Permissions as std::os::unix::fs::PermissionsExt>::from_mode(0o775),
+        )?;
+        Ok(())
+    }
+}
+
+/// Install `solana-verify` by building from Git sources
+#[cfg(not(any(target_os = "linux", target_os = "macos")))]
+fn install_solana_verify_from_source() -> Result<()> {
+    println!("Installing solana-verify from source...");
+    let status = Command::new("cargo")
         .args([
             "install",
             "solana-verify",
             "--git",
             "https://github.com/Ellipsis-Labs/solana-verifiable-build",
             "--rev",
-            "568cb334709e88b9b45fc24f1f440eecacf5db54",
+            &format!("v{SOLANA_VERIFY_VERSION}"),
             "--root",
             AVM_HOME.to_str().unwrap(),
             "--force",
             "--locked",
         ])
-        .stdout(Stdio::inherit())
-        .stderr(Stdio::inherit())
-        .output()
-        .map_err(|e| anyhow!("`cargo install` for `solana-verify` failed: {e}"))?;
-
-    if !solana_verify_install_output.status.success() {
-        return Err(anyhow!("Failed to install `solana-verify`"));
-    }
-    println!("solana-verify successfully installed.");
-
-    // If .version file is empty or not parseable, write the newly installed version to it
-    if current_version().is_err() {
-        let mut current_version_file = fs::File::create(current_version_file_path())?;
-        current_version_file.write_all(version.to_string().as_bytes())?;
+        .status()
+        .context("executing `cargo install solana-verify`")?;
+    if status.success() {
+        Ok(())
+    } else {
+        bail!("failed to install `solana-verify`");
     }
-
-    use_version(Some(version))
 }
 
 /// Remove an installed version of anchor-cli

+ 5 - 1
avm/src/main.rs

@@ -33,6 +33,9 @@ pub enum Commands {
         #[clap(long)]
         /// Build from source code rather than downloading prebuilt binaries
         from_source: bool,
+        #[clap(long)]
+        /// Install `solana-verify` as well
+        verify: bool,
     },
     #[clap(about = "Uninstall a version of Anchor")]
     Uninstall {
@@ -80,13 +83,14 @@ pub fn entry(opts: Cli) -> Result<()> {
             path,
             force,
             from_source,
+            verify,
         } => {
             let install_target = if let Some(path) = path {
                 InstallTarget::Path(path.into())
             } else {
                 parse_install_target(&version_or_commit.unwrap())?
             };
-            avm::install_version(install_target, force, from_source)
+            avm::install_version(install_target, force, from_source, verify)
         }
         Commands::Uninstall { version } => avm::uninstall_version(&version),
         Commands::List {} => avm::list_versions(),

+ 38 - 11
cli/src/lib.rs

@@ -10,7 +10,7 @@ use anchor_lang::solana_program::bpf_loader_upgradeable;
 use anchor_lang::{AccountDeserialize, AnchorDeserialize, AnchorSerialize, Discriminator};
 use anchor_lang_idl::convert::convert_idl;
 use anchor_lang_idl::types::{Idl, IdlArrayLen, IdlDefinedFields, IdlType, IdlTypeDefTy};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use checks::{check_anchor_version, check_deps, check_idl_build_feature, check_overflow};
 use clap::{CommandFactory, Parser};
 use dirs::home_dir;
@@ -41,6 +41,7 @@ use std::path::{Path, PathBuf};
 use std::process::{Child, Stdio};
 use std::str::FromStr;
 use std::string::ToString;
+use std::sync::LazyLock;
 
 mod checks;
 pub mod config;
@@ -53,6 +54,16 @@ pub const DOCKER_BUILDER_VERSION: &str = VERSION;
 /// Default RPC port
 pub const DEFAULT_RPC_PORT: u16 = 8899;
 
+pub static AVM_HOME: LazyLock<PathBuf> = LazyLock::new(|| {
+    if let Ok(avm_home) = std::env::var("AVM_HOME") {
+        PathBuf::from(avm_home)
+    } else {
+        let mut user_home = dirs::home_dir().expect("Could not find home directory");
+        user_home.push(".avm");
+        user_home
+    }
+});
+
 #[derive(Debug, Parser)]
 #[clap(version = VERSION)]
 pub struct Opts {
@@ -662,17 +673,10 @@ fn override_toolchain(cfg_override: &ConfigOverride) -> Result<RestoreToolchainC
                         "`anchor` {anchor_version} is not installed with `avm`. Installing...\n"
                     );
 
-                    let exit_status = std::process::Command::new("avm")
-                        .arg("install")
-                        .arg(anchor_version)
-                        .spawn()?
-                        .wait()?;
-                    if !exit_status.success() {
+                    if let Err(e) = install_with_avm(anchor_version, false) {
                         eprintln!(
-                            "Failed to install `anchor` {anchor_version}, \
-                            using {current_version} instead"
+                            "Failed to install `anchor`: {e}, using {current_version} instead"
                         );
-
                         return Ok(restore_cbs);
                     }
                 }
@@ -692,6 +696,23 @@ fn override_toolchain(cfg_override: &ConfigOverride) -> Result<RestoreToolchainC
     Ok(restore_cbs)
 }
 
+/// Installs Anchor using AVM, passing `--force` (and optionally) installing
+/// `solana-verify`.
+fn install_with_avm(version: &str, verify: bool) -> Result<()> {
+    let mut cmd = std::process::Command::new("avm");
+    cmd.arg("install");
+    cmd.arg(version);
+    cmd.arg("--force");
+    if verify {
+        cmd.arg("--verify");
+    }
+    let status = cmd.status().context("running AVM")?;
+    if !status.success() {
+        bail!("failed to install `anchor` {version} with avm");
+    }
+    Ok(())
+}
+
 /// Restore toolchain to how it was before the command was run.
 fn restore_toolchain(restore_cbs: RestoreToolchainCallbacks) -> Result<()> {
     for restore_toolchain in restore_cbs {
@@ -1898,7 +1919,13 @@ pub fn verify(
     command_args.extend(args);
 
     println!("Verifying program {program_id}");
-    let status = std::process::Command::new("solana-verify")
+    let verify_path = AVM_HOME.join("bin").join("solana-verify");
+    if !verify_path.exists() {
+        install_with_avm(env!("CARGO_PKG_VERSION"), true)
+            .context("installing Anchor with solana-verify")?;
+    }
+
+    let status = std::process::Command::new(verify_path)
         .arg("verify-from-repo")
         .args(&command_args)
         .stdout(std::process::Stdio::inherit())