diff --git a/docs/src/running-validator/validator-start.md b/docs/src/running-validator/validator-start.md index f8e96fb9c0..353893ad46 100644 --- a/docs/src/running-validator/validator-start.md +++ b/docs/src/running-validator/validator-start.md @@ -157,6 +157,15 @@ You can generate a custom vanity keypair using solana-keygen. For instance: solana-keygen grind --starts-with e1v1s:1 ``` +You may request that the generated vanity keypair be expressed as a seed phrase +which allows recovery of the keypair from the seed phrase and an optionally +supplied passphrase (note that this is significantly slower than grinding without +a mnemonic): + +```bash +solana-keygen grind --use-mnemonic --starts-with e1v1s:1 +``` + Depending on the string requested, it may take days to find a match... --- diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index 5453707bc7..a6473b07a4 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -10,7 +10,7 @@ use solana_clap_utils::{ keypair_from_seed_phrase, prompt_passphrase, signer_from_path, SKIP_SEED_PHRASE_VALIDATION_ARG, }, - DisplayError, + ArgConstant, DisplayError, }; use solana_cli_config::{Config, CONFIG_FILE}; use solana_remote_wallet::remote_wallet::RemoteWalletManager; @@ -41,6 +41,86 @@ struct GrindMatch { count: AtomicU64, } +const WORD_COUNT_ARG: ArgConstant<'static> = ArgConstant { + long: "word-count", + name: "word_count", + help: "Specify the number of words that will be present in the generated seed phrase", +}; + +const LANGUAGE_ARG: ArgConstant<'static> = ArgConstant { + long: "language", + name: "language", + help: "Specify the mnemonic lanaguage that will be present in the generated seed phrase", +}; + +const NO_PASSPHRASE_ARG: ArgConstant<'static> = ArgConstant { + long: "no-bip39-passphrase", + name: "no_passphrase", + help: "Do not prompt for a BIP39 passphrase", +}; + +const NO_OUTFILE_ARG: ArgConstant<'static> = ArgConstant { + long: "no-outfile", + name: "no_outfile", + help: "Only print a seed phrase and pubkey. Do not output a keypair file", +}; + +fn word_count_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name(WORD_COUNT_ARG.name) + .long(WORD_COUNT_ARG.long) + .possible_values(&["12", "15", "18", "21", "24"]) + .default_value("12") + .value_name("NUMBER") + .takes_value(true) + .help(WORD_COUNT_ARG.help) +} + +fn language_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name(LANGUAGE_ARG.name) + .long(LANGUAGE_ARG.long) + .possible_values(&[ + "english", + "chinese-simplified", + "chinese-traditional", + "japanese", + "spanish", + "korean", + "french", + "italian", + ]) + .default_value("english") + .value_name("LANGUAGE") + .takes_value(true) + .help(LANGUAGE_ARG.help) +} + +fn no_passphrase_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name(NO_PASSPHRASE_ARG.name) + .long(NO_PASSPHRASE_ARG.long) + .alias("no-passphrase") + .help(NO_PASSPHRASE_ARG.help) +} + +fn no_outfile_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name(NO_OUTFILE_ARG.name) + .long(NO_OUTFILE_ARG.long) + .conflicts_with_all(&["outfile", "silent"]) + .help(NO_OUTFILE_ARG.help) +} + +trait KeyGenerationCommonArgs { + fn key_generation_common_args(self) -> Self; +} + +impl KeyGenerationCommonArgs for App<'_, '_> { + fn key_generation_common_args(self) -> Self { + self.arg(word_count_arg()) + .arg(language_arg()) + .arg(no_passphrase_arg()) + .arg(no_outfile_arg()) + } +} + fn check_for_overwrite(outfile: &str, matches: &ArgMatches) { let force = matches.is_present("force"); if !force && Path::new(outfile).exists() { @@ -131,6 +211,45 @@ fn grind_validator_starts_and_ends_with(v: String) -> Result<(), String> { Ok(()) } +fn acquire_language(matches: &ArgMatches<'_>) -> Language { + match matches.value_of(LANGUAGE_ARG.name).unwrap() { + "english" => Language::English, + "chinese-simplified" => Language::ChineseSimplified, + "chinese-traditional" => Language::ChineseTraditional, + "japanese" => Language::Japanese, + "spanish" => Language::Spanish, + "korean" => Language::Korean, + "french" => Language::French, + "italian" => Language::Italian, + _ => unreachable!(), + } +} + +fn no_passphrase_and_message() -> (String, String) { + (NO_PASSPHRASE.to_string(), "".to_string()) +} + +fn acquire_passphrase_and_message( + matches: &ArgMatches<'_>, +) -> Result<(String, String), Box> { + if matches.is_present(NO_PASSPHRASE_ARG.name) { + Ok(no_passphrase_and_message()) + } else { + match prompt_passphrase( + "\nFor added security, enter a BIP39 passphrase\n\ + \nNOTE! This passphrase improves security of the recovery seed phrase NOT the\n\ + keypair file itself, which is stored as insecure plain text\n\ + \nBIP39 Passphrase (empty for none): ", + ) { + Ok(passphrase) => { + println!(); + Ok((passphrase, " and your BIP39 passphrase".to_string())) + } + Err(e) => Err(e), + } + } +} + fn grind_print_info(grind_matches: &[GrindMatch], num_threads: usize) { println!("Searching with {} threads for:", num_threads); for gm in grind_matches { @@ -265,42 +384,13 @@ fn main() -> Result<(), Box> { .long("force") .help("Overwrite the output file if it exists"), ) - .arg( - Arg::with_name("word_count") - .long("word-count") - .possible_values(&["12", "15", "18", "21", "24"]) - .default_value("12") - .value_name("NUMBER") - .takes_value(true) - .help("Specify the number of words that will be present in the generated seed phrase"), - ) - .arg( - Arg::with_name("language") - .long("language") - .possible_values(&["english", "chinese-simplified", "chinese-traditional", "japanese", "spanish", "korean", "french", "italian"]) - .default_value("english") - .value_name("LANGUAGE") - .takes_value(true) - .help("Specify the mnemonic lanaguage that will be present in the generated seed phrase"), - ) - .arg( - Arg::with_name("no_passphrase") - .long("no-bip39-passphrase") - .alias("no-passphrase") - .help("Do not prompt for a BIP39 passphrase"), - ) - .arg( - Arg::with_name("no_outfile") - .long("no-outfile") - .conflicts_with_all(&["outfile", "silent"]) - .help("Only print a seed phrase and pubkey. Do not output a keypair file"), - ) .arg( Arg::with_name("silent") .short("s") .long("silent") .help("Do not display seed phrase. Useful when piping output to other programs that prompt for user input, like gpg"), ) + .key_generation_common_args() ) .subcommand( SubCommand::with_name("grind") @@ -349,7 +439,13 @@ fn main() -> Result<(), Box> { .validator(is_parsable::) .default_value(&default_num_threads) .help("Specify the number of grind threads"), - ), + ) + .arg( + Arg::with_name("use_mnemonic") + .long("use-mnemonic") + .help("Generate using a mnemonic key phrase. Expect a significant slowdown in this mode"), + ) + .key_generation_common_args() ) .subcommand( SubCommand::with_name("pubkey") @@ -438,7 +534,7 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box> { let mut path = dirs_next::home_dir().expect("home directory"); let outfile = if matches.is_present("outfile") { matches.value_of("outfile") - } else if matches.is_present("no_outfile") { + } else if matches.is_present(NO_OUTFILE_ARG.name) { None } else { path.extend(&[".config", "solana", "id.json"]); @@ -451,43 +547,16 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box> { None => (), } - let word_count = value_t!(matches.value_of("word_count"), usize).unwrap(); + let word_count = value_t!(matches.value_of(WORD_COUNT_ARG.name), usize).unwrap(); let mnemonic_type = MnemonicType::for_word_count(word_count)?; - let language = match matches.value_of("language").unwrap() { - "english" => Language::English, - "chinese-simplified" => Language::ChineseSimplified, - "chinese-traditional" => Language::ChineseTraditional, - "japanese" => Language::Japanese, - "spanish" => Language::Spanish, - "korean" => Language::Korean, - "french" => Language::French, - "italian" => Language::Italian, - _ => unreachable!(), - }; + let language = acquire_language(matches); let silent = matches.is_present("silent"); if !silent { println!("Generating a new keypair"); } let mnemonic = Mnemonic::new(mnemonic_type, language); - let passphrase = if matches.is_present("no_passphrase") { - NO_PASSPHRASE.to_string() - } else { - let passphrase = prompt_passphrase( - "\nFor added security, enter a BIP39 passphrase\n\ - \nNOTE! This passphrase improves security of the recovery seed phrase NOT the\n\ - keypair file itself, which is stored as insecure plain text\n\ - \nBIP39 Passphrase (empty for none): ", - )?; - println!(); - passphrase - }; - - let passphrase_message = if passphrase == NO_PASSPHRASE { - "".to_string() - } else { - " and your BIP39 passphrase".to_string() - }; + let (passphrase, passphrase_message) = acquire_passphrase_and_message(matches).unwrap(); let seed = Seed::new(&mnemonic, &passphrase); let keypair = keypair_from_seed(seed.as_bytes())?; @@ -571,6 +640,19 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box> { num_threads, ); + let use_mnemonic = matches.is_present("use_mnemonic"); + + let word_count = value_t!(matches.value_of(WORD_COUNT_ARG.name), usize).unwrap(); + let mnemonic_type = MnemonicType::for_word_count(word_count)?; + let language = acquire_language(matches); + + let (passphrase, passphrase_message) = if use_mnemonic { + acquire_passphrase_and_message(matches).unwrap() + } else { + no_passphrase_and_message() + }; + let no_outfile = matches.is_present(NO_OUTFILE_ARG.name); + let grind_matches_thread_safe = Arc::new(grind_matches); let attempts = Arc::new(AtomicU64::new(1)); let found = Arc::new(AtomicU64::new(0)); @@ -583,6 +665,8 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box> { let attempts = attempts.clone(); let found = found.clone(); let grind_matches_thread_safe = grind_matches_thread_safe.clone(); + let passphrase = passphrase.clone(); + let passphrase_message = passphrase_message.clone(); thread::spawn(move || loop { if done.load(Ordering::Relaxed) { @@ -597,7 +681,13 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box> { found.load(Ordering::Relaxed), ); } - let keypair = Keypair::new(); + let (keypair, phrase) = if use_mnemonic { + let mnemonic = Mnemonic::new(mnemonic_type, language); + let seed = Seed::new(&mnemonic, &passphrase); + (keypair_from_seed(seed.as_bytes()).unwrap(), mnemonic.phrase().to_string()) + } else { + (Keypair::new(), "".to_string()) + }; let mut pubkey = bs58::encode(keypair.pubkey()).into_string(); if ignore_case { pubkey = pubkey.to_lowercase(); @@ -623,12 +713,24 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box> { grind_matches_thread_safe[i] .count .fetch_sub(1, Ordering::Relaxed); - println!( - "Wrote keypair to {}", - &format!("{}.json", keypair.pubkey()) - ); - write_keypair_file(&keypair, &format!("{}.json", keypair.pubkey())) + if !no_outfile { + write_keypair_file(&keypair, &format!("{}.json", keypair.pubkey())) .unwrap(); + println!( + "Wrote keypair to {}", + &format!("{}.json", keypair.pubkey()) + ); + } + if use_mnemonic { + let divider = String::from_utf8(vec![b'='; phrase.len()]).unwrap(); + println!( + "{}\nFound matching key {}", + ÷r, keypair.pubkey()); + println!( + "\nSave this seed phrase{} to recover your new keypair:\n{}\n{}", + passphrase_message, phrase, ÷r + ); + } } } if total_matches_found == grind_matches_thread_safe.len() {