Skip to content

Commit 061f15a

Browse files
committed
Merge #682: Add a custom signer for hardware wallets
138acc3 Change `populate_test_db` to not return empty input (wszdexdrf) d6e1dd1 Change CI to add test using ledger emulator (wszdexdrf) 7603477 Add a custom signer for hardware wallets (wszdexdrf) Pull request description: Also adds a new test in CI for building and testing on a virtual hardware wallet. ### Description This PR would enable BDK users to sign transactions using a hardware wallet. It is just the beginning hence there are no complex features, but I hope not for long. I have added a test in CI for building a ledger emulator and running the new test on it. The test is similar to the one on bitcoindevkit/rust-hwi. ### Notes to the reviewers The PR is incomplete (and wouldn't work, as the rust-hwi in `cargo.toml` is pointing to a local crate, temporarily) as a small change is required in rust-hwi (bitcoindevkit/rust-hwi#42). ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature * [x] I've updated `CHANGELOG.md` ACKs for top commit: afilini: ACK 138acc3 Tree-SHA512: 54337f06247829242b4dc60f733346173d957de8e9f8b80beb91304d679cfb4e0e4db722c967469265a5b6ede2bd641ba5c089760391c671995dc30de37897de
2 parents 2bff4e5 + 138acc3 commit 061f15a

File tree

9 files changed

+182
-5
lines changed

9 files changed

+182
-5
lines changed

.github/workflows/cont_integration.yml

+29
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,32 @@ jobs:
172172
run: rustup update
173173
- name: Check fmt
174174
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
175+
176+
test_harware_wallet:
177+
runs-on: ubuntu-latest
178+
strategy:
179+
matrix:
180+
rust:
181+
- version: 1.60.0 # STABLE
182+
- version: 1.56.1 # MSRV
183+
steps:
184+
- name: Checkout
185+
uses: actions/checkout@v3
186+
- name: Build simulator image
187+
run: docker build -t hwi/ledger_emulator ./ci -f ci/Dockerfile.ledger
188+
- name: Run simulator image
189+
run: docker run --name simulator --network=host hwi/ledger_emulator &
190+
- name: Install Python
191+
uses: actions/setup-python@v4
192+
with:
193+
python-version: '3.9'
194+
- name: Install python dependencies
195+
run: pip install hwi==2.1.1 protobuf==3.20.1
196+
- name: Set default toolchain
197+
run: rustup default ${{ matrix.rust.version }}
198+
- name: Set profile
199+
run: rustup set profile minimal
200+
- name: Update toolchain
201+
run: rustup update
202+
- name: Test
203+
run: cargo test --features test-hardware-signer

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88
- Add capacity to create FeeRate from sats/kvbytes and sats/kwu.
99
- Rename `as_sat_vb` to `as_sat_per_vb`. Move all `FeeRate` test to `types.rs`.
10+
- Add custom Harware Wallet Signer `HwiSigner` in `src/wallet/harwaresigner/` module.
1011

1112
## [v0.21.0] - [v0.20.0]
1213

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ rocksdb = { version = "0.14", default-features = false, features = ["snappy"], o
3333
cc = { version = ">=1.0.64", optional = true }
3434
socks = { version = "0.3", optional = true }
3535
lazy_static = { version = "1.4", optional = true }
36+
hwi = { version = "0.2.2", optional = true }
3637

3738
bip39 = { version = "1.0.1", optional = true }
3839
bitcoinconsensus = { version = "0.19.0-3", optional = true }
@@ -61,6 +62,7 @@ key-value-db = ["sled"]
6162
all-keys = ["keys-bip39"]
6263
keys-bip39 = ["bip39"]
6364
rpc = ["bitcoincore-rpc"]
65+
hardware-signer = ["hwi"]
6466

6567
# We currently provide mulitple implementations of `Blockchain`, all are
6668
# 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
9395
test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_0_20_0", "test-blockchains"]
9496
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"]
9597
test-md-docs = ["electrum"]
98+
test-hardware-signer = ["hardware-signer"]
9699

97100
[dev-dependencies]
98101
lazy_static = "1.4"

ci/Dockerfile.ledger

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Taken from bitcoindevkit/rust-hwi
2+
FROM ghcr.io/ledgerhq/speculos
3+
4+
RUN apt-get update
5+
RUN apt-get install wget -y
6+
RUN wget "https://github.com/LedgerHQ/speculos/blob/master/apps/nanos%23btc%232.1%231c8db8da.elf?raw=true" -O /speculos/btc.elf
7+
ADD automation.json /speculos/automation.json
8+
9+
ENTRYPOINT ["python", "./speculos.py", "--automation", "file:automation.json", "--display", "headless", "--vnc-port", "41000", "btc.elf"]

ci/automation.json

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"version": 1,
3+
"rules": [
4+
{
5+
"regexp": "Address \\(\\d/\\d\\)|Message hash \\(\\d/\\d\\)|Confirm|Fees|Review|Amount",
6+
"actions": [
7+
[ "button", 2, true ],
8+
[ "button", 2, false ]
9+
]
10+
},
11+
{
12+
"text": "Sign",
13+
"conditions": [
14+
[ "seen", false ]
15+
],
16+
"actions": [
17+
[ "button", 2, true ],
18+
[ "button", 2, false ],
19+
[ "setbool", "seen", true ]
20+
]
21+
},
22+
{
23+
"regexp": "Approve|Sign|Accept",
24+
"actions": [
25+
[ "button", 3, true ],
26+
[ "button", 3, false ]
27+
]
28+
}
29+
]
30+
}

src/database/memory.rs

+4-5
Original file line numberDiff line numberDiff line change
@@ -491,11 +491,10 @@ macro_rules! populate_test_db {
491491
let mut db = $db;
492492
let tx_meta = $tx_meta;
493493
let current_height: Option<u32> = $current_height;
494-
let input = if $is_coinbase {
495-
vec![$crate::bitcoin::TxIn::default()]
496-
} else {
497-
vec![]
498-
};
494+
let mut input = vec![$crate::bitcoin::TxIn::default()];
495+
if !$is_coinbase {
496+
input[0].previous_output.vout = 0;
497+
}
499498
let tx = $crate::bitcoin::Transaction {
500499
version: 1,
501500
lock_time: 0,

src/wallet/hardwaresigner.rs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Bitcoin Dev Kit
2+
// Written in 2020 by Alekos Filini <[email protected]>
3+
//
4+
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
5+
//
6+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
7+
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
8+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
9+
// You may not use this file except in accordance with one or both of these
10+
// licenses.
11+
12+
//! HWI Signer
13+
//!
14+
//! This module contains a simple implementation of a Custom signer for rust-hwi
15+
16+
use bitcoin::psbt::PartiallySignedTransaction;
17+
use bitcoin::secp256k1::{All, Secp256k1};
18+
use bitcoin::util::bip32::Fingerprint;
19+
20+
use hwi::error::Error;
21+
use hwi::types::{HWIChain, HWIDevice};
22+
use hwi::HWIClient;
23+
24+
use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
25+
26+
#[derive(Debug)]
27+
/// Custom signer for Hardware Wallets
28+
///
29+
/// This ignores `sign_options` and leaves the decisions up to the hardware wallet.
30+
pub struct HWISigner {
31+
fingerprint: Fingerprint,
32+
client: HWIClient,
33+
}
34+
35+
impl HWISigner {
36+
/// Create a instance from the specified device and chain
37+
pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
38+
let client = HWIClient::get_client(device, false, chain)?;
39+
Ok(HWISigner {
40+
fingerprint: device.fingerprint,
41+
client,
42+
})
43+
}
44+
}
45+
46+
impl SignerCommon for HWISigner {
47+
fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
48+
SignerId::Fingerprint(self.fingerprint)
49+
}
50+
}
51+
52+
/// This implementation ignores `sign_options`
53+
impl TransactionSigner for HWISigner {
54+
fn sign_transaction(
55+
&self,
56+
psbt: &mut PartiallySignedTransaction,
57+
_sign_options: &crate::SignOptions,
58+
_secp: &crate::wallet::utils::SecpCtx,
59+
) -> Result<(), SignerError> {
60+
psbt.combine(self.client.sign_tx(psbt)?.psbt)
61+
.expect("Failed to combine HW signed psbt with passed PSBT");
62+
Ok(())
63+
}
64+
}

src/wallet/mod.rs

+32
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ pub(crate) mod utils;
4848
#[cfg_attr(docsrs, doc(cfg(feature = "verify")))]
4949
pub mod verify;
5050

51+
#[cfg(feature = "hardware-signer")]
52+
pub mod hardwaresigner;
53+
5154
pub use utils::IsDust;
5255

5356
#[allow(deprecated)]
@@ -5466,4 +5469,33 @@ pub(crate) mod test {
54665469
// ...and checking that everything is fine
54675470
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
54685471
}
5472+
5473+
#[cfg(feature = "test-hardware-signer")]
5474+
#[test]
5475+
fn test_create_signer() {
5476+
use crate::wallet::hardwaresigner::HWISigner;
5477+
use hwi::types::HWIChain;
5478+
use hwi::HWIClient;
5479+
5480+
let devices = HWIClient::enumerate().unwrap();
5481+
let device = devices.first().expect("No devices found");
5482+
let client = HWIClient::get_client(device, true, HWIChain::Regtest).unwrap();
5483+
let descriptors = client.get_descriptors(None).unwrap();
5484+
let custom_signer = HWISigner::from_device(device, HWIChain::Regtest).unwrap();
5485+
5486+
let (mut wallet, _, _) = get_funded_wallet(&descriptors.internal[0]);
5487+
wallet.add_signer(
5488+
KeychainKind::External,
5489+
SignerOrdering(200),
5490+
Arc::new(custom_signer),
5491+
);
5492+
5493+
let addr = wallet.get_address(LastUnused).unwrap();
5494+
let mut builder = wallet.build_tx();
5495+
builder.drain_to(addr.script_pubkey()).drain_wallet();
5496+
let (mut psbt, _) = builder.finish().unwrap();
5497+
5498+
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
5499+
assert!(finalized);
5500+
}
54695501
}

src/wallet/signer.rs

+10
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ pub enum SignerError {
159159
InvalidSighash,
160160
/// Error while computing the hash to sign
161161
SighashError(sighash::Error),
162+
/// Error while signing using hardware wallets
163+
#[cfg(feature = "hardware-signer")]
164+
HWIError(hwi::error::Error),
165+
}
166+
167+
#[cfg(feature = "hardware-signer")]
168+
impl From<hwi::error::Error> for SignerError {
169+
fn from(e: hwi::error::Error) -> Self {
170+
SignerError::HWIError(e)
171+
}
162172
}
163173

164174
impl From<sighash::Error> for SignerError {

0 commit comments

Comments
 (0)