use snarkvm::console::{
account::{Address, PrivateKey, Signature},
network::{CanaryV0, MainnetV0, Network, TestnetV0},
prelude::{Environment, Uniform},
program::{ToFields, Value},
types::Field,
};
use anyhow::{Result, anyhow, bail};
use clap::Parser;
use colored::Colorize;
use core::str::FromStr;
use crossterm::ExecutableCommand;
use rand::SeedableRng;
use rand_chacha::ChaChaRng;
use rayon::prelude::*;
use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
};
use zeroize::Zeroize;
#[derive(Debug, Parser, Zeroize)]
pub enum Account {
New {
#[clap(default_value = "0", long = "network")]
network: u16,
#[clap(short = 's', long)]
seed: Option<String>,
#[clap(short = 'v', long)]
vanity: Option<String>,
#[clap(long)]
discreet: bool,
#[clap(long = "save-to-file")]
save_to_file: Option<String>,
},
Sign {
#[clap(default_value = "0", long = "network")]
network: u16,
#[clap(long = "private-key")]
private_key: Option<String>,
#[clap(long = "private-key-file")]
private_key_file: Option<String>,
#[clap(short = 'm', long)]
message: String,
#[clap(short = 'r', long)]
raw: bool,
},
Verify {
#[clap(default_value = "0", long = "network")]
network: u16,
#[clap(short = 'a', long)]
address: String,
#[clap(short = 's', long)]
signature: String,
#[clap(short = 'm', long)]
message: String,
#[clap(short = 'r', long)]
raw: bool,
},
}
fn aleo_literal_to_fields<N: Network>(input: &str) -> Result<Vec<Field<N>>> {
Value::<N>::from_str(input)?.to_fields()
}
impl Account {
pub fn parse(self) -> Result<String> {
match self {
Self::New { network, seed, vanity, discreet, save_to_file } => {
if seed.is_some() && vanity.is_some() {
bail!("Cannot specify both the '--seed' and '--vanity' flags");
}
if save_to_file.is_some() && vanity.is_some() {
bail!("Cannot specify both the '--save-to-file' and '--vanity' flags");
}
if save_to_file.is_some() && discreet {
bail!("Cannot specify both the '--save-to-file' and '--discreet' flags");
}
match vanity {
Some(vanity) => match network {
MainnetV0::ID => Self::new_vanity::<MainnetV0>(vanity.as_str(), discreet),
TestnetV0::ID => Self::new_vanity::<TestnetV0>(vanity.as_str(), discreet),
CanaryV0::ID => Self::new_vanity::<CanaryV0>(vanity.as_str(), discreet),
unknown_id => bail!("Unknown network ID ({unknown_id})"),
},
None => match network {
MainnetV0::ID => Self::new_seeded::<MainnetV0>(seed, discreet, save_to_file),
TestnetV0::ID => Self::new_seeded::<TestnetV0>(seed, discreet, save_to_file),
CanaryV0::ID => Self::new_seeded::<CanaryV0>(seed, discreet, save_to_file),
unknown_id => bail!("Unknown network ID ({unknown_id})"),
},
}
}
Self::Sign { network, message, raw, private_key, private_key_file } => {
let key = match (private_key, private_key_file) {
(Some(private_key), None) => private_key,
(None, Some(private_key_file)) => {
let path = private_key_file.parse::<PathBuf>().map_err(|e| anyhow!("Invalid path - {e}"))?;
std::fs::read_to_string(path)?.trim().to_string()
}
(None, None) => bail!("Missing the '--private-key' or '--private-key-file' argument"),
(Some(_), Some(_)) => {
bail!("Cannot specify both the '--private-key' and '--private-key-file' flags")
}
};
match network {
MainnetV0::ID => Self::sign::<MainnetV0>(key, message, raw),
TestnetV0::ID => Self::sign::<TestnetV0>(key, message, raw),
CanaryV0::ID => Self::sign::<CanaryV0>(key, message, raw),
unknown_id => bail!("Unknown network ID ({unknown_id})"),
}
}
Self::Verify { network, address, signature, message, raw } => {
match network {
MainnetV0::ID => Self::verify::<MainnetV0>(address, signature, message, raw),
TestnetV0::ID => Self::verify::<TestnetV0>(address, signature, message, raw),
CanaryV0::ID => Self::verify::<CanaryV0>(address, signature, message, raw),
unknown_id => bail!("Unknown network ID ({unknown_id})"),
}
}
}
}
fn new_vanity<N: Network>(vanity: &str, discreet: bool) -> Result<String> {
let sample_account = || snarkos_account::Account::<N>::new(&mut rand::thread_rng());
const ITERATIONS: u128 = u16::MAX as u128;
const ITERATIONS_STR: &str = "65,535";
if !crate::helpers::is_in_bech32m_charset(vanity) {
bail!(
"The vanity string '{vanity}' contains invalid bech32m characters. Try using characters from the bech32m character set: {}",
crate::helpers::BECH32M_CHARSET
);
}
if vanity.len() > 4 {
let message =
format!(" The vanity string '{vanity}' contains 5 or more characters and will take a while to find.\n");
println!("{}", message.yellow());
}
loop {
let timer = std::time::Instant::now();
let account = (0..ITERATIONS).into_par_iter().find_map_any(|_| {
let mut account = None;
if let Ok(candidate) = sample_account() {
let address = candidate.address().to_string();
if crate::helpers::has_vanity_string(&address, vanity) {
account = Some(candidate);
}
}
account
});
if let Some(account) = account {
println!(); if !discreet {
return Ok(account.to_string());
}
display_string_discreetly(
&format!("{:>12} {}", "Private Key".cyan().bold(), account.private_key()),
"### Do not share or lose this private key! Press any key to complete. ###",
)
.unwrap();
let account_info = format!(
" {:>12} {}\n {:>12} {}",
"View Key".cyan().bold(),
account.view_key(),
"Address".cyan().bold(),
account.address()
);
return Ok(account_info);
} else {
let rate = ITERATIONS / timer.elapsed().as_millis();
let rate = format!("[{rate} a/ms]");
println!(" {} Sampled {ITERATIONS_STR} accounts, searching...", rate.dimmed());
}
}
}
fn new_seeded<N: Network>(seed: Option<String>, discreet: bool, save_to_file: Option<String>) -> Result<String> {
let seed = match seed {
Some(seed) => {
Field::new(<N as Environment>::Field::from_str(&seed).map_err(|e| anyhow!("Invalid seed - {e}"))?)
}
None => Field::rand(&mut ChaChaRng::from_entropy()),
};
let private_key =
PrivateKey::try_from(seed).map_err(|_| anyhow!("Failed to convert the seed into a valid private key"))?;
let account = snarkos_account::Account::<N>::try_from(private_key)?;
if let Some(path) = save_to_file {
crate::check_parent_permissions(&path)?;
let mut file = File::create_new(path)?;
file.write_all(account.private_key().to_string().as_bytes())?;
crate::set_user_read_only(&file)?;
}
if !discreet {
return Ok(account.to_string());
}
display_string_discreetly(
&format!("{:>12} {}", "Private Key".cyan().bold(), account.private_key()),
"### Do not share or lose this private key! Press any key to complete. ###",
)
.unwrap();
let account_info = format!(
" {:>12} {}\n {:>12} {}",
"View Key".cyan().bold(),
account.view_key(),
"Address".cyan().bold(),
account.address()
);
Ok(account_info)
}
fn sign<N: Network>(key: String, message: String, raw: bool) -> Result<String> {
let mut rng = ChaChaRng::from_entropy();
let private_key =
PrivateKey::<N>::from_str(&key).map_err(|_| anyhow!("Failed to parse a valid private key"))?;
let signature = if raw {
private_key.sign_bytes(message.as_bytes(), &mut rng)
} else {
let fields =
aleo_literal_to_fields::<N>(&message).map_err(|_| anyhow!("Failed to parse a valid Aleo literal"))?;
private_key.sign(&fields, &mut rng)
}
.map_err(|_| anyhow!("Failed to sign the message"))?
.to_string();
Ok(signature)
}
fn verify<N: Network>(address: String, signature: String, message: String, raw: bool) -> Result<String> {
let address = Address::<N>::from_str(&address).map_err(|_| anyhow!("Failed to parse a valid address"))?;
let signature =
Signature::<N>::from_str(&signature).map_err(|_| anyhow!("Failed to parse a valid signature"))?;
let verified = if raw {
signature.verify_bytes(&address, message.as_bytes())
} else {
let fields =
aleo_literal_to_fields(&message).map_err(|_| anyhow!("Failed to parse a valid Aleo literal"))?;
signature.verify(&address, &fields)
};
match verified {
true => Ok("✅ The signature is valid".to_string()),
false => bail!("❌ The signature is invalid"),
}
}
}
fn display_string_discreetly(discreet_string: &str, continue_message: &str) -> Result<()> {
use crossterm::{
style::Print,
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
let mut stdout = std::io::stdout();
stdout.execute(EnterAlternateScreen)?;
stdout.execute(Print(format!("{discreet_string}\n{continue_message}")))?;
stdout.flush()?;
wait_for_keypress();
stdout.execute(LeaveAlternateScreen)?;
Ok(())
}
fn wait_for_keypress() {
let mut single_key = [0u8];
std::io::stdin().read_exact(&mut single_key).unwrap();
}
#[cfg(test)]
mod tests {
use crate::commands::Account;
use std::{fs, fs::Permissions, io::Write};
use tempfile::{NamedTempFile, TempDir};
use colored::Colorize;
#[test]
fn test_new() {
for _ in 0..3 {
let account = Account::New { network: 0, seed: None, vanity: None, discreet: false, save_to_file: None };
assert!(account.parse().is_ok());
}
}
#[test]
fn test_new_seeded() {
let seed = Some(1231275789u64.to_string());
let mut expected = format!(
" {:>12} {}\n",
"Private Key".cyan().bold(),
"APrivateKey1zkp2n22c19hNdGF8wuEoQcuiyuWbquY6up4CtG5DYKqPX2X"
);
expected += &format!(
" {:>12} {}\n",
"View Key".cyan().bold(),
"AViewKey1pNxZHn79XVJ4D2WG5Vn2YWsAzf5wzAs3dAuQtUAmUFF7"
);
expected += &format!(
" {:>12} {}",
"Address".cyan().bold(),
"aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5"
);
let vanity = None;
let account = Account::New { network: 0, seed, vanity, discreet: false, save_to_file: None };
let actual = account.parse().unwrap();
assert_eq!(expected, actual);
}
#[test]
fn test_new_seeded_with_256bits_input() {
let seed = Some("38868010450269069756484274649022187108349082664538872491798902858296683054657".to_string());
let mut expected = format!(
" {:>12} {}\n",
"Private Key".cyan().bold(),
"APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p"
);
expected += &format!(
" {:>12} {}\n",
"View Key".cyan().bold(),
"AViewKey1eYEGtb78FVg38SSYyzAeXnBdnWCba5t5YxUxtkTtvNAE"
);
expected += &format!(
" {:>12} {}",
"Address".cyan().bold(),
"aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j"
);
let vanity = None;
let account = Account::New { network: 0, seed, vanity, discreet: false, save_to_file: None };
let actual = account.parse().unwrap();
assert_eq!(expected, actual);
}
#[cfg(unix)]
#[test]
fn test_new_save_to_file() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().expect("Failed to create temp folder");
let dir_path = dir.path();
fs::set_permissions(dir_path, Permissions::from_mode(0o700)).expect("Failed to set permissions");
let mut file = dir.path().to_owned();
file.push("my-private-key-file");
let file = file.display().to_string();
let seed = Some(1231275789u64.to_string());
let vanity = None;
let discreet = false;
let save_to_file = Some(file.clone());
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
let actual = account.parse().unwrap();
let expected = "APrivateKey1zkp2n22c19hNdGF8wuEoQcuiyuWbquY6up4CtG5DYKqPX2X";
assert!(actual.contains(expected));
let content = fs::read_to_string(&file).expect("Failed to read private-key-file");
assert_eq!(expected, content);
let metadata = fs::metadata(file).unwrap();
let permissions = metadata.permissions();
assert_eq!(permissions.mode() & 0o777, 0o400, "File permissions are not 0o400");
}
#[cfg(unix)]
#[test]
fn test_new_prevent_save_to_file_in_non_protected_folder() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().expect("Failed to create temp folder");
let dir_path = dir.path();
fs::set_permissions(dir_path, Permissions::from_mode(0o444)).expect("Failed to set permissions");
let mut file = dir.path().to_owned();
file.push("my-private-key-file");
let file = file.display().to_string();
let seed = None;
let vanity = None;
let discreet = false;
let save_to_file = Some(file);
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
let res = account.parse();
assert!(res.is_err());
}
#[test]
fn test_new_prevent_save_to_file_in_non_existing_folder() {
let dir = TempDir::new().expect("Failed to create temp folder");
let mut file = dir.path().to_owned();
file.push("missing-folder");
file.push("my-private-key-file");
let file = file.display().to_string();
let seed = None;
let vanity = None;
let discreet = false;
let save_to_file = Some(file);
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
let res = account.parse();
assert!(res.is_err());
}
#[test]
fn test_new_prevent_overwrite_existing_file() {
let mut file = NamedTempFile::new().expect("Failed to create temp file");
write!(file, "don't overwrite me").expect("Failed to write secret to file");
let seed = None;
let vanity = None;
let discreet = false;
let path = file.path().display().to_string();
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file: Some(path) };
let res = account.parse();
assert!(res.is_err());
let expected = "don't overwrite me";
let content = fs::read_to_string(file).expect("Failed to read private-key-file");
assert_eq!(expected, content);
}
#[test]
fn test_new_disallow_save_to_file_with_discreet() {
let seed = None;
let vanity = None;
let discreet = true;
let save_to_file = Some("/tmp/not-important".to_string());
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
let res = account.parse();
assert!(res.is_err());
}
#[test]
fn test_new_disallow_save_to_file_with_vanity() {
let seed = None;
let vanity = Some("foo".to_string());
let discreet = false;
let save_to_file = Some("/tmp/not-important".to_string());
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file };
let res = account.parse();
assert!(res.is_err());
}
#[test]
fn test_signature_raw() {
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
let message = "Hello, world!".to_string();
let account = Account::Sign { network: 0, private_key: Some(key), private_key_file: None, message, raw: true };
assert!(account.parse().is_ok());
}
#[test]
fn test_signature_raw_using_private_key_file() {
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
let message = "Hello, world!".to_string();
let mut file = NamedTempFile::new().expect("Failed to create temp file");
writeln!(file, "{}", key).expect("Failed to write key to temp file");
let path = file.path().display().to_string();
let account = Account::Sign { network: 0, private_key: None, private_key_file: Some(path), message, raw: true };
assert!(account.parse().is_ok());
}
#[cfg(unix)]
#[test]
fn test_signature_raw_using_private_key_file_from_account_new() {
use std::os::unix::fs::PermissionsExt;
let message = "Hello, world!".to_string();
let dir = TempDir::new().expect("Failed to create temp folder");
let dir_path = dir.path();
fs::set_permissions(dir_path, Permissions::from_mode(0o700)).expect("Failed to set permissions");
let mut file = dir.path().to_owned();
file.push("my-private-key-file");
let file = file.display().to_string();
let seed = None;
let vanity = None;
let discreet = false;
let account = Account::New { network: 0, seed, vanity, discreet, save_to_file: Some(file.clone()) };
assert!(account.parse().is_ok());
let account = Account::Sign { network: 0, private_key: None, private_key_file: Some(file), message, raw: true };
assert!(account.parse().is_ok());
}
#[test]
fn test_signature() {
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
let message = "5field".to_string();
let account = Account::Sign { network: 0, private_key: Some(key), private_key_file: None, message, raw: false };
assert!(account.parse().is_ok());
}
#[test]
fn test_signature_fail() {
let key = "APrivateKey1zkp61PAYmrYEKLtRWeWhUoDpFnGLNuHrCciSqN49T86dw3p".to_string();
let message = "not a literal value".to_string();
let account = Account::Sign { network: 0, private_key: Some(key), private_key_file: None, message, raw: false };
assert!(account.parse().is_err());
}
#[test]
fn test_verify_raw() {
let address = "aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j";
let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
let message = "Hello, world!".to_string();
let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: true };
let actual = account.parse();
assert!(actual.is_ok());
let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
let message = "Different Message".to_string();
let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: true };
let actual = account.parse();
assert!(actual.is_err());
let signature = "sign1nnvrjlksrkxdpwsrw8kztjukzhmuhe5zf3srk38h7g32u4kqtqpxn3j5a6k8zrqcfx580a96956nsjvluzt64cqf54pdka9mgksfqp8esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkwsnaqq".to_string();
let message = "Hello, world!".to_string();
let wrong_address = "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5".to_string();
let account = Account::Verify { network: 0, address: wrong_address, signature, message, raw: true };
let actual = account.parse();
assert!(actual.is_err());
let signature = "sign1424ztyt9hcm77nq450gvdszrvtg9kvhc4qadg4nzy9y0ah7wdqq7t36cxal42p9jj8e8pjpmc06lfev9nvffcpqv0cxwyr0a2j2tjqlesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk3yrr50".to_string();
let message = "Different Message".to_string();
let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: true };
let actual = account.parse();
assert!(actual.is_ok());
}
#[test]
fn test_verify() {
let address = "aleo1zecnqchckrzw7dlsyf65g6z5le2rmys403ecwmcafrag0e030yxqrnlg8j";
let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
let message = "5field".to_string();
let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: false };
let actual = account.parse();
assert!(actual.is_ok());
let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
let message = "10field".to_string();
let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: false };
let actual = account.parse();
assert!(actual.is_err());
let signature = "sign1j7swjfnyujt2vme3ulu88wdyh2ddj85arh64qh6c6khvrx8wvsp8z9wtzde0sahqj2qwz8rgzt803c0ceega53l4hks2mf5sfsv36qhesm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qkdetews".to_string();
let message = "5field".to_string();
let wrong_address = "aleo1uxl69laseuv3876ksh8k0nd7tvpgjt6ccrgccedpjk9qwyfensxst9ftg5".to_string();
let account = Account::Verify { network: 0, address: wrong_address, signature, message, raw: false };
let actual = account.parse();
assert!(actual.is_err());
let signature = "sign1t9v2t5tljk8pr5t6vkcqgkus0a3v69vryxmfrtwrwg0xtj7yv5qj2nz59e5zcyl50w23lhntxvt6vzeqfyu6dt56698zvfj2l6lz6q0esm5elrqqunzqzmac7kzutl6zk7mqht3c0m9kg4hklv7h2js0qmxavwnpuwyl4lzldl6prs4qeqy9wxyp8y44nnydg3h8sg6ue99qk8rh9kt".to_string();
let message = "10field".to_string();
let account = Account::Verify { network: 0, address: address.to_string(), signature, message, raw: false };
let actual = account.parse();
assert!(actual.is_ok());
}
}