diff --git a/.changelog/unreleased/features/2118-hw-wallet-integration-on-0.25.0.md b/.changelog/unreleased/features/2118-hw-wallet-integration-on-0.25.0.md new file mode 100644 index 00000000000..c6414655bcd --- /dev/null +++ b/.changelog/unreleased/features/2118-hw-wallet-integration-on-0.25.0.md @@ -0,0 +1,2 @@ +- Added Ledger support to the CLI client. + ([\#2118](https://github.com/anoma/namada/pull/2118)) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9da4a10125e..227b030b994 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2943,6 +2943,18 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hidapi" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798154e4b6570af74899d71155fb0072d5b17e6aa12f39c8ef22c60fb8ec99e7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "winapi", +] + [[package]] name = "hmac" version = "0.7.1" @@ -3463,6 +3475,72 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +[[package]] +name = "ledger-apdu" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe435806c197dfeaa5efcded5e623c4b8230fd28fdf1e91e7a86e40ef2acbf90" +dependencies = [ + "arrayref", + "no-std-compat", + "snafu", +] + +[[package]] +name = "ledger-namada-rs" +version = "0.0.1" +source = "git+https://github.com/heliaxdev/ledger-namada?branch=murisi/fix-rs-0.24.0#920c6288de42a7e1cd782ad7c18f5b106a94de6f" +dependencies = [ + "bincode", + "byteorder", + "ed25519-dalek", + "leb128", + "ledger-transport", + "ledger-zondax-generic", + "prost", + "prost-types", + "sha2 0.10.6", + "thiserror", +] + +[[package]] +name = "ledger-transport" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1117f2143d92c157197785bf57711d7b02f2cfa101e162f8ca7900fb7f976321" +dependencies = [ + "async-trait", + "ledger-apdu", +] + +[[package]] +name = "ledger-transport-hid" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ba81a1f5f24396b37211478aff7fbcd605dd4544df8dbed07b9da3c2057aee" +dependencies = [ + "byteorder", + "cfg-if 1.0.0", + "hex", + "hidapi", + "ledger-transport", + "libc", + "log", + "thiserror", +] + +[[package]] +name = "ledger-zondax-generic" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02036c84eab9c48e85bc568d269221ba4f5e1cfbc785c3c2c2f6bb8c131f9287" +dependencies = [ + "async-trait", + "ledger-transport", + "serde 1.0.163", + "thiserror", +] + [[package]] name = "lexical-core" version = "0.7.6" @@ -4021,6 +4099,8 @@ dependencies = [ "git2", "itertools 0.10.5", "lazy_static", + "ledger-namada-rs", + "ledger-transport-hid", "libc", "libloading", "masp_primitives", @@ -4392,6 +4472,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "5.1.3" @@ -6339,6 +6425,28 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "socket2" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index ae3e6ae08dd..9df1db0ba2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,8 @@ index-set = {git = "https://github.com/heliaxdev/index-set", tag = "v0.8.0", fea itertools = "0.10.0" k256 = { version = "0.13.0", default-features = false, features = ["ecdsa", "pkcs8", "precomputed-tables", "serde", "std"]} lazy_static = "1.4.0" +ledger-namada-rs = { git = "https://github.com/heliaxdev/ledger-namada", branch = "murisi/fix-rs-0.24.0" } +ledger-transport-hid = "0.10.0" libc = "0.2.97" libloading = "0.7.2" # branch = "murisi/namada-integration" diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 8a783d4dc53..0c4cf81909e 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -66,8 +66,8 @@ abciplus = [ [dependencies] -namada = {path = "../shared", features = ["ferveo-tpke", "masp-tx-gen", "multicore", "http-client"]} -namada_sdk = {path = "../sdk", default-features = false, features = ["wasm-runtime", "masp-tx-gen"]} +namada = {path = "../shared", features = ["ferveo-tpke", "multicore", "http-client"]} +namada_sdk = {path = "../sdk", default-features = false, features = ["wasm-runtime"]} namada_test_utils = {path = "../test_utils", optional = true} ark-serialize.workspace = true ark-std.workspace = true @@ -100,6 +100,8 @@ flate2.workspace = true futures.workspace = true itertools.workspace = true lazy_static.workspace= true +ledger-namada-rs.workspace = true +ledger-transport-hid.workspace = true libc.workspace = true libloading.workspace = true masp_primitives = { workspace = true, features = ["transparent-inputs"] } diff --git a/apps/src/bin/namada-wallet/main.rs b/apps/src/bin/namada-wallet/main.rs index 30d4a641568..6ee0bc0bd7d 100644 --- a/apps/src/bin/namada-wallet/main.rs +++ b/apps/src/bin/namada-wallet/main.rs @@ -2,9 +2,10 @@ use color_eyre::eyre::Result; use namada_apps::cli; use namada_apps::cli::api::{CliApi, CliIo}; -pub fn main() -> Result<()> { +#[tokio::main] +pub async fn main() -> Result<()> { color_eyre::install()?; let (cmd, ctx) = cli::namada_wallet_cli()?; // run the CLI - CliApi::handle_wallet_command(cmd, ctx, &CliIo) + CliApi::handle_wallet_command(cmd, ctx, &CliIo).await } diff --git a/apps/src/lib/bench_utils.rs b/apps/src/lib/bench_utils.rs index 4f12c5b6b19..c35841afbd3 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -704,15 +704,17 @@ impl Default for BenchShieldedCtx { let mut chain_ctx = ctx.take_chain_or_exit(); // Generate spending key for Albert and Bertha - chain_ctx.wallet.gen_spending_key( + chain_ctx.wallet.gen_store_spending_key( ALBERT_SPENDING_KEY.to_string(), None, true, + &mut OsRng, ); - chain_ctx.wallet.gen_spending_key( + chain_ctx.wallet.gen_store_spending_key( BERTHA_SPENDING_KEY.to_string(), None, true, + &mut OsRng, ); crate::wallet::save(&chain_ctx.wallet).unwrap(); diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index b9b4d157803..039b2395649 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -483,7 +483,7 @@ pub mod cmds { #[derive(Clone, Debug)] #[allow(clippy::large_enum_variant)] pub enum WalletKey { - Restore(KeyRestore), + Derive(KeyDerive), Gen(KeyGen), Find(KeyFind), List(KeyList), @@ -496,7 +496,7 @@ pub mod cmds { fn parse(matches: &ArgMatches) -> Option { matches.subcommand_matches(Self::CMD).and_then(|matches| { let generate = SubCmd::parse(matches).map(Self::Gen); - let restore = SubCmd::parse(matches).map(Self::Restore); + let restore = SubCmd::parse(matches).map(Self::Derive); let lookup = SubCmd::parse(matches).map(Self::Find); let list = SubCmd::parse(matches).map(Self::List); let export = SubCmd::parse(matches).map(Self::Export); @@ -512,7 +512,7 @@ pub mod cmds { ) .subcommand_required(true) .arg_required_else_help(true) - .subcommand(KeyRestore::def()) + .subcommand(KeyDerive::def()) .subcommand(KeyGen::def()) .subcommand(KeyFind::def()) .subcommand(KeyList::def()) @@ -522,26 +522,27 @@ pub mod cmds { /// Restore a keypair and implicit address from the mnemonic code #[derive(Clone, Debug)] - pub struct KeyRestore(pub args::KeyAndAddressRestore); + pub struct KeyDerive(pub args::KeyAndAddressDerive); - impl SubCmd for KeyRestore { - const CMD: &'static str = "restore"; + impl SubCmd for KeyDerive { + const CMD: &'static str = "derive"; fn parse(matches: &ArgMatches) -> Option { matches .subcommand_matches(Self::CMD) - .map(|matches| Self(args::KeyAndAddressRestore::parse(matches))) + .map(|matches| Self(args::KeyAndAddressDerive::parse(matches))) } fn def() -> App { App::new(Self::CMD) .about( - "Restores a keypair from the given mnemonic code and HD \ + "Derives a keypair from the given mnemonic code and HD \ derivation path and derives the implicit address from \ its public key. Stores the keypair and the address with \ - the given alias.", + the given alias. A hardware wallet can be used, in which \ + case a private key is not derivable.", ) - .add_args::() + .add_args::() } } @@ -795,7 +796,7 @@ pub mod cmds { #[derive(Clone, Debug)] pub enum WalletAddress { Gen(AddressGen), - Restore(AddressRestore), + Derive(AddressDerive), Find(AddressOrAliasFind), List(AddressList), Add(AddressAdd), @@ -807,7 +808,7 @@ pub mod cmds { fn parse(matches: &ArgMatches) -> Option { matches.subcommand_matches(Self::CMD).and_then(|matches| { let gen = SubCmd::parse(matches).map(Self::Gen); - let restore = SubCmd::parse(matches).map(Self::Restore); + let restore = SubCmd::parse(matches).map(Self::Derive); let find = SubCmd::parse(matches).map(Self::Find); let list = SubCmd::parse(matches).map(Self::List); let add = SubCmd::parse(matches).map(Self::Add); @@ -824,7 +825,7 @@ pub mod cmds { .subcommand_required(true) .arg_required_else_help(true) .subcommand(AddressGen::def()) - .subcommand(AddressRestore::def()) + .subcommand(AddressDerive::def()) .subcommand(AddressOrAliasFind::def()) .subcommand(AddressList::def()) .subcommand(AddressAdd::def()) @@ -857,26 +858,27 @@ pub mod cmds { /// Restore a keypair and an implicit address from the mnemonic code #[derive(Clone, Debug)] - pub struct AddressRestore(pub args::KeyAndAddressRestore); + pub struct AddressDerive(pub args::KeyAndAddressDerive); - impl SubCmd for AddressRestore { - const CMD: &'static str = "restore"; + impl SubCmd for AddressDerive { + const CMD: &'static str = "derive"; fn parse(matches: &ArgMatches) -> Option { matches.subcommand_matches(Self::CMD).map(|matches| { - AddressRestore(args::KeyAndAddressRestore::parse(matches)) + AddressDerive(args::KeyAndAddressDerive::parse(matches)) }) } fn def() -> App { App::new(Self::CMD) .about( - "Restores a keypair from the given mnemonic code and HD \ + "Derives a keypair from the given mnemonic code and HD \ derivation path and derives the implicit address from \ its public key. Stores the keypair and the address with \ - the given alias.", + the given alias. A hardware wallet can be used, in which \ + case a private key is not derivable.", ) - .add_args::() + .add_args::() } } @@ -2765,9 +2767,8 @@ pub mod args { arg("genesis-validator").opt(); pub const HALT_ACTION: ArgFlag = flag("halt"); pub const HASH_LIST: Arg = arg("hash-list"); - pub const HD_WALLET_DERIVATION_PATH: Arg = arg("hd-path"); - pub const HD_WALLET_DERIVATION_PATH_OPT: ArgOpt = - HD_WALLET_DERIVATION_PATH.opt(); + pub const HD_WALLET_DERIVATION_PATH: ArgDefault = + arg_default("hd-path", DefaultFn(|| "default".to_string())); pub const HISTORIC: ArgFlag = flag("historic"); pub const IBC_TRANSFER_MEMO_PATH: ArgOpt = arg_opt("memo-path"); pub const LEDGER_ADDRESS_ABOUT: &str = @@ -2855,6 +2856,7 @@ pub mod args { pub const THRESOLD: ArgOpt = arg_opt("threshold"); pub const UNSAFE_DONT_ENCRYPT: ArgFlag = flag("unsafe-dont-encrypt"); pub const UNSAFE_SHOW_SECRET: ArgFlag = flag("unsafe-show-secret"); + pub const USE_DEVICE: ArgFlag = flag("use-device"); pub const VALIDATOR: Arg = arg("validator"); pub const VALIDATOR_OPT: ArgOpt = VALIDATOR.opt(); pub const VALIDATOR_ACCOUNT_KEY: ArgOpt = @@ -3765,7 +3767,7 @@ pub mod args { public_keys: self .public_keys .iter() - .map(|pk| chain_ctx.get_cached(pk)) + .map(|pk| chain_ctx.get(pk)) .collect(), threshold: self.threshold, } @@ -3819,7 +3821,7 @@ pub mod args { account_keys: self .account_keys .iter() - .map(|x| chain_ctx.get_cached(x)) + .map(|x| chain_ctx.get(x)) .collect(), threshold: self.threshold, consensus_key: self @@ -3829,9 +3831,7 @@ pub mod args { .eth_cold_key .map(|x| chain_ctx.get_cached(&x)), eth_hot_key: self.eth_hot_key.map(|x| chain_ctx.get_cached(&x)), - protocol_key: self - .protocol_key - .map(|x| chain_ctx.get_cached(&x)), + protocol_key: self.protocol_key.map(|x| chain_ctx.get(&x)), commission_rate: self.commission_rate, max_commission_rate_change: self.max_commission_rate_change, validator_vp_code_path: self @@ -3948,7 +3948,7 @@ pub mod args { public_keys: self .public_keys .iter() - .map(|pk| chain_ctx.get_cached(pk)) + .map(|pk| chain_ctx.get(pk)) .collect(), threshold: self.threshold, } @@ -4397,7 +4397,7 @@ pub mod args { let chain_ctx = ctx.borrow_mut_chain_or_exit(); RevealPk:: { tx, - public_key: chain_ctx.get_cached(&self.public_key), + public_key: chain_ctx.get(&self.public_key), } } } @@ -5199,7 +5199,7 @@ pub mod args { .collect(), verification_key: self .verification_key - .map(|public_key| ctx.get_cached(&public_key)), + .map(|public_key| ctx.get(&public_key)), disposable_signing_key: self.disposable_signing_key, tx_reveal_code_path: self.tx_reveal_code_path, password: self.password, @@ -5210,6 +5210,7 @@ pub mod args { wrapper_fee_payer: self .wrapper_fee_payer .map(|x| ctx.get_cached(&x)), + use_device: self.use_device, } } } @@ -5335,6 +5336,10 @@ pub mod args { ) .conflicts_with(DISPOSABLE_SIGNING_KEY.name), ) + .arg(USE_DEVICE.def().help( + "Use an attached hardware wallet device to sign the \ + transaction.", + )) } fn parse(matches: &ArgMatches) -> Self { @@ -5362,6 +5367,7 @@ pub mod args { let password = None; let wrapper_fee_payer = FEE_PAYER_OPT.parse(matches); let output_folder = OUTPUT_FOLDER_PATH.parse(matches); + let use_device = USE_DEVICE.parse(matches); Self { dry_run, dry_run_wrapper, @@ -5385,6 +5391,7 @@ pub mod args { chain_id, wrapper_fee_payer, output_folder, + use_device, } } } @@ -5559,18 +5566,20 @@ pub mod args { } } - impl Args for KeyAndAddressRestore { + impl Args for KeyAndAddressDerive { fn parse(matches: &ArgMatches) -> Self { let scheme = SCHEME.parse(matches); let alias = ALIAS_OPT.parse(matches); let alias_force = ALIAS_FORCE.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); - let derivation_path = HD_WALLET_DERIVATION_PATH_OPT.parse(matches); + let use_device = USE_DEVICE.parse(matches); + let derivation_path = HD_WALLET_DERIVATION_PATH.parse(matches); Self { scheme, alias, alias_force, unsafe_dont_encrypt, + use_device, derivation_path, } } @@ -5594,7 +5603,11 @@ pub mod args { "UNSAFE: Do not encrypt the keypair. Do not use this for keys \ used in a live network.", )) - .arg(HD_WALLET_DERIVATION_PATH_OPT.def().help( + .arg(USE_DEVICE.def().help( + "Derive an address and public key from the seed stored on the \ + connected hardware wallet.", + )) + .arg(HD_WALLET_DERIVATION_PATH.def().help( "HD key derivation path. Use keyword `default` to refer to a \ scheme default path:\n- m/44'/60'/0'/0/0 for secp256k1 \ scheme\n- m/44'/877'/0'/0'/0' for ed25519 scheme.\nFor \ @@ -5612,7 +5625,7 @@ pub mod args { let alias_force = ALIAS_FORCE.parse(matches); let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); - let derivation_path = HD_WALLET_DERIVATION_PATH_OPT.parse(matches); + let derivation_path = HD_WALLET_DERIVATION_PATH.parse(matches); Self { scheme, alias, @@ -5644,7 +5657,7 @@ pub mod args { "UNSAFE: Do not encrypt the keypair. Do not use this for keys \ used in a live network.", )) - .arg(HD_WALLET_DERIVATION_PATH_OPT.def().help( + .arg(HD_WALLET_DERIVATION_PATH.def().help( "Generate a new key and wallet using BIP39 mnemonic code and \ HD derivation path. Use keyword `default` to refer to a \ scheme default path:\n- m/44'/60'/0'/0/0 for secp256k1 \ diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index be0c2e3c4de..51c8ba4eac7 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -408,15 +408,15 @@ impl ArgFromMutContext for common::SecretKey { FromStr::from_str(raw).or_else(|_parse_err| { // Or it can be an alias ctx.wallet - .find_key(raw, None) + .find_secret_key(raw, None) .map_err(|_find_err| format!("Unknown key {}", raw)) }) } } -impl ArgFromMutContext for common::PublicKey { - fn arg_from_mut_ctx( - ctx: &mut ChainContext, +impl ArgFromContext for common::PublicKey { + fn arg_from_ctx( + ctx: &ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -425,15 +425,11 @@ impl ArgFromMutContext for common::PublicKey { // Or it can be a public key hash in hex string FromStr::from_str(raw) .map(|pkh: PublicKeyHash| { - let key = ctx.wallet.find_key_by_pkh(&pkh, None).unwrap(); - key.ref_to() + ctx.wallet.find_public_key_by_pkh(&pkh).unwrap() }) // Or it can be an alias that may be found in the wallet .or_else(|_parse_err| { - ctx.wallet - .find_key(raw, None) - .map(|x| x.ref_to()) - .map_err(|x| x.to_string()) + ctx.wallet.find_public_key(raw).map_err(|x| x.to_string()) }) }) } diff --git a/apps/src/lib/cli/wallet.rs b/apps/src/lib/cli/wallet.rs index 3b43e09b74d..4d22365f804 100644 --- a/apps/src/lib/cli/wallet.rs +++ b/apps/src/lib/cli/wallet.rs @@ -2,18 +2,24 @@ use std::fs::File; use std::io::{self, Write}; +use std::str::FromStr; +use borsh::BorshDeserialize; use borsh_ext::BorshSerializeExt; use color_eyre::eyre::Result; use itertools::sorted; +use ledger_namada_rs::{BIP44Path, NamadaApp}; +use ledger_transport_hid::hidapi::HidApi; +use ledger_transport_hid::TransportNativeHID; use masp_primitives::zip32::ExtendedFullViewingKey; +use namada::types::address::Address; use namada::types::io::Io; use namada::types::key::*; use namada::types::masp::{MaspValue, PaymentAddress}; use namada_sdk::masp::find_valid_diversifier; use namada_sdk::wallet::{ - DecryptionError, FindKeyError, GenRestoreKeyError, Wallet, WalletIo, - WalletStorage, + DecryptionError, DerivationPath, DerivationPathError, FindKeyError, Wallet, + WalletIo, WalletStorage, }; use namada_sdk::{display, display_line, edisplay_line}; use rand_core::OsRng; @@ -28,19 +34,20 @@ use crate::wallet::{ }; impl CliApi { - pub fn handle_wallet_command( + pub async fn handle_wallet_command( cmd: cmds::NamadaWallet, mut ctx: Context, io: &impl Io, ) -> Result<()> { match cmd { cmds::NamadaWallet::Key(sub) => match sub { - cmds::WalletKey::Restore(cmds::KeyRestore(args)) => { - key_and_address_restore( + cmds::WalletKey::Derive(cmds::KeyDerive(args)) => { + key_and_address_derive( &mut ctx.borrow_mut_chain_or_exit().wallet, io, args, ) + .await } cmds::WalletKey::Gen(cmds::KeyGen(args)) => { key_and_address_gen(ctx, io, args) @@ -59,12 +66,13 @@ impl CliApi { cmds::WalletAddress::Gen(cmds::AddressGen(args)) => { key_and_address_gen(ctx, io, args) } - cmds::WalletAddress::Restore(cmds::AddressRestore(args)) => { - key_and_address_restore( + cmds::WalletAddress::Derive(cmds::AddressDerive(args)) => { + key_and_address_derive( &mut ctx.borrow_mut_chain_or_exit().wallet, io, args, ) + .await } cmds::WalletAddress::Find(cmds::AddressOrAliasFind(args)) => { address_or_alias_find(ctx, io, args) @@ -261,7 +269,8 @@ fn spending_key_gen( let mut wallet = load_wallet(ctx, is_pre_genesis); let alias = alias.to_lowercase(); let password = read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (alias, _key) = wallet.gen_spending_key(alias, password, alias_force); + let (alias, _key) = + wallet.gen_store_spending_key(alias, password, alias_force, &mut OsRng); wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); display_line!( io, @@ -334,12 +343,7 @@ fn address_key_add( let password = read_and_confirm_encryption_password(unsafe_dont_encrypt); let alias = wallet - .encrypt_insert_spending_key( - alias, - spending_key, - password, - alias_force, - ) + .insert_spending_key(alias, spending_key, password, alias_force) .unwrap_or_else(|| { edisplay_line!(io, "Spending key not added"); cli::safe_exit(1); @@ -365,38 +369,113 @@ fn address_key_add( ); } -/// Restore a keypair and an implicit address from the mnemonic code in the +/// Decode the derivation path from the given string unless it is "default", +/// in which case use the default derivation path for the given scheme. +pub fn decode_derivation_path( + scheme: SchemeType, + derivation_path: String, +) -> Result { + let is_default = derivation_path.eq_ignore_ascii_case("DEFAULT"); + let parsed_derivation_path = if is_default { + DerivationPath::default_for_scheme(scheme) + } else { + DerivationPath::from_path_str(scheme, &derivation_path)? + }; + if !parsed_derivation_path.is_compatible(scheme) { + println!( + "WARNING: the specified derivation path may be incompatible with \ + the chosen cryptography scheme." + ) + } + println!("Using HD derivation path {}", parsed_derivation_path); + Ok(parsed_derivation_path) +} + +/// Derives a keypair and an implicit address from the mnemonic code in the /// wallet. -fn key_and_address_restore( +async fn key_and_address_derive( wallet: &mut Wallet, io: &impl Io, - args::KeyAndAddressRestore { + args::KeyAndAddressDerive { scheme, alias, alias_force, unsafe_dont_encrypt, derivation_path, - }: args::KeyAndAddressRestore, + use_device, + }: args::KeyAndAddressDerive, ) { - let encryption_password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (alias, _key) = wallet - .derive_key_from_user_mnemonic_code( - scheme, - alias, - alias_force, - derivation_path, - None, - encryption_password, - ) + let derivation_path = decode_derivation_path(scheme, derivation_path) .unwrap_or_else(|err| { edisplay_line!(io, "{}", err); cli::safe_exit(1) - }) - .unwrap_or_else(|| { - display_line!(io, "No changes are persisted. Exiting."); - cli::safe_exit(0); }); + let alias = if !use_device { + let encryption_password = + read_and_confirm_encryption_password(unsafe_dont_encrypt); + wallet + .derive_key_from_mnemonic_code( + scheme, + alias, + alias_force, + derivation_path, + None, + encryption_password, + ) + .unwrap_or_else(|err| { + edisplay_line!(io, "{}", err); + display_line!(io, "No changes are persisted. Exiting."); + cli::safe_exit(1) + }) + .0 + } else { + let hidapi = HidApi::new().unwrap_or_else(|err| { + edisplay_line!(io, "Failed to create Hidapi: {}", err); + cli::safe_exit(1) + }); + let app = NamadaApp::new( + TransportNativeHID::new(&hidapi).unwrap_or_else(|err| { + edisplay_line!(io, "Unable to connect to Ledger: {}", err); + cli::safe_exit(1) + }), + ); + let response = app + .get_address_and_pubkey( + &BIP44Path { + path: derivation_path.to_string(), + }, + true, + ) + .await + .unwrap_or_else(|err| { + edisplay_line!( + io, + "Unable to connect to query address and public key from \ + Ledger: {}", + err + ); + cli::safe_exit(1) + }); + + let pubkey = common::PublicKey::try_from_slice(&response.public_key) + .expect("unable to decode public key from hardware wallet"); + let pkh = PublicKeyHash::from(&pubkey); + let address = Address::from_str(&response.address_str) + .expect("unable to decode address from hardware wallet"); + + wallet + .insert_public_key( + alias.unwrap_or_else(|| pkh.to_string()), + pubkey, + Some(address), + Some(derivation_path), + alias_force, + ) + .unwrap_or_else(|| { + display_line!(io, "No changes are persisted. Exiting."); + cli::safe_exit(1) + }) + }; wallet .save() .unwrap_or_else(|err| edisplay_line!(io, "{}", err)); @@ -424,27 +503,33 @@ fn key_and_address_gen( let mut wallet = load_wallet(ctx, is_pre_genesis); let encryption_password = read_and_confirm_encryption_password(unsafe_dont_encrypt); + let derivation_path = decode_derivation_path(scheme, derivation_path) + .unwrap_or_else(|err| { + edisplay_line!(io, "{}", err); + cli::safe_exit(1) + }); let mut rng = OsRng; - let derivation_path_and_mnemonic_rng = - derivation_path.map(|p| (p, &mut rng)); - let (alias, _key, _mnemonic) = wallet - .gen_key( + let (_mnemonic, seed) = Wallet::::gen_hd_seed( + None, &mut rng, + ) + .unwrap_or_else(|err| { + edisplay_line!(io, "{}", err); + cli::safe_exit(1) + }); + let alias = wallet + .derive_store_hd_secret_key( scheme, alias, alias_force, - None, + seed, + derivation_path, encryption_password, - derivation_path_and_mnemonic_rng, ) - .unwrap_or_else(|err| match err { - GenRestoreKeyError::KeyStorageError => { - display_line!(io, "No changes are persisted. Exiting."); - cli::safe_exit(0); - } - _ => { - edisplay_line!(io, "{}", err); - cli::safe_exit(1); - } + .map(|x| x.0) + .unwrap_or_else(|err| { + eprintln!("{}", err); + println!("No changes are persisted. Exiting."); + cli::safe_exit(0); }); wallet .save() @@ -482,7 +567,9 @@ fn key_find( ); cli::safe_exit(1) } - Some(alias) => wallet.find_key(alias.to_lowercase(), None), + Some(alias) => { + wallet.find_secret_key(alias.to_lowercase(), None) + } } } }; @@ -512,8 +599,8 @@ fn key_list( }: args::KeyList, ) { let wallet = load_wallet(ctx, is_pre_genesis); - let known_keys = wallet.get_keys(); - if known_keys.is_empty() { + let known_public_keys = wallet.get_public_keys(); + if known_public_keys.is_empty() { display_line!( io, "No known keys. Try `key gen --alias my-key` to generate a new \ @@ -523,45 +610,47 @@ fn key_list( let stdout = io::stdout(); let mut w = stdout.lock(); display_line!(io, &mut w; "Known keys:").unwrap(); - for (alias, (stored_keypair, pkh)) in known_keys { - let encrypted = if stored_keypair.is_encrypted() { - "encrypted" - } else { - "not encrypted" + let known_secret_keys = wallet.get_secret_keys(); + for (alias, public_key) in known_public_keys { + let stored_keypair = known_secret_keys.get(&alias); + let encrypted = match stored_keypair { + None => "external", + Some((stored_keypair, _pkh)) + if stored_keypair.is_encrypted() => + { + "encrypted" + } + Some(_) => "not encrypted", }; display_line!(io, &mut w; " Alias \"{}\" ({}):", alias, encrypted, ) .unwrap(); - if let Some(pkh) = pkh { - display_line!(io, &mut w; " Public key hash: {}", pkh) - .unwrap(); - } - match stored_keypair.get::(decrypt, None) { - Ok(keypair) => { - display_line!(io, - &mut w; - " Public key: {}", keypair.ref_to(), - ) - .unwrap(); - if unsafe_show_secret { + display_line!(io, &mut w; " Public key hash: {}", PublicKeyHash::from(&public_key)) + .unwrap(); + display_line!(io, &mut w; " Public key: {}", public_key) + .unwrap(); + if let Some((stored_keypair, _pkh)) = stored_keypair { + match stored_keypair.get::(decrypt, None) { + Ok(keypair) if unsafe_show_secret => { display_line!(io, - &mut w; - " Secret key: {}", keypair, + &mut w; + " Secret key: {}", keypair, ) .unwrap(); } - } - Err(DecryptionError::NotDecrypting) if !decrypt => { - continue; - } - Err(err) => { - display_line!(io, - &mut w; - " Couldn't decrypt the keypair: {}", err, - ) - .unwrap(); + Ok(_keypair) => {} + Err(DecryptionError::NotDecrypting) if !decrypt => { + continue; + } + Err(err) => { + display_line!(io, + &mut w; + " Couldn't decrypt the keypair: {}", err, + ) + .unwrap(); + } } } } @@ -579,7 +668,7 @@ fn key_export( ) { let mut wallet = load_wallet(ctx, is_pre_genesis); wallet - .find_key(alias.to_lowercase(), None) + .find_secret_key(alias.to_lowercase(), None) .map(|keypair| { let file_data = keypair.serialize_to_vec(); let file_name = format!("key_{}", alias.to_lowercase()); @@ -676,7 +765,7 @@ fn address_add( ) { let mut wallet = load_wallet(ctx, is_pre_genesis); if wallet - .add_address(alias.to_lowercase(), address, alias_force) + .insert_address(alias.to_lowercase(), address, alias_force) .is_none() { edisplay_line!(io, "Address not added"); diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index d939d5691e0..83ce66ebc9b 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1,6 +1,12 @@ +use std::collections::HashSet; use std::fs::File; use std::io::Write; +use borsh::BorshDeserialize; +use borsh_ext::BorshSerializeExt; +use ledger_namada_rs::{BIP44Path, NamadaApp}; +use ledger_transport_hid::hidapi::HidApi; +use ledger_transport_hid::TransportNativeHID; use namada::core::ledger::governance::cli::offline::{ OfflineProposal, OfflineSignedProposal, OfflineVote, }; @@ -8,7 +14,7 @@ use namada::core::ledger::governance::cli::onchain::{ DefaultProposal, PgfFundingProposal, PgfStewardProposal, ProposalVote, }; use namada::ibc::applications::transfer::Memo; -use namada::proto::Tx; +use namada::proto::{CompressedSignature, Section, Signer, Tx}; use namada::types::address::{Address, ImplicitAddress}; use namada::types::dec::Dec; use namada::types::io::Io; @@ -16,10 +22,12 @@ use namada::types::key::{self, *}; use namada::types::transaction::pos::InitValidator; use namada_sdk::rpc::{TxBroadcastData, TxResponse}; use namada_sdk::{display_line, edisplay_line, error, signing, tx, Namada}; +use rand::rngs::OsRng; use super::rpc; use crate::cli::{args, safe_exit}; use crate::client::rpc::query_wasm_code_hash; +use crate::client::tx::signing::{default_sign, SigningTxData}; use crate::client::tx::tx::ProcessTxResponse; use crate::config::TendermintMode; use crate::facade::tendermint_rpc::endpoint::broadcast::tx_sync::Response; @@ -57,6 +65,137 @@ pub async fn aux_signing_data<'a>( Ok(signing_data) } +// Sign the given transaction using a hardware wallet as a backup +pub async fn sign<'a>( + context: &impl Namada<'a>, + tx: &mut Tx, + args: &args::Tx, + signing_data: SigningTxData, +) -> Result<(), error::Error> { + // Setup a reusable context for signing transactions using the Ledger + if args.use_device { + // Setup a reusable context for signing transactions using the Ledger + let hidapi = HidApi::new().map_err(|err| { + error::Error::Other(format!("Failed to create Hidapi: {}", err)) + })?; + let app = NamadaApp::new(TransportNativeHID::new(&hidapi).map_err( + |err| { + error::Error::Other(format!( + "Unable to connect to Ledger: {}", + err + )) + }, + )?); + // A closure to facilitate signing transactions also using the Ledger + let with_hw = + |mut tx: Tx, + pubkey: common::PublicKey, + parts: HashSet| { + let app = &app; + async move { + // Obtain derivation path corresponding to the signing + // public key + let path = context + .wallet() + .await + .find_path_by_pkh(&(&pubkey).into()) + .map_err(|_| { + error::Error::Other( + "Unable to find derivation path for key" + .to_string(), + ) + })?; + let path = BIP44Path { + path: path.to_string(), + }; + // Now check that the public key at this path in the Ledger + // matches + let response_pubkey = app + .get_address_and_pubkey(&path, false) + .await + .map_err(|err| error::Error::Other(err.to_string()))?; + let response_pubkey = common::PublicKey::try_from_slice( + &response_pubkey.public_key, + ) + .map_err(|err| { + error::Error::Other(format!( + "unable to decode public key from hardware \ + wallet: {}", + err + )) + })?; + if response_pubkey != pubkey { + return Err(error::Error::Other(format!( + "Unrecognized public key fetched fom Ledger: {}. \ + Expected {}.", + response_pubkey, pubkey, + ))); + } + // Get the Ledger to sign using our obtained derivation path + let response = app + .sign(&path, &tx.serialize_to_vec()) + .await + .map_err(|err| error::Error::Other(err.to_string()))?; + // Sign the raw header if that is requested + if parts.contains(&signing::Signable::RawHeader) { + let pubkey = + common::PublicKey::try_from_slice(&response.pubkey) + .expect( + "unable to parse public key from Ledger", + ); + let signature = common::Signature::try_from_slice( + &response.raw_signature, + ) + .expect("unable to parse signature from Ledger"); + // Signatures from the Ledger come back in compressed + // form + let compressed = CompressedSignature { + targets: response.raw_indices, + signer: Signer::PubKeys(vec![pubkey]), + signatures: [(0, signature)].into(), + }; + // Expand out the signature before adding it to the + // transaction + tx.add_section(Section::Signature( + compressed.expand(&tx), + )); + } + // Sign the fee header if that is requested + if parts.contains(&signing::Signable::FeeHeader) { + let pubkey = + common::PublicKey::try_from_slice(&response.pubkey) + .expect( + "unable to parse public key from Ledger", + ); + let signature = common::Signature::try_from_slice( + &response.wrapper_signature, + ) + .expect("unable to parse signature from Ledger"); + // Signatures from the Ledger come back in compressed + // form + let compressed = CompressedSignature { + targets: response.wrapper_indices, + signer: Signer::PubKeys(vec![pubkey]), + signatures: [(0, signature)].into(), + }; + // Expand out the signature before adding it to the + // transaction + tx.add_section(Section::Signature( + compressed.expand(&tx), + )); + } + Ok(tx) + } + }; + // Finally, begin the signing with the Ledger as backup + context.sign(tx, args, signing_data, with_hw).await?; + } else { + // Otherwise sign without a backup procedure + context.sign(tx, args, signing_data, default_sign).await?; + } + Ok(()) +} + // Build a transaction to reveal the signer of the given transaction. pub async fn submit_reveal_aux<'a>( context: &impl Namada<'a>, @@ -68,12 +207,11 @@ pub async fn submit_reveal_aux<'a>( } if let Address::Implicit(ImplicitAddress(pkh)) = address { - let key = context + let public_key = context .wallet_mut() .await - .find_key_by_pkh(pkh, args.clone().password) + .find_public_key_by_pkh(pkh) .map_err(|e| error::Error::Other(e.to_string()))?; - let public_key = key.ref_to(); if tx::is_reveal_pk_needed(context.client(), address, args.force) .await? @@ -87,7 +225,7 @@ pub async fn submit_reveal_aux<'a>( signing::generate_test_vector(context, &tx).await?; - context.sign(&mut tx, &args, signing_data).await?; + sign(context, &mut tx, &args, signing_data).await?; context.submit(tx, &args).await?; } @@ -109,7 +247,9 @@ pub async fn submit_bridge_pool_tx<'a, N: Namada<'a>>( tx::dump_tx(namada.io(), &args.tx, tx); } else { submit_reveal_aux(namada, tx_args.clone(), &args.sender).await?; - namada.sign(&mut tx, &tx_args, signing_data).await?; + + sign(namada, &mut tx, &tx_args, signing_data).await?; + namada.submit(tx, &tx_args).await?; } @@ -132,7 +272,8 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; + namada.submit(tx, &args.tx).await?; } @@ -153,7 +294,8 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; + namada.submit(tx, &args.tx).await?; } @@ -175,7 +317,8 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; + namada.submit(tx, &args.tx).await?; } @@ -247,14 +390,13 @@ pub async fn submit_init_validator<'a>( let password = read_and_confirm_encryption_password(unsafe_dont_encrypt); wallet - .gen_key( + .gen_store_secret_key( // Note that TM only allows ed25519 for consensus key SchemeType::Ed25519, Some(consensus_key_alias.clone()), tx_args.wallet_alias_force, - None, password, - None, + &mut OsRng, ) .expect("Key generation should not fail.") .1 @@ -276,14 +418,13 @@ pub async fn submit_init_validator<'a>( let password = read_and_confirm_encryption_password(unsafe_dont_encrypt); wallet - .gen_key( + .gen_store_secret_key( // Note that ETH only allows secp256k1 SchemeType::Secp256k1, Some(eth_cold_key_alias.clone()), tx_args.wallet_alias_force, - None, password, - None, + &mut OsRng, ) .expect("Key generation should not fail.") .1 @@ -306,14 +447,13 @@ pub async fn submit_init_validator<'a>( let password = read_and_confirm_encryption_password(unsafe_dont_encrypt); wallet - .gen_key( + .gen_store_secret_key( // Note that ETH only allows secp256k1 SchemeType::Secp256k1, Some(eth_hot_key_alias.clone()), tx_args.wallet_alias_force, - None, password, - None, + &mut OsRng, ) .expect("Key generation should not fail.") .1 @@ -411,7 +551,7 @@ pub async fn submit_init_validator<'a>( if tx_args.dump_tx { tx::dump_tx(namada.io(), &tx_args, tx); } else { - namada.sign(&mut tx, &tx_args, signing_data).await?; + sign(namada, &mut tx, &tx_args, signing_data).await?; let result = namada.submit(tx, &tx_args).await?.initialized_accounts(); @@ -532,7 +672,8 @@ pub async fn submit_transfer<'a>( tx::dump_tx(namada.io(), &args.tx, tx); break; } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; + let result = namada.submit(tx, &args.tx).await?; let submission_epoch = rpc::query_and_print_epoch(namada).await; @@ -577,7 +718,8 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; + namada.submit(tx, &args.tx).await?; } @@ -703,7 +845,8 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx_builder); } else { - namada.sign(&mut tx_builder, &args.tx, signing_data).await?; + sign(namada, &mut tx_builder, &args.tx, signing_data).await?; + namada.submit(tx_builder, &args.tx).await?; } @@ -780,7 +923,8 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx_builder); } else { - namada.sign(&mut tx_builder, &args.tx, signing_data).await?; + sign(namada, &mut tx_builder, &args.tx, signing_data).await?; + namada.submit(tx_builder, &args.tx).await?; } @@ -897,7 +1041,7 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; namada.submit(tx, &args.tx).await?; } @@ -919,7 +1063,7 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; namada.submit(tx, &args.tx).await?; @@ -943,7 +1087,7 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; namada.submit(tx, &args.tx).await?; } @@ -964,7 +1108,7 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; namada.submit(tx, &args.tx).await?; } @@ -986,7 +1130,7 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; namada.submit(tx, &args.tx).await?; } @@ -1008,7 +1152,7 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; namada.submit(tx, &args.tx).await?; } @@ -1031,7 +1175,8 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; + namada.submit(tx, &args.tx).await?; } @@ -1052,7 +1197,8 @@ where if args.tx.dump_tx { tx::dump_tx(namada.io(), &args.tx, tx); } else { - namada.sign(&mut tx, &args.tx, signing_data).await?; + sign(namada, &mut tx, &args.tx, signing_data).await?; + namada.submit(tx, &args.tx).await?; } diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index a38b8714cae..f77c5f9ba1b 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -610,8 +610,9 @@ pub fn init_genesis_validator( let (mut source_wallet, wallet_file) = load_pre_genesis_wallet_or_exit(&global_args.base_dir); - let source_key = - source_wallet.find_key(&source, None).unwrap_or_else(|err| { + let source_key = source_wallet + .find_secret_key(&source, None) + .unwrap_or_else(|err| { eprintln!( "Couldn't find key for source \"{source}\" in the pre-genesis \ wallet {}. Failed with {err}.", diff --git a/apps/src/lib/config/genesis/chain.rs b/apps/src/lib/config/genesis/chain.rs index 711266c5424..0f7b9cdd7e7 100644 --- a/apps/src/lib/config/genesis/chain.rs +++ b/apps/src/lib/config/genesis/chain.rs @@ -101,7 +101,7 @@ impl Finalized { ) -> Wallet { let mut wallet = crate::wallet::load_or_new(base_dir); for (alias, config) in &self.tokens.token { - wallet.add_address( + wallet.insert_address( alias.normalize(), config.address.clone(), false, @@ -113,7 +113,7 @@ impl Finalized { } if let Some(txs) = &self.transactions.validator_account { for tx in txs { - wallet.add_address( + wallet.insert_address( tx.tx.alias.normalize(), tx.address.clone(), false, @@ -122,7 +122,7 @@ impl Finalized { } if let Some(txs) = &self.transactions.established_account { for tx in txs { - wallet.add_address( + wallet.insert_address( tx.tx.alias.normalize(), tx.address.clone(), false, diff --git a/apps/src/lib/config/genesis/transactions.rs b/apps/src/lib/config/genesis/transactions.rs index 21db15f43a9..e48668535b8 100644 --- a/apps/src/lib/config/genesis/transactions.rs +++ b/apps/src/lib/config/genesis/transactions.rs @@ -330,7 +330,9 @@ pub fn sign_delegation_bond_tx( // Try to look-up the source from wallet first - if it's an alias of an // implicit account that should give us the right key let found_key = match alias { - AliasOrPk::Alias(alias) => wallet.find_key(&alias.normalize(), None), + AliasOrPk::Alias(alias) => { + wallet.find_secret_key(&alias.normalize(), None) + } AliasOrPk::PublicKey(pk) => wallet.find_key_by_pk(pk, None), }; let source_key = match found_key { diff --git a/apps/src/lib/node/ledger/shell/testing/client.rs b/apps/src/lib/node/ledger/shell/testing/client.rs index 790587a549b..5e4c04b07f3 100644 --- a/apps/src/lib/node/ledger/shell/testing/client.rs +++ b/apps/src/lib/node/ledger/shell/testing/client.rs @@ -59,7 +59,7 @@ pub fn run( let cmd = cmds::NamadaWallet::parse(&matches) .expect("Could not parse wallet command"); - CliApi::handle_wallet_command(cmd, ctx, &TestingIo) + rt.block_on(CliApi::handle_wallet_command(cmd, ctx, &TestingIo)) } Bin::Relayer => { args.insert(0, "relayer"); diff --git a/apps/src/lib/wallet/defaults.rs b/apps/src/lib/wallet/defaults.rs index 57894d002e6..6c31629848f 100644 --- a/apps/src/lib/wallet/defaults.rs +++ b/apps/src/lib/wallet/defaults.rs @@ -56,7 +56,7 @@ mod dev { } /// The default keys with their aliases. - pub fn keys() -> Vec<(Alias, common::SecretKey)> { + pub fn keys() -> HashMap { vec![ ("albert".into(), albert_keypair()), ("bertha".into(), bertha_keypair()), @@ -65,6 +65,8 @@ mod dev { ("ester".into(), ester_keypair()), ("validator".into(), validator_keypair()), ] + .into_iter() + .collect() } /// The default tokens with their aliases. @@ -83,8 +85,8 @@ mod dev { } /// The default addresses with their aliases. - pub fn addresses() -> Vec<(Alias, Address)> { - let mut addresses: Vec<(Alias, Address)> = vec![ + pub fn addresses() -> HashMap { + let mut addresses: HashMap = vec![ ("pos".into(), pos::ADDRESS), ("pos_slash_pool".into(), pos::SLASH_POOL_ADDRESS), ("governance".into(), governance::ADDRESS), @@ -95,7 +97,9 @@ mod dev { ("christel".into(), christel_address()), ("daewon".into(), daewon_address()), ("ester".into(), ester_address()), - ]; + ] + .into_iter() + .collect(); let token_addresses = tokens() .into_iter() .map(|(addr, alias)| (alias.into(), addr)); diff --git a/apps/src/lib/wallet/pre_genesis.rs b/apps/src/lib/wallet/pre_genesis.rs index 6144c857afb..ddd3fca0b1f 100644 --- a/apps/src/lib/wallet/pre_genesis.rs +++ b/apps/src/lib/wallet/pre_genesis.rs @@ -8,6 +8,7 @@ use namada_sdk::wallet::pre_genesis::{ ReadError, ValidatorStore, ValidatorWallet, }; use namada_sdk::wallet::{gen_key_to_store, WalletIo}; +use rand::rngs::OsRng; use zeroize::Zeroizing; use crate::wallet::store::gen_validator_keys; @@ -110,18 +111,21 @@ fn gen( scheme: SchemeType, password: Option>, ) -> ValidatorWallet { - let (account_key, account_sk) = gen_key_to_store(scheme, password.clone()); + let (account_key, account_sk) = + gen_key_to_store(scheme, password.clone(), &mut OsRng); let (consensus_key, consensus_sk) = gen_key_to_store( // Note that TM only allows ed25519 for consensus key SchemeType::Ed25519, password.clone(), + &mut OsRng, ); let (eth_cold_key, eth_cold_sk) = - gen_key_to_store(SchemeType::Secp256k1, password.clone()); + gen_key_to_store(SchemeType::Secp256k1, password.clone(), &mut OsRng); let (tendermint_node_key, tendermint_node_sk) = gen_key_to_store( // Note that TM only allows ed25519 for node IDs SchemeType::Ed25519, password, + &mut OsRng, ); let validator_keys = gen_validator_keys(None, None, scheme); let eth_hot_key = validator_keys.eth_bridge_keypair.clone(); diff --git a/apps/src/lib/wallet/store.rs b/apps/src/lib/wallet/store.rs index b358c886b1b..e46726d74dc 100644 --- a/apps/src/lib/wallet/store.rs +++ b/apps/src/lib/wallet/store.rs @@ -4,7 +4,10 @@ use ark_std::rand::prelude::*; use ark_std::rand::SeedableRng; use namada::types::key::*; use namada::types::transaction::EllipticCurve; -use namada_sdk::wallet::{gen_sk_rng, LoadStoreError, Store, ValidatorKeys}; +use namada_sdk::wallet::{ + gen_secret_key, LoadStoreError, Store, ValidatorKeys, +}; +use rand::rngs::OsRng; use crate::wallet::CliWalletUtils; @@ -48,9 +51,9 @@ pub fn gen_validator_keys( } k }) - .unwrap_or_else(|| gen_sk_rng(SchemeType::Secp256k1)); - let protocol_keypair = - protocol_keypair.unwrap_or_else(|| gen_sk_rng(protocol_keypair_scheme)); + .unwrap_or_else(|| gen_secret_key(SchemeType::Secp256k1, &mut OsRng)); + let protocol_keypair = protocol_keypair + .unwrap_or_else(|| gen_secret_key(protocol_keypair_scheme, &mut OsRng)); let dkg_keypair = ferveo_common::Keypair::::new( &mut StdRng::from_entropy(), ); diff --git a/core/src/types/key/common.rs b/core/src/types/key/common.rs index 33f300d8848..6f428a80cf1 100644 --- a/core/src/types/key/common.rs +++ b/core/src/types/key/common.rs @@ -31,8 +31,6 @@ use crate::types::string_encoding; Ord, PartialOrd, Hash, - Serialize, - Deserialize, BorshSerialize, BorshDeserialize, BorshSchema, @@ -44,6 +42,51 @@ pub enum PublicKey { Secp256k1(secp256k1::PublicKey), } +const ED25519_PK_PREFIX: &str = "ED25519_PK_PREFIX"; +const SECP256K1_PK_PREFIX: &str = "SECP256K1_PK_PREFIX"; + +impl Serialize for PublicKey { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + // String encoded, because toml doesn't support enums + let prefix = match self { + PublicKey::Ed25519(_) => ED25519_PK_PREFIX, + PublicKey::Secp256k1(_) => SECP256K1_PK_PREFIX, + }; + let keypair_string = format!("{}{}", prefix, self); + Serialize::serialize(&keypair_string, serializer) + } +} + +impl<'de> Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + + let keypair_string: String = + serde::Deserialize::deserialize(deserializer) + .map_err(D::Error::custom)?; + if let Some(raw) = keypair_string.strip_prefix(ED25519_PK_PREFIX) { + PublicKey::from_str(raw).map_err(D::Error::custom) + } else if let Some(raw) = + keypair_string.strip_prefix(SECP256K1_PK_PREFIX) + { + PublicKey::from_str(raw).map_err(D::Error::custom) + } else { + Err(D::Error::custom( + "Could not deserialize SecretKey do to invalid prefix", + )) + } + } +} + impl super::PublicKey for PublicKey { const TYPE: SchemeType = SigScheme::TYPE; diff --git a/genesis/localnet/src/pre-genesis/wallet.toml b/genesis/localnet/src/pre-genesis/wallet.toml index 2444ba3f737..d042f9c8965 100644 --- a/genesis/localnet/src/pre-genesis/wallet.toml +++ b/genesis/localnet/src/pre-genesis/wallet.toml @@ -4,7 +4,7 @@ [payment_addrs] -[keys] +[secret_keys] albert-key = "unencrypted:0083318ccceac6c08a0177667840b4b93f0e455e45d4c38c28b73b8f8462fbf548" bertha-key = "unencrypted:0073ed61817720d27784e7a9bca4a60668d763a6f7ecac2d45ed1f241aa5c59e99" christel-key = "unencrypted:003625b939a58ef60402d7cf8fc04250026cc1ba90cc3028af1ce6b22be857ffc7" @@ -13,6 +13,17 @@ ester = "unencrypted:01369093e2035d84f72a7e5a17c89b7a938b5d08cc87b2289805e3afcc6 faucet-key = "unencrypted:00548aa8393422b88dce5f4be8ee0320638061c3e0649ada1b0dacbec4c0c75bb2" validator-0-balance-key = "unencrypted:000b9c8cb8f3ad6b8a387b064a11c0e98189814e9733aa7bb1e802425f6356f98a" +[public_keys] +albert-key = "ED25519_PK_PREFIXpktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +bertha-key = "ED25519_PK_PREFIXpktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +christel-key = "ED25519_PK_PREFIXpktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +daewon = "ED25519_PK_PREFIXpktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd" +ester = "SECP256K1_PK_PREFIXpktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd" +faucet-key = "ED25519_PK_PREFIXpktest1qzh2d8vk9wvj2j63fa3cvjru9uldpdjctjjxpafl5r8vwwf56pdgy2dpher" +validator-0-balance-key = "ED25519_PK_PREFIXpktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup" + +[derivation_paths] + [addresses] albert-key = "atest1d9khqw36x56rgvphx5erjwp48y6y2s6y89qnwdzygce5xsfkx9rrsd35xerrjdp3gsunz33n3zytee" bertha-key = "atest1d9khqw36xvm5zwpjxqm5zwz9x56rydp5gscnwsf4geznjdpegvmrwv29xcmnxvp4x5u5vdjp4wcjdf" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 7162f927723..5b112e6ae5b 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -25,16 +25,10 @@ ferveo-tpke = [ "namada_core/ferveo-tpke", ] -masp-tx-gen = [ - "rand", - "rand_core", -] - multicore = ["masp_proofs/multicore"] namada-sdk = [ "tendermint-rpc", - "masp-tx-gen", "ferveo-tpke", "masp_primitives/transparent-inputs" ] @@ -66,8 +60,6 @@ testing = [ "namada_ethereum_bridge/testing", "namada_proof_of_stake/testing", "async-client", - "rand_core", - "rand", ] [dependencies] @@ -94,8 +86,8 @@ owo-colors = "3.5.0" parse_duration = "2.1.1" paste.workspace = true prost.workspace = true -rand = {optional = true, workspace = true} -rand_core = {optional = true, workspace = true} +rand.workspace = true +rand_core.workspace = true ripemd.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/sdk/src/args.rs b/sdk/src/args.rs index f8454648b50..52c3a8b9172 100644 --- a/sdk/src/args.rs +++ b/sdk/src/args.rs @@ -1528,6 +1528,8 @@ pub struct Tx { pub verification_key: Option, /// Password to decrypt key pub password: Option>, + /// Use device to sign the transaction + pub use_device: bool, } /// Builder functions for Tx @@ -1744,12 +1746,12 @@ pub struct KeyAndAddressGen { /// Don't encrypt the keypair pub unsafe_dont_encrypt: bool, /// BIP44 derivation path - pub derivation_path: Option, + pub derivation_path: String, } /// Wallet restore key and implicit address arguments #[derive(Clone, Debug)] -pub struct KeyAndAddressRestore { +pub struct KeyAndAddressDerive { /// Scheme type pub scheme: SchemeType, /// Key alias @@ -1759,7 +1761,9 @@ pub struct KeyAndAddressRestore { /// Don't encrypt the keypair pub unsafe_dont_encrypt: bool, /// BIP44 derivation path - pub derivation_path: Option, + pub derivation_path: String, + /// Use device to generate key and address + pub use_device: bool, } /// Wallet key lookup arguments diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index e53b7cfb696..ea6f7b20775 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -40,6 +40,7 @@ pub mod io; pub mod queries; pub mod wallet; +use std::collections::HashSet; use std::path::PathBuf; use std::str::FromStr; @@ -140,6 +141,7 @@ pub trait Namada<'a>: Sized { tx_reveal_code_path: PathBuf::from(TX_REVEAL_PK), verification_key: None, password: None, + use_device: false, } } @@ -403,13 +405,14 @@ pub trait Namada<'a>: Sized { } /// Sign the given transaction using the given signing data - async fn sign( + async fn sign>>( &self, tx: &mut Tx, args: &args::Tx, signing_data: SigningTxData, + with: impl Fn(Tx, common::PublicKey, HashSet) -> F, ) -> crate::error::Result<()> { - signing::sign_tx(*self.wallet_mut().await, args, tx, signing_data) + signing::sign_tx(self, args, tx, signing_data, with).await } /// Process the given transaction using the given flags @@ -508,6 +511,7 @@ where tx_reveal_code_path: PathBuf::from(TX_REVEAL_PK), verification_key: None, password: None, + use_device: false, }, } } diff --git a/sdk/src/masp.rs b/sdk/src/masp.rs index 5e22dba3cc7..b794fe86f41 100644 --- a/sdk/src/masp.rs +++ b/sdk/src/masp.rs @@ -3,7 +3,6 @@ use std::collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet}; use std::env; use std::fmt::Debug; -#[cfg(feature = "masp-tx-gen")] use std::ops::Deref; use std::path::PathBuf; @@ -30,7 +29,6 @@ use masp_primitives::sapling::redjubjub::PublicKey; use masp_primitives::sapling::{ Diversifier, Node, Note, Nullifier, ViewingKey, }; -#[cfg(feature = "masp-tx-gen")] use masp_primitives::transaction::builder::{self, *}; use masp_primitives::transaction::components::sapling::builder::SaplingMetadata; use masp_primitives::transaction::components::transparent::builder::TransparentBuilder; @@ -63,10 +61,8 @@ use namada_core::types::token::{ use namada_core::types::transaction::{ AffineCurve, EllipticCurve, PairingEngine, WrapperTx, }; -#[cfg(feature = "masp-tx-gen")] use rand_core::{CryptoRng, OsRng, RngCore}; use ripemd::Digest as RipemdDigest; -#[cfg(feature = "masp-tx-gen")] use sha2::Digest; use thiserror::Error; @@ -419,7 +415,6 @@ pub fn to_viewing_key(esk: &ExtendedSpendingKey) -> FullViewingKey { /// Generate a valid diversifier, i.e. one that has a diversified base. Return /// also this diversified base. -#[cfg(feature = "masp-tx-gen")] pub fn find_valid_diversifier( rng: &mut R, ) -> (Diversifier, masp_primitives::jubjub::SubgroupPoint) { @@ -1504,7 +1499,6 @@ impl ShieldedContext { /// UTXOs are sometimes used to make transactions balanced, but it is /// understood that transparent account changes are effected only by the /// amounts and signatures specified by the containing Transfer object. - #[cfg(feature = "masp-tx-gen")] pub async fn gen_shielded_transfer<'a>( context: &impl Namada<'a>, source: &TransferSource, diff --git a/sdk/src/signing.rs b/sdk/src/signing.rs index 381df346347..78b5dc8c348 100644 --- a/sdk/src/signing.rs +++ b/sdk/src/signing.rs @@ -1,5 +1,5 @@ //! Functions to sign transactions -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::Display; use borsh::BorshDeserialize; @@ -30,6 +30,7 @@ use namada_core::types::transaction::governance::{ use namada_core::types::transaction::pos::InitValidator; use namada_core::types::transaction::{pos, Fee}; use prost::Message; +use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; use sha2::Digest; use zeroize::Zeroizing; @@ -197,6 +198,25 @@ pub async fn tx_signers<'a>( } } +/// The different parts of a transaction that can be signed +#[derive(Eq, Hash, PartialEq)] +pub enum Signable { + FeeHeader, + RawHeader, +} + +/// Causes sign_tx to attempt signing using only the software wallet +pub async fn default_sign( + _tx: Tx, + pubkey: common::PublicKey, + _parts: HashSet, +) -> Result { + Err(Error::Other(format!( + "unable to sign transaction with {}", + pubkey + ))) +} + /// Sign a transaction with a given signing key or public key of a given signer. /// If no explicit signer given, use the `default`. If no `default` is given, /// Error. @@ -208,42 +228,96 @@ pub async fn tx_signers<'a>( /// hashes needed for monitoring the tx on chain. /// /// If it is a dry run, it is not put in a wrapper, but returned as is. -pub fn sign_tx( - wallet: &mut Wallet, +pub async fn sign_tx<'a, F: std::future::Future>>( + context: &impl Namada<'a>, args: &args::Tx, tx: &mut Tx, signing_data: SigningTxData, + sign: impl Fn(Tx, common::PublicKey, HashSet) -> F, ) -> Result<(), Error> { + let mut used_pubkeys = HashSet::new(); + + // First try to sign the raw header with the supplied signatures if !args.signatures.is_empty() { let signatures = args .signatures .iter() - .map(|bytes| SignatureIndex::deserialize(bytes).unwrap()) + .map(|bytes| { + let sigidx = SignatureIndex::deserialize(bytes).unwrap(); + used_pubkeys.insert(sigidx.pubkey.clone()); + sigidx + }) .collect(); tx.add_signatures(signatures); - } else if let Some(account_public_keys_map) = - signing_data.account_public_keys_map + } + + // Then try to sign the raw header with private keys in the software wallet + if let Some(account_public_keys_map) = signing_data.account_public_keys_map { + let mut wallet = context.wallet_mut().await; let signing_tx_keypairs = signing_data .public_keys .iter() .filter_map(|public_key| { - match find_key_by_pk(wallet, args, public_key) { - Ok(secret_key) => Some(secret_key), - Err(_) => None, + if used_pubkeys.contains(public_key) { + None + } else { + match find_key_by_pk(*wallet, args, public_key) { + Ok(secret_key) => { + used_pubkeys.insert(public_key.clone()); + Some(secret_key) + } + Err(_) => None, + } } }) .collect::>(); - tx.sign_raw( - signing_tx_keypairs, - account_public_keys_map, - signing_data.owner, - ); + if !signing_tx_keypairs.is_empty() { + tx.sign_raw( + signing_tx_keypairs, + account_public_keys_map, + signing_data.owner, + ); + } + } + + // Then try to sign the raw header using the hardware wallet + for pubkey in signing_data.public_keys { + if !used_pubkeys.contains(&pubkey) && pubkey != signing_data.fee_payer { + if let Ok(ntx) = sign( + tx.clone(), + pubkey.clone(), + HashSet::from([Signable::RawHeader]), + ) + .await + { + *tx = ntx; + used_pubkeys.insert(pubkey.clone()); + } + } } - let fee_payer_keypair = - find_key_by_pk(wallet, args, &signing_data.fee_payer)?; - tx.sign_wrapper(fee_payer_keypair); + // Then try signing the fee header with the software wallet otherwise use + // the fallback + let key = { + // Lock the wallet just long enough to extract a key from it without + // interfering with the sign closure call + let mut wallet = context.wallet_mut().await; + find_key_by_pk(*wallet, args, &signing_data.fee_payer) + }; + match key { + Ok(fee_payer_keypair) => { + tx.sign_wrapper(fee_payer_keypair); + } + Err(_) => { + *tx = sign( + tx.clone(), + signing_data.fee_payer.clone(), + HashSet::from([Signable::FeeHeader, Signable::RawHeader]), + ) + .await?; + } + } Ok(()) } @@ -287,7 +361,7 @@ pub async fn aux_signing_data<'a>( context .wallet_mut() .await - .generate_disposable_signing_key() + .gen_disposable_signing_key(&mut OsRng) .to_public() } else { match &args.wrapper_fee_payer { diff --git a/sdk/src/tx.rs b/sdk/src/tx.rs index 2a7919895ba..abfa0f0e18f 100644 --- a/sdk/src/tx.rs +++ b/sdk/src/tx.rs @@ -469,7 +469,7 @@ pub async fn save_initialized_accounts<'a, N: Namada<'a>>( None => N::WalletUtils::read_alias(&encoded).into(), }; let alias = alias.into_owned(); - let added = context.wallet_mut().await.add_address( + let added = context.wallet_mut().await.insert_address( alias.clone(), address.clone(), args.wallet_alias_force, diff --git a/sdk/src/wallet/derivation_path.rs b/sdk/src/wallet/derivation_path.rs index 7751e51701f..c1aba983bbf 100644 --- a/sdk/src/wallet/derivation_path.rs +++ b/sdk/src/wallet/derivation_path.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use derivation_path::{ChildIndex, DerivationPath as DerivationPathInner}; use namada_core::types::key::SchemeType; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; use tiny_hderive::bip44::{ DerivationPath as HDeriveDerivationPath, @@ -19,7 +20,7 @@ pub enum DerivationPathError { InvalidDerivationPath(String), } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct DerivationPath(DerivationPathInner); impl DerivationPath { @@ -105,6 +106,29 @@ impl fmt::Display for DerivationPath { } } +impl FromStr for DerivationPath { + type Err = DerivationPathError; + + fn from_str(s: &str) -> Result { + DerivationPathInner::from_str(s).map(Self).map_err(|err| { + DerivationPathError::InvalidDerivationPath(err.to_string()) + }) + } +} + +impl Serialize for DerivationPath { + fn serialize(&self, s: S) -> Result { + s.collect_str(self) + } +} + +impl<'de> Deserialize<'de> for DerivationPath { + fn deserialize>(d: D) -> Result { + let string = String::deserialize(d)?; + string.parse().map_err(serde::de::Error::custom) + } +} + impl IntoHDeriveDerivationPath for DerivationPath { fn into(self) -> Result { HDeriveDerivationPath::from_str(&self.0.to_string()) diff --git a/sdk/src/wallet/mod.rs b/sdk/src/wallet/mod.rs index 4570c8a283a..28b25c10768 100644 --- a/sdk/src/wallet/mod.rs +++ b/sdk/src/wallet/mod.rs @@ -12,21 +12,22 @@ use std::str::FromStr; use alias::Alias; use bip39::{Language, Mnemonic, MnemonicType, Seed}; use borsh::{BorshDeserialize, BorshSerialize}; -use masp_primitives::zip32::ExtendedFullViewingKey; use namada_core::types::address::Address; use namada_core::types::key::*; use namada_core::types::masp::{ ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, }; pub use pre_genesis::gen_key_to_store; +use rand::CryptoRng; use rand_core::RngCore; -pub use store::{gen_sk_rng, AddressVpType, Store}; +pub use store::{AddressVpType, Store}; use thiserror::Error; use zeroize::Zeroizing; -use self::derivation_path::{DerivationPath, DerivationPathError}; +pub use self::derivation_path::{DerivationPath, DerivationPathError}; pub use self::keys::{DecryptionError, StoredKeypair}; pub use self::store::{ConfirmationResponse, ValidatorData, ValidatorKeys}; +use crate::wallet::store::derive_hd_secret_key; /// Errors of key generation / recovery #[derive(Error, Debug)] @@ -225,6 +226,30 @@ pub mod fs { } } +/// Generate a new secret key. +pub fn gen_secret_key( + scheme: SchemeType, + csprng: &mut (impl CryptoRng + RngCore), +) -> common::SecretKey { + match scheme { + SchemeType::Ed25519 => ed25519::SigScheme::generate(csprng).try_to_sk(), + SchemeType::Secp256k1 => { + secp256k1::SigScheme::generate(csprng).try_to_sk() + } + SchemeType::Common => common::SigScheme::generate(csprng).try_to_sk(), + } + .unwrap() +} + +fn gen_spending_key( + csprng: &mut (impl CryptoRng + RngCore), +) -> ExtendedSpendingKey { + let mut spend_key = [0; 32]; + csprng.fill_bytes(&mut spend_key); + masp_primitives::zip32::ExtendedSpendingKey::master(spend_key.as_ref()) + .into() +} + /// The error that is produced when a given key cannot be obtained #[derive(Error, Debug)] pub enum FindKeyError { @@ -393,19 +418,28 @@ impl Wallet { } /// Get all known keys by their alias, paired with PKH, if known. - pub fn get_keys( + pub fn get_secret_keys( &self, ) -> HashMap< String, (&StoredKeypair, Option<&PublicKeyHash>), > { self.store - .get_keys() + .get_secret_keys() .into_iter() .map(|(alias, value)| (alias.into(), value)) .collect() } + /// Get all known public keys by their alias. + pub fn get_public_keys(&self) -> HashMap { + self.store + .get_public_keys() + .iter() + .map(|(alias, value)| (alias.into(), value.clone())) + .collect() + } + /// Get all known addresses by their alias, paired with PKH, if known. pub fn get_addresses(&self) -> HashMap { self.store @@ -458,29 +492,6 @@ impl Wallet { } impl Wallet { - fn gen_and_store_key( - &mut self, - scheme: SchemeType, - alias: Option, - alias_force: bool, - seed_and_derivation_path: Option<(Seed, DerivationPath)>, - password: Option>, - ) -> Option<(String, common::SecretKey)> { - self.store - .gen_key::( - scheme, - alias, - alias_force, - seed_and_derivation_path, - password, - ) - .map(|(alias, key)| { - // Cache the newly added key - self.decrypted_key_cache.insert(alias.clone(), key.clone()); - (alias.into(), key) - }) - } - /// Restore a keypair from the user mnemonic code (read from stdin) using /// a given BIP44 derivation path and derive an implicit address from its /// public part and insert them into the store with the provided alias, @@ -490,34 +501,15 @@ impl Wallet { /// provided, will prompt for password from stdin. /// Stores the key in decrypted key cache and returns the alias of the key /// and a reference-counting pointer to the key. - pub fn derive_key_from_user_mnemonic_code( + pub fn derive_key_from_mnemonic_code( &mut self, scheme: SchemeType, alias: Option, alias_force: bool, - derivation_path: Option, + derivation_path: DerivationPath, mnemonic_passphrase: Option<(Mnemonic, Zeroizing)>, password: Option>, - ) -> Result, GenRestoreKeyError> { - let parsed_derivation_path = derivation_path - .map(|p| { - let is_default = p.eq_ignore_ascii_case("DEFAULT"); - if is_default { - Ok(DerivationPath::default_for_scheme(scheme)) - } else { - DerivationPath::from_path_str(scheme, &p) - .map_err(GenRestoreKeyError::DerivationPathError) - } - }) - .transpose()? - .unwrap_or_else(|| DerivationPath::default_for_scheme(scheme)); - if !parsed_derivation_path.is_compatible(scheme) { - println!( - "WARNING: the specified derivation path may be incompatible \ - with the chosen cryptography scheme." - ) - } - println!("Using HD derivation path {}", parsed_derivation_path); + ) -> Result<(String, common::SecretKey), GenRestoreKeyError> { let (mnemonic, passphrase) = if let Some(mnemonic_passphrase) = mnemonic_passphrase { mnemonic_passphrase @@ -525,17 +517,42 @@ impl Wallet { (U::read_mnemonic_code()?, U::read_mnemonic_passphrase(false)) }; let seed = Seed::new(&mnemonic, &passphrase); - - Ok(self.gen_and_store_key( + let sk = derive_hd_secret_key( scheme, - alias, + seed.as_bytes(), + derivation_path.clone(), + ); + + self.insert_keypair( + alias.unwrap_or_default(), alias_force, - Some((seed, parsed_derivation_path)), + sk.clone(), password, - )) + None, + Some(derivation_path), + ) + .map(|alias| (alias, sk)) } - /// Generate a new keypair and derive an implicit address from its public + /// Generate a spending key similarly to how it's done for keypairs + pub fn gen_store_spending_key( + &mut self, + alias: String, + password: Option>, + force_alias: bool, + csprng: &mut (impl CryptoRng + RngCore), + ) -> (String, ExtendedSpendingKey) { + let spendkey = gen_spending_key(csprng); + if let Some(alias) = + self.insert_spending_key(alias, spendkey, password, force_alias) + { + (alias, spendkey) + } else { + panic!("Action cancelled, no changes persisted."); + } + } + + /// Generate a new keypair, derive an implicit address from its public key /// and insert them into the store with the provided alias, converted to /// lower case. If none provided, the alias will be the public key hash (in /// lowercase too). If the alias already exists, optionally force overwrite @@ -545,78 +562,87 @@ impl Wallet { /// Stores the key in decrypted key cache and /// returns the alias of the key and a reference-counting pointer to the /// key. - /// If a derivation path is specified, derive the key from a generated BIP39 - /// mnemonic code. Use provided rng for mnemonic code generation. - pub fn gen_key( + pub fn gen_store_secret_key( &mut self, scheme: SchemeType, alias: Option, alias_force: bool, - passphrase: Option>, password: Option>, - derivation_path_and_mnemonic_rng: Option<(String, &mut U::Rng)>, - ) -> Result<(String, common::SecretKey, Option), GenRestoreKeyError> - { - let parsed_path_and_rng = derivation_path_and_mnemonic_rng - .map(|(raw_derivation_path, rng)| { - let is_default = - raw_derivation_path.eq_ignore_ascii_case("DEFAULT"); - let parsed_derivation_path = if is_default { - Ok(DerivationPath::default_for_scheme(scheme)) - } else { - DerivationPath::from_path_str(scheme, &raw_derivation_path) - .map_err(GenRestoreKeyError::DerivationPathError) - }; - parsed_derivation_path.map(|p| (p, rng)) - }) - .transpose()?; - - // Check if the path is compatible with the selected scheme - if parsed_path_and_rng.is_some() { - let (parsed_derivation_path, _) = - parsed_path_and_rng.as_ref().unwrap(); - if !parsed_derivation_path.is_compatible(scheme) { - println!( - "WARNING: the specified derivation path may be \ - incompatible with the chosen cryptography scheme." - ) - } - println!("Using HD derivation path {}", parsed_derivation_path); - } + rng: &mut (impl CryptoRng + RngCore), + ) -> Result<(String, common::SecretKey), GenRestoreKeyError> { + let sk = gen_secret_key(scheme, rng); + self.insert_keypair( + alias.unwrap_or_default(), + alias_force, + sk.clone(), + password, + None, + None, + ) + .map(|alias| (alias, sk)) + } - let mut mnemonic_opt = None; - let seed_and_derivation_path //: Option> - = parsed_path_and_rng.map(|(path, rng)| { - const MNEMONIC_TYPE: MnemonicType = MnemonicType::Words24; - let mnemonic = mnemonic_opt - .insert(U::generate_mnemonic_code(MNEMONIC_TYPE, rng)?); - println!( - "Safely store your {} words mnemonic.", - MNEMONIC_TYPE.word_count() - ); - println!("{}", mnemonic.clone().into_phrase()); - - let passphrase = passphrase - .unwrap_or_else(|| U::read_mnemonic_passphrase(true)); - Ok((Seed::new(mnemonic, &passphrase), path)) - }).transpose()?; - - let (alias, key) = self - .gen_and_store_key( - scheme, - alias, - alias_force, - seed_and_derivation_path, - password, - ) - .ok_or(GenRestoreKeyError::KeyStorageError)?; - Ok((alias, key, mnemonic_opt)) + /// Generate a BIP39 mnemonic code, and derive HD wallet seed from it using + /// the given passphrase. + pub fn gen_hd_seed( + passphrase: Option>, + rng: &mut U::Rng, + ) -> Result<(Mnemonic, Seed), GenRestoreKeyError> { + const MNEMONIC_TYPE: MnemonicType = MnemonicType::Words24; + let mnemonic = U::generate_mnemonic_code(MNEMONIC_TYPE, rng)?; + println!( + "Safely store your {} words mnemonic.", + MNEMONIC_TYPE.word_count() + ); + println!("{}", mnemonic.clone().into_phrase()); + + let passphrase = + passphrase.unwrap_or_else(|| U::read_mnemonic_passphrase(true)); + let seed = Seed::new(&mnemonic, &passphrase); + Ok((mnemonic, seed)) + } + + /// Derive a keypair from the given seed and path, derive an implicit + /// address from this keypair, and insert them into the store with the + /// provided alias, converted to lower case. If none provided, the alias + /// will be the public key hash (in lowercase too). If the alias already + /// exists, optionally force overwrite the keypair for the alias. + /// If no encryption password is provided, the keypair will be stored raw + /// without encryption. + /// Stores the key in decrypted key cache and returns the alias of the key + /// and the key itself. + pub fn derive_store_hd_secret_key( + &mut self, + scheme: SchemeType, + alias: Option, + alias_force: bool, + seed: Seed, + derivation_path: DerivationPath, + password: Option>, + ) -> Result<(String, common::SecretKey), GenRestoreKeyError> { + let sk = derive_hd_secret_key( + scheme, + seed.as_bytes(), + derivation_path.clone(), + ); + self.insert_keypair( + alias.unwrap_or_default(), + alias_force, + sk.clone(), + password, + None, + Some(derivation_path), + ) + .map(|alias| (alias, sk)) } /// Generate a disposable signing key for fee payment and store it under the /// precomputed alias in the wallet. This is simply a wrapper around /// `gen_key` to manage the alias - pub fn generate_disposable_signing_key(&mut self) -> common::SecretKey { + pub fn gen_disposable_signing_key( + &mut self, + rng: &mut (impl CryptoRng + RngCore), + ) -> common::SecretKey { // Create the alias let mut ctr = 1; let mut alias = format!("disposable_{ctr}"); @@ -628,34 +654,25 @@ impl Wallet { // Generate a disposable keypair to sign the wrapper if requested // TODO: once the wrapper transaction has been accepted, this key can be // deleted from wallet - let (alias, disposable_keypair, _mnemonic) = self - .gen_key(SchemeType::Ed25519, Some(alias), false, None, None, None) + let (alias, disposable_keypair) = self + .gen_store_secret_key( + SchemeType::Ed25519, + Some(alias), + false, + None, + rng, + ) .expect("Failed to initialize disposable keypair"); println!("Created disposable keypair with alias {alias}"); disposable_keypair } - /// Generate a spending key and store it under the given alias in the wallet - pub fn gen_spending_key( - &mut self, - alias: String, - password: Option>, - force_alias: bool, - ) -> (String, ExtendedSpendingKey) { - let (alias, key) = - self.store - .gen_spending_key::(alias, password, force_alias); - // Cache the newly added key - self.decrypted_spendkey_cache.insert(alias.clone(), key); - (alias.into(), key) - } - /// Find the stored key by an alias, a public key hash or a public key. /// If the key is encrypted and password not supplied, then password will be /// interactively prompted. Any keys that are decrypted are stored in and /// read from a cache to avoid prompting for password multiple times. - pub fn find_key( + pub fn find_secret_key( &mut self, alias_pkh_or_pk: impl AsRef, password: Option>, @@ -670,7 +687,7 @@ impl Wallet { // If not cached, look-up in store let stored_key = self .store - .find_key(alias_pkh_or_pk.as_ref()) + .find_secret_key(alias_pkh_or_pk.as_ref()) .ok_or(FindKeyError::KeyNotFound)?; Self::decrypt_stored_key::<_>( &mut self.decrypted_key_cache, @@ -680,6 +697,17 @@ impl Wallet { ) } + /// Find the public key by an alias or a public key hash. + pub fn find_public_key( + &self, + alias_or_pkh: impl AsRef, + ) -> Result { + self.store + .find_public_key(alias_or_pkh.as_ref()) + .cloned() + .ok_or(FindKeyError::KeyNotFound) + } + /// Find the spending key with the given alias in the wallet and return it. /// If the spending key is encrypted but a password is not supplied, then it /// will be interactively prompted. @@ -718,25 +746,31 @@ impl Wallet { ) -> Result { // Try to look-up alias for the given pk. Otherwise, use the PKH string. let pkh: PublicKeyHash = pk.into(); - let alias = self - .store - .find_alias_by_pkh(&pkh) - .unwrap_or_else(|| pkh.to_string().into()); - // Try read cache - if let Some(cached_key) = self.decrypted_key_cache.get(&alias) { - return Ok(cached_key.clone()); - } - // Look-up from store - let stored_key = self - .store - .find_key_by_pk(pk) - .ok_or(FindKeyError::KeyNotFound)?; - Self::decrypt_stored_key::<_>( - &mut self.decrypted_key_cache, - stored_key, - alias, - password, - ) + self.find_key_by_pkh(&pkh, password) + } + + /// Find a derivation path by public key hash + pub fn find_path_by_pkh( + &self, + pkh: &PublicKeyHash, + ) -> Result { + self.store + .find_path_by_pkh(pkh) + .ok_or(FindKeyError::KeyNotFound) + } + + /// Find the public key by a public key hash. + /// If the key is encrypted and password not supplied, then password will be + /// interactively prompted for. Any keys that are decrypted are stored in + /// and read from a cache to avoid prompting for password multiple times. + pub fn find_public_key_by_pkh( + &self, + pkh: &PublicKeyHash, + ) -> Result { + self.store + .find_public_key_by_pkh(pkh) + .cloned() + .ok_or(FindKeyError::KeyNotFound) } /// Find the stored key by a public key hash. @@ -807,7 +841,7 @@ impl Wallet { /// alias is desired, or the alias creation should be cancelled. Return /// the chosen alias if the address has been added, otherwise return /// nothing. - pub fn add_address( + pub fn insert_address( &mut self, alias: impl AsRef, address: Address, @@ -818,17 +852,50 @@ impl Wallet { .map(Into::into) } - /// Insert a new key with the given alias. If the alias is already used, - /// will prompt for overwrite confirmation. pub fn insert_keypair( &mut self, alias: String, - keypair: StoredKeypair, - pkh: PublicKeyHash, + alias_force: bool, + sk: common::SecretKey, + password: Option>, + address: Option
, + path: Option, + ) -> Result { + self.store + .insert_keypair::( + alias.into(), + sk.clone(), + password, + address, + path, + alias_force, + ) + .map(|alias| { + // Cache the newly added key + self.decrypted_key_cache.insert(alias.clone(), sk); + alias.into() + }) + .ok_or(GenRestoreKeyError::KeyStorageError) + } + + /// Insert a new public key with the given alias. If the alias is already + /// used, then display a prompt for overwrite confirmation. + pub fn insert_public_key( + &mut self, + alias: String, + pubkey: common::PublicKey, + address: Option
, + path: Option, force_alias: bool, ) -> Option { self.store - .insert_keypair::(alias.into(), keypair, pkh, force_alias) + .insert_public_key::( + alias.into(), + pubkey, + address, + path, + force_alias, + ) .map(Into::into) } @@ -846,25 +913,6 @@ impl Wallet { /// Insert a spending key into the wallet under the given alias pub fn insert_spending_key( - &mut self, - alias: String, - spend_key: StoredKeypair, - viewkey: ExtendedViewingKey, - force_alias: bool, - ) -> Option { - self.store - .insert_spending_key::( - alias.into(), - spend_key, - viewkey, - force_alias, - ) - .map(Into::into) - } - - /// Encrypt the given spending key and insert it into the wallet under the - /// given alias - pub fn encrypt_insert_spending_key( &mut self, alias: String, spend_key: ExtendedSpendingKey, @@ -874,10 +922,16 @@ impl Wallet { self.store .insert_spending_key::( alias.into(), - StoredKeypair::new(spend_key, password).0, - ExtendedFullViewingKey::from(&spend_key.into()).into(), + spend_key, + password, force_alias, ) + .map(|alias| { + // Cache the newly added key + self.decrypted_spendkey_cache + .insert(alias.clone(), spend_key); + alias + }) .map(Into::into) } diff --git a/sdk/src/wallet/pre_genesis.rs b/sdk/src/wallet/pre_genesis.rs index 916ec437819..689015c2343 100644 --- a/sdk/src/wallet/pre_genesis.rs +++ b/sdk/src/wallet/pre_genesis.rs @@ -1,11 +1,12 @@ //! Provides functionality for managing validator keys use namada_core::types::key::{common, SchemeType}; +use rand::{CryptoRng, Rng}; use serde::{Deserialize, Serialize}; use thiserror::Error; use zeroize::Zeroizing; use crate::wallet; -use crate::wallet::{store, StoredKeypair}; +use crate::wallet::StoredKeypair; /// Ways in which wallet store operations can fail #[derive(Error, Debug)] @@ -75,8 +76,9 @@ impl ValidatorStore { pub fn gen_key_to_store( scheme: SchemeType, password: Option>, + rng: &mut (impl CryptoRng + Rng), ) -> (StoredKeypair, common::SecretKey) { - let sk = store::gen_sk_rng(scheme); + let sk = wallet::gen_secret_key(scheme, rng); StoredKeypair::new(sk, password) } diff --git a/sdk/src/wallet/store.rs b/sdk/src/wallet/store.rs index 4b13291e035..70fa98060ff 100644 --- a/sdk/src/wallet/store.rs +++ b/sdk/src/wallet/store.rs @@ -5,7 +5,6 @@ use std::fmt::Display; use std::str::FromStr; use bimap::BiBTreeMap; -use bip39::Seed; use itertools::Itertools; use masp_primitives::zip32::ExtendedFullViewingKey; use namada_core::types::address::{Address, ImplicitAddress}; @@ -14,8 +13,6 @@ use namada_core::types::key::*; use namada_core::types::masp::{ ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, }; -#[cfg(feature = "masp-tx-gen")] -use rand_core::RngCore; use serde::{Deserialize, Serialize}; use slip10_ed25519; use zeroize::Zeroizing; @@ -73,7 +70,11 @@ pub struct Store { /// Known payment addresses payment_addrs: BTreeMap, /// Cryptographic keypairs - keys: BTreeMap>, + secret_keys: BTreeMap>, + /// Known public keys + public_keys: BTreeMap, + /// Known derivation paths + derivation_paths: BTreeMap, /// Namada address book addresses: BiBTreeMap, /// Known mappings of public key hashes to their aliases in the `keys` @@ -94,13 +95,13 @@ pub enum AddressVpType { impl Store { /// Find the stored key by an alias, a public key hash or a public key. - pub fn find_key( + pub fn find_secret_key( &self, alias_pkh_or_pk: impl AsRef, ) -> Option<&StoredKeypair> { let alias_pkh_or_pk = alias_pkh_or_pk.as_ref(); // Try to find by alias - self.keys + self.secret_keys .get(&alias_pkh_or_pk.into()) // Try to find by PKH .or_else(|| { @@ -114,6 +115,22 @@ impl Store { }) } + /// Find the stored key by an alias, a public key hash or a public key. + pub fn find_public_key( + &self, + alias_or_pkh: impl AsRef, + ) -> Option<&common::PublicKey> { + let alias_or_pkh = alias_or_pkh.as_ref(); + // Try to find by alias + self.public_keys + .get(&alias_or_pkh.into()) + // Try to find by PKH + .or_else(|| { + let pkh = PublicKeyHash::from_str(alias_or_pkh).ok()?; + self.find_public_key_by_pkh(&pkh) + }) + } + /// Find the spending key with the given alias and return it pub fn find_spending_key( &self, @@ -153,7 +170,7 @@ impl Store { pkh: &PublicKeyHash, ) -> Option<&StoredKeypair> { let alias = self.pkhs.get(pkh)?; - self.keys.get(alias) + self.secret_keys.get(alias) } /// Find the stored alias for a public key hash. @@ -161,6 +178,22 @@ impl Store { self.pkhs.get(pkh).cloned() } + /// Find a derivation path by public key hash + pub fn find_path_by_pkh( + &self, + pkh: &PublicKeyHash, + ) -> Option { + self.derivation_paths.get(self.pkhs.get(pkh)?).cloned() + } + + /// Find the public key by a public key hash. + pub fn find_public_key_by_pkh( + &self, + pkh: &PublicKeyHash, + ) -> Option<&common::PublicKey> { + self.public_keys.get(self.pkhs.get(pkh)?) + } + /// Find the stored address by an alias. pub fn find_address(&self, alias: impl AsRef) -> Option<&Address> { self.addresses.get_by_left(&alias.into()) @@ -172,7 +205,7 @@ impl Store { } /// Get all known keys by their alias, paired with PKH, if known. - pub fn get_keys( + pub fn get_secret_keys( &self, ) -> BTreeMap< Alias, @@ -185,11 +218,11 @@ impl Store { .pkhs .iter() .filter_map(|(pkh, alias)| { - let key = &self.keys.get(alias)?; + let key = &self.secret_keys.get(alias)?; Some((alias.clone(), (*key, Some(pkh)))) }) .collect(); - self.keys.iter().for_each(|(alias, key)| { + self.secret_keys.iter().for_each(|(alias, key)| { if !keys.contains_key(alias) { keys.insert(alias.clone(), (key, None)); } @@ -197,6 +230,11 @@ impl Store { keys } + /// Get all known public keys by their alias. + pub fn get_public_keys(&self) -> &BTreeMap { + &self.public_keys + } + /// Get all known addresses by their alias. pub fn get_addresses(&self) -> &BiBTreeMap { &self.addresses @@ -219,100 +257,6 @@ impl Store { &self.spend_keys } - #[cfg(feature = "masp-tx-gen")] - fn generate_spending_key() -> ExtendedSpendingKey { - use rand::rngs::OsRng; - let mut spend_key = [0; 32]; - OsRng.fill_bytes(&mut spend_key); - masp_primitives::zip32::ExtendedSpendingKey::master(spend_key.as_ref()) - .into() - } - - /// Generate a new keypair and insert it into the store with the provided - /// alias. If none provided, the alias will be the public key hash. - /// If the alias already exists, optionally force overwrite the keypair - /// for the alias. - /// If no encryption password is provided, the keypair will be stored raw - /// without encryption. - /// Optionally, use a given random seed and a BIP44 derivation path. - /// Returns the alias of the key and a reference-counting pointer to the - /// key. - /// Returns None if the alias already exists and the user decides to skip - /// it. No changes in the wallet store are made. - pub fn gen_key( - &mut self, - scheme: SchemeType, - alias: Option, - alias_force: bool, - seed_and_derivation_path: Option<(Seed, DerivationPath)>, - password: Option>, - ) -> Option<(Alias, common::SecretKey)> { - // We cannot generate keys for reserved aliases - if alias.as_ref().and_then(Alias::is_reserved).is_some() { - return None; - } - let sk = if let Some((seed, derivation_path)) = seed_and_derivation_path - { - gen_sk_from_seed_and_derivation_path( - scheme, - seed.as_bytes(), - derivation_path, - ) - } else { - gen_sk_rng(scheme) - }; - let pkh: PublicKeyHash = PublicKeyHash::from(&sk.ref_to()); - let (keypair_to_store, raw_keypair) = StoredKeypair::new(sk, password); - let address = Address::Implicit(ImplicitAddress(pkh.clone())); - let alias: Alias = alias.unwrap_or_else(|| pkh.clone().into()).into(); - let alias = self.insert_keypair::( - alias, - keypair_to_store, - pkh, - alias_force, - )?; - if self - .insert_address::(alias.clone(), address, alias_force) - .is_none() - { - panic!("Action cancelled, no changes persisted."); - } - Some((alias, raw_keypair)) - } - - /// Generate a spending key similarly to how it's done for keypairs - pub fn gen_spending_key( - &mut self, - alias: String, - password: Option>, - force_alias: bool, - ) -> (Alias, ExtendedSpendingKey) { - if Alias::is_reserved(&alias).is_some() { - panic!( - "Tried to generated spending key with reserved alias: {}. \ - Action cancelled, no changes persisted.", - alias - ); - } - let spendkey = Self::generate_spending_key(); - let viewkey = ExtendedFullViewingKey::from(&spendkey.into()).into(); - let (spendkey_to_store, _raw_spendkey) = - StoredKeypair::new(spendkey, password); - let alias = Alias::from(alias); - if self - .insert_spending_key::( - alias.clone(), - spendkey_to_store, - viewkey, - force_alias, - ) - .is_none() - { - panic!("Action cancelled, no changes persisted."); - } - (alias, spendkey) - } - /// Add validator data to the store pub fn add_validator_data( &mut self, @@ -342,21 +286,36 @@ impl Store { self.validator_data } - /// Insert a new key with the given alias. If the alias is already used, - /// will prompt for overwrite/reselection confirmation. If declined, then - /// keypair is not inserted and nothing is returned, otherwise selected + /// Insert a new secret key with the given alias. If the alias is already + /// used, will prompt for overwrite/reselection confirmation. If declined, + /// then keypair is not inserted and nothing is returned, otherwise selected /// alias is returned. pub fn insert_keypair( &mut self, - alias: Alias, - keypair: StoredKeypair, - pkh: PublicKeyHash, + mut alias: Alias, + keypair: common::SecretKey, + password: Option>, + address: Option
, + path: Option, force: bool, ) -> Option { // abort if the key already exists - if self.pkhs.contains_key(&pkh) { - println!("The key already exists."); - return None; + let pubkey = keypair.ref_to(); + let pkh = PublicKeyHash::from(&pubkey); + let address = address + .unwrap_or_else(|| Address::Implicit(ImplicitAddress(pkh.clone()))); + if !force { + if self.pkhs.contains_key(&pkh) { + println!("The key already exists."); + return None; + } else if let Some(alias) = self.addresses.get_by_right(&address) { + println!( + "Address {} already exists in the wallet with alias {}", + address.encode(), + alias, + ); + return None; + } } // abort if the alias is reserved @@ -366,41 +325,34 @@ impl Store { } if alias.is_empty() { - println!( - "Empty alias given, defaulting to {}.", - Into::::into(pkh.to_string()) - ); + alias = pkh.to_string().into(); + println!("Empty alias given, defaulting to {}.", alias); } - // Addresses and keypairs can share aliases, so first remove any - // addresses sharing the same namesake before checking if alias has been - // used. - let counterpart_address = self.addresses.remove_by_left(&alias); if self.contains_alias(&alias) && !force { match U::show_overwrite_confirmation(&alias, "a key") { ConfirmationResponse::Replace => {} ConfirmationResponse::Reselect(new_alias) => { - // Restore the removed address in case the recursive prompt - // terminates with a cancellation - counterpart_address - .map(|x| self.addresses.insert(alias.clone(), x.1)); - return self - .insert_keypair::(new_alias, keypair, pkh, false); + return self.insert_keypair::( + new_alias, + keypair, + password, + Some(address), + path, + false, + ); } ConfirmationResponse::Skip => { - // Restore the removed address since this insertion action - // has now been cancelled - counterpart_address - .map(|x| self.addresses.insert(alias.clone(), x.1)); return None; } } } self.remove_alias(&alias); - self.keys.insert(alias.clone(), keypair); + self.secret_keys + .insert(alias.clone(), StoredKeypair::new(keypair, password).0); + self.public_keys.insert(alias.clone(), pubkey); self.pkhs.insert(pkh, alias.clone()); - // Since it is intended for the inserted keypair to share its namesake - // with the pre-existing address - counterpart_address.map(|x| self.addresses.insert(alias.clone(), x.1)); + self.addresses.insert(alias.clone(), address); + path.map(|x| self.derivation_paths.insert(alias.clone(), x)); Some(alias) } @@ -408,8 +360,8 @@ impl Store { pub fn insert_spending_key( &mut self, alias: Alias, - spendkey: StoredKeypair, - viewkey: ExtendedViewingKey, + spendkey: ExtendedSpendingKey, + password: Option>, force: bool, ) -> Option { // abort if the alias is reserved @@ -427,15 +379,18 @@ impl Store { ConfirmationResponse::Replace => {} ConfirmationResponse::Reselect(new_alias) => { return self.insert_spending_key::( - new_alias, spendkey, viewkey, false, + new_alias, spendkey, password, false, ); } ConfirmationResponse::Skip => return None, } } self.remove_alias(&alias); - self.spend_keys.insert(alias.clone(), spendkey); + let (spendkey_to_store, _raw_spendkey) = + StoredKeypair::new(spendkey, password); + self.spend_keys.insert(alias.clone(), spendkey_to_store); // Simultaneously add the derived viewing key to ease balance viewing + let viewkey = ExtendedFullViewingKey::from(&spendkey.into()).into(); self.view_keys.insert(alias.clone(), viewkey); Some(alias) } @@ -472,23 +427,56 @@ impl Store { Some(alias) } - /// Check if any map of the wallet contains the given alias - pub fn contains_alias(&self, alias: &Alias) -> bool { - self.payment_addrs.contains_key(alias) - || self.view_keys.contains_key(alias) - || self.spend_keys.contains_key(alias) - || self.keys.contains_key(alias) - || self.addresses.contains_left(alias) - } - - /// Completely remove the given alias from all maps in the wallet - fn remove_alias(&mut self, alias: &Alias) { - self.payment_addrs.remove(alias); - self.view_keys.remove(alias); - self.spend_keys.remove(alias); - self.keys.remove(alias); - self.addresses.remove_by_left(alias); - self.pkhs.retain(|_key, val| val != alias); + /// Insert public keys + pub fn insert_public_key( + &mut self, + mut alias: Alias, + pubkey: common::PublicKey, + address: Option
, + path: Option, + force: bool, + ) -> Option { + let pkh = PublicKeyHash::from(&pubkey); + let address = address + .unwrap_or_else(|| Address::Implicit(ImplicitAddress(pkh.clone()))); + if !force { + if self.pkhs.contains_key(&pkh) { + println!("The key already exists."); + return None; + } else if let Some(alias) = self.addresses.get_by_right(&address) { + println!( + "Address {} already exists in the wallet with alias {}", + address.encode(), + alias, + ); + return None; + } + } + if alias.is_empty() { + alias = pkh.to_string().into(); + println!("Empty alias given, defaulting to {}.", alias); + } + if self.contains_alias(&alias) && !force { + match U::show_overwrite_confirmation(&alias, "a public key") { + ConfirmationResponse::Replace => {} + ConfirmationResponse::Reselect(new_alias) => { + return self.insert_public_key::( + new_alias, + pubkey, + Some(address), + path, + false, + ); + } + ConfirmationResponse::Skip => return None, + } + } + self.remove_alias(&alias); + self.public_keys.insert(alias.clone(), pubkey); + path.map(|x| self.derivation_paths.insert(alias.clone(), x)); + self.pkhs.insert(pkh, alias.clone()); + self.addresses.insert(alias.clone(), address); + Some(alias) } /// Insert payment addresses similarly to how it's done for keypairs @@ -526,30 +514,13 @@ impl Store { Some(alias) } - /// Helper function to restore keypair given alias-keypair mapping and the - /// pkhs-alias mapping. - fn restore_keypair( - &mut self, - alias: Alias, - key: Option>, - pkh: Option, - ) { - // abort if the alias is reserved - if Alias::is_reserved(&alias).is_some() { - println!("The alias {} is reserved", alias); - return; - } - key.map(|x| self.keys.insert(alias.clone(), x)); - pkh.map(|x| self.pkhs.insert(x, alias.clone())); - } - /// Insert a new address with the given alias. If the alias is already used, /// will prompt for overwrite/reselection confirmation, which when declined, /// the address won't be added. Return the selected alias if the address has /// been added. pub fn insert_address( &mut self, - alias: Alias, + mut alias: Alias, address: Address, force: bool, ) -> Option { @@ -569,53 +540,49 @@ impl Store { } if alias.is_empty() { - println!("Empty alias given, defaulting to {}.", address.encode()); + alias = address.encode().into(); + println!("Empty alias given, defaulting to {}.", alias); } - // Addresses and keypairs can share aliases, so first remove any keys - // sharing the same namesake before checking if alias has been used. - let counterpart_key = self.keys.remove(&alias); - let mut counterpart_pkh = None; - self.pkhs.retain(|k, v| { - if v == &alias { - counterpart_pkh = Some(k.clone()); - false - } else { - true - } - }); - if self.addresses.contains_left(&alias) && !force { + if self.contains_alias(&alias) && !force { match U::show_overwrite_confirmation(&alias, "an address") { ConfirmationResponse::Replace => {} ConfirmationResponse::Reselect(new_alias) => { - // Restore the removed keypair in case the recursive prompt - // terminates with a cancellation - self.restore_keypair( - alias, - counterpart_key, - counterpart_pkh, - ); return self.insert_address::(new_alias, address, false); } ConfirmationResponse::Skip => { - // Restore the removed keypair since this insertion action - // has now been cancelled - self.restore_keypair( - alias, - counterpart_key, - counterpart_pkh, - ); return None; } } } self.remove_alias(&alias); self.addresses.insert(alias.clone(), address); - // Since it is intended for the inserted address to share its namesake - // with the pre-existing keypair - self.restore_keypair(alias.clone(), counterpart_key, counterpart_pkh); Some(alias) } + /// Check if any map of the wallet contains the given alias + pub fn contains_alias(&self, alias: &Alias) -> bool { + self.payment_addrs.contains_key(alias) + || self.view_keys.contains_key(alias) + || self.spend_keys.contains_key(alias) + || self.secret_keys.contains_key(alias) + || self.addresses.contains_left(alias) + || self.pkhs.values().contains(alias) + || self.public_keys.contains_key(alias) + || self.derivation_paths.contains_key(alias) + } + + /// Completely remove the given alias from all maps in the wallet + fn remove_alias(&mut self, alias: &Alias) { + self.payment_addrs.remove(alias); + self.view_keys.remove(alias); + self.spend_keys.remove(alias); + self.secret_keys.remove(alias); + self.addresses.remove_by_left(alias); + self.pkhs.retain(|_key, val| val != alias); + self.public_keys.remove(alias); + self.derivation_paths.remove(alias); + } + /// Extend this store from another store (typically pre-genesis). /// Note that this method ignores `validator_data` if any. pub fn extend(&mut self, store: Store) { @@ -623,7 +590,9 @@ impl Store { view_keys, spend_keys, payment_addrs, - keys, + secret_keys, + public_keys, + derivation_paths, addresses, pkhs, validator_data: _, @@ -632,7 +601,9 @@ impl Store { view_keys.extend(store.view_keys); spend_keys.extend(store.spend_keys); payment_addrs.extend(store.payment_addrs); - keys.extend(store.keys); + secret_keys.extend(store.secret_keys); + public_keys.extend(store.public_keys); + derivation_paths.extend(store.derivation_paths); addresses.extend(store.addresses); pkhs.extend(store.pkhs); address_vp_types.extend(store.address_vp_types); @@ -659,7 +630,7 @@ impl Store { other.store.tendermint_node_key, ), ]; - self.keys.extend(keys.into_iter()); + self.secret_keys.extend(keys.into_iter()); let account_pk = other.account_key.ref_to(); let consensus_pk = other.consensus_key.ref_to(); @@ -723,25 +694,8 @@ impl Store { } } -/// Generate a new secret key. -pub fn gen_sk_rng(scheme: SchemeType) -> common::SecretKey { - use rand::rngs::OsRng; - let mut csprng = OsRng {}; - match scheme { - SchemeType::Ed25519 => ed25519::SigScheme::generate(&mut csprng) - .try_to_sk() - .unwrap(), - SchemeType::Secp256k1 => secp256k1::SigScheme::generate(&mut csprng) - .try_to_sk() - .unwrap(), - SchemeType::Common => common::SigScheme::generate(&mut csprng) - .try_to_sk() - .unwrap(), - } -} - /// Generate a new secret key from the seed. -pub fn gen_sk_from_seed_and_derivation_path( +pub fn derive_hd_secret_key( scheme: SchemeType, seed: &[u8], derivation_path: DerivationPath, @@ -820,7 +774,7 @@ impl<'de> Deserialize<'de> for AddressVpType { #[cfg(test)] mod test_wallet { use base58::{self, FromBase58}; - use bip39::{Language, Mnemonic}; + use bip39::{Language, Mnemonic, Seed}; use data_encoding::HEXLOWER; use super::super::derivation_path::DerivationPath; @@ -848,11 +802,7 @@ mod test_wallet { DerivationPath::from_path_str(SCHEME, DERIVATION_PATH) .expect("Derivation path construction cannot fail"); - let sk = gen_sk_from_seed_and_derivation_path( - SCHEME, - seed.as_bytes(), - derivation_path, - ); + let sk = derive_hd_secret_key(SCHEME, seed.as_bytes(), derivation_path); assert_eq!(&sk.to_string()[2..], SK_EXPECTED); } @@ -882,13 +832,9 @@ mod test_wallet { DerivationPath::from_path_str(SCHEME, DERIVATION_PATH_HARDENED) .expect("Derivation path construction cannot fail"); - let sk = gen_sk_from_seed_and_derivation_path( - SCHEME, - seed.as_bytes(), - derivation_path, - ); + let sk = derive_hd_secret_key(SCHEME, seed.as_bytes(), derivation_path); - let sk_hard = gen_sk_from_seed_and_derivation_path( + let sk_hard = derive_hd_secret_key( SCHEME, seed.as_bytes(), derivation_path_hardened, @@ -904,7 +850,7 @@ mod test_wallet { derivation_path: &str, priv_key: &str, ) { - let sk = gen_sk_from_seed_and_derivation_path( + let sk = derive_hd_secret_key( scheme, HEXLOWER .decode(seed.as_bytes()) diff --git a/shared/Cargo.toml b/shared/Cargo.toml index ce39a22ef87..6ee2da3cfe9 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -69,12 +69,6 @@ ibc-mocks = [ "namada_sdk/ibc-mocks", ] -masp-tx-gen = [ - "rand", - "rand_core", - "namada_sdk/masp-tx-gen", -] - # for integration tests and test utilies testing = [ "namada_core/testing", @@ -83,14 +77,11 @@ testing = [ "namada_sdk/testing", "async-client", "proptest", - "rand_core", - "rand", "tempfile", ] namada-sdk = [ "tendermint-rpc", - "masp-tx-gen", "ferveo-tpke", "masp_primitives/transparent-inputs", "namada_sdk/namada-sdk", @@ -132,8 +123,8 @@ parse_duration = "2.1.1" paste.workspace = true proptest = {version = "1.2.0", optional = true} prost.workspace = true -rand = {optional = true, workspace = true} -rand_core = {optional = true, workspace = true} +rand.workspace = true +rand_core.workspace = true rayon = {version = "=1.5.3", optional = true} ripemd.workspace = true serde.workspace = true diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 809feb334d4..3e8466e255d 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -3081,6 +3081,8 @@ fn implicit_account_reveal_pk() -> Result<()> { &["key", "gen", "--alias", &key_alias, "--unsafe-dont-encrypt"], Some(20), )?; + cmd.exp_string("Enter BIP39 passphrase (empty for none): ")?; + cmd.send_line("")?; cmd.assert_success(); // Apply the key_alias once the key is generated to obtain tx args diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index 5e6fb9787f2..27ae0144a98 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -33,6 +33,7 @@ use namada_sdk::wallet::alias::Alias; use namada_tx_prelude::token; use namada_vp_prelude::HashSet; use once_cell::sync::Lazy; +use rand::rngs::OsRng; use rand::Rng; use serde_json; use tempfile::{tempdir, tempdir_in, TempDir}; @@ -148,8 +149,14 @@ where let mut wallet = wallet::load(&wallet_path) .expect("Could not locate pre-genesis wallet used for e2e tests."); let alias = format!("validator-{}-balance-key", val); - let (alias, sk, _mnemonic) = wallet - .gen_key(SchemeType::Ed25519, Some(alias), true, None, None, None) + let (alias, sk) = wallet + .gen_store_secret_key( + SchemeType::Ed25519, + Some(alias), + true, + None, + &mut OsRng, + ) .unwrap_or_else(|_| { panic!("Could not generate new key for validator-{}", val) }); diff --git a/tests/src/e2e/wallet_tests.rs b/tests/src/e2e/wallet_tests.rs index ca9d290f553..5140f13fe49 100644 --- a/tests/src/e2e/wallet_tests.rs +++ b/tests/src/e2e/wallet_tests.rs @@ -39,6 +39,8 @@ fn wallet_encrypted_key_cmds() -> Result<()> { cmd.send_line(password)?; cmd.exp_string("Enter same passphrase again: ")?; cmd.send_line(password)?; + cmd.exp_string("Enter BIP39 passphrase (empty for none): ")?; + cmd.send_line("")?; cmd.exp_string(&format!( "Successfully added a key and an address with alias: \"{}\"", key_alias.to_lowercase() @@ -87,6 +89,9 @@ fn wallet_encrypted_key_cmds_env_var() -> Result<()> { Some(20), )?; + cmd.exp_string("Enter BIP39 passphrase (empty for none): ")?; + cmd.send_line("")?; + cmd.exp_string(&format!( "Successfully added a key and an address with alias: \"{}\"", key_alias @@ -126,6 +131,8 @@ fn wallet_unencrypted_key_cmds() -> Result<()> { &["key", "gen", "--alias", key_alias, "--unsafe-dont-encrypt"], Some(20), )?; + cmd.exp_string("Enter BIP39 passphrase (empty for none): ")?; + cmd.send_line("")?; cmd.exp_string(&format!( "Successfully added a key and an address with alias: \"{}\"", key_alias @@ -174,6 +181,8 @@ fn wallet_address_cmds() -> Result<()> { ], Some(20), )?; + cmd.exp_string("Enter BIP39 passphrase (empty for none): ")?; + cmd.send_line("")?; cmd.exp_string(&format!( "Successfully added a key and an address with alias: \"{}\"", gen_address_alias diff --git a/wasm/checksums.json b/wasm/checksums.json index 38a44429e53..9ca79c701b6 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,23 +1,23 @@ { - "tx_bond.wasm": "tx_bond.f371b0615f8931ef71e12ecb11ff361d1b52904d2197b1bcd0b245c4b6dc6b85.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.fd90aa41331ba118a8ad4c8a6eaff633620f9c13f7f0689d5193650ed0fe5441.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.f6905933556182a5a8e63a26687c5b7dc08a597caa4d95243362dc9bc9872200.wasm", - "tx_ibc.wasm": "tx_ibc.71fc5960466ebfb2e2304fd2da36206f37ffee463309e80490de0efd05a9774e.wasm", - "tx_init_account.wasm": "tx_init_account.ddaf039045f7a438781afa6359a984d967c31919d0ed624026b7c807359f4ed1.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.ae97d0ce0a99d41e977d0df5e8f1b7840d1b55c56925a33c340e809b332008f7.wasm", - "tx_init_validator.wasm": "tx_init_validator.0d80054aec7009c96b86e2760c17f96acbec2916d4625826b968875fc9080992.wasm", - "tx_redelegate.wasm": "tx_redelegate.201a6d7fe5ba0c8a549398979d96547bb4e2ab2c590346814512e6c6dfbd4b33.wasm", - "tx_resign_steward.wasm": "tx_resign_steward.de1e20e7c59bf3ca16a6eeaa38d3e9d84d0b767ad67693a968982ce5bf29c580.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.f54ef16721912c58c9b82729c384857d5e9a755679e54d36d051fed9516e3986.wasm", - "tx_transfer.wasm": "tx_transfer.dbb8e3391339072c6947036b54ddbda386405ed17fdd550ce0e223479d8dfe33.wasm", - "tx_unbond.wasm": "tx_unbond.5fbc7162efa1361257bdb56d7df327013cb918ab8a0ebd49cdebe948a9bb8a0f.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.948a6c50d04ad9a0da1a4936d49380f0ba457225110788abc9443761a4fb8ec0.wasm", - "tx_update_account.wasm": "tx_update_account.1bc336c242913481719c6ba756e5403f9f569b97d05cf3a6b117573dc73f4e45.wasm", - "tx_update_steward_commission.wasm": "tx_update_steward_commission.9cadf5b48ad25f6371bbc0ac3e3e010664ce4b9b9104f916f4b87ad2e81eff29.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.a8ae56dc352635a01d7e6e953c188c03309810d2e84983516a30832f34a12533.wasm", - "tx_withdraw.wasm": "tx_withdraw.690ad6b17768e24f8523dd5e09ed458fce4b2a3b85710f81731306fac1f28ac7.wasm", - "vp_implicit.wasm": "vp_implicit.05fa0994305859d2e76950744982bb245be36a58d6e6413e6f7eb4fb1d59bce3.wasm", - "vp_masp.wasm": "vp_masp.adbb83cced017ca4f06a5eec4764f81ab30b56d73a179303aa12aa2c03b26964.wasm", - "vp_user.wasm": "vp_user.f50b392a7bc587a126f1377d287ec159cf2859ad2f737c189a97964b3fab7bc9.wasm", - "vp_validator.wasm": "vp_validator.d1fd61cca046ce85e1fd63cc85245e19c7b06c23f5705eb4949f855a8b3ed1a4.wasm" + "tx_bond.wasm": "tx_bond.fbe498589d64ed3acecf8d8eddb6126f264e53d6afbf40ae1586296cec95fa60.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.07fcfa9627d6a2d160234b70f14b7162507016c158fd072d81e34b91fbaa4178.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.9c04a672607733dded08535880ca77f08b81331ab4ab20890209f052748dbba2.wasm", + "tx_ibc.wasm": "tx_ibc.0668e4bfd9f920313903622d7a640e57e4bba95fb345a788eb53fdb4d82f369f.wasm", + "tx_init_account.wasm": "tx_init_account.ef6852b8b4bbea38b2de7c8d28e61dc72f7e8a42dd8ae6ebc130cbee1874948c.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.b8387ef40c06514161dbc84c2b9cd9a0e63f6d65753b7fd6c3d4de91f815134f.wasm", + "tx_init_validator.wasm": "tx_init_validator.42aaf2081e5f480f82770a1c3f54a3467334f9c2e0459d829e47e5a2ab8f96cc.wasm", + "tx_redelegate.wasm": "tx_redelegate.5d53095cd3861c6cac818f683492e455cd8cd7c11fa5eaffda7728abc0b8fc4c.wasm", + "tx_resign_steward.wasm": "tx_resign_steward.f11372d848753feb60f3681a641282f3e7d76d486b083efbac6e4c58e2d39e29.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.63a3be41bf03364bc77c4e9792cf795f33105b9d803ad4d040cf452ebca5e90c.wasm", + "tx_transfer.wasm": "tx_transfer.fc84bb83ad62d7aab618e165c99c8603515213b531b6986b7166f198dfe35eff.wasm", + "tx_unbond.wasm": "tx_unbond.9d89ca2f854e5be3863dfacd8c5ea49e872b92913dc1ac23150dddefb13179d4.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.321aa22df71c9f5209eceaae8f6036abb320fc209d5c258c608074809e5686c1.wasm", + "tx_update_account.wasm": "tx_update_account.faf576ce31c485eeb4e2a14b7a857a00d2bed1370a014e025b3e876f477664e5.wasm", + "tx_update_steward_commission.wasm": "tx_update_steward_commission.19793a7b32d0873902d60e32c7540a4c35fdcc0bbf657aa36bce5b32e366bf54.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.30149b3a3399cb3b33190a4ad0998d00ddbb51eb2c159b2b4e44a54eae429f74.wasm", + "tx_withdraw.wasm": "tx_withdraw.5a4f1e624ce7fa9498c77221bb3d8df5a7f1d469fb07e44b6fe6ec7ee1850a0d.wasm", + "vp_implicit.wasm": "vp_implicit.425c90b1b60d355334c650270415a88477dc90f5122311db4341003d6b09c3b8.wasm", + "vp_masp.wasm": "vp_masp.7e927e27f4338b55ea065e876a3c36d6398cd03fbbe4d7574f7f4df4a53aee4e.wasm", + "vp_user.wasm": "vp_user.b13865d5281e1157e51e02338983d7077b53cd16d1527e48e30b8ec4326a83c4.wasm", + "vp_validator.wasm": "vp_validator.2c6a51e0f9e9f3d72b808908124a48e9f46e9da4da40495ec7a985b898296178.wasm" } \ No newline at end of file