From 76034772cba4d3d6fa1bdcb08977c2b9d7a157c2 Mon Sep 17 00:00:00 2001 From: wszdexdrf Date: Tue, 26 Jul 2022 00:07:04 +0530 Subject: [PATCH 1/3] Add a custom signer for hardware wallets Also add function to get funded wallet with coinbase --- CHANGELOG.md | 1 + Cargo.toml | 3 ++ src/wallet/hardwaresigner.rs | 64 ++++++++++++++++++++++++++++++++++++ src/wallet/mod.rs | 32 ++++++++++++++++++ src/wallet/signer.rs | 10 ++++++ 5 files changed, 110 insertions(+) create mode 100644 src/wallet/hardwaresigner.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index bfbb1804..2859b7ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Add capacity to create FeeRate from sats/kvbytes and sats/kwu. - Rename `as_sat_vb` to `as_sat_per_vb`. Move all `FeeRate` test to `types.rs`. +- Add custom Harware Wallet Signer `HwiSigner` in `src/wallet/harwaresigner/` module. ## [v0.21.0] - [v0.20.0] diff --git a/Cargo.toml b/Cargo.toml index d61e4f37..5fcea942 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ rocksdb = { version = "0.14", default-features = false, features = ["snappy"], o cc = { version = ">=1.0.64", optional = true } socks = { version = "0.3", optional = true } lazy_static = { version = "1.4", optional = true } +hwi = { version = "0.2.2", optional = true } bip39 = { version = "1.0.1", optional = true } bitcoinconsensus = { version = "0.19.0-3", optional = true } @@ -61,6 +62,7 @@ key-value-db = ["sled"] all-keys = ["keys-bip39"] keys-bip39 = ["bip39"] rpc = ["bitcoincore-rpc"] +hardware-signer = ["hwi"] # We currently provide mulitple implementations of `Blockchain`, all are # blocking except for the `EsploraBlockchain` which can be either async or @@ -93,6 +95,7 @@ test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-bl test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_0_20_0", "test-blockchains"] test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"] test-md-docs = ["electrum"] +test-hardware-signer = ["hardware-signer"] [dev-dependencies] lazy_static = "1.4" diff --git a/src/wallet/hardwaresigner.rs b/src/wallet/hardwaresigner.rs new file mode 100644 index 00000000..7e4f74bc --- /dev/null +++ b/src/wallet/hardwaresigner.rs @@ -0,0 +1,64 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! HWI Signer +//! +//! This module contains a simple implementation of a Custom signer for rust-hwi + +use bitcoin::psbt::PartiallySignedTransaction; +use bitcoin::secp256k1::{All, Secp256k1}; +use bitcoin::util::bip32::Fingerprint; + +use hwi::error::Error; +use hwi::types::{HWIChain, HWIDevice}; +use hwi::HWIClient; + +use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner}; + +#[derive(Debug)] +/// Custom signer for Hardware Wallets +/// +/// This ignores `sign_options` and leaves the decisions up to the hardware wallet. +pub struct HWISigner { + fingerprint: Fingerprint, + client: HWIClient, +} + +impl HWISigner { + /// Create a instance from the specified device and chain + pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result { + let client = HWIClient::get_client(device, false, chain)?; + Ok(HWISigner { + fingerprint: device.fingerprint, + client, + }) + } +} + +impl SignerCommon for HWISigner { + fn id(&self, _secp: &Secp256k1) -> SignerId { + SignerId::Fingerprint(self.fingerprint) + } +} + +/// This implementation ignores `sign_options` +impl TransactionSigner for HWISigner { + fn sign_transaction( + &self, + psbt: &mut PartiallySignedTransaction, + _sign_options: &crate::SignOptions, + _secp: &crate::wallet::utils::SecpCtx, + ) -> Result<(), SignerError> { + psbt.combine(self.client.sign_tx(psbt)?.psbt) + .expect("Failed to combine HW signed psbt with passed PSBT"); + Ok(()) + } +} diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 3506b1f5..7bd2d6ba 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -48,6 +48,9 @@ pub(crate) mod utils; #[cfg_attr(docsrs, doc(cfg(feature = "verify")))] pub mod verify; +#[cfg(feature = "hardware-signer")] +pub mod hardwaresigner; + pub use utils::IsDust; #[allow(deprecated)] @@ -5414,4 +5417,33 @@ pub(crate) mod test { // ...and checking that everything is fine assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate); } + + #[cfg(feature = "test-hardware-signer")] + #[test] + fn test_create_signer() { + use crate::wallet::hardwaresigner::HWISigner; + use hwi::types::HWIChain; + use hwi::HWIClient; + + let devices = HWIClient::enumerate().unwrap(); + let device = devices.first().expect("No devices found"); + let client = HWIClient::get_client(device, true, HWIChain::Regtest).unwrap(); + let descriptors = client.get_descriptors(None).unwrap(); + let custom_signer = HWISigner::from_device(device, HWIChain::Regtest).unwrap(); + + let (mut wallet, _, _) = get_funded_wallet(&descriptors.internal[0]); + wallet.add_signer( + KeychainKind::External, + SignerOrdering(200), + Arc::new(custom_signer), + ); + + let addr = wallet.get_address(LastUnused).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + } } diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index 7548b321..1704a953 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -159,6 +159,16 @@ pub enum SignerError { InvalidSighash, /// Error while computing the hash to sign SighashError(sighash::Error), + /// Error while signing using hardware wallets + #[cfg(feature = "hardware-signer")] + HWIError(hwi::error::Error), +} + +#[cfg(feature = "hardware-signer")] +impl From for SignerError { + fn from(e: hwi::error::Error) -> Self { + SignerError::HWIError(e) + } } impl From for SignerError { From d6e1dd104063075f49b617786d82d29c1f9c6a0a Mon Sep 17 00:00:00 2001 From: wszdexdrf Date: Tue, 26 Jul 2022 00:07:56 +0530 Subject: [PATCH 2/3] Change CI to add test using ledger emulator --- .github/workflows/cont_integration.yml | 29 +++++++++++++++++++++++++ ci/Dockerfile.ledger | 9 ++++++++ ci/automation.json | 30 ++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 ci/Dockerfile.ledger create mode 100644 ci/automation.json diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 3088cd6e..af7e256f 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -172,3 +172,32 @@ jobs: run: rustup update - name: Check fmt run: cargo fmt --all -- --config format_code_in_doc_comments=true --check + + test_harware_wallet: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - version: 1.60.0 # STABLE + - version: 1.56.1 # MSRV + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Build simulator image + run: docker build -t hwi/ledger_emulator ./ci -f ci/Dockerfile.ledger + - name: Run simulator image + run: docker run --name simulator --network=host hwi/ledger_emulator & + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Install python dependencies + run: pip install hwi==2.1.1 protobuf==3.20.1 + - name: Set default toolchain + run: rustup default ${{ matrix.rust.version }} + - name: Set profile + run: rustup set profile minimal + - name: Update toolchain + run: rustup update + - name: Test + run: cargo test --features test-hardware-signer diff --git a/ci/Dockerfile.ledger b/ci/Dockerfile.ledger new file mode 100644 index 00000000..5fe2dd88 --- /dev/null +++ b/ci/Dockerfile.ledger @@ -0,0 +1,9 @@ +# Taken from bitcoindevkit/rust-hwi +FROM ghcr.io/ledgerhq/speculos + +RUN apt-get update +RUN apt-get install wget -y +RUN wget "https://github.com/LedgerHQ/speculos/blob/master/apps/nanos%23btc%232.1%231c8db8da.elf?raw=true" -O /speculos/btc.elf +ADD automation.json /speculos/automation.json + +ENTRYPOINT ["python", "./speculos.py", "--automation", "file:automation.json", "--display", "headless", "--vnc-port", "41000", "btc.elf"] diff --git a/ci/automation.json b/ci/automation.json new file mode 100644 index 00000000..9de2f60e --- /dev/null +++ b/ci/automation.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "rules": [ + { + "regexp": "Address \\(\\d/\\d\\)|Message hash \\(\\d/\\d\\)|Confirm|Fees|Review|Amount", + "actions": [ + [ "button", 2, true ], + [ "button", 2, false ] + ] + }, + { + "text": "Sign", + "conditions": [ + [ "seen", false ] + ], + "actions": [ + [ "button", 2, true ], + [ "button", 2, false ], + [ "setbool", "seen", true ] + ] + }, + { + "regexp": "Approve|Sign|Accept", + "actions": [ + [ "button", 3, true ], + [ "button", 3, false ] + ] + } + ] +} From 138acc3b7d137788d0518182e2167504e58ebc48 Mon Sep 17 00:00:00 2001 From: wszdexdrf Date: Thu, 25 Aug 2022 22:59:59 +0530 Subject: [PATCH 3/3] Change `populate_test_db` to not return empty input --- src/database/memory.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/database/memory.rs b/src/database/memory.rs index 7d806eb4..ad1eda96 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -490,11 +490,10 @@ macro_rules! populate_test_db { let mut db = $db; let tx_meta = $tx_meta; let current_height: Option = $current_height; - let input = if $is_coinbase { - vec![$crate::bitcoin::TxIn::default()] - } else { - vec![] - }; + let mut input = vec![$crate::bitcoin::TxIn::default()]; + if !$is_coinbase { + input[0].previous_output.vout = 0; + } let tx = $crate::bitcoin::Transaction { version: 1, lock_time: 0,