Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(hwi,wallet): enhance hardware wallet integration and testing #1492

Closed
wants to merge 10 commits into from
25 changes: 23 additions & 2 deletions .github/workflows/code_coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
uses: Swatinem/[email protected]
- name: Install grcov
run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi
# TODO: re-enable the hwi tests
# ==== hwi emulator setup ====
- name: Build simulator image
run: docker build -t hwi/ledger_emulator ./ci -f ci/Dockerfile.ledger
- name: Run simulator image
Expand All @@ -37,7 +37,28 @@ jobs:
with:
python-version: '3.9'
- name: Install python dependencies
run: pip install hwi==2.1.1 protobuf==3.20.1
run: pip install hwi==2.1.1 protobuf==3.20.1 requests
- name: Patch openssl.cnf for ripemd160 support
run: |
cat << 'EOF' > /tmp/openssl.cnf
openssl_conf = openssl_init

[openssl_init]
providers = provider_sect

[provider_sect]
default = default_sect
legacy = legacy_sect

[default_sect]
activate = 1

[legacy_sect]
activate = 1
EOF
- name: Setup OPENSSL_CONF environment variable
run: echo "OPENSSL_CONF=/tmp/openssl.cnf" >> $GITHUB_ENV
# ==== End hwi emulator setup ====
- name: Test
run: cargo test --all-features
- name: Make coverage directory
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/cont_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,38 @@ jobs:
cargo update -p cc --precise "1.0.105"
- name: Build
run: cargo build ${{ matrix.features }}
# ==== hwi emulator setup ====
- 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 requests
- name: Patch openssl.cnf for ripemd160 support
run: |
cat << 'EOF' > /tmp/openssl.cnf
openssl_conf = openssl_init

[openssl_init]
providers = provider_sect

[provider_sect]
default = default_sect
legacy = legacy_sect

[default_sect]
activate = 1

[legacy_sect]
activate = 1
EOF
- name: Setup OPENSSL_CONF environment variable
run: echo "OPENSSL_CONF=/tmp/openssl.cnf" >> $GITHUB_ENV
# ==== End hwi emulator setup ====
- name: Test
run: cargo test ${{ matrix.features }}

Expand Down
4 changes: 2 additions & 2 deletions ci/Dockerfile.ledger
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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
RUN wget "https://github.com/LedgerHQ/speculos/blob/master/apps/nanox%23btc%232.0.2%231c8db8da.elf?raw=true" -O /speculos/btc.elf
ADD automation.json /speculos/automation.json

ENTRYPOINT ["python", "./speculos.py", "--automation", "file:automation.json", "--model", "nanos", "--display", "headless", "--vnc-port", "41000", "btc.elf"]
ENTRYPOINT ["python", "./speculos.py", "--automation", "file:automation.json", "--model", "nanox", "--display", "headless", "--vnc-port", "41000", "btc.elf"]
3 changes: 3 additions & 0 deletions crates/hwi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ readme = "README.md"
[dependencies]
bdk_wallet = { path = "../wallet", version = "1.0.0-alpha.13" }
hwi = { version = "0.9.0", features = [ "miniscript"] }

[dev-dependencies]
bdk_wallet = { path = "../wallet", version = "1.0.0-alpha.13", features = ["test-util"] }
131 changes: 81 additions & 50 deletions crates/hwi/src/signer.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
use bdk_wallet::bitcoin::bip32::Fingerprint;
use bdk_wallet::bitcoin::secp256k1::{All, Secp256k1};
use bdk_wallet::bitcoin::Psbt;

use bdk_wallet::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
use hwi::error::Error;
use hwi::types::{HWIChain, HWIDevice};
use hwi::HWIClient;

use bdk_wallet::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};

#[derive(Debug)]
/// Custom signer for Hardware Wallets
///
Expand Down Expand Up @@ -41,54 +39,87 @@ impl TransactionSigner for HWISigner {
_sign_options: &bdk_wallet::SignOptions,
_secp: &Secp256k1<All>,
) -> Result<(), SignerError> {
psbt.combine(
self.client
.sign_tx(psbt)
.map_err(|e| {
SignerError::External(format!("While signing with hardware wallet: {}", e))
})?
.psbt,
)
.expect("Failed to combine HW signed psbt with passed PSBT");
let signed_psbt = self
.client
.sign_tx(psbt)
.map_err(|e| {
SignerError::External(format!("While signing with hardware wallet: {}", e))
})?
.psbt;

psbt.combine(signed_psbt).map_err(|e| {
SignerError::External(format!(
"Failed to combine HW signed PSBT with passed PSBT: {}",
e
))
})?;

Ok(())
}
}

// TODO: re-enable this once we have the `get_funded_wallet` test util
// #[cfg(test)]
// mod tests {
// #[test]
// fn test_hardware_signer() {
// use std::sync::Arc;
//
// use bdk_wallet::tests::get_funded_wallet;
// use bdk_wallet::signer::SignerOrdering;
// use bdk_wallet::bitcoin::Network;
// use crate::HWISigner;
// use hwi::HWIClient;
//
// let mut devices = HWIClient::enumerate().unwrap();
// if devices.is_empty() {
// panic!("No devices found!");
// }
// let device = devices.remove(0).unwrap();
// let client = HWIClient::get_client(&device, true, Network::Regtest.into()).unwrap();
// let descriptors = client.get_descriptors::<String>(None).unwrap();
// let custom_signer = HWISigner::from_device(&device, Network::Regtest.into()).unwrap();
//
// let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]);
// wallet.add_signer(
// bdk_wallet::KeychainKind::External,
// SignerOrdering(200),
// Arc::new(custom_signer),
// );
//
// let addr = wallet.get_address(bdk_wallet::wallet::AddressIndex::LastUnused);
// 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);
// }
// }
#[cfg(test)]
mod tests {
use super::*;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::signer::SignerOrdering;
// use bdk_wallet::wallet::common::get_funded_wallet;

use bdk_wallet::wallet::test_util::get_funded_wallet;
// use bdk_wallet::wallet::AddressIndex;
use bdk_wallet::KeychainKind;
use std::sync::Arc;

#[test]
fn test_hardware_signer() {
let mut devices = match HWIClient::enumerate() {
Ok(devices) => devices,
Err(e) => panic!("Failed to enumerate devices: {}", e),
};

if devices.is_empty() {
panic!("No devices found!");
}

let device = match devices.remove(0) {
Ok(device) => device,
Err(e) => panic!("Failed to remove device: {}", e),
};

let client = match HWIClient::get_client(&device, true, Network::Regtest.into()) {
Ok(client) => client,
Err(e) => panic!("Failed to get client: {}", e),
};

let descriptors = match client.get_descriptors::<String>(None) {
Ok(descriptors) => descriptors,
Err(e) => panic!("Failed to get descriptors: {}", e),
};

let custom_signer = match HWISigner::from_device(&device, Network::Regtest.into()) {
Ok(signer) => signer,
Err(e) => panic!("Failed to create HWISigner: {}", e),
};

let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]);

wallet.add_signer(
KeychainKind::External,
SignerOrdering(200),
Arc::new(custom_signer),
);

let addr = wallet
// ,(AddressIndex::LastUnused)
.peek_address(KeychainKind::External, 0);

let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let mut psbt = builder.finish().expect("Failed to build transaction");

let finalized = wallet
.sign(&mut psbt, Default::default())
.expect("Failed to sign transaction");
assert!(finalized);
}
}
1 change: 1 addition & 0 deletions crates/wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std"]
compiler = ["miniscript/compiler"]
all-keys = ["keys-bip39"]
keys-bip39 = ["bip39"]
test-util = []

[dev-dependencies]
lazy_static = "1.4"
Expand Down
2 changes: 2 additions & 0 deletions crates/wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ use bdk_chain::tx_graph::CalculateFeeError;
pub mod coin_selection;
pub mod export;
pub mod signer;
#[cfg(any(test, feature = "test-util"))]
pub mod test_util;
pub mod tx_builder;
pub(crate) mod utils;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <[email protected]>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

//! Test util
//!
//! Test utilities for the wallet module. Only enabled while testing or when the
//! `test-utils` feature is enabled.

#![allow(unused)]
use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
use bdk_wallet::{
use crate::{
wallet::{Update, Wallet},
KeychainKind, LocalOutput,
};
use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
use bitcoin::{
hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction,
TxIn, TxOut, Txid,
Expand Down Expand Up @@ -121,14 +137,17 @@ pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) {
get_funded_wallet_with_change(descriptor, change)
}

#[allow(missing_docs)]
pub fn get_funded_wallet_wpkh() -> (Wallet, bitcoin::Txid) {
get_funded_wallet_with_change(get_test_wpkh(), get_test_wpkh_change())
}

#[allow(missing_docs)]
pub fn get_test_wpkh() -> &'static str {
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
}

#[allow(missing_docs)]
pub fn get_test_wpkh_with_change_desc() -> (&'static str, &'static str) {
(
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
Expand All @@ -140,50 +159,61 @@ fn get_test_wpkh_change() -> &'static str {
"wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/0)"
}

#[allow(missing_docs)]
pub fn get_test_single_sig_csv() -> &'static str {
// and(pk(Alice),older(6))
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))"
}

#[allow(missing_docs)]
pub fn get_test_a_or_b_plus_csv() -> &'static str {
// or(pk(Alice),and(pk(Bob),older(144)))
"wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))"
}

#[allow(missing_docs)]
pub fn get_test_single_sig_cltv() -> &'static str {
// and(pk(Alice),after(100000))
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))"
}

#[allow(missing_docs)]
pub fn get_test_tr_single_sig() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)"
}

#[allow(missing_docs)]
pub fn get_test_tr_with_taptree() -> &'static str {
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}

#[allow(missing_docs)]
pub fn get_test_tr_with_taptree_both_priv() -> &'static str {
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(cNaQCDwmmh4dS9LzCgVtyy1e1xjCJ21GUDHe9K98nzb689JvinGV)})"
}

#[allow(missing_docs)]
pub fn get_test_tr_repeated_key() -> &'static str {
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})"
}

#[allow(missing_docs)]
pub fn get_test_tr_single_sig_xprv() -> &'static str {
"tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)"
}

#[allow(missing_docs)]
pub fn get_test_tr_single_sig_xprv_with_change_desc() -> (&'static str, &'static str) {
("tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/0/*)",
"tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/1/*)")
}

#[allow(missing_docs)]
pub fn get_test_tr_with_taptree_xprv() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}

#[allow(missing_docs)]
pub fn get_test_tr_dup_keys() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}
Expand Down
4 changes: 2 additions & 2 deletions crates/wallet/tests/psbt.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, TxIn};
use bdk_wallet::{psbt, KeychainKind, SignOptions};
use core::str::FromStr;
mod common;
use common::*;

use bdk_wallet::wallet::test_util::*;

// from bip 174
const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA";
Expand Down
3 changes: 1 addition & 2 deletions crates/wallet/tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ use bitcoin::{
use rand::rngs::StdRng;
use rand::SeedableRng;

mod common;
use common::*;
use bdk_wallet::wallet::test_util::*;

fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) -> OutPoint {
let addr = wallet.next_unused_address(KeychainKind::External).address;
Expand Down
Loading