Merge bitcoindevkit/bdk#682: Add a custom signer for hardware wallets

138acc3b7d Change `populate_test_db` to not return empty input (wszdexdrf)
d6e1dd1040 Change CI to add test using ledger emulator (wszdexdrf)
76034772cb 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 (https://github.com/bitcoindevkit/rust-hwi/pull/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 138acc3b7d

Tree-SHA512: 54337f06247829242b4dc60f733346173d957de8e9f8b80beb91304d679cfb4e0e4db722c967469265a5b6ede2bd641ba5c089760391c671995dc30de37897de
This commit is contained in:
Alekos Filini
2022-08-29 16:14:33 +02:00
9 changed files with 182 additions and 5 deletions

View File

@@ -491,11 +491,10 @@ macro_rules! populate_test_db {
let mut db = $db;
let tx_meta = $tx_meta;
let current_height: Option<u32> = $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,

View File

@@ -0,0 +1,64 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// 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.
//! 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<HWISigner, Error> {
let client = HWIClient::get_client(device, false, chain)?;
Ok(HWISigner {
fingerprint: device.fingerprint,
client,
})
}
}
impl SignerCommon for HWISigner {
fn id(&self, _secp: &Secp256k1<All>) -> 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(())
}
}

View File

@@ -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)]
@@ -5466,4 +5469,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);
}
}

View File

@@ -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<hwi::error::Error> for SignerError {
fn from(e: hwi::error::Error) -> Self {
SignerError::HWIError(e)
}
}
impl From<sighash::Error> for SignerError {