Merge bitcoindevkit/bdk#682: Add a custom signer for hardware wallets
138acc3b7d137788d0518182e2167504e58ebc48 Change `populate_test_db` to not return empty input (wszdexdrf) d6e1dd104063075f49b617786d82d29c1f9c6a0a Change CI to add test using ledger emulator (wszdexdrf) 76034772cba4d3d6fa1bdcb08977c2b9d7a157c2 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 138acc3b7d137788d0518182e2167504e58ebc48 Tree-SHA512: 54337f06247829242b4dc60f733346173d957de8e9f8b80beb91304d679cfb4e0e4db722c967469265a5b6ede2bd641ba5c089760391c671995dc30de37897de
This commit is contained in:
commit
061f15af00
29
.github/workflows/cont_integration.yml
vendored
29
.github/workflows/cont_integration.yml
vendored
@ -172,3 +172,32 @@ jobs:
|
|||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Check fmt
|
- name: Check fmt
|
||||||
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
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
|
||||||
|
@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
- Add capacity to create FeeRate from sats/kvbytes and sats/kwu.
|
- 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`.
|
- 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]
|
## [v0.21.0] - [v0.20.0]
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ rocksdb = { version = "0.14", default-features = false, features = ["snappy"], o
|
|||||||
cc = { version = ">=1.0.64", optional = true }
|
cc = { version = ">=1.0.64", optional = true }
|
||||||
socks = { version = "0.3", optional = true }
|
socks = { version = "0.3", optional = true }
|
||||||
lazy_static = { version = "1.4", optional = true }
|
lazy_static = { version = "1.4", optional = true }
|
||||||
|
hwi = { version = "0.2.2", optional = true }
|
||||||
|
|
||||||
bip39 = { version = "1.0.1", optional = true }
|
bip39 = { version = "1.0.1", optional = true }
|
||||||
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
||||||
@ -61,6 +62,7 @@ key-value-db = ["sled"]
|
|||||||
all-keys = ["keys-bip39"]
|
all-keys = ["keys-bip39"]
|
||||||
keys-bip39 = ["bip39"]
|
keys-bip39 = ["bip39"]
|
||||||
rpc = ["bitcoincore-rpc"]
|
rpc = ["bitcoincore-rpc"]
|
||||||
|
hardware-signer = ["hwi"]
|
||||||
|
|
||||||
# We currently provide mulitple implementations of `Blockchain`, all are
|
# We currently provide mulitple implementations of `Blockchain`, all are
|
||||||
# blocking except for the `EsploraBlockchain` which can be either async or
|
# 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-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-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"]
|
||||||
test-md-docs = ["electrum"]
|
test-md-docs = ["electrum"]
|
||||||
|
test-hardware-signer = ["hardware-signer"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
|
9
ci/Dockerfile.ledger
Normal file
9
ci/Dockerfile.ledger
Normal file
@ -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"]
|
30
ci/automation.json
Normal file
30
ci/automation.json
Normal file
@ -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 ]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -491,11 +491,10 @@ macro_rules! populate_test_db {
|
|||||||
let mut db = $db;
|
let mut db = $db;
|
||||||
let tx_meta = $tx_meta;
|
let tx_meta = $tx_meta;
|
||||||
let current_height: Option<u32> = $current_height;
|
let current_height: Option<u32> = $current_height;
|
||||||
let input = if $is_coinbase {
|
let mut input = vec![$crate::bitcoin::TxIn::default()];
|
||||||
vec![$crate::bitcoin::TxIn::default()]
|
if !$is_coinbase {
|
||||||
} else {
|
input[0].previous_output.vout = 0;
|
||||||
vec![]
|
}
|
||||||
};
|
|
||||||
let tx = $crate::bitcoin::Transaction {
|
let tx = $crate::bitcoin::Transaction {
|
||||||
version: 1,
|
version: 1,
|
||||||
lock_time: 0,
|
lock_time: 0,
|
||||||
|
64
src/wallet/hardwaresigner.rs
Normal file
64
src/wallet/hardwaresigner.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
@ -48,6 +48,9 @@ pub(crate) mod utils;
|
|||||||
#[cfg_attr(docsrs, doc(cfg(feature = "verify")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "verify")))]
|
||||||
pub mod verify;
|
pub mod verify;
|
||||||
|
|
||||||
|
#[cfg(feature = "hardware-signer")]
|
||||||
|
pub mod hardwaresigner;
|
||||||
|
|
||||||
pub use utils::IsDust;
|
pub use utils::IsDust;
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
@ -5466,4 +5469,33 @@ pub(crate) mod test {
|
|||||||
// ...and checking that everything is fine
|
// ...and checking that everything is fine
|
||||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,6 +159,16 @@ pub enum SignerError {
|
|||||||
InvalidSighash,
|
InvalidSighash,
|
||||||
/// Error while computing the hash to sign
|
/// Error while computing the hash to sign
|
||||||
SighashError(sighash::Error),
|
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 {
|
impl From<sighash::Error> for SignerError {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user