diff --git a/archiver/src/main.rs b/archiver/src/main.rs index 4513f8a1b7..28d4c65a0e 100644 --- a/archiver/src/main.rs +++ b/archiver/src/main.rs @@ -1,15 +1,18 @@ use clap::{crate_description, crate_name, App, Arg}; use console::style; -use solana_clap_utils::input_validators::is_keypair; +use solana_clap_utils::{ + input_validators::is_keypair, + keypair::{ + self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG, + SKIP_SEED_PHRASE_VALIDATION_ARG, + }, +}; use solana_core::{ archiver::Archiver, cluster_info::{Node, VALIDATOR_PORT_RANGE}, contact_info::ContactInfo, }; -use solana_sdk::{ - commitment_config::CommitmentConfig, - signature::{read_keypair_file, Keypair, KeypairUtil}, -}; +use solana_sdk::{commitment_config::CommitmentConfig, signature::KeypairUtil}; use std::{net::SocketAddr, path::PathBuf, process::exit, sync::Arc}; fn main() { @@ -19,9 +22,9 @@ fn main() { .about(crate_description!()) .version(solana_clap_utils::version!()) .arg( - Arg::with_name("identity") + Arg::with_name("identity_keypair") .short("i") - .long("identity") + .long("identity-keypair") .value_name("PATH") .takes_value(true) .validator(is_keypair) @@ -52,30 +55,48 @@ fn main() { .long("storage-keypair") .value_name("PATH") .takes_value(true) - .required(true) .validator(is_keypair) .help("File containing the storage account keypair"), ) + .arg( + Arg::with_name(ASK_SEED_PHRASE_ARG.name) + .long(ASK_SEED_PHRASE_ARG.long) + .value_name("KEYPAIR NAME") + .multiple(true) + .takes_value(true) + .possible_values(&["identity-keypair", "storage-keypair"]) + .help(ASK_SEED_PHRASE_ARG.help), + ) + .arg( + Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name) + .long(SKIP_SEED_PHRASE_VALIDATION_ARG.long) + .requires(ASK_SEED_PHRASE_ARG.name) + .help(SKIP_SEED_PHRASE_VALIDATION_ARG.help), + ) .get_matches(); let ledger_path = PathBuf::from(matches.value_of("ledger").unwrap()); - let keypair = if let Some(identity) = matches.value_of("identity") { - read_keypair_file(identity).unwrap_or_else(|err| { - eprintln!("{}: Unable to open keypair file: {}", err, identity); + let identity_keypair = keypair_input(&matches, "identity_keypair") + .unwrap_or_else(|err| { + eprintln!("Identity keypair input failed: {}", err); exit(1); }) - } else { - Keypair::new() - }; - let storage_keypair = if let Some(storage_keypair) = matches.value_of("storage_keypair") { - read_keypair_file(storage_keypair).unwrap_or_else(|err| { - eprintln!("{}: Unable to open keypair file: {}", err, storage_keypair); - exit(1); - }) - } else { - Keypair::new() - }; + .keypair; + let KeypairWithSource { + keypair: storage_keypair, + source: storage_keypair_source, + } = keypair_input(&matches, "storage_keypair").unwrap_or_else(|err| { + eprintln!("Storage keypair input failed: {}", err); + exit(1); + }); + if storage_keypair_source == keypair::Source::Generated { + clap::Error::with_description( + "The `storage-keypair` argument was not found", + clap::ErrorKind::ArgumentNotFound, + ) + .exit(); + } let entrypoint_addr = matches .value_of("entrypoint") @@ -91,8 +112,11 @@ fn main() { addr.set_ip(solana_net_utils::get_public_ip_addr(&entrypoint_addr).unwrap()); addr }; - let node = - Node::new_archiver_with_external_ip(&keypair.pubkey(), &gossip_addr, VALIDATOR_PORT_RANGE); + let node = Node::new_archiver_with_external_ip( + &identity_keypair.pubkey(), + &gossip_addr, + VALIDATOR_PORT_RANGE, + ); println!( "{} version {} (branch={}, commit={})", @@ -101,10 +125,10 @@ fn main() { option_env!("CI_BRANCH").unwrap_or("unknown"), option_env!("CI_COMMIT").unwrap_or("unknown") ); - solana_metrics::set_host_id(keypair.pubkey().to_string()); + solana_metrics::set_host_id(identity_keypair.pubkey().to_string()); println!( - "replicating the data with keypair={:?} gossip_addr={:?}", - keypair.pubkey(), + "replicating the data with identity_keypair={:?} gossip_addr={:?}", + identity_keypair.pubkey(), gossip_addr ); @@ -113,7 +137,7 @@ fn main() { &ledger_path, node, entrypoint_info, - Arc::new(keypair), + Arc::new(identity_keypair), Arc::new(storage_keypair), CommitmentConfig::recent(), ) diff --git a/book/src/running-archiver.md b/book/src/running-archiver.md index d3c2701fa6..68d5a4fab5 100644 --- a/book/src/running-archiver.md +++ b/book/src/running-archiver.md @@ -138,7 +138,7 @@ Note: Every time the testnet restarts, run the steps to setup the archiver accou To start the archiver: ```bash -solana-archiver --entrypoint testnet.solana.com:8001 --identity archiver-keypair.json --storage-keypair storage-keypair.json --ledger archiver-ledger +solana-archiver --entrypoint testnet.solana.com:8001 --identity-keypair archiver-keypair.json --storage-keypair storage-keypair.json --ledger archiver-ledger ``` ## Verify Archiver Setup diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index 9356030b3e..fa4b78e007 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -1,3 +1,4 @@ +use crate::keypair::{keypair_from_seed_phrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG}; use clap::ArgMatches; use solana_sdk::{ native_token::sol_to_lamports, @@ -32,7 +33,12 @@ where // Return the keypair for an argument with filename `name` or None if not present. pub fn keypair_of(matches: &ArgMatches<'_>, name: &str) -> Option { if let Some(value) = matches.value_of(name) { - read_keypair_file(value).ok() + if value == ASK_KEYWORD { + let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); + keypair_from_seed_phrase(name, skip_validation).ok() + } else { + read_keypair_file(value).ok() + } } else { None } diff --git a/clap-utils/src/input_validators.rs b/clap-utils/src/input_validators.rs index fd9c1d2182..6ec3cc1023 100644 --- a/clap-utils/src/input_validators.rs +++ b/clap-utils/src/input_validators.rs @@ -1,3 +1,4 @@ +use crate::keypair::ASK_KEYWORD; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::read_keypair_file; @@ -16,6 +17,16 @@ pub fn is_keypair(string: String) -> Result<(), String> { .map_err(|err| format!("{:?}", err)) } +// Return an error if a keypair file cannot be parsed +pub fn is_keypair_or_ask_keyword(string: String) -> Result<(), String> { + if string.as_str() == ASK_KEYWORD { + return Ok(()); + } + read_keypair_file(&string) + .map(|_| ()) + .map_err(|err| format!("{:?}", err)) +} + // Return an error if string cannot be parsed as pubkey string or keypair file location pub fn is_pubkey_or_keypair(string: String) -> Result<(), String> { is_pubkey(string.clone()).or_else(|_| is_keypair(string)) diff --git a/clap-utils/src/keypair.rs b/clap-utils/src/keypair.rs index baa7881fd3..783e3fbbeb 100644 --- a/clap-utils/src/keypair.rs +++ b/clap-utils/src/keypair.rs @@ -1,3 +1,4 @@ +use crate::ArgConstant; use bip39::{Language, Mnemonic, Seed}; use clap::values_t; use rpassword::prompt_password_stderr; @@ -7,11 +8,41 @@ use solana_sdk::signature::{ }; use std::error; -pub const ASK_SEED_PHRASE_ARG: &str = "ask_seed_phrase"; -pub const SKIP_SEED_PHRASE_VALIDATION_ARG: &str = "skip_seed_phrase_validation"; +// Keyword used to indicate that the user should be asked for a keypair seed phrase +pub const ASK_KEYWORD: &str = "ASK"; + +pub const ASK_SEED_PHRASE_ARG: ArgConstant<'static> = ArgConstant { + long: "ask-seed-phrase", + name: "ask_seed_phrase", + help: "Securely recover a keypair using a seed phrase and optional passphrase", +}; + +pub const SKIP_SEED_PHRASE_VALIDATION_ARG: ArgConstant<'static> = ArgConstant { + long: "skip-seed-phrase-validation", + name: "skip_seed_phrase_validation", + help: "Skip validation of seed phrases. Use this if your phrase does not use the BIP39 official English word list", +}; + +#[derive(Debug, PartialEq)] +pub enum Source { + File, + Generated, + SeedPhrase, +} + +pub struct KeypairWithSource { + pub keypair: Keypair, + pub source: Source, +} + +impl KeypairWithSource { + fn new(keypair: Keypair, source: Source) -> Self { + Self { keypair, source } + } +} /// Reads user input from stdin to retrieve a seed phrase and passphrase for keypair derivation -pub fn keypair_from_seed_phrase( +pub(crate) fn keypair_from_seed_phrase( keypair_name: &str, skip_validation: bool, ) -> Result> { @@ -33,17 +64,6 @@ pub fn keypair_from_seed_phrase( } } -pub struct KeypairWithGenerated { - pub keypair: Keypair, - pub generated: bool, -} - -impl KeypairWithGenerated { - fn new(keypair: Keypair, generated: bool) -> Self { - Self { keypair, generated } - } -} - /// Checks CLI arguments to determine whether a keypair should be: /// - inputted securely via stdin, /// - read in from a file, @@ -51,33 +71,32 @@ impl KeypairWithGenerated { pub fn keypair_input( matches: &clap::ArgMatches, keypair_name: &str, -) -> Result> { +) -> Result> { let ask_seed_phrase_matches = - values_t!(matches.values_of(ASK_SEED_PHRASE_ARG), String).unwrap_or_default(); + values_t!(matches.values_of(ASK_SEED_PHRASE_ARG.name), String).unwrap_or_default(); let keypair_match_name = keypair_name.replace('-', "_"); if ask_seed_phrase_matches .iter() .any(|s| s.as_str() == keypair_name) { if matches.value_of(keypair_match_name).is_some() { - let ask_seed_phrase_kebab = ASK_SEED_PHRASE_ARG.replace('_', "-"); clap::Error::with_description( &format!( "`--{} {}` cannot be used with `{} `", - ask_seed_phrase_kebab, keypair_name, keypair_name + ASK_SEED_PHRASE_ARG.long, keypair_name, keypair_name ), clap::ErrorKind::ArgumentConflict, ) .exit(); } - let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG); + let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); keypair_from_seed_phrase(keypair_name, skip_validation) - .map(|keypair| KeypairWithGenerated::new(keypair, false)) + .map(|keypair| KeypairWithSource::new(keypair, Source::SeedPhrase)) } else if let Some(keypair_file) = matches.value_of(keypair_match_name) { - read_keypair_file(keypair_file).map(|keypair| KeypairWithGenerated::new(keypair, false)) + read_keypair_file(keypair_file).map(|keypair| KeypairWithSource::new(keypair, Source::File)) } else { - Ok(KeypairWithGenerated::new(Keypair::new(), true)) + Ok(KeypairWithSource::new(Keypair::new(), Source::Generated)) } } @@ -89,7 +108,7 @@ mod tests { #[test] fn test_keypair_input() { let arg_matches = ArgMatches::default(); - let KeypairWithGenerated { generated, .. } = keypair_input(&arg_matches, "").unwrap(); - assert!(generated); + let KeypairWithSource { source, .. } = keypair_input(&arg_matches, "").unwrap(); + assert_eq!(source, Source::Generated); } } diff --git a/clap-utils/src/lib.rs b/clap-utils/src/lib.rs index fbddbd2acc..ea5bd45d46 100644 --- a/clap-utils/src/lib.rs +++ b/clap-utils/src/lib.rs @@ -17,6 +17,12 @@ macro_rules! version { }; } +pub struct ArgConstant<'a> { + pub long: &'a str, + pub name: &'a str, + pub help: &'a str, +} + pub mod input_parsers; pub mod input_validators; pub mod keypair; diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 92d88df1b8..df8922900a 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -219,19 +219,28 @@ pub struct CliConfig { pub rpc_client: Option, } -impl Default for CliConfig { - fn default() -> CliConfig { +impl CliConfig { + pub fn default_keypair_path() -> String { let mut keypair_path = dirs::home_dir().expect("home directory"); keypair_path.extend(&[".config", "solana", "id.json"]); + keypair_path.to_str().unwrap().to_string() + } + pub fn default_json_rpc_url() -> String { + "http://127.0.0.1:8899".to_string() + } +} + +impl Default for CliConfig { + fn default() -> CliConfig { CliConfig { command: CliCommand::Balance { pubkey: Some(Pubkey::default()), use_lamports_unit: false, }, - json_rpc_url: "http://127.0.0.1:8899".to_string(), + json_rpc_url: Self::default_json_rpc_url(), keypair: Keypair::new(), - keypair_path: Some(keypair_path.to_str().unwrap().to_string()), + keypair_path: Some(Self::default_keypair_path()), rpc_client: None, } } diff --git a/cli/src/config.rs b/cli/src/config.rs index c455e870fa..829b4693c5 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -16,14 +16,14 @@ lazy_static! { #[derive(Serialize, Deserialize, Default, Debug, PartialEq)] pub struct Config { pub url: String, - pub keypair: String, + pub keypair_path: String, } impl Config { - pub fn new(url: &str, keypair: &str) -> Self { + pub fn new(url: &str, keypair_path: &str) -> Self { Self { url: url.to_string(), - keypair: keypair.to_string(), + keypair_path: keypair_path.to_string(), } } diff --git a/cli/src/main.rs b/cli/src/main.rs index e1c829f956..2a9d7b9102 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,7 +1,13 @@ use clap::{crate_description, crate_name, Arg, ArgGroup, ArgMatches, SubCommand}; use console::style; -use solana_clap_utils::input_validators::is_url; +use solana_clap_utils::{ + input_validators::is_url, + keypair::{ + self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG, + SKIP_SEED_PHRASE_VALIDATION_ARG, + }, +}; use solana_cli::{ cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliError}, config::{self, Config}, @@ -15,22 +21,25 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result { if let Some(config_file) = matches.value_of("config_file") { - let default_cli_config = CliConfig::default(); let config = Config::load(config_file).unwrap_or_default(); if let Some(field) = subcommand_matches.value_of("specific_setting") { let (value, default_value) = match field { - "url" => (config.url, default_cli_config.json_rpc_url), - "keypair" => (config.keypair, default_cli_config.keypair_path.unwrap()), + "url" => (config.url, CliConfig::default_json_rpc_url()), + "keypair" => (config.keypair_path, CliConfig::default_keypair_path()), _ => unreachable!(), }; println_name_value_or(&format!("* {}:", field), &value, &default_value); } else { println_name_value("Wallet Config:", config_file); - println_name_value_or("* url:", &config.url, &default_cli_config.json_rpc_url); + println_name_value_or( + "* url:", + &config.url, + &CliConfig::default_json_rpc_url(), + ); println_name_value_or( "* keypair:", - &config.keypair, - &default_cli_config.keypair_path.unwrap(), + &config.keypair_path, + &CliConfig::default_keypair_path(), ); } } else { @@ -48,12 +57,12 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result) -> Result ( + keypair, + Some(matches.value_of("keypair").unwrap().to_string()), + ), + keypair::Source::SeedPhrase => (keypair, None), + keypair::Source::Generated => { + let keypair_path = if config.keypair_path != "" { + config.keypair_path + } else { + let default_keypair_path = CliConfig::default_keypair_path(); + if !std::path::Path::new(&default_keypair_path).exists() { + return Err(CliError::KeypairFileNotFound( + "Generate a new keypair with `solana-keygen new`".to_string(), + ) + .into()); + } + default_keypair_path + }; + + let keypair = read_keypair_file(&keypair_path).or_else(|err| { + Err(CliError::BadParameter(format!( + "{}: Unable to open keypair file: {}", + err, keypair_path + ))) + })?; + + (keypair, Some(keypair_path)) } - maybe_keypair_path - }; - let keypair = read_keypair_file(&keypair_path).or_else(|err| { - Err(CliError::BadParameter(format!( - "{}: Unable to open keypair file: {}", - err, keypair_path - ))) - })?; - (keypair, Some(keypair_path.to_string())) + } } else { let default = CliConfig::default(); (default.keypair, None) @@ -164,6 +182,21 @@ fn main() -> Result<(), Box> { .takes_value(true) .help("/path/to/id.json"), ) + .arg( + Arg::with_name(ASK_SEED_PHRASE_ARG.name) + .long(ASK_SEED_PHRASE_ARG.long) + .value_name("KEYPAIR NAME") + .global(true) + .takes_value(true) + .possible_values(&["keypair"]) + .help(ASK_SEED_PHRASE_ARG.help), + ) + .arg( + Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name) + .long(SKIP_SEED_PHRASE_VALIDATION_ARG.long) + .global(true) + .help(SKIP_SEED_PHRASE_VALIDATION_ARG.help), + ) .subcommand( SubCommand::with_name("get") .about("Get cli config settings") diff --git a/cli/src/stake.rs b/cli/src/stake.rs index feb0439343..1a6ef896ad 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -41,7 +41,7 @@ impl StakeSubCommands for App<'_, '_> { .value_name("STAKE ACCOUNT") .takes_value(true) .required(true) - .validator(is_keypair) + .validator(is_keypair_or_ask_keyword) .help("Keypair of the stake account to fund") ) .arg( diff --git a/cli/src/storage.rs b/cli/src/storage.rs index 04f33dbf1d..7720c687be 100644 --- a/cli/src/storage.rs +++ b/cli/src/storage.rs @@ -35,7 +35,7 @@ impl StorageSubCommands for App<'_, '_> { .value_name("STORAGE ACCOUNT") .takes_value(true) .required(true) - .validator(is_keypair), + .validator(is_keypair_or_ask_keyword), ), ) .subcommand( @@ -55,7 +55,7 @@ impl StorageSubCommands for App<'_, '_> { .value_name("STORAGE ACCOUNT") .takes_value(true) .required(true) - .validator(is_keypair), + .validator(is_keypair_or_ask_keyword), ), ) .subcommand( diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 7dd0c9749e..b6837b42ac 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -30,7 +30,7 @@ impl VoteSubCommands for App<'_, '_> { .value_name("VOTE ACCOUNT KEYPAIR") .takes_value(true) .required(true) - .validator(is_keypair) + .validator(is_keypair_or_ask_keyword) .help("Vote account keypair to fund"), ) .arg( diff --git a/multinode-demo/archiver.sh b/multinode-demo/archiver.sh index 9fa5df946b..f9b7abfec6 100755 --- a/multinode-demo/archiver.sh +++ b/multinode-demo/archiver.sh @@ -18,7 +18,7 @@ while [[ -n $1 ]]; do entrypoint=$2 args+=("$1" "$2") shift 2 - elif [[ $1 = --identity ]]; then + elif [[ $1 = --identity-keypair ]]; then identity_keypair=$2 [[ -r $identity_keypair ]] || { echo "$identity_keypair does not exist" @@ -74,7 +74,7 @@ if [[ ! -r $storage_keypair ]]; then fi default_arg --entrypoint "$entrypoint" -default_arg --identity "$identity_keypair" +default_arg --identity-keypair "$identity_keypair" default_arg --storage-keypair "$storage_keypair" default_arg --ledger "$ledger" diff --git a/net/remote/remote-node.sh b/net/remote/remote-node.sh index abee99a59f..ac1eb25763 100755 --- a/net/remote/remote-node.sh +++ b/net/remote/remote-node.sh @@ -415,7 +415,7 @@ EOF ) if [[ $airdropsEnabled != true ]]; then - # If this ever becomes a problem, we need to provide the `--identity` + # If this ever becomes a problem, we need to provide the `--identity-keypair` # argument to an existing system account with lamports in it echo "Error: archivers not supported without airdrops" exit 1 diff --git a/validator/src/main.rs b/validator/src/main.rs index fa579a77e9..e3058b67ad 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -7,7 +7,8 @@ use solana_clap_utils::{ input_parsers::pubkey_of, input_validators::{is_keypair, is_pubkey_or_keypair}, keypair::{ - keypair_input, KeypairWithGenerated, ASK_SEED_PHRASE_ARG, SKIP_SEED_PHRASE_VALIDATION_ARG, + self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG, + SKIP_SEED_PHRASE_VALIDATION_ARG, }, }; use solana_client::rpc_client::RpcClient; @@ -326,19 +327,19 @@ pub fn main() { .help("Stream entries to this unix domain socket path") ) .arg( - Arg::with_name(ASK_SEED_PHRASE_ARG) - .long("ask-seed-phrase") + Arg::with_name(ASK_SEED_PHRASE_ARG.name) + .long(ASK_SEED_PHRASE_ARG.long) .value_name("KEYPAIR NAME") .multiple(true) .takes_value(true) .possible_values(&["identity-keypair", "storage-keypair", "voting-keypair"]) - .help("Securely recover a keypair using a seed phrase and optional passphrase"), + .help(ASK_SEED_PHRASE_ARG.help), ) .arg( - Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG) - .long("skip-seed-phrase-validation") - .requires(ASK_SEED_PHRASE_ARG) - .help("Skip validation of seed phrases. Use this if your phrase does not use the BIP39 official English word list"), + Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name) + .long(SKIP_SEED_PHRASE_VALIDATION_ARG.long) + .requires(ASK_SEED_PHRASE_ARG.name) + .help(SKIP_SEED_PHRASE_VALIDATION_ARG.help), ) .arg( Arg::with_name("identity_keypair") @@ -546,13 +547,14 @@ pub fn main() { }) .keypair, ); - let KeypairWithGenerated { + let KeypairWithSource { keypair: voting_keypair, - generated: ephemeral_voting_keypair, + source: voting_keypair_source, } = keypair_input(&matches, "voting-keypair").unwrap_or_else(|err| { eprintln!("Voting keypair input failed: {}", err); exit(1); }); + let ephemeral_voting_keypair = voting_keypair_source == keypair::Source::Generated; let storage_keypair = keypair_input(&matches, "storage-keypair") .unwrap_or_else(|err| { eprintln!("Storage keypair input failed: {}", err);