diff --git a/install/Cargo.toml b/install/Cargo.toml index 449d3e1a5..a3e836536 100644 --- a/install/Cargo.toml +++ b/install/Cargo.toml @@ -8,20 +8,29 @@ repository = "https://github.com/solana-labs/solana" license = "Apache-2.0" homepage = "https://solana.com/" +[features] +cuda = [] + [dependencies] bincode = "1.1.2" bs58 = "0.2.0" -clap = { version = "2.32.0"} +bzip2 = "0.3.3" +console = "0.7.5" chrono = { version = "0.4.0", features = ["serde"] } +clap = { version = "2.32.0"} dirs = "1.0.5" +indicatif = "0.11.0" lazy_static = "1.3.0" log = "0.4.2" +reqwest = "0.9.11" +ring = "0.13.2" serde = "1.0.89" serde_derive = "1.0.89" serde_yaml = "0.8.8" solana-client = { path = "../client", version = "0.13.0" } +solana-config-api = { path = "../programs/config_api", version = "0.13.0" } solana-logger = { path = "../logger", version = "0.13.0" } solana-sdk = { path = "../sdk", version = "0.13.0" } - -[features] -cuda = [] +tar = "0.4.22" +tempdir = "0.3.7" +url = "1.7.2" diff --git a/install/src/command.rs b/install/src/command.rs index e89fec27f..02df9bd06 100644 --- a/install/src/command.rs +++ b/install/src/command.rs @@ -1,57 +1,481 @@ use crate::config::Config; +use crate::update_manifest::{SignedUpdateManifest, UpdateManifest}; +use chrono::{Local, TimeZone}; +use console::{style, Emoji}; +use indicatif::{ProgressBar, ProgressStyle}; +use ring::digest::{Context, Digest, SHA256}; +use solana_client::rpc_client::RpcClient; +use solana_config_api::ConfigInstruction; use solana_sdk::pubkey::Pubkey; -use std::time::Duration; +use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil, Signable}; +use solana_sdk::transaction::Transaction; +use std::fs::{self, File}; +use std::io::{self, BufReader, Read}; +use std::path::{Path, PathBuf}; +use std::thread::sleep; +use std::time::SystemTime; +use std::time::{Duration, Instant}; +use tempdir::TempDir; +use url::Url; + +static TRUCK: Emoji = Emoji("🚚 ", ""); +static LOOKING_GLASS: Emoji = Emoji("🔍 ", ""); +static BULLET: Emoji = Emoji("• ", "* "); +static SPARKLE: Emoji = Emoji("✨ ", ""); +static PACKAGE: Emoji = Emoji("📦 ", ""); + +/// Creates a new process bar for processing that will take an unknown amount of time +fn new_spinner_progress_bar() -> ProgressBar { + let progress_bar = ProgressBar::new(42); + progress_bar + .set_style(ProgressStyle::default_spinner().template("{spinner:.green} {wide_msg}")); + progress_bar.enable_steady_tick(100); + progress_bar +} + +/// Pretty print a "name value" +fn println_name_value(name: &str, value: &str) { + println!("{} {}", style(name).bold(), value); +} + +/// Downloads the release archive at `url` to a temporary location. If `expected_sha256` is +/// Some(_), produce an error if the release SHA256 doesn't match. +/// +/// Returns a tuple consisting of: +/// * TempDir - drop this value to clean up the temporary location +/// * PathBuf - path to the downloaded release (within `TempDir`) +/// * String - SHA256 of the release +/// +fn download_to_temp_archive( + url: &str, + expected_sha256: Option<&str>, +) -> Result<(TempDir, PathBuf, String), Box> { + fn sha256_digest(mut reader: R) -> Result> { + let mut context = Context::new(&SHA256); + let mut buffer = [0; 1024]; + + loop { + let count = reader.read(&mut buffer)?; + if count == 0 { + break; + } + context.update(&buffer[..count]); + } + + Ok(context.finish()) + } + + fn sha256_file_digest>(path: P) -> Result> { + let input = File::open(path)?; + let reader = BufReader::new(input); + sha256_digest(reader) + } + + let url = Url::parse(url).map_err(|err| format!("Unable to parse {}: {}", url, err))?; + + let temp_dir = TempDir::new(clap::crate_name!())?; + let temp_file = temp_dir.path().join("release.tar.bz2"); + + let client = reqwest::Client::new(); + + let progress_bar = new_spinner_progress_bar(); + progress_bar.set_message(&format!("{}Downloading...", TRUCK)); + + let response = client.get(url).send()?; + let download_size = { + response + .headers() + .get(reqwest::header::CONTENT_LENGTH) + .and_then(|content_length| content_length.to_str().ok()) + .and_then(|content_length| content_length.parse().ok()) + .unwrap_or(0) + }; + + progress_bar.set_length(download_size); + progress_bar.set_style( + ProgressStyle::default_bar() + .template(&format!( + "{}{}{}", + "{spinner:.green} ", + TRUCK, + "Downloading [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})" + )) + .progress_chars("=> "), + ); + + struct DownloadProgress { + progress_bar: ProgressBar, + response: R, + } + + impl Read for DownloadProgress { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.response.read(buf).map(|n| { + self.progress_bar.inc(n as u64); + n + }) + } + } + + let mut source = DownloadProgress { + progress_bar, + response, + }; + + let mut file = File::create(&temp_file)?; + std::io::copy(&mut source, &mut file)?; + + let temp_file_sha256 = sha256_file_digest(&temp_file) + .map_err(|err| format!("Unable to hash {:?}: {}", temp_file, err))?; + let temp_file_sha256 = bs58::encode(temp_file_sha256).into_string(); + + if expected_sha256.is_some() && expected_sha256 != Some(&temp_file_sha256) { + Err(io::Error::new(io::ErrorKind::Other, "Incorrect hash"))?; + } + Ok((temp_dir, temp_file, temp_file_sha256)) +} + +/// Extracts the release archive into the specified directory +fn extract_release_archive( + archive: &Path, + extract_dir: &Path, +) -> Result<(), Box> { + use bzip2::bufread::BzDecoder; + use tar::Archive; + + let progress_bar = new_spinner_progress_bar(); + progress_bar.set_message(&format!("{}Extracting...", PACKAGE)); + + let _ = fs::remove_dir_all(extract_dir); + fs::create_dir_all(extract_dir)?; + + let tar_bz2 = File::open(archive)?; + let tar = BzDecoder::new(BufReader::new(tar_bz2)); + let mut release = Archive::new(tar); + release.unpack(extract_dir)?; + + progress_bar.finish_and_clear(); + Ok(()) +} + +/// Reads the supported TARGET triple for the given release +fn load_release_target(release_dir: &Path) -> Result> { + use serde_derive::Deserialize; + #[derive(Deserialize, Debug)] + pub struct ReleaseVersion { + pub target: String, + pub commit: String, + channel: String, + } + + let mut version_yml = PathBuf::from(release_dir); + version_yml.push("solana-release"); + version_yml.push("version.yml"); + + let file = File::open(&version_yml)?; + let version: ReleaseVersion = serde_yaml::from_reader(file)?; + Ok(version.target) +} + +/// Time in seconds since the UNIX_EPOCH +fn timestamp_secs() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() +} + +/// Create an empty update manifest for the given `update_manifest_keypair` if it doesn't already +/// exist on the cluster +fn new_update_manifest( + rpc_client: &RpcClient, + from_keypair: &Keypair, + update_manifest_keypair: &Keypair, +) -> Result<(), Box> { + if rpc_client + .get_account_data(&update_manifest_keypair.pubkey()) + .is_err() + { + let recect_blockhash = rpc_client.get_recent_blockhash()?; + + let new_account = ConfigInstruction::new_account::( + &from_keypair.pubkey(), + &update_manifest_keypair.pubkey(), + 1, // lamports + ); + let mut transaction = Transaction::new(vec![new_account]); + transaction.sign(&[from_keypair], recect_blockhash); + + rpc_client.send_and_confirm_transaction(&mut transaction, from_keypair)?; + } + Ok(()) +} + +/// Update the update manifest on the cluster with new content +fn store_update_manifest( + rpc_client: &RpcClient, + from_keypair: &Keypair, + update_manifest_keypair: &Keypair, + update_manifest: &SignedUpdateManifest, +) -> Result<(), Box> { + let recect_blockhash = rpc_client.get_recent_blockhash()?; + + let new_store = ConfigInstruction::new_store::( + &from_keypair.pubkey(), + &update_manifest_keypair.pubkey(), + update_manifest, + ); + let mut transaction = Transaction::new(vec![new_store]); + transaction.sign(&[from_keypair, update_manifest_keypair], recect_blockhash); + rpc_client.send_and_confirm_transaction(&mut transaction, from_keypair)?; + Ok(()) +} + +/// Read the current contents of the update manifest from the cluster +fn get_update_manifest( + rpc_client: &RpcClient, + update_manifest_pubkey: &Pubkey, +) -> Result { + let data = rpc_client + .get_account_data(update_manifest_pubkey) + .map_err(|err| format!("Unable to fetch update manifest: {}", err))?; + + let signed_update_manifest = + SignedUpdateManifest::deserialize(update_manifest_pubkey, &data) + .map_err(|err| format!("Unable to deserialize update manifest: {}", err))?; + Ok(signed_update_manifest.manifest) +} + +/// Bug the user if bin_dir is not in their PATH +fn check_env_path_for_bin_dir(config: &Config) { + use std::env; + + let found = match env::var_os("PATH") { + Some(paths) => env::split_paths(&paths).any(|path| { + if let Ok(path) = path.canonicalize() { + if path == *config.bin_dir() { + return true; + } + } + false + }), + None => false, + }; + + if !found { + println!( + "\nPlease update your PATH environment variable to include the solana programs:\n PATH=\"{}:$PATH\"\n", + config.bin_dir().to_str().unwrap() + ); + } +} pub fn init( config_file: &str, data_dir: &str, json_rpc_url: &str, - update_pubkey: &Pubkey, + update_manifest_pubkey: &Pubkey, ) -> Result<(), String> { - println!("init {:?} {:?}", json_rpc_url, update_pubkey); - Config { - data_dir: data_dir.to_string(), - json_rpc_url: json_rpc_url.to_string(), - update_pubkey: *update_pubkey, - current_install: None, - } - .save(config_file)?; - - Err("Not implemented".to_string()) + let config = Config::new(data_dir, json_rpc_url, update_manifest_pubkey); + config.save(config_file)?; + update(config_file)?; + Ok(()) } -pub fn info(config_file: &str, local_info_only: bool) -> Result<(), String> { +pub fn info(config_file: &str, local_info_only: bool) -> Result, String> { let config = Config::load(config_file)?; - println!("config dir: {:?}", config_file); - println!("configuration: {:?}", config); - if local_info_only { - return Ok(()); + println_name_value("JSON RPC URL:", &config.json_rpc_url); + println_name_value( + "Update manifest pubkey:", + &config.update_manifest_pubkey.to_string(), + ); + + fn print_update_manifest(update_manifest: &UpdateManifest) { + let when = Local.timestamp(update_manifest.timestamp_secs as i64, 0); + println_name_value( + &format!("{}release date", BULLET), + &when.format("%c").to_string(), + ); + println_name_value( + &format!("{}download URL", BULLET), + &update_manifest.download_url, + ); } - // TODO: fetch info about current update manifest from the cluster + match config.current_update_manifest { + Some(ref update_manifest) => { + println_name_value("Installed version:", ""); + print_update_manifest(&update_manifest); + } + None => { + println_name_value("Installed version:", "None"); + } + } - Err("Not implemented".to_string()) + if local_info_only { + Ok(None) + } else { + let progress_bar = new_spinner_progress_bar(); + progress_bar.set_message(&format!("{}Checking for updates...", LOOKING_GLASS)); + let rpc_client = RpcClient::new(config.json_rpc_url.clone()); + let manifest = get_update_manifest(&rpc_client, &config.update_manifest_pubkey)?; + progress_bar.finish_and_clear(); + + if Some(&manifest) == config.current_update_manifest.as_ref() { + println!("\n{}", style("Installation is up to date").italic()); + Ok(None) + } else { + println!("\n{}", style("An update is available:").bold()); + print_update_manifest(&manifest); + Ok(Some(manifest)) + } + } } pub fn deploy( - config_file: &str, + json_rpc_url: &str, + from_keypair_file: &str, download_url: &str, - update_manifest_keypair: &str, + update_manifest_keypair_file: &str, ) -> Result<(), String> { - println!("deploy {:?} {:?}", download_url, update_manifest_keypair); - let _config = Config::load(config_file)?; - Err("Not implemented".to_string()) + let from_keypair = read_keypair(from_keypair_file) + .map_err(|err| format!("Unable to read {}: {}", from_keypair_file, err))?; + let update_manifest_keypair = read_keypair(update_manifest_keypair_file) + .map_err(|err| format!("Unable to read {}: {}", update_manifest_keypair_file, err))?; + + // Download the release + let (temp_dir, temp_archive, temp_archive_sha256) = + download_to_temp_archive(download_url, None) + .map_err(|err| format!("Unable to download {}: {}", download_url, err))?; + + // Extract it and load the release version metadata + let temp_release_dir = temp_dir.path().join("archive"); + extract_release_archive(&temp_archive, &temp_release_dir).map_err(|err| { + format!( + "Unable to extract {:?} into {:?}: {}", + temp_archive, temp_release_dir, err + ) + })?; + + let release_target = load_release_target(&temp_release_dir).map_err(|err| { + format!( + "Unable to load release target from {:?}: {}", + temp_release_dir, err + ) + })?; + + println_name_value("JSON RPC URL:", json_rpc_url); + println_name_value("Update target:", &release_target); + println_name_value( + "Update manifest pubkey:", + &update_manifest_keypair.pubkey().to_string(), + ); + + let progress_bar = new_spinner_progress_bar(); + progress_bar.set_message(&format!("{}Deploying update...", PACKAGE)); + + // Construct an update manifest for the release + let mut update_manifest = SignedUpdateManifest { + account_pubkey: update_manifest_keypair.pubkey(), + ..SignedUpdateManifest::default() + }; + + update_manifest.manifest.timestamp_secs = timestamp_secs(); + update_manifest.manifest.download_url = download_url.to_string(); + update_manifest.manifest.download_sha256 = temp_archive_sha256; + + update_manifest.sign(&update_manifest_keypair); + assert!(update_manifest.verify()); + + // Store the new update manifest on the cluster + let rpc_client = RpcClient::new(json_rpc_url.to_string()); + + new_update_manifest(&rpc_client, &from_keypair, &update_manifest_keypair) + .map_err(|err| format!("Unable to create update manifest: {}", err))?; + store_update_manifest( + &rpc_client, + &from_keypair, + &update_manifest_keypair, + &update_manifest, + ) + .map_err(|err| format!("Unable to store update manifest: {:?}", err))?; + + progress_bar.finish_and_clear(); + println!(" {}{}", SPARKLE, style("Deployment successful").bold()); + Ok(()) } -pub fn update(config_file: &str) -> Result<(), String> { - println!( - "update: BUILD_SECONDS_SINCE_UNIX_EPOCH={:?}", - Duration::from_secs( - u64::from_str_radix(crate::build_env::BUILD_SECONDS_SINCE_UNIX_EPOCH, 10).unwrap() +pub fn update(config_file: &str) -> Result { + let update_manifest = info(config_file, false)?; + if update_manifest.is_none() { + return Ok(false); + } + let update_manifest = update_manifest.unwrap(); + + if timestamp_secs() + < u64::from_str_radix(crate::build_env::BUILD_SECONDS_SINCE_UNIX_EPOCH, 10).unwrap() + { + Err("Unable to update as system time seems unreliable".to_string())? + } + + let mut config = Config::load(config_file)?; + if let Some(ref current_update_manifest) = config.current_update_manifest { + if update_manifest.timestamp_secs < current_update_manifest.timestamp_secs { + Err("Unable to update to an older version".to_string())? + } + } + + let (_temp_dir, temp_archive, _temp_archive_sha256) = download_to_temp_archive( + &update_manifest.download_url, + Some(&update_manifest.download_sha256), + ) + .map_err(|err| { + format!( + "Unable to download {}: {}", + update_manifest.download_url, err ) - ); - let _config = Config::load(config_file)?; - Err("Not implemented".to_string()) + })?; + + let release_dir = config.release_dir(&update_manifest.download_sha256); + + extract_release_archive(&temp_archive, &release_dir).map_err(|err| { + format!( + "Unable to extract {:?} to {:?}: {}", + temp_archive, release_dir, err + ) + })?; + + let release_target = load_release_target(&release_dir).map_err(|err| { + format!( + "Unable to load release target from {:?}: {}", + release_dir, err + ) + })?; + + if release_target != crate::build_env::TARGET { + Err(format!("Incompatible update target: {}", release_target))?; + } + + let _ = fs::remove_dir_all(config.bin_dir()); + std::os::unix::fs::symlink( + release_dir.join("solana-release").join("bin"), + config.bin_dir(), + ) + .map_err(|err| { + format!( + "Unable to symlink {:?} to {:?}: {}", + release_dir, + config.bin_dir(), + err + ) + })?; + + config.current_update_manifest = Some(update_manifest); + config.save(config_file)?; + + check_env_path_for_bin_dir(&config); + println!(" {}{}", SPARKLE, style("Update successful").bold()); + Ok(true) } pub fn run( @@ -59,7 +483,63 @@ pub fn run( program_name: &str, program_arguments: Vec<&str>, ) -> Result<(), String> { - println!("run {:?} {:?}", program_name, program_arguments); - let _config = Config::load(config_file)?; - Err("Not implemented".to_string()) + let config = Config::load(config_file)?; + + let full_program_path = config.bin_dir().join(program_name); + if !full_program_path.exists() { + Err(format!( + "{} does not exist", + full_program_path.to_str().unwrap() + ))?; + } + + let mut child_option: Option = None; + let mut now = Instant::now(); + loop { + child_option = match child_option { + Some(mut child) => match child.try_wait() { + Ok(Some(status)) => { + println_name_value( + &format!("{} exited with:", program_name), + &status.to_string(), + ); + None + } + Ok(None) => Some(child), + Err(err) => { + eprintln!("Error attempting to wait for program to exit: {}", err); + None + } + }, + None => { + match std::process::Command::new(&full_program_path) + .args(&program_arguments) + .spawn() + { + Ok(child) => Some(child), + Err(err) => { + eprintln!("Failed to spawn {}: {:?}", program_name, err); + None + } + } + } + }; + + if now.elapsed().as_secs() > config.update_poll_secs { + match update(config_file) { + Ok(true) => { + // Update successful, kill current process so it will be restart + if let Some(ref mut child) = child_option { + println!("Killing program: {:?}", child.kill()); + } + } + Ok(false) => {} // No update available + Err(err) => { + eprintln!("Failed to apply update: {:?}", err); + } + }; + now = Instant::now(); + } + sleep(Duration::from_secs(1)); + } } diff --git a/install/src/config.rs b/install/src/config.rs index d63f2a94f..e43f571b5 100644 --- a/install/src/config.rs +++ b/install/src/config.rs @@ -1,24 +1,36 @@ -use crate::update_manifest::SignedUpdateManifest; +use crate::update_manifest::UpdateManifest; use serde_derive::{Deserialize, Serialize}; -use serde_yaml; use solana_sdk::pubkey::Pubkey; use std::fs::{create_dir_all, File}; -use std::io::{Error, ErrorKind, Write}; -use std::path::Path; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; #[derive(Serialize, Deserialize, Default, Debug, PartialEq)] pub struct Config { - pub data_dir: String, pub json_rpc_url: String, - pub update_pubkey: Pubkey, - pub current_install: Option, + pub update_manifest_pubkey: Pubkey, + pub current_update_manifest: Option, + pub update_poll_secs: u64, + releases_dir: PathBuf, + bin_dir: PathBuf, } impl Config { - fn _load(config_file: &str) -> Result { + pub fn new(data_dir: &str, json_rpc_url: &str, update_manifest_pubkey: &Pubkey) -> Self { + Self { + json_rpc_url: json_rpc_url.to_string(), + update_manifest_pubkey: *update_manifest_pubkey, + current_update_manifest: None, + update_poll_secs: 60, // check for updates once a minute + releases_dir: PathBuf::from(data_dir).join("releases"), + bin_dir: PathBuf::from(data_dir).join("bin"), + } + } + + fn _load(config_file: &str) -> Result { let file = File::open(config_file.to_string())?; let config = serde_yaml::from_reader(file) - .map_err(|err| Error::new(ErrorKind::Other, format!("{:?}", err)))?; + .map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?; Ok(config) } @@ -26,9 +38,9 @@ impl Config { Self::_load(config_file).map_err(|err| format!("Unable to load {}: {:?}", config_file, err)) } - fn _save(&self, config_file: &str) -> Result<(), Error> { + fn _save(&self, config_file: &str) -> Result<(), io::Error> { let serialized = serde_yaml::to_string(self) - .map_err(|err| Error::new(ErrorKind::Other, format!("{:?}", err)))?; + .map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?; if let Some(outdir) = Path::new(&config_file).parent() { create_dir_all(outdir)?; @@ -43,4 +55,12 @@ impl Config { self._save(config_file) .map_err(|err| format!("Unable to save {}: {:?}", config_file, err)) } + + pub fn bin_dir(&self) -> &PathBuf { + &self.bin_dir + } + + pub fn release_dir(&self, release_sha256: &str) -> PathBuf { + self.releases_dir.join(release_sha256) + } } diff --git a/install/src/defaults.rs b/install/src/defaults.rs index 8ae3352b9..43b081e14 100644 --- a/install/src/defaults.rs +++ b/install/src/defaults.rs @@ -2,24 +2,29 @@ pub const JSON_RPC_URL: &str = "https://api.testnet.solana.com/"; lazy_static! { pub static ref CONFIG_FILE: Option = { - dirs::config_dir().map(|mut path| { - path.push("solana"); - path.push("install.yml"); + dirs::home_dir().map(|mut path| { + path.extend(&[".config", "solana", "install", "config.yml"]); + path.to_str().unwrap().to_string() + }) + }; + pub static ref USER_KEYPAIR: Option = { + dirs::home_dir().map(|mut path| { + path.extend(&[".config", "solana", "id.json"]); path.to_str().unwrap().to_string() }) }; pub static ref DATA_DIR: Option = { - dirs::data_dir().map(|mut path| { - path.push("solana"); + dirs::home_dir().map(|mut path| { + path.extend(&[".local", "share", "solana", "install"]); path.to_str().unwrap().to_string() }) }; } -pub fn update_pubkey(target: &str) -> Option<&str> { +pub fn update_manifest_pubkey(target: &str) -> Option<&str> { match target { - "x86_64-apple-darwin" => Some("9XX329sPuskWhH4DQh6k16c87dHKhXLBZTL3Gxmve8Gp"), // TODO: This pubkey is invalid - "x86_64-unknown-linux-gnu" => Some("8XX329sPuskWhH4DQh6k16c87dHKhXLBZTL3Gxmve8Gp"), // TODO: This pubkey is invalid + "x86_64-apple-darwin" => None, + "x86_64-unknown-linux-gnu" => Some("EVS4V6wha5J5qzw8ZJBroMq9g6EKMg5iFWqCRrKwfo3z"), // SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu _ => None, } } diff --git a/install/src/main.rs b/install/src/main.rs index 6a877989a..e41f46692 100644 --- a/install/src/main.rs +++ b/install/src/main.rs @@ -10,6 +10,20 @@ mod config; mod defaults; mod update_manifest; +fn url_validator(url: String) -> Result<(), String> { + match url::Url::parse(&url) { + Ok(_) => Ok(()), + Err(err) => Err(format!("{:?}", err)), + } +} + +fn pubkey_validator(pubkey: String) -> Result<(), String> { + match pubkey.parse::() { + Ok(_) => Ok(()), + Err(err) => Err(format!("{:?}", err)), + } +} + fn main() -> Result<(), String> { solana_logger::setup(); @@ -39,10 +53,11 @@ fn main() -> Result<(), String> { .long("data_dir") .value_name("PATH") .takes_value(true) + .required(true) .help("Directory to store install data"); match *defaults::DATA_DIR { Some(ref data_dir) => arg.default_value(&data_dir), - None => arg.required(true), + None => arg, } }) .arg( @@ -52,23 +67,22 @@ fn main() -> Result<(), String> { .value_name("URL") .takes_value(true) .default_value(defaults::JSON_RPC_URL) + .validator(url_validator) .help("JSON RPC URL for the solana cluster"), ) .arg({ - let arg = Arg::with_name("update_pubkey") + let arg = Arg::with_name("update_manifest_pubkey") .short("p") .long("pubkey") .value_name("PUBKEY") .takes_value(true) - .validator(|value| match value.parse::() { - Ok(_) => Ok(()), - Err(err) => Err(format!("{:?}", err)), - }) + .required(true) + .validator(pubkey_validator) .help("Public key of the update manifest"); - match defaults::update_pubkey(build_env::TARGET) { + match defaults::update_manifest_pubkey(build_env::TARGET) { Some(default_value) => arg.default_value(default_value), - None => arg.required(true), + None => arg, } }), ) @@ -89,14 +103,38 @@ fn main() -> Result<(), String> { SubCommand::with_name("deploy") .about("deploys a new update") .setting(AppSettings::DisableVersion) + .arg({ + let arg = Arg::with_name("from_keypair_file") + .short("k") + .long("keypair") + .value_name("PATH") + .takes_value(true) + .required(true) + .help("Keypair file of the account that funds the deployment"); + match *defaults::USER_KEYPAIR { + Some(ref config_file) => arg.default_value(&config_file), + None => arg, + } + }) + .arg( + Arg::with_name("json_rpc_url") + .short("u") + .long("url") + .value_name("URL") + .takes_value(true) + .default_value(defaults::JSON_RPC_URL) + .validator(url_validator) + .help("JSON RPC URL for the solana cluster"), + ) .arg( Arg::with_name("download_url") .index(1) .required(true) + .validator(url_validator) .help("URL to the solana release archive"), ) .arg( - Arg::with_name("update_manifest_keypair") + Arg::with_name("update_manifest_keypair_file") .index(2) .required(true) .help("Keypair file for the update manifest (/path/to/keypair.json)"), @@ -132,24 +170,32 @@ fn main() -> Result<(), String> { match matches.subcommand() { ("init", Some(matches)) => { let json_rpc_url = matches.value_of("json_rpc_url").unwrap(); - let update_pubkey = matches - .value_of("update_pubkey") + let update_manifest_pubkey = matches + .value_of("update_manifest_pubkey") .unwrap() .parse::() .unwrap(); let data_dir = matches.value_of("data_dir").unwrap(); - command::init(config_file, data_dir, json_rpc_url, &update_pubkey) + command::init(config_file, data_dir, json_rpc_url, &update_manifest_pubkey) } ("info", Some(matches)) => { let local_info_only = matches.is_present("local_info_only"); - command::info(config_file, local_info_only) + command::info(config_file, local_info_only).map(|_| ()) } ("deploy", Some(matches)) => { + let from_keypair_file = matches.value_of("from_keypair_file").unwrap(); + let json_rpc_url = matches.value_of("json_rpc_url").unwrap(); let download_url = matches.value_of("download_url").unwrap(); - let update_manifest_keypair = matches.value_of("update_manifest_keypair").unwrap(); - command::deploy(config_file, download_url, update_manifest_keypair) + let update_manifest_keypair_file = + matches.value_of("update_manifest_keypair_file").unwrap(); + command::deploy( + json_rpc_url, + from_keypair_file, + download_url, + update_manifest_keypair_file, + ) } - ("update", Some(_matches)) => command::update(config_file), + ("update", Some(_matches)) => command::update(config_file).map(|_| ()), ("run", Some(matches)) => { let program_name = matches.value_of("program_name").unwrap(); let program_arguments = matches diff --git a/install/src/update_manifest.rs b/install/src/update_manifest.rs index 16e99f305..6c3f1eb13 100644 --- a/install/src/update_manifest.rs +++ b/install/src/update_manifest.rs @@ -1,19 +1,61 @@ use serde_derive::{Deserialize, Serialize}; -use solana_sdk::signature::Signature; +use solana_config_api::ConfigState; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::signature::{Signable, Signature}; +use std::error; +use std::io; /// Information required to download and apply a given update #[derive(Serialize, Deserialize, Default, Debug, PartialEq)] pub struct UpdateManifest { - pub target: String, // Target triple (TARGET) - pub commit: String, // git sha1 of this update, must match the commit sha1 in the release tar.bz2 pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH pub download_url: String, // Download URL to the release tar.bz2 - pub download_signature: Signature, // Signature of the release tar.bz2 file, verify with the Account public key + pub download_sha256: String, // SHA256 digest of the release tar.bz2 file } /// Userdata of an Update Manifest program Account. #[derive(Serialize, Deserialize, Default, Debug, PartialEq)] pub struct SignedUpdateManifest { pub manifest: UpdateManifest, - pub manifest_signature: Signature, // Signature of UpdateInfo, verify with the Account public key + pub manifest_signature: Signature, + #[serde(skip_serializing)] + pub account_pubkey: Pubkey, +} + +impl Signable for SignedUpdateManifest { + fn pubkey(&self) -> Pubkey { + self.account_pubkey + } + + fn signable_data(&self) -> Vec { + bincode::serialize(&self.manifest).expect("serialize") + } + fn get_signature(&self) -> Signature { + self.manifest_signature + } + fn set_signature(&mut self, signature: Signature) { + self.manifest_signature = signature + } +} + +impl SignedUpdateManifest { + pub fn deserialize(account_pubkey: &Pubkey, input: &[u8]) -> Result> { + let mut manifest: SignedUpdateManifest = bincode::deserialize(input)?; + manifest.account_pubkey = *account_pubkey; + if !manifest.verify() { + Err(io::Error::new( + io::ErrorKind::Other, + "Manifest failed to verify", + ))?; + } + Ok(manifest) + } +} + +impl ConfigState for SignedUpdateManifest { + fn max_space() -> u64 { + // TODO: Use a fully populated manifest to compute a better value + // bincode::serialized_size(&Self::default()).unwrap() + 256 + } } diff --git a/proposals/src/installer.md b/proposals/src/installer.md index 4b334e448..07986aec9 100644 --- a/proposals/src/installer.md +++ b/proposals/src/installer.md @@ -67,18 +67,18 @@ use solana_sdk::signature::Signature; /// Information required to download and apply a given update pub struct UpdateManifest { - pub target: String, // Target triple (TARGET) - pub commit: String, // git sha1 of this update, must match the commit sha1 in the release tar.bz2 - pub timestamp_secs: u64, // When the release was deployed (seconds since UNIX EPOC) - pub download_url: String, // Download URL to the release tar.bz2 - pub download_signature: Signature, // Signature of the release tar.bz2 file, verify with the Account public key + pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH + pub download_url: String, // Download URL to the release tar.bz2 + pub download_sha256: String, // SHA256 digest of the release tar.bz2 file } /// Userdata of an Update Manifest program Account. +#[derive(Serialize, Deserialize, Default, Debug, PartialEq)] pub struct SignedUpdateManifest { - pub manifest: UpdateManifest, - pub manifest_signature: Signature, // Signature of UpdateInfo, verify with the Account public key + pub manifest: UpdateManifest, + pub manifest_signature: Signature, } + ``` Note that the `manifest` field itself contains a corresponding signature @@ -92,9 +92,8 @@ update with an older `timestamp_secs` than what is currently installed. A release archive is expected to be a tar file compressed with bzip2 with the following internal structure: -* `/version.yml` - a simple YAML file containing the fields (1) `"commit"` - the git - sha1 for this release, and (2) `"target"` - the target tuple. Any additional - fields are ignored. +* `/version.yml` - a simple YAML file containing the field `"target"` - the + target tuple. Any additional fields are ignored. * `/bin/` -- directory containing available programs in the release. `solana-install` will symlink this directory to `~/.local/share/solana-install/bin` for use by the `PATH` environment @@ -106,10 +105,9 @@ bzip2 with the following internal structure: The `solana-install` tool is used by the user to install and update their cluster software. It manages the following files and directories in the user's home directory: -* `~/.config/solana/updater.json` - user configuration and information about currently installed software version -* `~/.local/share/solana-install/bin` - a symlink to the current release. eg, `~/.local/share/solana-update/-/bin` -* `~/.local/share/solana-install/-/` - contents of the release -* `~/.local/share/solana-install/-.tmp/` - temporary directory used while downloading a new release +* `~/.config/solana/install/config.yml` - user configuration and information about currently installed software version +* `~/.local/share/solana/install/bin` - a symlink to the current release. eg, `~/.local/share/solana-update/-/bin` +* `~/.local/share/solana/install/releases//` - contents of a release #### Command-line Interface