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 0000000000..c6414655bc --- /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/.github/workflows/automation.yml b/.github/workflows/automation.yml index f43f22f065..29a5829e6e 100644 --- a/.github/workflows/automation.yml +++ b/.github/workflows/automation.yml @@ -11,7 +11,6 @@ permissions: env: GIT_LFS_SKIP_SMUDGE: 1 - CHAIN_BUCKET: anoma-iac-files-master jobs: with_py: # tasks that are using python scripts from anoma-ci-iac @@ -21,23 +20,13 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] make: - name: Update wasm comment: pls update wasm command: update-wasm.py logs: 'false' timeout: 15 - - name: Publish wasm - comment: pls publish wasm - command: publish-wasm.py - logs: 'false' - timeout: 15 - - name: Spawn devnet - comment: pls spawn devnet - command: spawn-devnet.py - logs: 'false' - timeout: 25 steps: - name: Configure AWS Credentials diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 8531328645..5f22328b9e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -41,9 +41,9 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] wasm_cache_version: ["v2"] - mold_version: [2.1.0] + mold_version: [2.3.2] steps: - name: Checkout repo @@ -99,10 +99,10 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] wasm_cache_version: ["v2"] nightly_version: [nightly-2023-06-01] - mold_version: [2.1.0] + mold_version: [2.3.2] steps: - name: Checkout repo @@ -162,10 +162,10 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] wasm_cache_version: ["v2"] nightly_version: [nightly-2023-06-01] - mold_version: [2.1.0] + mold_version: [2.3.2] steps: - name: Download wasm artifacts @@ -188,14 +188,14 @@ jobs: unit-and-integration-tests: runs-on: group: gians-runners - timeout-minutes: 50 + timeout-minutes: 30 needs: [build-wasm] strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] nightly_version: [nightly-2023-06-01] - mold_version: [2.1.0] + mold_version: [2.3.2] make: - name: ABCI @@ -212,6 +212,8 @@ jobs: # See comment in build-and-test.yml with: ref: ${{ github.event.pull_request.head.sha }} + - name: Install libudev + run: sudo apt-get update && sudo apt-get -y install libudev-dev - name: Install Protoc uses: heliaxdev/setup-protoc@v2 with: @@ -284,14 +286,14 @@ jobs: run-benchmarks: runs-on: group: gians-runners - timeout-minutes: 30 + timeout-minutes: 60 needs: [build-wasm] strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] nightly_version: [nightly-2023-06-01] - mold_version: [2.1.0] + mold_version: [2.3.2] make: - name: ABCI @@ -308,6 +310,8 @@ jobs: # See comment in build-and-test.yml with: ref: ${{ github.event.pull_request.head.sha }} + - name: Install libudev + run: sudo apt-get update && sudo apt-get -y install libudev-dev - name: Install Protoc uses: heliaxdev/setup-protoc@v2 with: @@ -374,8 +378,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] - mold_version: [2.1.0] + os: [ubuntu-latest] + mold_version: [2.3.2] make: - name: ABCI Release build @@ -392,6 +396,8 @@ jobs: # See comment in build-and-test.yml with: ref: ${{ github.event.pull_request.head.sha }} + - name: Install libudev + run: sudo apt-get update && sudo apt-get -y install libudev-dev - name: Install Protoc uses: heliaxdev/setup-protoc@v2 with: @@ -473,9 +479,9 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] nightly_version: [nightly-2023-06-01] - mold_version: [2.1.0] + mold_version: [2.3.2] comet_bft: [0.37.2] hermes: [1.6.0-namada-beta1] make: @@ -497,6 +503,8 @@ jobs: # See comment in build-and-test.yml with: ref: ${{ github.event.pull_request.head.sha }} + - name: Install libudev + run: sudo apt-get update && sudo apt-get -y install libudev-dev - name: Install Protoc uses: heliaxdev/setup-protoc@v2 with: diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 6da22da803..7c5f589e08 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] nightly_version: [nightly-2023-06-01] make: - name: Clippy @@ -59,6 +59,8 @@ jobs: sudo rm -rf /opt/ghc sudo rm -rf "/usr/local/share/boost" sudo rm -rf "$AGENT_TOOLSDIRECTORY" + - name: Install libudev + run: sudo apt-get update && sudo apt-get -y install libudev-dev - name: Install Protoc uses: heliaxdev/setup-protoc@v2 with: diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 7c35586998..96fa86a8ed 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] nightly_version: [nightly-2023-06-01] make: - name: Audit @@ -54,6 +54,8 @@ jobs: restore-keys: ${{ runner.os }}-cargo- - name: Install cargo ${{ matrix.make.command }} run: curl https://i.jpillora.com/${{ matrix.make.version }}! | bash + - name: Install libudev + run: sudo apt-get update && sudo apt-get -y install libudev-dev - name: Install Protoc uses: heliaxdev/setup-protoc@v2 with: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5a7e021297..e7c527c6c2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -27,7 +27,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] make: - name: Build & Push WASM docker image image: wasm diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c37023e17e..a5fe474594 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] nightly_version: [nightly-2023-06-01] mdbook_version: [rust-lang/mdbook@v0.4.18] mdbook_mermaid: [badboy/mdbook-mermaid@v0.11.1] @@ -60,6 +60,8 @@ jobs: # See comment in build-and-test.yml with: ref: ${{ github.event.pull_request.head.sha }} + - name: Install libudev + run: sudo apt-get update && sudo apt-get -y install libudev-dev - name: Install Protoc uses: heliaxdev/setup-protoc@v2 with: @@ -116,7 +118,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] steps: - name: Checkout repo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59989b16d2..df5ed22dab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,8 @@ jobs: - name: Switch to tag if specified if: "${{ github.event.inputs.tag != '' }}" run: git checkout ${{ github.event.inputs.tag }} + - name: Install libudev + run: sudo apt-get update && sudo apt-get -y install libudev-dev - name: Install Protoc uses: heliaxdev/setup-protoc@v2 with: diff --git a/.github/workflows/triggerable.yml b/.github/workflows/triggerable.yml index 811bc55fed..bcc9cbddb5 100644 --- a/.github/workflows/triggerable.yml +++ b/.github/workflows/triggerable.yml @@ -32,8 +32,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] - mold_version: [2.1.0] + os: [ubuntu-latest] + mold_version: [2.3.2] name: ["Run PoS state-machine tests"] command: ["make test-pos-sm"] timeout: [360] @@ -42,6 +42,8 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ inputs.branch }} + - name: Install libudev + run: sudo apt-get update && sudo apt-get -y install libudev-dev - name: Install Protoc uses: heliaxdev/setup-protoc@v2 with: diff --git a/Cargo.lock b/Cargo.lock index 12333386b0..38c7d1417f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1422,6 +1422,19 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1642,10 +1655,19 @@ dependencies = [ "elliptic-curve", "rfc6979", "serdect", - "signature", + "signature 2.1.0", "spki", ] +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + [[package]] name = "ed25519" version = "2.2.2" @@ -1653,7 +1675,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" dependencies = [ "pkcs8", - "signature", + "signature 2.1.0", ] [[package]] @@ -1684,6 +1706,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519 1.5.3", + "rand 0.7.3", + "serde 1.0.190", + "sha2 0.9.9", + "zeroize", +] + [[package]] name = "either" version = "1.8.1" @@ -2675,6 +2711,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" @@ -3161,7 +3209,7 @@ dependencies = [ "once_cell", "serdect", "sha2 0.10.6", - "signature", + "signature 2.1.0", ] [[package]] @@ -3191,6 +3239,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?rev=7e861c440de0fdabaf51e30d97f5c8736be348f3#7e861c440de0fdabaf51e30d97f5c8736be348f3" +dependencies = [ + "bincode", + "byteorder", + "ed25519-dalek", + "leb128", + "ledger-transport", + "ledger-zondax-generic", + "prost 0.11.9", + "prost-types 0.11.9", + "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.190", + "thiserror", +] + [[package]] name = "lexical-core" version = "0.7.6" @@ -3719,6 +3833,8 @@ dependencies = [ "git2", "itertools 0.10.5", "lazy_static", + "ledger-namada-rs", + "ledger-transport-hid", "libc", "libloading", "masp_primitives", @@ -4088,6 +4204,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" @@ -6008,6 +6130,12 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + [[package]] name = "signature" version = "2.1.0" @@ -6054,6 +6182,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" @@ -6284,7 +6434,7 @@ checksum = "bc2294fa667c8b548ee27a9ba59115472d0a09c2ba255771092a7f1dcf03a789" dependencies = [ "bytes", "digest 0.10.6", - "ed25519", + "ed25519 2.2.2", "ed25519-consensus 2.1.0", "flex-error", "futures", @@ -6297,7 +6447,7 @@ dependencies = [ "serde_json", "serde_repr", "sha2 0.10.6", - "signature", + "signature 2.1.0", "subtle 2.4.1", "subtle-encoding", "tendermint-proto", diff --git a/Cargo.toml b/Cargo.toml index b9bf45dd6c..0b27724164 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,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", rev = "7e861c440de0fdabaf51e30d97f5c8736be348f3" } +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 9910adf96b..95cca10154 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -62,8 +62,8 @@ integration = [] [dependencies] -namada = {path = "../shared", features = ["masp-tx-gen", "multicore", "http-client", "tendermint-rpc"]} -namada_sdk = {path = "../sdk", default-features = false, features = ["wasm-runtime", "masp-tx-gen"]} +namada = {path = "../shared", features = ["multicore", "http-client", "tendermint-rpc"]} +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 @@ -94,6 +94,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 30d4a64156..6ee0bc0bd7 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 d9429bcd70..1b21ec4567 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -735,15 +735,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 f2e03377ec..ccc733802e 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -510,7 +510,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), @@ -523,7 +523,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); @@ -539,7 +539,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()) @@ -549,26 +549,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::() } } @@ -822,7 +823,7 @@ pub mod cmds { #[derive(Clone, Debug)] pub enum WalletAddress { Gen(AddressGen), - Restore(AddressRestore), + Derive(AddressDerive), Find(AddressOrAliasFind), List(AddressList), Add(AddressAdd), @@ -834,7 +835,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); @@ -851,7 +852,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()) @@ -884,26 +885,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::() } } @@ -2942,9 +2944,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 = @@ -3032,6 +3033,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 = @@ -3962,7 +3964,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, } @@ -4016,7 +4018,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 @@ -4026,9 +4028,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, email: self.email, @@ -4175,7 +4175,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, } @@ -4624,7 +4624,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), } } } @@ -5622,7 +5622,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, @@ -5633,6 +5633,7 @@ pub mod args { wrapper_fee_payer: self .wrapper_fee_payer .map(|x| ctx.get_cached(&x)), + use_device: self.use_device, } } } @@ -5758,6 +5759,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 { @@ -5785,6 +5790,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, @@ -5808,6 +5814,7 @@ pub mod args { chain_id, wrapper_fee_payer, output_folder, + use_device, } } } @@ -5982,18 +5989,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, } } @@ -6017,7 +6026,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 \ @@ -6035,7 +6048,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, @@ -6067,7 +6080,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 be0c2e3c4d..51c8ba4eac 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 3b43e09b74..4d22365f80 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 1ee8be9082..306c08c412 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?; } @@ -251,14 +394,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 @@ -280,14 +422,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 @@ -310,14 +451,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 @@ -424,7 +564,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(); @@ -545,7 +685,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; @@ -590,7 +731,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?; } @@ -716,7 +858,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?; } @@ -793,7 +936,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?; } @@ -910,7 +1054,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?; } @@ -932,7 +1076,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?; @@ -956,7 +1100,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?; } @@ -978,7 +1122,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?; } @@ -999,7 +1143,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?; } @@ -1021,7 +1165,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?; } @@ -1043,7 +1187,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?; } @@ -1065,7 +1209,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?; } @@ -1087,7 +1231,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?; } @@ -1109,7 +1253,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?; } @@ -1132,7 +1276,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?; } @@ -1153,7 +1298,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 2c66d68f9c..0ae2388fd2 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -614,8 +614,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 711266c542..0f7b9cdd7e 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 47e1cf4343..d457ef87f5 100644 --- a/apps/src/lib/config/genesis/transactions.rs +++ b/apps/src/lib/config/genesis/transactions.rs @@ -338,7 +338,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 790587a549..5e4c04b07f 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 ff80ce2887..e994203c88 100644 --- a/apps/src/lib/wallet/defaults.rs +++ b/apps/src/lib/wallet/defaults.rs @@ -32,7 +32,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()), @@ -41,6 +41,8 @@ mod dev { ("ester".into(), ester_keypair()), ("validator".into(), validator_keypair()), ] + .into_iter() + .collect() } /// The default tokens with their aliases. @@ -59,8 +61,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), @@ -71,7 +73,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)); @@ -126,7 +130,7 @@ mod dev { } let path = root_dir.join("genesis/localnet/src/pre-genesis"); let wallet = crate::wallet::load(&path).unwrap(); - let sk = match wallet.get_keys().get(name).unwrap().0 { + let sk = match wallet.get_secret_keys().get(name).unwrap().0 { namada_sdk::wallet::StoredKeypair::Encrypted(_) => { panic!("{}'s keypair should not be encrypted", name) } diff --git a/apps/src/lib/wallet/pre_genesis.rs b/apps/src/lib/wallet/pre_genesis.rs index 9b4f5510c6..7ff77407fb 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 18fc721d63..c721b02d9c 100644 --- a/apps/src/lib/wallet/store.rs +++ b/apps/src/lib/wallet/store.rs @@ -1,7 +1,10 @@ use std::path::{Path, PathBuf}; use namada::types::key::*; -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; @@ -45,9 +48,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)); ValidatorKeys { protocol_keypair, eth_bridge_keypair, diff --git a/core/src/types/key/common.rs b/core/src/types/key/common.rs index 72b8037c28..583293930a 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/docker/namada-wasm/Dockerfile b/docker/namada-wasm/Dockerfile index f625830774..b6422340bd 100644 --- a/docker/namada-wasm/Dockerfile +++ b/docker/namada-wasm/Dockerfile @@ -11,6 +11,7 @@ RUN rustup target add wasm32-unknown-unknown RUN apt-get update && apt-get install -y \ protobuf-compiler \ + libudev-dev \ && apt-get clean # Download binaryen and extract wasm-opt diff --git a/docker/namada/Dockerfile b/docker/namada/Dockerfile index 18f4dbfe3a..617b65be61 100644 --- a/docker/namada/Dockerfile +++ b/docker/namada/Dockerfile @@ -15,6 +15,7 @@ RUN apt-get update && apt-get install -y \ libssl-dev \ pkg-config \ protobuf-compiler \ + libudev-dev \ && apt-get clean COPY --from=planner /app/recipe.json recipe.json @@ -32,7 +33,7 @@ FROM debian:bullseye-slim AS runtime ENV NAMADA_BASE_DIR=/.namada ENV NAMADA_LOG_COLOR=false -RUN apt-get update && apt-get install libcurl4-openssl-dev -y && apt-get clean +RUN apt-get update && apt-get install libcurl4-openssl-dev libudev-dev -y && apt-get clean RUN useradd --create-home namada USER namada diff --git a/genesis/localnet/src/pre-genesis/wallet.toml b/genesis/localnet/src/pre-genesis/wallet.toml index 9d01374279..14efec25d9 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_PREFIXtpknam1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63we33t3r" +bertha-key = "ED25519_PK_PREFIXtpknam1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3nds48x83t" +christel-key = "ED25519_PK_PREFIXtpknam1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzwgc7uu" +daewon = "ED25519_PK_PREFIXtpknam1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkykqqvjm" +ester = "SECP256K1_PK_PREFIXtpknam1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqs74gxv4" +faucet-key = "ED25519_PK_PREFIXtpknam1qzh2d8vk9wvj2j63fa3cvjru9uldpdjctjjxpafl5r8vwwf56pdgyq0vra4" +validator-0-balance-key = "ED25519_PK_PREFIXtpknam1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agjf64ch" + +[derivation_paths] + [addresses] albert-key = "tnam1qp2yqaffsk2wekd8fheu5c0cv3heg8v37vmdd45u" bertha-key = "tnam1qqm6sgr63e2zgngh5hlff8r8rennq4vldgdyqdaa" diff --git a/genesis/localnet/transactions.toml b/genesis/localnet/transactions.toml index cafe123854..212113edab 100644 --- a/genesis/localnet/transactions.toml +++ b/genesis/localnet/transactions.toml @@ -7,6 +7,10 @@ [[validator_account]] alias = "validator-0" +<<<<<<< HEAD +======= +dkg_key = "dpknam1vqqqqqqndwmp3cvzfuepzvdh3fmt4dvjwd4vx6ffxk6t4vm6waysng2q8zh3yu8hdnxhwnwq22p8fwaqlyzcggef6k6qv3h3qtyy8c8c686w50v7d0z49ufd6zje46ujzc4ew5z7sdh45d94cvywy2v40yz9jlck3p6lxe" +>>>>>>> murisi/hw-on-base vp = "vp_validator" commission_rate = "0.05" max_commission_rate_change = "0.01" @@ -14,6 +18,7 @@ email = "null@null.net" net_address = "127.0.0.1:27656" [validator_account.account_key] +<<<<<<< HEAD pk = "tpknam1qzuaykg8nrmzuaefut39aaxl0gl66y62285cnwe37gq0a96qaf4qgp5yqtw" authorization = "signam1qpcvzjdqkvhud9lez3exq3vy7dnp4kyjgsk92yx5cslwzjs0q9czx8ajxztx5pftx9hsf9w2shh6rt0uv6xeyu30ljqkrqj6hnrwn5gqfd9pwf" @@ -36,6 +41,30 @@ authorization = "signam1q9fd4nkz8df5ajh6jrsn6mrt0apvn4266walr8pnczl0xtn847u6usde [validator_account.eth_cold_key] pk = "tpknam1qyp4aw7a07f4ltuq3f0c4sgyq3kzgh6z40w8jt40wl7uhfj7e3m3u9qr792xg" authorization = "signam1qyqcc7wstsnuadu75jnwg8hxz34nglxj9h9yqyx2c4qqdpxw50ff60hu0swfj7xdkz34tgl9e50jlmzphkgtpw3gg0hsgp35lhsl6rmeqqq28kph" +======= +pk = "tpknam1qpqznlesv6fcgsz9x4rsn7l4dsnp7h5sswvs9atw0zgq3uwp0jll2fh8yx5" +authorization = "signam1qrcy6a095uj6n8ryc9fkfw3hn2jn285g82hdratzrvmg22xp5qdmyr7e9q97t77v4zr8a73x4c3k53pcec3mp4ugt5jqd2w0fvd7nhcpw8587v" + +[validator_account.consensus_key] +pk = "tpknam1qpk2a5m26cgnrp32l2c2hss03draxwnctu92llhlr6kjesn2kt892ugmzmh" +authorization = "signam1qzajuey408zf66wgw8x3awd5jx9x249csx63n80clehj238qqpsf9cdr630edqjr8fw95vywlngy368r087sge2xy89qpcsrxvwtm3grwdulku" + +[validator_account.protocol_key] +pk = "tpknam1qpfv9pj4zmelcg98rtw8wezztzdgpuczuxjmx946klkxajmvvcxjzmwac78" +authorization = "signam1qz0aerskgtzwmgxwgml72zyz4974njfz6s9dl7r399d0v64pzsmctkwe8f28dwyef52hklj6r2fqmeypncvv3qg2x4hg4u3g23kw23qtlt64na" + +[validator_account.tendermint_node_key] +pk = "tpknam1qp7shxxfdkp8ft8xfkgf2vqy7u6effdp82w0cq6aq8575txul33t6m0fe73" +authorization = "signam1qq87l6zjuswmrfeysxp8j0tu5zlklnswzaqgcuyaq23p49hhvupa57r3rwrh3txfuvq8lh5e7wu7d90js8cn7a7dygs7v5yx2a9ljtqx09f0l6" + +[validator_account.eth_hot_key] +pk = "tpknam1qypdzza7uqtklzzs50hhy07h5ru9p0v8y5666wwulqnd3mdpuj0tcxstywa2d" +authorization = "signam1q9qtzvwlqhnft5jgeyms2w757dlly8rjg4g89593ayhan0fx7uvjzvm7r7ltgsly9tp6783pqpl6ezxty240udrrknxmunymz6lm523qqqlu57sc" + +[validator_account.eth_cold_key] +pk = "tpknam1qypzca0n6l890jc83nk5lljuzps04xmfccz3f87xc6ez40t2wvql48shkqpgm" +authorization = "signam1q8qmjstc2jtt3dwueepv0njk9te5qpj6z9yj6w0duhagswz80vyws3ywdsy9n49qnjecvnpxwlna3578vn423tgd4rrtss06ejgkj7c5qyasq8gl" +>>>>>>> murisi/hw-on-base [[transfer]] token = "nam" @@ -48,7 +77,11 @@ signature = "signam1qz8ch7n6jp7g7hvnzkhmprhqc5umd0gatarlttm7gpcdx6g8hmgqffdq67fn source = "validator-0" validator = "validator-0" amount = "100000" +<<<<<<< HEAD signature = "signam1qrd7wg2sdla73z7626r2ep8895d9ed4ljq37nz6kmy2rcm36yqjugw6vle7kl70l0d585t382s9vd28k2duwz0lu7ux0vua6aaarjssgdf46n2" +======= +signature = "signam1qpss2x7vewnr2sr2wxj0ftldcmvv7v7t7qsr26k2l2hx4gls7y9sksrcq0m9vv2ymdqqxgmmw0dmuwazflxuqt4v2p2wng4endtcfcc0dq0drt" +>>>>>>> murisi/hw-on-base # 2. diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index b2c38caa5a..2e69d31ed1 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -15,17 +15,12 @@ version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["rand", "rand_core"] -masp-tx-gen = [ - "rand", - "rand_core", -] +default = [] multicore = ["masp_proofs/multicore"] namada-sdk = [ "tendermint-rpc", - "masp-tx-gen", "masp_primitives/transparent-inputs" ] @@ -56,8 +51,6 @@ testing = [ "namada_ethereum_bridge/testing", "namada_proof_of_stake/testing", "async-client", - "rand_core", - "rand", ] [dependencies] @@ -84,8 +77,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 c2280074cc..138828007d 100644 --- a/sdk/src/args.rs +++ b/sdk/src/args.rs @@ -1739,6 +1739,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 @@ -1955,12 +1957,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 @@ -1970,7 +1972,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 bff45b2d80..c82c8a063f 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -27,6 +27,7 @@ pub mod io; pub mod queries; pub mod wallet; +use std::collections::HashSet; use std::path::PathBuf; use std::str::FromStr; @@ -128,6 +129,7 @@ pub trait Namada<'a>: Sized { tx_reveal_code_path: PathBuf::from(TX_REVEAL_PK), verification_key: None, password: None, + use_device: false, } } @@ -456,13 +458,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 @@ -561,6 +564,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 be4e0f194c..2ead173e52 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; @@ -61,10 +59,8 @@ use namada_core::types::token::{ Change, MaspDenom, Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, }; use namada_core::types::transaction::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; @@ -417,7 +413,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) { @@ -1502,7 +1497,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 694b5b7476..39120f8261 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,9 +30,9 @@ 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; use super::masp::{ShieldedContext, ShieldedTransfer}; use crate::args::SdkTypes; @@ -88,7 +88,6 @@ pub struct SigningTxData { pub async fn find_pk<'a>( context: &impl Namada<'a>, addr: &Address, - password: Option>, ) -> Result { match addr { Address::Established(_) => { @@ -107,7 +106,7 @@ pub async fn find_pk<'a>( Address::Implicit(ImplicitAddress(pkh)) => Ok(context .wallet_mut() .await - .find_key_by_pkh(pkh, password) + .find_public_key_by_pkh(pkh) .map_err(|err| { Error::Other(format!( "Unable to load the keypair from the wallet for the \ @@ -115,8 +114,7 @@ pub async fn find_pk<'a>( addr.encode(), err )) - })? - .ref_to()), + })?), Address::Internal(_) => other_err(format!( "Internal address {} doesn't have any signing keys.", addr @@ -188,9 +186,7 @@ pub async fn tx_signers<'a>( match signer { Some(signer) if signer == masp() => Ok(vec![masp_tx_key().ref_to()]), - Some(signer) => Ok(vec![ - find_pk(context, &signer, args.password.clone()).await?, - ]), + Some(signer) => Ok(vec![find_pk(context, &signer).await?]), None => other_err( "All transactions must be signed; please either specify the key \ or the address from which to look up the signing key." @@ -199,6 +195,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. @@ -210,42 +225,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(()) } @@ -289,7 +358,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 df626662e2..2e1a786d12 100644 --- a/sdk/src/tx.rs +++ b/sdk/src/tx.rs @@ -268,7 +268,8 @@ pub async fn build_reveal_pk<'a>( public_key: &common::PublicKey, ) -> Result<(Tx, SigningTxData, Option)> { let signing_data = - signing::aux_signing_data(context, args, None, None).await?; + signing::aux_signing_data(context, args, None, Some(public_key.into())) + .await?; build( context, @@ -474,7 +475,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 7751e51701..c1aba983bb 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 4570c8a283..28b25c1076 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 916ec43781..689015c234 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 4d8af29c8e..980aa6ae70 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}; @@ -13,8 +12,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; @@ -69,7 +66,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` @@ -90,13 +91,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(|| { @@ -110,6 +111,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, @@ -149,7 +166,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. @@ -157,6 +174,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()) @@ -168,7 +201,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, @@ -181,11 +214,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)); } @@ -193,6 +226,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 @@ -215,100 +253,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, @@ -338,21 +282,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 @@ -362,41 +321,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) } @@ -404,8 +356,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 @@ -423,15 +375,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) } @@ -468,23 +423,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 @@ -522,30 +510,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 { @@ -565,53 +536,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) { @@ -619,7 +586,9 @@ impl Store { view_keys, spend_keys, payment_addrs, - keys, + secret_keys, + public_keys, + derivation_paths, addresses, pkhs, validator_data: _, @@ -628,7 +597,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); @@ -655,7 +626,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(); @@ -719,25 +690,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, @@ -816,7 +770,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; @@ -844,11 +798,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); } @@ -878,13 +828,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, @@ -900,7 +846,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 509b412636..2d83a73498 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -58,12 +58,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", @@ -72,14 +66,11 @@ testing = [ "namada_sdk/testing", "async-client", "proptest", - "rand_core", - "rand", "tempfile", ] namada-sdk = [ "tendermint-rpc", - "masp-tx-gen", "masp_primitives/transparent-inputs", "namada_sdk/namada-sdk", ] @@ -120,8 +111,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 eb4156a58c..19a6d3ab1a 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -3032,6 +3032,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 a3d2440abb..4d9dda8f66 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 76387f5db6..ad6b86f985 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 23100c618c..0586ce2261 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,27 +1,27 @@ { - "tx_bond.wasm": "tx_bond.70e7863eade59d637217f133cf8dacbb28fbe7c22936396055e8228c87199f83.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.69971943e6ad81a537197ee4d7fe8f1211882a17de572b28e71906572e8193af.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.94615c76f6f32b627adbbb1279a345bd06c6bd0d4eb65a6286c0038f78e91321.wasm", - "tx_change_validator_metadata.wasm": "tx_change_validator_metadata.e13d10fb9eaa40510c17495d4aee612c05526ee8fa7ec7b4c619e5d68e0f1bcd.wasm", - "tx_claim_rewards.wasm": "tx_claim_rewards.1a165fcb82a58712074e9880e2b1ae8904e0af2d5aca4338c4c6367dc719b9de.wasm", - "tx_deactivate_validator.wasm": "tx_deactivate_validator.38bdc4c2cda3f09c9c233ccea8f8df64e1d3b7916f67723d079248f36042ec82.wasm", - "tx_ibc.wasm": "tx_ibc.a74c56e43a62bc89018b2ccc19a4daf02d24a2f7c25ccc7b8e57c1f8ce272b82.wasm", - "tx_init_account.wasm": "tx_init_account.67cf30a5dc66ed3456e209f6983bb2d17c44b0dc952aeecccee6499939c5c58d.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.42f0735b45cebb72c10c4f2a6298d838adf89c1ab2d8eb31e0c726e33b1efdbd.wasm", - "tx_init_validator.wasm": "tx_init_validator.b33e386e3695a676a028040fbe6ce32762c9299e41f673f8636feb959535982d.wasm", - "tx_reactivate_validator.wasm": "tx_reactivate_validator.60f635af60a6172d2f32ae69332185cd658a0a49eecf2ad232f372d35a96c905.wasm", - "tx_redelegate.wasm": "tx_redelegate.5fc225fed027677e5f374a6e03ba14a97ffe730e2b600fccf23f4990ca7a19d8.wasm", - "tx_resign_steward.wasm": "tx_resign_steward.0852161a0947dc129fd1a581f79e5725c63760aed6cc71bbf3e2b554f913a540.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.57f11af958daa0d52db27c1036a3d82eea964e976238561ad48baf89423b110b.wasm", - "tx_transfer.wasm": "tx_transfer.c3cecec1bbd1eb452a5aef6f70d45e1ed8590da4ff670fafe28163847405e934.wasm", - "tx_unbond.wasm": "tx_unbond.fc0a8f16733ee5cedb4a3e56e211bba168cb5f97e977d2eeebdf2c28ff26a4a6.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.2f973ea13bf7a0060fdec2faa4a30d95fd018b54aa4bb6283b2a53fe21b5ab77.wasm", - "tx_update_account.wasm": "tx_update_account.a4b02b42cc0428030085ffde5b5169ecb4b5d02c8f316b162df5bb1e3ea15c3a.wasm", - "tx_update_steward_commission.wasm": "tx_update_steward_commission.17439af7a44254111415e28cc8a5d8ab62e0a67d2825a82dcb2eb5666689fad9.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.8b5d3d2a258f08eb0fe7727c81f55b60134985bb30ac2e13814d1f3a38082cb2.wasm", - "tx_withdraw.wasm": "tx_withdraw.26952ab447ba48aa08ffa7d12700877b1beef3c73b3e8fb2b8ddb1839e80fa8f.wasm", - "vp_implicit.wasm": "vp_implicit.32f0dee40e3c6730327193a32a191bdd6bfca768d9176f02bb63da04eb97a17f.wasm", - "vp_masp.wasm": "vp_masp.a5fc24c02e34d2345353abfd355f7fc665b980b185676e921f0994c02ca6bb6d.wasm", - "vp_user.wasm": "vp_user.238f0f88b402059130a1e4868ff62e4880320725155c275c626798d69d04cecf.wasm", - "vp_validator.wasm": "vp_validator.e8721ed7520d7b0940adb4d8f195ab4d1d2eeca2fa36688b618e6ecd545f25fc.wasm" -} + "tx_bond.wasm": "tx_bond.650c705ede1bf1cc8e8ec754147cad77114be4a8e3a392ccdfd9d1788582ad6d.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.dcc25f8337e67909b0794a5c61a872f7adc22a0bdd52effab213ca64e205a346.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.228748757e6fda18ff98148645fdb6837a445399d0520b052248a0afa48b7471.wasm", + "tx_change_validator_metadata.wasm": "tx_change_validator_metadata.83971ee3b45bd93275f6cdc3040c48fa032d21e4e12622ae9680b3afe97135ce.wasm", + "tx_claim_rewards.wasm": "tx_claim_rewards.52151217f06fc1fa634f1d602edc9edcf58d01f8734b57f70dfb60eb7bd4f009.wasm", + "tx_deactivate_validator.wasm": "tx_deactivate_validator.2a4867440f4b84d8109be88b343e2db78dcebfa345e3e6e12f8b62c90f43ba3b.wasm", + "tx_ibc.wasm": "tx_ibc.39f2ba7bfd12efab2b47df2fa81b30a05c44af02853f3d54901b8d8dd3f89d29.wasm", + "tx_init_account.wasm": "tx_init_account.d0e93684efdc5e91bbf9380cd8f37edc6d34ce1387327fb418a07cbbca4cc12f.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.bd791535baa2050a6516074628a1c7ab1ec1b1a6443a90ec88d6121a9aace434.wasm", + "tx_init_validator.wasm": "tx_init_validator.49af01cd824808cbfa4c1e8a6b84e0936806bafdf86cbde3d3665055e8a23db3.wasm", + "tx_reactivate_validator.wasm": "tx_reactivate_validator.8064535f5d034b2384e8b10c8c01ac7f12c12a3dfaafb572ec2d11cf0656a293.wasm", + "tx_redelegate.wasm": "tx_redelegate.0c3e040a0c41a1be023b583a6cc112287ca99490ac66eef381da037db8b7c94a.wasm", + "tx_resign_steward.wasm": "tx_resign_steward.e63e14cb3b465165b6f92d8b684958288f8e1d08a9823064879d7543bdd66d64.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.7a7d65250ef9f4ddc92de5a76b80f8be0057db43333e65bfde960ddfe2d15438.wasm", + "tx_transfer.wasm": "tx_transfer.174e64bde2c879ba1f0d7e01d1add7ebd373504dc6f775e3505abb1bc674563d.wasm", + "tx_unbond.wasm": "tx_unbond.7b58ad5c6a32554fcfbe615a75ab0676145d2be9ecb00ad5c0e607933d523a24.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.781ffee194d892bf1d2e2c8ef74862aedb5957246f12ef330140b5c5ff46c457.wasm", + "tx_update_account.wasm": "tx_update_account.4bea39ea11d1104bcabaf8bf4b5d4c8b2cdb6bdf8c1c8743f6f36bb5f2a88a72.wasm", + "tx_update_steward_commission.wasm": "tx_update_steward_commission.d150feb3bd751c45057ecd6b2dd743ab94ab40fb97f719ce15f7dd48bdfc7870.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.c56289b2f51ac995b643a6daf53f3ef84366aa2cc15e96b3ad30950456751413.wasm", + "tx_withdraw.wasm": "tx_withdraw.011e183d88acc91ab207c732b5463fee60708c0f1f2f45e2d116b310535d6001.wasm", + "vp_implicit.wasm": "vp_implicit.96c62a3de847006b345393227b34a5a6688265c8b1f38ce5f38e01d9c349cf7c.wasm", + "vp_masp.wasm": "vp_masp.1be018ceacbe7e1c2b6a5ceb9ef22a29f4eeffd2c69289d63ca97985ab8402dd.wasm", + "vp_user.wasm": "vp_user.7d977f312843a14e6fc401a808a42ebda22e8870e7930902f11cefbf9fdde0ee.wasm", + "vp_validator.wasm": "vp_validator.5c3493d47abb91c35d35e4e98b4c7daab42b4646b915478639fdce6e9ee57c15.wasm" +} \ No newline at end of file diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index c9750fdfbc..721c641ff1 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -3428,7 +3428,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ea360eafe1022f7cc56cd7b869ed57330fb2453d0c7831d99b74c65d2f5597" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.38",