Compare commits

...

27 Commits

Author SHA1 Message Date
Steve Myers
2275b5fa9a Bump version to 0.24.0 2022-11-05 14:45:02 -05:00
Steve Myers
a73771eebf Bump version to 0.24.0-rc.1 2022-10-30 12:22:03 -05:00
Steve Myers
5c42102c79 Bump version to 0.24.0 2022-10-26 23:27:30 -05:00
Steve Myers
5d5b2fb88c Merge bitcoindevkit/bdk#765: Fix how descriptor checksums are calculated
648282e602 Update docs and tests based on review comments (Steve Myers)
60057a7bf7 Deprecate backward compatible get_checksum_bytes, get_checksum functions (Steve Myers)
e2a4a5884b Ensure backward compatibility of the  "checksum inception" bug (志宇)
fd34956c29 `get_checksum_bytes` now checks input data for checksum (志宇)

Pull request description:

  ### Description

  Previously, the methods `get_checksum_bytes` and `get_checksum` do not check input data to see whether the input data already has a checksum.

  This PR does the following:

  * Introduce a `exclude_hash: bool` flag for `get_checksum_bytes`, that excludes the checksum portion of the original data when calculating the checksum. In addition to this, if the calculated checksum does not match the original checksum, an error is returned for extra safety.
  * Ensure `Wallet` is still backwards compatible with databases created with the "checksum inception" bug.

  ### Notes to the reviewers

  Thank you.

  ### Changelog notice

  Fix the "checksum inception" bug, where we may accidentally calculate the checksum of a descriptor that already has a checksum.

  ### 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

  #### Bugfixes:

  * [x] This pull request breaks the existing API
  * [x] I've added tests to reproduce the issue which are now passing
  ~* [ ] I'm linking the issue being fixed by this PR~

Top commit has no ACKs.

Tree-SHA512: 7ea2721dcd56459b6996e56a3ddfc3559a0c64869a08f5312a8f0f4fcb5dbef7ac7461a4ab017acde4a62fed02d8a620c402dd384323aba85736610514fcb7e1
2022-10-26 22:39:38 -05:00
Steve Myers
9cb6f70fc0 Merge branch 'master' into fix_wallet_checksum 2022-10-26 22:01:07 -05:00
Steve Myers
5720e38033 Merge bitcoindevkit/bdk#770: Upgrade to rust-bitcoin 0.29
c7a43d941f Remove unused code (Alekos Filini)
1ffd59d469 Upgrade to rust-bitcoin 0.29 (Alekos Filini)
ae4f4e5416 Upgrade `rand` to `0.8` (Alekos Filini)
9854fd34ea Remove deprecated address validators (Alekos Filini)

Pull request description:

  ### Description

  Upgrade BDK to rust-bitcoin 0.29

  Missing pieces:

  - [x] rust-miniscript `update_output_with_descriptor` - rust-bitcoin/rust-miniscript#465
  - [x] rust-miniscript 8.0.0 release - rust-bitcoin/rust-miniscript#462
  - [x] Upgrade rust-hwi to bitcoin 0.29 bitcoindevkit/rust-hwi#50
  - [x] Upgrade esplora-client to bitcoin 0.29 https://github.com/bitcoindevkit/rust-esplora-client/pull/20
  - [x] Upgrade rand to 0.8 like secp256k1 did

  ### Notes to the reviewers

  The commits still need to be reordered and cleaned up

  ### Changelog notice

  - Upgrade rust-bitcoin to 0.29
  - Remove deprecated "address validators"

  ### 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

ACKs for top commit:
  notmandatory:
    ACK c7a43d941f

Tree-SHA512: 718a1baf3613b31ec1de39fe63467ebee38617963a4ce0670a617e20fe4f46a57c5786933cdde6cfad9fc76ce0af08843f58844fb4a89f5948cb42c697f802ef
2022-10-26 21:46:41 -05:00
Steve Myers
648282e602 Update docs and tests based on review comments 2022-10-25 11:20:22 -05:00
Alekos Filini
c7a43d941f Remove unused code 2022-10-25 12:14:36 +02:00
Alekos Filini
1ffd59d469 Upgrade to rust-bitcoin 0.29 2022-10-25 11:16:02 +02:00
Alekos Filini
ae4f4e5416 Upgrade rand to 0.8 2022-10-25 11:15:59 +02:00
Alekos Filini
9854fd34ea Remove deprecated address validators 2022-10-25 11:08:47 +02:00
Steve Myers
60057a7bf7 Deprecate backward compatible get_checksum_bytes, get_checksum functions
Rename replacement functions calc_checksum_bytes and calc_checksum
2022-10-24 14:24:54 -05:00
Alekos Filini
ea47d7a35b Merge bitcoindevkit/bdk#758: Add HWI example in docs
1437e1ecfe Add the hardware_signer example (Daniela Brozzoni)
1a71eb1f47 Update the hardwaresigner module documentation (Daniela Brozzoni)
0695e9fb3e Bump HWI to 0.2.3 (Daniela Brozzoni)
a4a43ea860 Re-export HWI if the hardware-signer feature is set (Daniela Brozzoni)

Pull request description:

  ### Description

  ### Notes to the reviewers

  ### Changelog notice

  - bdk re-exports the `hwi` create when the feature `hardware-signer` is on
  - Add `examples/hardware_signer.rs`

  ### 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

ACKs for top commit:
  afilini:
    ACK 1437e1ecfe

Tree-SHA512: 181f4d14dce11e19497fbf30e0af8de21c2c210d37129d7d879ed5670ed09a25be1c8d371389c431e18df9e76870cf5e4afe7b29a6c05fe59b3e1816bc8cf673
2022-10-24 10:53:39 +02:00
Steve Myers
f2181f5467 Merge bitcoindevkit/bdk#782: Make psbt mod public and add required docs
34987d58ec Make psbt mod public and add required docs (Steve Myers)

Pull request description:

  ### Description

  Make psbt mod public and add required docs. The module needs to be public so `bdk-ffi` can expose the new PSBT `fee_amount()` and `fee_rate()` functions.

  ### Notes to the reviewers

  I should have done this as part of #728.

  ### Changelog notice

  Make psbt module public to expose PsbtUtils trait to downstream projects.

  ### 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:

  * [ ] I've added tests for the new feature
  * [x] I've added docs for the new feature

ACKs for top commit:
  rajarshimaitra:
    Concept + tACK 34987d58ec

Tree-SHA512: 99e91e948bccb7593a3da3ac5468232103d4ba90ad4e5888ef6aebb0d16511ad3a3286951779789c05587b4bb996bc359baa28b0f4c3c55e29b24bfc12a10073
2022-10-21 17:58:47 -05:00
Steve Myers
34987d58ec Make psbt mod public and add required docs 2022-10-18 15:26:12 -05:00
Daniela Brozzoni
1c76084db8 Merge bitcoindevkit/bdk#779: Add signature grinding for ECDSA signatures
68dd6d2031 Add signature grinding for ECDSA signatures (Vladimir Fomene)

Pull request description:

  ### Description

  This PR adds a new field called `allow_grinding`
  in the Signer's `SignOptions` struct that is used
  to determine whether or not to grind an ECDSA signature during the signing process.

  ### Changelog notice

  Breaking change: the BDK Signer now produces low-R signatures by default, saving one byte. If you want to preserve the original behavior, set `allow_grinding` in the `SignOptions` to `false`.

  ### Notes to the reviewers

  This PR resolves issue #695

  #### 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:

  * [ ] I've added tests for the new feature
  * [x] I've added docs for the new feature

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [ ] I've added tests to reproduce the issue which are now passing
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  danielabrozzoni:
    ACK 68dd6d2031
  rajarshimaitra:
    ACK 68dd6d2031

Tree-SHA512: 6472338c611b4b32986cf66fcd313ef84f17f5b0ae9e7991ea7da47142641ab812f8b325d4d18314e1a58abe462683101160e62e2363a048fdab3f18aee4d699
2022-10-17 11:48:19 +01:00
Vladimir Fomene
68dd6d2031 Add signature grinding for ECDSA signatures
This PR adds a new field called `allow_grinding`
in the Signer's `SignOptions` struct that is used
to determine whether or not to grind an ECDSA signature
during the signing process.
2022-10-17 12:27:35 +03:00
Daniela Brozzoni
1437e1ecfe Add the hardware_signer example 2022-10-13 10:46:49 +01:00
Daniela Brozzoni
1a71eb1f47 Update the hardwaresigner module documentation
Add a little example on how to use the HWISigner, slightly improve
the module description
2022-10-13 10:46:47 +01:00
Daniela Brozzoni
0695e9fb3e Bump HWI to 0.2.3 2022-10-12 14:24:09 +01:00
Daniela Brozzoni
a4a43ea860 Re-export HWI if the hardware-signer feature is set 2022-10-12 14:23:42 +01:00
Daniela Brozzoni
b627455b8f Merge bitcoindevkit/bdk#780: Update psbt_signer example to use descriptor! macro
1331193800 Update psbt_signer example to use descriptor! macro (Steve Myers)

Pull request description:

  ### Description

  This is a small fix to the psbt_signer example to also use the `descriptor!` macro.

  ### Notes to the reviewers

  I also added more docs to at the beginning of the example.

  ### Changelog notice

  None

  ### 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 Example:

  * [x] I've added docs for the new example

ACKs for top commit:
  danielabrozzoni:
    ACK 1331193800

Tree-SHA512: 602fa317313dea77bc4804abce500db33d5834625704019c6590ae6b80cf339cbaddffef667eaef2696e8e769756a2c2405c84109409ef33816db60d3df4d53d
2022-10-12 11:07:29 +01:00
Steve Myers
1331193800 Update psbt_signer example to use descriptor! macro 2022-10-09 21:39:04 -05:00
Steve Myers
7de8be46c0 Add enhancement request github issue template 2022-10-01 10:14:37 -05:00
Alekos Filini
55145f57a1 Bump version to 0.23.0 2022-09-29 20:57:36 +02:00
志宇
e2a4a5884b Ensure backward compatibility of the "checksum inception" bug
`Wallet` stores the descriptors' checksum in the database for safety.
Previously, the checksum used was a checksum of a descriptor that
already had a checksum.

This PR allows for backward-compatibility of databases created with this
bug.
2022-09-29 14:45:24 +08:00
志宇
fd34956c29 get_checksum_bytes now checks input data for checksum
If `exclude_hash` is set, we split the input data, and if a checksum
already existed within the original data, we check the calculated
checksum against the original checksum.

Additionally, the implementation of `IntoWalletDescriptor` for `&str`
has been refactored for clarity.
2022-09-29 13:06:03 +08:00
38 changed files with 971 additions and 1229 deletions

View File

@@ -0,0 +1,17 @@
---
name: Enhancement request
about: Request a new feature or change to an existing feature
title: ''
labels: 'enhancement'
assignees: ''
---
**Describe the enhancement**
<!-- A clear and concise description of what you would like added or changed. -->
**Use case**
<!-- Tell us how you or others will use this new feature or change to an existing feature. -->
**Additional context**
<!-- Add any other context about the enhancement here. -->

View File

@@ -154,7 +154,7 @@ jobs:
- name: Update toolchain - name: Update toolchain
run: rustup update run: rustup update
- name: Check - name: Check
run: cargo check --target wasm32-unknown-unknown --features use-esplora-async --no-default-features run: cargo check --target wasm32-unknown-unknown --features use-esplora-async,dev-getrandom-wasm --no-default-features
fmt: fmt:
name: Rust fmt name: Rust fmt

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "bdk" name = "bdk"
version = "0.22.0" version = "0.24.0"
edition = "2018" edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"] authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org" homepage = "https://bitcoindevkit.org"
@@ -14,16 +14,16 @@ license = "MIT OR Apache-2.0"
[dependencies] [dependencies]
bdk-macros = "^0.6" bdk-macros = "^0.6"
log = "^0.4" log = "^0.4"
miniscript = { version = "7.0", features = ["use-serde"] } miniscript = { version = "8.0", features = ["serde"] }
bitcoin = { version = "0.28.1", features = ["use-serde", "base64", "rand"] } bitcoin = { version = "0.29.1", features = ["serde", "base64", "rand"] }
serde = { version = "^1.0", features = ["derive"] } serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" } serde_json = { version = "^1.0" }
rand = "^0.7" rand = "^0.8"
# Optional dependencies # Optional dependencies
sled = { version = "0.34", optional = true } sled = { version = "0.34", optional = true }
electrum-client = { version = "0.11", optional = true } electrum-client = { version = "0.12", optional = true }
esplora-client = { version = "0.1.1", default-features = false, optional = true } esplora-client = { version = "0.2", default-features = false, optional = true }
rusqlite = { version = "0.27.0", optional = true } rusqlite = { version = "0.27.0", optional = true }
ahash = { version = "0.7.6", optional = true } ahash = { version = "0.7.6", optional = true }
futures = { version = "0.3", optional = true } futures = { version = "0.3", optional = true }
@@ -31,22 +31,22 @@ async-trait = { version = "0.1", optional = true }
rocksdb = { version = "0.14", default-features = false, features = ["snappy"], optional = true } rocksdb = { version = "0.14", default-features = false, features = ["snappy"], optional = true }
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 }
hwi = { version = "0.2.2", optional = true } hwi = { version = "0.3.0", 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 }
# Needed by bdk_blockchain_tests macro and the `rpc` feature # Needed by bdk_blockchain_tests macro and the `rpc` feature
bitcoincore-rpc = { version = "0.15", optional = true } bitcoincore-rpc = { version = "0.16", optional = true }
# Platform-specific dependencies # Platform-specific dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["rt"] } tokio = { version = "1", features = ["rt"] }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = "0.2"
async-trait = "0.1" async-trait = "0.1"
js-sys = "0.3" js-sys = "0.3"
rand = { version = "^0.7", features = ["wasm-bindgen"] }
[features] [features]
minimal = [] minimal = []
@@ -98,13 +98,18 @@ test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoi
test-md-docs = ["electrum"] test-md-docs = ["electrum"]
test-hardware-signer = ["hardware-signer"] test-hardware-signer = ["hardware-signer"]
# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended
# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support
dev-getrandom-wasm = ["getrandom/js"]
[dev-dependencies] [dev-dependencies]
lazy_static = "1.4" lazy_static = "1.4"
env_logger = "0.7" env_logger = "0.7"
electrsd = "0.20" electrsd = "0.21"
# Move back to importing from rust-bitcoin once https://github.com/rust-bitcoin/rust-bitcoin/pull/1342 is released
base64 = "^0.13"
[[example]]
name = "address_validator"
[[example]] [[example]]
name = "compact_filters_balance" name = "compact_filters_balance"
required-features = ["compact_filters"] required-features = ["compact_filters"]
@@ -128,6 +133,11 @@ name = "psbt_signer"
path = "examples/psbt_signer.rs" path = "examples/psbt_signer.rs"
required-features = ["electrum"] required-features = ["electrum"]
[[example]]
name = "hardware_signer"
path = "examples/hardware_signer.rs"
required-features = ["electrum", "hardware-signer"]
[workspace] [workspace]
members = ["macros"] members = ["macros"]
[package.metadata.docs.rs] [package.metadata.docs.rs]

View File

@@ -95,7 +95,7 @@ use bdk::blockchain::ElectrumBlockchain;
use bdk::electrum_client::Client; use bdk::electrum_client::Client;
use bdk::wallet::AddressIndex::New; use bdk::wallet::AddressIndex::New;
use bitcoin::base64; use base64;
use bitcoin::consensus::serialize; use bitcoin::consensus::serialize;
fn main() -> Result<(), bdk::Error> { fn main() -> Result<(), bdk::Error> {
@@ -132,7 +132,7 @@ fn main() -> Result<(), bdk::Error> {
```rust,no_run ```rust,no_run
use bdk::{Wallet, SignOptions, database::MemoryDatabase}; use bdk::{Wallet, SignOptions, database::MemoryDatabase};
use bitcoin::base64; use base64;
use bitcoin::consensus::deserialize; use bitcoin::consensus::deserialize;
fn main() -> Result<(), bdk::Error> { fn main() -> Result<(), bdk::Error> {
@@ -171,6 +171,17 @@ cargo test --features test-electrum
The other options are `test-esplora`, `test-rpc` or `test-rpc-legacy` which runs against an older version of Bitcoin Core. The other options are `test-esplora`, `test-rpc` or `test-rpc-legacy` which runs against an older version of Bitcoin Core.
Note that `electrs` and `bitcoind` binaries are automatically downloaded (on mac and linux), to specify you already have installed binaries you must use `--no-default-features` and provide `BITCOIND_EXE` and `ELECTRS_EXE` as environment variables. Note that `electrs` and `bitcoind` binaries are automatically downloaded (on mac and linux), to specify you already have installed binaries you must use `--no-default-features` and provide `BITCOIND_EXE` and `ELECTRS_EXE` as environment variables.
## Running under WASM
If you want to run this library under WASM you will probably have to add the following lines to you `Cargo.toml`:
```toml
[dependencies]
getrandom = { version = "0.2", features = ["js"] }
```
This enables the `rand` crate to work in environments where JavaScript is available. See [this link](https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support) to learn more.
## License ## License
Licensed under either of Licensed under either of

View File

@@ -1,63 +0,0 @@
// 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.
use std::sync::Arc;
use bdk::bitcoin;
use bdk::database::MemoryDatabase;
use bdk::descriptor::HdKeyPaths;
#[allow(deprecated)]
use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError};
use bdk::KeychainKind;
use bdk::Wallet;
use bdk::wallet::AddressIndex::New;
use bitcoin::hashes::hex::FromHex;
use bitcoin::util::bip32::Fingerprint;
use bitcoin::{Network, Script};
#[derive(Debug)]
struct DummyValidator;
#[allow(deprecated)]
impl AddressValidator for DummyValidator {
fn validate(
&self,
keychain: KeychainKind,
hd_keypaths: &HdKeyPaths,
script: &Script,
) -> Result<(), AddressValidatorError> {
let (_, path) = hd_keypaths
.values()
.find(|(fing, _)| fing == &Fingerprint::from_hex("bc123c3e").unwrap())
.ok_or(AddressValidatorError::InvalidScript)?;
println!(
"Validating `{:?}` {} address, script: {}",
keychain, path, script
);
Ok(())
}
}
fn main() -> Result<(), bdk::Error> {
let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))";
let mut wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
#[allow(deprecated)]
wallet.add_address_validator(Arc::new(DummyValidator));
wallet.get_address(New)?;
wallet.get_address(New)?;
wallet.get_address(New)?;
Ok(())
}

103
examples/hardware_signer.rs Normal file
View File

@@ -0,0 +1,103 @@
use bdk::bitcoin::{Address, Network};
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
use bdk::database::MemoryDatabase;
use bdk::hwi::{types::HWIChain, HWIClient};
use bdk::signer::SignerOrdering;
use bdk::wallet::{hardwaresigner::HWISigner, AddressIndex};
use bdk::{FeeRate, KeychainKind, SignOptions, SyncOptions, Wallet};
use electrum_client::Client;
use std::str::FromStr;
use std::sync::Arc;
// This example shows how to sync a wallet, create a transaction, sign it
// and broadcast it using an external hardware wallet.
// The hardware wallet must be connected to the computer and unlocked before
// running the example. Also, the `hwi` python package should be installed
// and available in the environment.
//
// To avoid loss of funds, consider using an hardware wallet simulator:
// * Coldcard: https://github.com/Coldcard/firmware
// * Ledger: https://github.com/LedgerHQ/speculos
// * Trezor: https://docs.trezor.io/trezor-firmware/core/emulator/index.html
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Hold tight, I'm connecting to your hardware wallet...");
// Listing all the available hardware wallet devices...
let devices = HWIClient::enumerate()?;
let first_device = devices
.first()
.expect("No devices found. Either plug in a hardware wallet, or start a simulator.");
// ...and creating a client out of the first one
let client = HWIClient::get_client(first_device, true, HWIChain::Test)?;
println!("Look what I found, a {}!", first_device.model);
// Getting the HW's public descriptors
let descriptors = client.get_descriptors(None)?;
println!(
"The hardware wallet's descriptor is: {}",
descriptors.receive[0]
);
// Creating a custom signer from the device
let custom_signer = HWISigner::from_device(first_device, HWIChain::Test)?;
let mut wallet = Wallet::new(
&descriptors.receive[0],
Some(&descriptors.internal[0]),
Network::Testnet,
MemoryDatabase::default(),
)?;
// Adding the hardware signer to the BDK wallet
wallet.add_signer(
KeychainKind::External,
SignerOrdering(200),
Arc::new(custom_signer),
);
// create client for Blockstream's testnet electrum server
let blockchain =
ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
println!("Syncing the wallet...");
wallet.sync(&blockchain, SyncOptions::default())?;
// get deposit address
let deposit_address = wallet.get_address(AddressIndex::New)?;
let balance = wallet.get_balance()?;
println!("Wallet balances in SATs: {}", balance);
if balance.get_total() < 10000 {
println!(
"Send some sats from the u01.net testnet faucet to address '{addr}'.\nFaucet URL: https://bitcoinfaucet.uo1.net/?to={addr}",
addr = deposit_address.address
);
return Ok(());
}
let return_address = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")?;
let (mut psbt, _details) = {
let mut builder = wallet.build_tx();
builder
.drain_wallet()
.drain_to(return_address.script_pubkey())
.enable_rbf()
.fee_rate(FeeRate::from_sat_per_vb(5.0));
builder.finish()?
};
// `sign` will call the hardware wallet asking for a signature
assert!(
wallet.sign(&mut psbt, SignOptions::default())?,
"The hardware wallet couldn't finalize the transaction :("
);
println!("Let's broadcast your tx...");
let raw_transaction = psbt.extract_tx();
let txid = raw_transaction.txid();
blockchain.broadcast(&raw_transaction)?;
println!("Transaction broadcasted! TXID: {txid}.\nExplorer URL: https://mempool.space/testnet/tx/{txid}", txid = txid);
Ok(())
}

View File

@@ -9,20 +9,38 @@
use bdk::blockchain::{Blockchain, ElectrumBlockchain}; use bdk::blockchain::{Blockchain, ElectrumBlockchain};
use bdk::database::MemoryDatabase; use bdk::database::MemoryDatabase;
use bdk::wallet::AddressIndex; use bdk::wallet::AddressIndex;
use bdk::SyncOptions; use bdk::{descriptor, SyncOptions};
use bdk::{FeeRate, SignOptions, Wallet}; use bdk::{FeeRate, SignOptions, Wallet};
use bitcoin::secp256k1::Secp256k1;
use bitcoin::{Address, Network}; use bitcoin::{Address, Network};
use electrum_client::Client; use electrum_client::Client;
use miniscript::descriptor::DescriptorSecretKey;
use std::error::Error; use std::error::Error;
use std::str::FromStr; use std::str::FromStr;
/// This example shows how to sign and broadcast the transaction for a PSBT (Partially Signed
/// Bitcoin Transaction) for a single key, witness public key hash (WPKH) based descriptor wallet.
/// The electrum protocol is used to sync blockchain data from the testnet bitcoin network and
/// wallet data is stored in an ephemeral in-memory database. The process steps are:
/// 1. Create a "signing" wallet and a "watch-only" wallet based on the same private keys.
/// 2. Deposit testnet funds into the watch only wallet.
/// 3. Sync the watch only wallet and create a spending transaction to return all funds to the testnet faucet.
/// 4. Sync the signing wallet and sign and finalize the PSBT created by the watch only wallet.
/// 5. Broadcast the transactions from the finalized PSBT.
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
// test keys created with `bdk-cli key generate` and `bdk-cli key derive` commands // test key created with `bdk-cli key generate` and `bdk-cli key derive` commands
let signing_external_descriptor = "wpkh([e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/0/*)"; let external_secret_xkey = DescriptorSecretKey::from_str("[e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/0/*").unwrap();
let signing_internal_descriptor = "wpkh([e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/1/*)"; let internal_secret_xkey = DescriptorSecretKey::from_str("[e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/1/*").unwrap();
let watch_only_external_descriptor = "wpkh([e9824965/84'/1'/0']tpubDCcguXsm6uj79fSQt4V2EF7SF5b26zCuG2ZZNsbNQuw5G9YWSJuGhg2KknQBywRq4VGTu41zYTCh3QeVFyBdbsymgRX9Mrts94SW7obEdqs/0/*)"; let secp = Secp256k1::new();
let watch_only_internal_descriptor = "wpkh([e9824965/84'/1'/0']tpubDCcguXsm6uj79fSQt4V2EF7SF5b26zCuG2ZZNsbNQuw5G9YWSJuGhg2KknQBywRq4VGTu41zYTCh3QeVFyBdbsymgRX9Mrts94SW7obEdqs/1/*)"; let external_public_xkey = external_secret_xkey.to_public(&secp).unwrap();
let internal_public_xkey = internal_secret_xkey.to_public(&secp).unwrap();
let signing_external_descriptor = descriptor!(wpkh(external_secret_xkey)).unwrap();
let signing_internal_descriptor = descriptor!(wpkh(internal_secret_xkey)).unwrap();
let watch_only_external_descriptor = descriptor!(wpkh(external_public_xkey)).unwrap();
let watch_only_internal_descriptor = descriptor!(wpkh(internal_public_xkey)).unwrap();
// create client for Blockstream's testnet electrum server // create client for Blockstream's testnet electrum server
let blockchain = let blockchain =

View File

@@ -194,7 +194,7 @@ impl_from!(boxed rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")]
/// ); /// );
/// # } /// # }
/// ``` /// ```
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum AnyBlockchainConfig { pub enum AnyBlockchainConfig {
#[cfg(feature = "electrum")] #[cfg(feature = "electrum")]

View File

@@ -479,7 +479,7 @@ impl WalletSync for CompactFiltersBlockchain {
} }
/// Data to connect to a Bitcoin P2P peer /// Data to connect to a Bitcoin P2P peer
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
pub struct BitcoinPeerConfig { pub struct BitcoinPeerConfig {
/// Peer address such as 127.0.0.1:18333 /// Peer address such as 127.0.0.1:18333
pub address: String, pub address: String,
@@ -490,7 +490,7 @@ pub struct BitcoinPeerConfig {
} }
/// Configuration for a [`CompactFiltersBlockchain`] /// Configuration for a [`CompactFiltersBlockchain`]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
pub struct CompactFiltersBlockchainConfig { pub struct CompactFiltersBlockchainConfig {
/// List of peers to try to connect to for asking headers and filters /// List of peers to try to connect to for asking headers and filters
pub peers: Vec<BitcoinPeerConfig>, pub peers: Vec<BitcoinPeerConfig>,

View File

@@ -75,7 +75,10 @@ impl Mempool {
/// Look-up a transaction in the mempool given an [`Inventory`] request /// Look-up a transaction in the mempool given an [`Inventory`] request
pub fn get_tx(&self, inventory: &Inventory) -> Option<Transaction> { pub fn get_tx(&self, inventory: &Inventory) -> Option<Transaction> {
let identifer = match inventory { let identifer = match inventory {
Inventory::Error | Inventory::Block(_) | Inventory::WitnessBlock(_) => return None, Inventory::Error
| Inventory::Block(_)
| Inventory::WitnessBlock(_)
| Inventory::CompactBlock(_) => return None,
Inventory::Transaction(txid) => TxIdentifier::Txid(*txid), Inventory::Transaction(txid) => TxIdentifier::Txid(*txid),
Inventory::WitnessTransaction(txid) => TxIdentifier::Txid(*txid), Inventory::WitnessTransaction(txid) => TxIdentifier::Txid(*txid),
Inventory::WTx(wtxid) => TxIdentifier::Wtxid(*wtxid), Inventory::WTx(wtxid) => TxIdentifier::Wtxid(*wtxid),

View File

@@ -103,42 +103,42 @@ where
} }
impl Encodable for BundleStatus { impl Encodable for BundleStatus {
fn consensus_encode<W: Write>(&self, mut e: W) -> Result<usize, std::io::Error> { fn consensus_encode<W: Write + ?Sized>(&self, e: &mut W) -> Result<usize, std::io::Error> {
let mut written = 0; let mut written = 0;
match self { match self {
BundleStatus::Init => { BundleStatus::Init => {
written += 0x00u8.consensus_encode(&mut e)?; written += 0x00u8.consensus_encode(e)?;
} }
BundleStatus::CfHeaders { cf_headers } => { BundleStatus::CfHeaders { cf_headers } => {
written += 0x01u8.consensus_encode(&mut e)?; written += 0x01u8.consensus_encode(e)?;
written += VarInt(cf_headers.len() as u64).consensus_encode(&mut e)?; written += VarInt(cf_headers.len() as u64).consensus_encode(e)?;
for header in cf_headers { for header in cf_headers {
written += header.consensus_encode(&mut e)?; written += header.consensus_encode(e)?;
} }
} }
BundleStatus::CFilters { cf_filters } => { BundleStatus::CFilters { cf_filters } => {
written += 0x02u8.consensus_encode(&mut e)?; written += 0x02u8.consensus_encode(e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?; written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
for filter in cf_filters { for filter in cf_filters {
written += filter.consensus_encode(&mut e)?; written += filter.consensus_encode(e)?;
} }
} }
BundleStatus::Processed { cf_filters } => { BundleStatus::Processed { cf_filters } => {
written += 0x03u8.consensus_encode(&mut e)?; written += 0x03u8.consensus_encode(e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?; written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
for filter in cf_filters { for filter in cf_filters {
written += filter.consensus_encode(&mut e)?; written += filter.consensus_encode(e)?;
} }
} }
BundleStatus::Pruned => { BundleStatus::Pruned => {
written += 0x04u8.consensus_encode(&mut e)?; written += 0x04u8.consensus_encode(e)?;
} }
BundleStatus::Tip { cf_filters } => { BundleStatus::Tip { cf_filters } => {
written += 0x05u8.consensus_encode(&mut e)?; written += 0x05u8.consensus_encode(e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?; written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
for filter in cf_filters { for filter in cf_filters {
written += filter.consensus_encode(&mut e)?; written += filter.consensus_encode(e)?;
} }
} }
} }
@@ -148,51 +148,53 @@ impl Encodable for BundleStatus {
} }
impl Decodable for BundleStatus { impl Decodable for BundleStatus {
fn consensus_decode<D: Read>(mut d: D) -> Result<Self, bitcoin::consensus::encode::Error> { fn consensus_decode<D: Read + ?Sized>(
let byte_type = u8::consensus_decode(&mut d)?; d: &mut D,
) -> Result<Self, bitcoin::consensus::encode::Error> {
let byte_type = u8::consensus_decode(d)?;
match byte_type { match byte_type {
0x00 => Ok(BundleStatus::Init), 0x00 => Ok(BundleStatus::Init),
0x01 => { 0x01 => {
let num = VarInt::consensus_decode(&mut d)?; let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize; let num = num.0 as usize;
let mut cf_headers = Vec::with_capacity(num); let mut cf_headers = Vec::with_capacity(num);
for _ in 0..num { for _ in 0..num {
cf_headers.push(FilterHeader::consensus_decode(&mut d)?); cf_headers.push(FilterHeader::consensus_decode(d)?);
} }
Ok(BundleStatus::CfHeaders { cf_headers }) Ok(BundleStatus::CfHeaders { cf_headers })
} }
0x02 => { 0x02 => {
let num = VarInt::consensus_decode(&mut d)?; let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize; let num = num.0 as usize;
let mut cf_filters = Vec::with_capacity(num); let mut cf_filters = Vec::with_capacity(num);
for _ in 0..num { for _ in 0..num {
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?); cf_filters.push(Vec::<u8>::consensus_decode(d)?);
} }
Ok(BundleStatus::CFilters { cf_filters }) Ok(BundleStatus::CFilters { cf_filters })
} }
0x03 => { 0x03 => {
let num = VarInt::consensus_decode(&mut d)?; let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize; let num = num.0 as usize;
let mut cf_filters = Vec::with_capacity(num); let mut cf_filters = Vec::with_capacity(num);
for _ in 0..num { for _ in 0..num {
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?); cf_filters.push(Vec::<u8>::consensus_decode(d)?);
} }
Ok(BundleStatus::Processed { cf_filters }) Ok(BundleStatus::Processed { cf_filters })
} }
0x04 => Ok(BundleStatus::Pruned), 0x04 => Ok(BundleStatus::Pruned),
0x05 => { 0x05 => {
let num = VarInt::consensus_decode(&mut d)?; let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize; let num = num.0 as usize;
let mut cf_filters = Vec::with_capacity(num); let mut cf_filters = Vec::with_capacity(num);
for _ in 0..num { for _ in 0..num {
cf_filters.push(Vec::<u8>::consensus_decode(&mut d)?); cf_filters.push(Vec::<u8>::consensus_decode(d)?);
} }
Ok(BundleStatus::Tip { cf_filters }) Ok(BundleStatus::Tip { cf_filters })
@@ -276,7 +278,11 @@ impl ChainStore<Full> {
} }
pub fn start_snapshot(&self, from: usize) -> Result<ChainStore<Snapshot>, CompactFiltersError> { pub fn start_snapshot(&self, from: usize) -> Result<ChainStore<Snapshot>, CompactFiltersError> {
let new_cf_name: String = thread_rng().sample_iter(&Alphanumeric).take(16).collect(); let new_cf_name: String = thread_rng()
.sample_iter(&Alphanumeric)
.map(|byte| byte as char)
.take(16)
.collect();
let new_cf_name = format!("_headers:{}", new_cf_name); let new_cf_name = format!("_headers:{}", new_cf_name);
let mut write_store = self.store.write().unwrap(); let mut write_store = self.store.write().unwrap();
@@ -647,7 +653,7 @@ impl CfStore {
&first_key, &first_key,
( (
BundleStatus::Init, BundleStatus::Init,
filter.filter_header(&FilterHeader::from_hash(Default::default())), filter.filter_header(&FilterHeader::from_hash(Hash::all_zeros())),
) )
.serialize(), .serialize(),
)?; )?;

View File

@@ -14,6 +14,7 @@ use std::sync::{Arc, Mutex};
use std::time::Duration; use std::time::Duration;
use bitcoin::hash_types::{BlockHash, FilterHeader}; use bitcoin::hash_types::{BlockHash, FilterHeader};
use bitcoin::hashes::Hash;
use bitcoin::network::message::NetworkMessage; use bitcoin::network::message::NetworkMessage;
use bitcoin::network::message_blockdata::GetHeadersMessage; use bitcoin::network::message_blockdata::GetHeadersMessage;
use bitcoin::util::bip158::BlockFilter; use bitcoin::util::bip158::BlockFilter;
@@ -254,7 +255,7 @@ where
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new( peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
locators_vec, locators_vec,
Default::default(), Hash::all_zeros(),
)))?; )))?;
let (mut snapshot, mut last_hash) = if let NetworkMessage::Headers(headers) = peer let (mut snapshot, mut last_hash) = if let NetworkMessage::Headers(headers) = peer
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))? .recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?
@@ -276,7 +277,7 @@ where
while sync_height < peer.get_version().start_height as usize { while sync_height < peer.get_version().start_height as usize {
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new( peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
vec![last_hash], vec![last_hash],
Default::default(), Hash::all_zeros(),
)))?; )))?;
if let NetworkMessage::Headers(headers) = peer if let NetworkMessage::Headers(headers) = peer
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))? .recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?

View File

@@ -296,7 +296,7 @@ impl<'a, 'b, D: Database> TxCache<'a, 'b, D> {
} }
/// Configuration for an [`ElectrumBlockchain`] /// Configuration for an [`ElectrumBlockchain`]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
pub struct ElectrumBlockchainConfig { pub struct ElectrumBlockchainConfig {
/// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with `ssl://` or `tcp://` and include a port /// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with `ssl://` or `tcp://` and include a port
/// ///

View File

@@ -125,8 +125,9 @@ impl GetTx for EsploraBlockchain {
#[maybe_async] #[maybe_async]
impl GetBlockHash for EsploraBlockchain { impl GetBlockHash for EsploraBlockchain {
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> { fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
let block_header = await_or_block!(self.url_client.get_header(height as u32))?; Ok(await_or_block!(self
Ok(block_header.block_hash()) .url_client
.get_block_hash(height as u32))?)
} }
} }

View File

@@ -110,8 +110,7 @@ impl GetTx for EsploraBlockchain {
impl GetBlockHash for EsploraBlockchain { impl GetBlockHash for EsploraBlockchain {
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> { fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
let block_header = self.url_client.get_header(height as u32)?; Ok(self.url_client.get_block_hash(height as u32)?)
Ok(block_header.block_hash())
} }
} }

View File

@@ -33,7 +33,7 @@ mod blocking;
pub use self::blocking::*; pub use self::blocking::*;
/// Configuration for an [`EsploraBlockchain`] /// Configuration for an [`EsploraBlockchain`]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
pub struct EsploraBlockchainConfig { pub struct EsploraBlockchainConfig {
/// Base URL of the esplora service /// Base URL of the esplora service
/// ///

View File

@@ -35,7 +35,7 @@ use crate::bitcoin::hashes::hex::ToHex;
use crate::bitcoin::{Network, OutPoint, Transaction, TxOut, Txid}; use crate::bitcoin::{Network, OutPoint, Transaction, TxOut, Txid};
use crate::blockchain::*; use crate::blockchain::*;
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::descriptor::get_checksum; use crate::descriptor::calc_checksum;
use crate::error::MissingCachedScripts; use crate::error::MissingCachedScripts;
use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails}; use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
use bitcoin::Script; use bitcoin::Script;
@@ -77,7 +77,7 @@ impl Deref for RpcBlockchain {
} }
/// RpcBlockchain configuration options /// RpcBlockchain configuration options
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct RpcConfig { pub struct RpcConfig {
/// The bitcoin node url /// The bitcoin node url
pub url: String, pub url: String,
@@ -96,7 +96,7 @@ pub struct RpcConfig {
/// In general, BDK tries to sync `scriptPubKey`s cached in [`crate::database::Database`] with /// In general, BDK tries to sync `scriptPubKey`s cached in [`crate::database::Database`] with
/// `scriptPubKey`s imported in the Bitcoin Core Wallet. These parameters are used for determining /// `scriptPubKey`s imported in the Bitcoin Core Wallet. These parameters are used for determining
/// how the `importdescriptors` RPC calls are to be made. /// how the `importdescriptors` RPC calls are to be made.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct RpcSyncParams { pub struct RpcSyncParams {
/// The minimum number of scripts to scan for on initial sync. /// The minimum number of scripts to scan for on initial sync.
pub start_script_count: usize, pub start_script_count: usize,
@@ -167,7 +167,7 @@ impl Blockchain for RpcBlockchain {
.estimate_smart_fee(target as u16, None)? .estimate_smart_fee(target as u16, None)?
.fee_rate .fee_rate
.ok_or(Error::FeeRateUnavailable)? .ok_or(Error::FeeRateUnavailable)?
.as_sat() as f64; .to_sat() as f64;
Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32)) Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32))
} }
@@ -410,7 +410,12 @@ impl<'a, D: BatchDatabase> DbState<'a, D> {
updated = true; updated = true;
TransactionDetails { TransactionDetails {
txid: tx_res.info.txid, txid: tx_res.info.txid,
..Default::default() transaction: None,
received: 0,
sent: 0,
fee: None,
confirmation_time: None,
} }
}); });
@@ -430,7 +435,7 @@ impl<'a, D: BatchDatabase> DbState<'a, D> {
// update fee (if needed) // update fee (if needed)
if let (None, Some(new_fee)) = (db_tx.fee, tx_res.detail.fee) { if let (None, Some(new_fee)) = (db_tx.fee, tx_res.detail.fee) {
updated = true; updated = true;
db_tx.fee = Some(new_fee.as_sat().unsigned_abs()); db_tx.fee = Some(new_fee.to_sat().unsigned_abs());
} }
// update confirmation time (if needed) // update confirmation time (if needed)
@@ -603,7 +608,7 @@ impl<'a, D: BatchDatabase> DbState<'a, D> {
LocalUtxo { LocalUtxo {
outpoint: OutPoint::new(entry.txid, entry.vout), outpoint: OutPoint::new(entry.txid, entry.vout),
txout: TxOut { txout: TxOut {
value: entry.amount.as_sat(), value: entry.amount.to_sat(),
script_pubkey: entry.script_pub_key, script_pubkey: entry.script_pub_key,
}, },
keychain, keychain,
@@ -801,7 +806,7 @@ fn is_wallet_descriptor(client: &Client) -> Result<bool, Error> {
fn descriptor_from_script_pubkey(script: &Script) -> String { fn descriptor_from_script_pubkey(script: &Script) -> String {
let desc = format!("raw({})", script.to_hex()); let desc = format!("raw({})", script.to_hex());
format!("{}#{}", desc, get_checksum(&desc).unwrap()) format!("{}#{}", desc, calc_checksum(&desc).unwrap())
} }
/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`] /// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`]
@@ -873,15 +878,13 @@ impl BlockchainFactory for RpcBlockchainFactory {
mod test { mod test {
use super::*; use super::*;
use crate::{ use crate::{
descriptor::{into_wallet_descriptor_checked, AsDerived}, descriptor::into_wallet_descriptor_checked, testutils::blockchain_tests::TestClient,
testutils::blockchain_tests::TestClient,
wallet::utils::SecpCtx, wallet::utils::SecpCtx,
}; };
use bitcoin::{Address, Network}; use bitcoin::{Address, Network};
use bitcoincore_rpc::RpcApi; use bitcoincore_rpc::RpcApi;
use log::LevelFilter; use log::LevelFilter;
use miniscript::DescriptorTrait;
crate::bdk_blockchain_tests! { crate::bdk_blockchain_tests! {
fn test_instance(test_client: &TestClient) -> RpcBlockchain { fn test_instance(test_client: &TestClient) -> RpcBlockchain {
@@ -958,7 +961,7 @@ mod test {
// generate scripts (1 tx per script) // generate scripts (1 tx per script)
let scripts = (0..TX_COUNT) let scripts = (0..TX_COUNT)
.map(|index| desc.as_derived(index, &secp).script_pubkey()) .map(|index| desc.at_derivation_index(index).script_pubkey())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// import scripts and wait // import scripts and wait

View File

@@ -497,7 +497,7 @@ macro_rules! populate_test_db {
} }
let tx = $crate::bitcoin::Transaction { let tx = $crate::bitcoin::Transaction {
version: 1, version: 1,
lock_time: 0, lock_time: bitcoin::PackedLockTime(0),
input, input,
output: tx_meta output: tx_meta
.output .output

View File

@@ -41,12 +41,24 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
c c
} }
/// Computes the checksum bytes of a descriptor /// Computes the checksum bytes of a descriptor.
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> { /// `exclude_hash = true` ignores all data after the first '#' (inclusive).
pub(crate) fn calc_checksum_bytes_internal(
mut desc: &str,
exclude_hash: bool,
) -> Result<[u8; 8], DescriptorError> {
let mut c = 1; let mut c = 1;
let mut cls = 0; let mut cls = 0;
let mut clscount = 0; let mut clscount = 0;
let mut original_checksum = None;
if exclude_hash {
if let Some(split) = desc.split_once('#') {
desc = split.0;
original_checksum = Some(split.1);
}
}
for ch in desc.as_bytes() { for ch in desc.as_bytes() {
let pos = INPUT_CHARSET let pos = INPUT_CHARSET
.iter() .iter()
@@ -72,37 +84,96 @@ pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize]; checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
} }
// if input data already had a checksum, check calculated checksum against original checksum
if let Some(original_checksum) = original_checksum {
if original_checksum.as_bytes() != checksum {
return Err(DescriptorError::InvalidDescriptorChecksum);
}
}
Ok(checksum) Ok(checksum)
} }
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
pub fn calc_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
calc_checksum_bytes_internal(desc, true)
}
/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation
pub fn calc_checksum(desc: &str) -> Result<String, DescriptorError> {
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
calc_checksum_bytes_internal(desc, true)
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
}
// TODO in release 0.25.0, remove get_checksum_bytes and get_checksum
// TODO in release 0.25.0, consolidate calc_checksum_bytes_internal into calc_checksum_bytes
/// Compute the checksum bytes of a descriptor
#[deprecated(
since = "0.24.0",
note = "Use new `calc_checksum_bytes` function which excludes any existing checksum in the descriptor string before calculating the checksum hash bytes. See https://github.com/bitcoindevkit/bdk/pull/765."
)]
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
calc_checksum_bytes_internal(desc, false)
}
/// Compute the checksum of a descriptor /// Compute the checksum of a descriptor
#[deprecated(
since = "0.24.0",
note = "Use new `calc_checksum` function which excludes any existing checksum in the descriptor string before calculating the checksum hash. See https://github.com/bitcoindevkit/bdk/pull/765."
)]
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> { pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET` // unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
get_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) }) calc_checksum_bytes_internal(desc, false)
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::descriptor::get_checksum; use crate::descriptor::calc_checksum;
// test get_checksum() function; it should return the same value as Bitcoin Core // test calc_checksum() function; it should return the same value as Bitcoin Core
#[test] #[test]
fn test_get_checksum() { fn test_calc_checksum() {
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"; let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)";
assert_eq!(get_checksum(desc).unwrap(), "tqz0nc62"); assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)"; let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)";
assert_eq!(get_checksum(desc).unwrap(), "lasegmfs"); assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
}
// test calc_checksum() function; it should return the same value as Bitcoin Core even if the
// descriptor string includes a checksum hash
#[test]
fn test_calc_checksum_with_checksum_hash() {
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62";
assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmfs";
assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26";
assert!(matches!(
calc_checksum(desc).err(),
Some(DescriptorError::InvalidDescriptorChecksum)
));
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf";
assert!(matches!(
calc_checksum(desc).err(),
Some(DescriptorError::InvalidDescriptorChecksum)
));
} }
#[test] #[test]
fn test_get_checksum_invalid_character() { fn test_calc_checksum_invalid_character() {
let sparkle_heart = unsafe { std::str::from_utf8_unchecked(&[240, 159, 146, 150]) }; let sparkle_heart = unsafe { std::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart); let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
assert!(matches!( assert!(matches!(
get_checksum(&invalid_desc).err(), calc_checksum(&invalid_desc).err(),
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0] Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
)); ));
} }

View File

@@ -1,210 +0,0 @@
// 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.
//! Derived descriptor keys
//!
//! The [`DerivedDescriptorKey`] type is a wrapper over the standard [`DescriptorPublicKey`] which
//! guarantees that all the extended keys have a fixed derivation path, i.e. all the wildcards have
//! been replaced by actual derivation indexes.
//!
//! The [`AsDerived`] trait provides a quick way to derive descriptors to obtain a
//! `Descriptor<DerivedDescriptorKey>` type. This, in turn, can be used to derive public
//! keys for arbitrary derivation indexes.
//!
//! Combining this with [`Wallet::get_signers`], secret keys can also be derived.
//!
//! # Example
//!
//! ```
//! # use std::str::FromStr;
//! # use bitcoin::secp256k1::Secp256k1;
//! use bdk::descriptor::{AsDerived, DescriptorPublicKey};
//! use bdk::miniscript::{ToPublicKey, TranslatePk, MiniscriptKey};
//!
//! let secp = Secp256k1::gen_new();
//!
//! let key = DescriptorPublicKey::from_str("[aa600a45/84'/0'/0']tpubDCbDXFKoLTQp44wQuC12JgSn5g9CWGjZdpBHeTqyypZ4VvgYjTJmK9CkyR5bFvG9f4PutvwmvpYCLkFx2rpx25hiMs4sUgxJveW8ZzSAVAc/0/*")?;
//! let (descriptor, _, _) = bdk::descriptor!(wpkh(key))?;
//!
//! // derived: wpkh([aa600a45/84'/0'/0']tpubDCbDXFKoLTQp44wQuC12JgSn5g9CWGjZdpBHeTqyypZ4VvgYjTJmK9CkyR5bFvG9f4PutvwmvpYCLkFx2rpx25hiMs4sUgxJveW8ZzSAVAc/0/42)#3ladd0t2
//! let derived = descriptor.as_derived(42, &secp);
//! println!("derived: {}", derived);
//!
//! // with_pks: wpkh(02373ecb54c5e83bd7e0d40adf78b65efaf12fafb13571f0261fc90364eee22e1e)#p4jjgvll
//! let with_pks = derived.translate_pk_infallible(|pk| pk.to_public_key(), |pkh| pkh.to_public_key().to_pubkeyhash());
//! println!("with_pks: {}", with_pks);
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! [`Wallet::get_signers`]: crate::wallet::Wallet::get_signers
use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use bitcoin::hashes::hash160;
use bitcoin::{PublicKey, XOnlyPublicKey};
use miniscript::descriptor::{DescriptorSinglePub, SinglePubKey, Wildcard};
use miniscript::{Descriptor, DescriptorPublicKey, MiniscriptKey, ToPublicKey, TranslatePk};
use crate::wallet::utils::SecpCtx;
/// Extended [`DescriptorPublicKey`] that has been derived
///
/// Derived keys are guaranteed to never contain wildcards of any kind
#[derive(Debug, Clone)]
pub struct DerivedDescriptorKey<'s>(DescriptorPublicKey, &'s SecpCtx);
impl<'s> DerivedDescriptorKey<'s> {
/// Construct a new derived key
///
/// Panics if the key is wildcard
pub fn new(key: DescriptorPublicKey, secp: &'s SecpCtx) -> DerivedDescriptorKey<'s> {
if let DescriptorPublicKey::XPub(xpub) = &key {
assert!(xpub.wildcard == Wildcard::None)
}
DerivedDescriptorKey(key, secp)
}
}
impl<'s> Deref for DerivedDescriptorKey<'s> {
type Target = DescriptorPublicKey;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'s> PartialEq for DerivedDescriptorKey<'s> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<'s> Eq for DerivedDescriptorKey<'s> {}
impl<'s> PartialOrd for DerivedDescriptorKey<'s> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.0.partial_cmp(&other.0)
}
}
impl<'s> Ord for DerivedDescriptorKey<'s> {
fn cmp(&self, other: &Self) -> Ordering {
self.0.cmp(&other.0)
}
}
impl<'s> fmt::Display for DerivedDescriptorKey<'s> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl<'s> Hash for DerivedDescriptorKey<'s> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl<'s> MiniscriptKey for DerivedDescriptorKey<'s> {
type Hash = Self;
fn to_pubkeyhash(&self) -> Self::Hash {
DerivedDescriptorKey(self.0.to_pubkeyhash(), self.1)
}
fn is_uncompressed(&self) -> bool {
self.0.is_uncompressed()
}
}
impl<'s> ToPublicKey for DerivedDescriptorKey<'s> {
fn to_public_key(&self) -> PublicKey {
match &self.0 {
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::XOnly(_),
..
}) => panic!("Found x-only public key in non-tr descriptor"),
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::FullKey(ref pk),
..
}) => *pk,
DescriptorPublicKey::XPub(ref xpub) => PublicKey::new(
xpub.xkey
.derive_pub(self.1, &xpub.derivation_path)
.expect("Shouldn't fail, only normal derivations")
.public_key,
),
}
}
fn to_x_only_pubkey(&self) -> XOnlyPublicKey {
match &self.0 {
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::XOnly(ref pk),
..
}) => *pk,
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
key: SinglePubKey::FullKey(ref pk),
..
}) => XOnlyPublicKey::from(pk.inner),
DescriptorPublicKey::XPub(ref xpub) => XOnlyPublicKey::from(
xpub.xkey
.derive_pub(self.1, &xpub.derivation_path)
.expect("Shouldn't fail, only normal derivations")
.public_key,
),
}
}
fn hash_to_hash160(hash: &Self::Hash) -> hash160::Hash {
hash.to_public_key().to_pubkeyhash()
}
}
/// Utilities to derive descriptors
///
/// Check out the [module level] documentation for more.
///
/// [module level]: crate::descriptor::derived
pub trait AsDerived {
/// Derive a descriptor and transform all of its keys to `DerivedDescriptorKey`
fn as_derived<'s>(&self, index: u32, secp: &'s SecpCtx)
-> Descriptor<DerivedDescriptorKey<'s>>;
/// Transform the keys into `DerivedDescriptorKey`.
///
/// Panics if the descriptor is not "fixed", i.e. if it's derivable
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>>;
}
impl AsDerived for Descriptor<DescriptorPublicKey> {
fn as_derived<'s>(
&self,
index: u32,
secp: &'s SecpCtx,
) -> Descriptor<DerivedDescriptorKey<'s>> {
self.derive(index).translate_pk_infallible(
|key| DerivedDescriptorKey::new(key.clone(), secp),
|key| DerivedDescriptorKey::new(key.clone(), secp),
)
}
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>> {
assert!(!self.is_deriveable());
self.as_derived(0, secp)
}
}

View File

@@ -700,10 +700,10 @@ macro_rules! fragment {
$crate::keys::make_pkh($key, &secp) $crate::keys::make_pkh($key, &secp)
}); });
( after ( $value:expr ) ) => ({ ( after ( $value:expr ) ) => ({
$crate::impl_leaf_opcode_value!(After, $value) $crate::impl_leaf_opcode_value!(After, $crate::bitcoin::PackedLockTime($value)) // TODO!! https://github.com/rust-bitcoin/rust-bitcoin/issues/1302
}); });
( older ( $value:expr ) ) => ({ ( older ( $value:expr ) ) => ({
$crate::impl_leaf_opcode_value!(Older, $value) $crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!!
}); });
( sha256 ( $hash:expr ) ) => ({ ( sha256 ( $hash:expr ) ) => ({
$crate::impl_leaf_opcode_value!(Sha256, $hash) $crate::impl_leaf_opcode_value!(Sha256, $hash)
@@ -795,7 +795,7 @@ macro_rules! fragment {
mod test { mod test {
use bitcoin::hashes::hex::ToHex; use bitcoin::hashes::hex::ToHex;
use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap}; use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
use miniscript::{Descriptor, Legacy, Segwitv0}; use miniscript::{Descriptor, Legacy, Segwitv0};
use std::str::FromStr; use std::str::FromStr;
@@ -806,8 +806,6 @@ mod test {
use bitcoin::util::bip32; use bitcoin::util::bip32;
use bitcoin::PrivateKey; use bitcoin::PrivateKey;
use crate::descriptor::derived::AsDerived;
// test the descriptor!() macro // test the descriptor!() macro
// verify descriptor generates expected script(s) (if bare or pk) or address(es) // verify descriptor generates expected script(s) (if bare or pk) or address(es)
@@ -817,17 +815,15 @@ mod test {
is_fixed: bool, is_fixed: bool,
expected: &[&str], expected: &[&str],
) { ) {
let secp = Secp256k1::new();
let (desc, _key_map, _networks) = desc.unwrap(); let (desc, _key_map, _networks) = desc.unwrap();
assert_eq!(desc.is_witness(), is_witness); assert_eq!(desc.is_witness(), is_witness);
assert_eq!(!desc.is_deriveable(), is_fixed); assert_eq!(!desc.has_wildcard(), is_fixed);
for i in 0..expected.len() { for i in 0..expected.len() {
let index = i as u32; let index = i as u32;
let child_desc = if !desc.is_deriveable() { let child_desc = if !desc.has_wildcard() {
desc.as_derived_fixed(&secp) desc.at_derivation_index(0)
} else { } else {
desc.as_derived(index, &secp) desc.at_derivation_index(index)
}; };
let address = child_desc.address(Regtest); let address = child_desc.address(Regtest);
if let Ok(address) = address { if let Ok(address) = address {

View File

@@ -15,32 +15,30 @@
//! from [`miniscript`]. //! from [`miniscript`].
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Deref;
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource}; use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
use bitcoin::util::{psbt, taproot}; use bitcoin::util::{psbt, taproot};
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey}; use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
use bitcoin::{Network, Script, TxOut}; use bitcoin::{Network, TxOut};
use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey}; use miniscript::descriptor::{DefiniteDescriptorKey, DescriptorType, InnerXKey, SinglePubKey};
pub use miniscript::{ pub use miniscript::{
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor, descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0, DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
}; };
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk}; use miniscript::{ForEachKey, MiniscriptKey, TranslatePk};
use crate::descriptor::policy::BuildSatisfaction; use crate::descriptor::policy::BuildSatisfaction;
pub mod checksum; pub mod checksum;
pub mod derived;
#[doc(hidden)] #[doc(hidden)]
pub mod dsl; pub mod dsl;
pub mod error; pub mod error;
pub mod policy; pub mod policy;
pub mod template; pub mod template;
pub use self::checksum::get_checksum; pub use self::checksum::calc_checksum;
pub use self::derived::{AsDerived, DerivedDescriptorKey}; use self::checksum::calc_checksum_bytes;
pub use self::error::Error as DescriptorError; pub use self::error::Error as DescriptorError;
pub use self::policy::Policy; pub use self::policy::Policy;
use self::template::DescriptorTemplateOut; use self::template::DescriptorTemplateOut;
@@ -52,7 +50,7 @@ use crate::wallet::utils::SecpCtx;
pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>; pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
/// Alias for a [`Descriptor`] that contains extended **derived** keys /// Alias for a [`Descriptor`] that contains extended **derived** keys
pub type DerivedDescriptor<'s> = Descriptor<DerivedDescriptorKey<'s>>; pub type DerivedDescriptor = Descriptor<DefiniteDescriptorKey>;
/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or /// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or
/// [`psbt::Output`] /// [`psbt::Output`]
@@ -84,19 +82,15 @@ impl IntoWalletDescriptor for &str {
secp: &SecpCtx, secp: &SecpCtx,
network: Network, network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let descriptor = if self.contains('#') { let descriptor = match self.split_once('#') {
let parts: Vec<&str> = self.splitn(2, '#').collect(); Some((desc, original_checksum)) => {
if !get_checksum(parts[0]) let checksum = calc_checksum_bytes(desc)?;
.ok() if original_checksum.as_bytes() != checksum {
.map(|computed| computed == parts[1]) return Err(DescriptorError::InvalidDescriptorChecksum);
.unwrap_or(false) }
{ desc
return Err(DescriptorError::InvalidDescriptorChecksum);
} }
None => self,
parts[0]
} else {
self
}; };
ExtendedDescriptor::parse_descriptor(secp, descriptor)? ExtendedDescriptor::parse_descriptor(secp, descriptor)?
@@ -132,28 +126,76 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
use crate::keys::DescriptorKey; use crate::keys::DescriptorKey;
let check_key = |pk: &DescriptorPublicKey| { struct Translator<'s, 'd> {
let (pk, _, networks) = if self.0.is_witness() { secp: &'s SecpCtx,
let descriptor_key: DescriptorKey<miniscript::Segwitv0> = descriptor: &'d ExtendedDescriptor,
pk.clone().into_descriptor_key()?; network: Network,
descriptor_key.extract(secp)? }
} else {
let descriptor_key: DescriptorKey<miniscript::Legacy> =
pk.clone().into_descriptor_key()?;
descriptor_key.extract(secp)?
};
if networks.contains(&network) { impl<'s, 'd>
Ok(pk) miniscript::Translator<DescriptorPublicKey, miniscript::DummyKey, DescriptorError>
} else { for Translator<'s, 'd>
Err(DescriptorError::Key(KeyError::InvalidNetwork)) {
fn pk(
&mut self,
pk: &DescriptorPublicKey,
) -> Result<miniscript::DummyKey, DescriptorError> {
let secp = &self.secp;
let (_, _, networks) = if self.descriptor.is_taproot() {
let descriptor_key: DescriptorKey<miniscript::Tap> =
pk.clone().into_descriptor_key()?;
descriptor_key.extract(secp)?
} else if self.descriptor.is_witness() {
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
pk.clone().into_descriptor_key()?;
descriptor_key.extract(secp)?
} else {
let descriptor_key: DescriptorKey<miniscript::Legacy> =
pk.clone().into_descriptor_key()?;
descriptor_key.extract(secp)?
};
if networks.contains(&self.network) {
Ok(miniscript::DummyKey)
} else {
Err(DescriptorError::Key(KeyError::InvalidNetwork))
}
} }
}; fn sha256(
&mut self,
_sha256: &<DescriptorPublicKey as MiniscriptKey>::Sha256,
) -> Result<miniscript::DummySha256Hash, DescriptorError> {
Ok(Default::default())
}
fn hash256(
&mut self,
_hash256: &<DescriptorPublicKey as MiniscriptKey>::Hash256,
) -> Result<miniscript::DummyHash256Hash, DescriptorError> {
Ok(Default::default())
}
fn ripemd160(
&mut self,
_ripemd160: &<DescriptorPublicKey as MiniscriptKey>::Ripemd160,
) -> Result<miniscript::DummyRipemd160Hash, DescriptorError> {
Ok(Default::default())
}
fn hash160(
&mut self,
_hash160: &<DescriptorPublicKey as MiniscriptKey>::Hash160,
) -> Result<miniscript::DummyHash160Hash, DescriptorError> {
Ok(Default::default())
}
}
// check the network for the keys // check the network for the keys
let translated = self.0.translate_pk(check_key, check_key)?; self.0.translate_pk(&mut Translator {
secp,
network,
descriptor: &self.0,
})?;
Ok((translated, self.1)) Ok(self)
} }
} }
@@ -163,10 +205,17 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
_secp: &SecpCtx, _secp: &SecpCtx,
network: Network, network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let valid_networks = &self.2; struct Translator {
network: Network,
}
let fix_key = |pk: &DescriptorPublicKey| { impl miniscript::Translator<DescriptorPublicKey, DescriptorPublicKey, DescriptorError>
if valid_networks.contains(&network) { for Translator
{
fn pk(
&mut self,
pk: &DescriptorPublicKey,
) -> Result<DescriptorPublicKey, DescriptorError> {
// workaround for xpubs generated by other key types, like bip39: since when the // workaround for xpubs generated by other key types, like bip39: since when the
// conversion is made one network has to be chosen, what we generally choose // conversion is made one network has to be chosen, what we generally choose
// "mainnet", but then override the set of valid networks to specify that all of // "mainnet", but then override the set of valid networks to specify that all of
@@ -175,7 +224,7 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
let pk = match pk { let pk = match pk {
DescriptorPublicKey::XPub(ref xpub) => { DescriptorPublicKey::XPub(ref xpub) => {
let mut xpub = xpub.clone(); let mut xpub = xpub.clone();
xpub.xkey.network = network; xpub.xkey.network = self.network;
DescriptorPublicKey::XPub(xpub) DescriptorPublicKey::XPub(xpub)
} }
@@ -183,13 +232,20 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
}; };
Ok(pk) Ok(pk)
} else {
Err(DescriptorError::Key(KeyError::InvalidNetwork))
} }
}; miniscript::translate_hash_clone!(
DescriptorPublicKey,
DescriptorPublicKey,
DescriptorError
);
}
if !self.2.contains(&network) {
return Err(DescriptorError::Key(KeyError::InvalidNetwork));
}
// fixup the network for keys that need it // fixup the network for keys that need it
let translated = self.0.translate_pk(fix_key, fix_key)?; let translated = self.0.translate_pk(&mut Translator { network })?;
Ok((translated, self.1)) Ok((translated, self.1))
} }
@@ -210,7 +266,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
derivation_path, derivation_path,
wildcard, wildcard,
.. ..
}) = k.as_key() }) = k
{ {
return *wildcard == Wildcard::Hardened return *wildcard == Wildcard::Hardened
|| derivation_path.into_iter().any(ChildNumber::is_hardened); || derivation_path.into_iter().any(ChildNumber::is_hardened);
@@ -257,7 +313,6 @@ pub trait ExtractPolicy {
} }
pub(crate) trait XKeyUtils { pub(crate) trait XKeyUtils {
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath;
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint; fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
} }
@@ -265,27 +320,6 @@ impl<T> XKeyUtils for DescriptorXKey<T>
where where
T: InnerXKey, T: InnerXKey,
{ {
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
let full_path = match self.origin {
Some((_, ref path)) => path
.into_iter()
.chain(self.derivation_path.into_iter())
.cloned()
.collect(),
None => self.derivation_path.clone(),
};
if self.wildcard != Wildcard::None {
full_path
.into_iter()
.chain(append.iter())
.cloned()
.collect()
} else {
full_path
}
}
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint { fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
match self.origin { match self.origin {
Some((fingerprint, _)) => fingerprint, Some((fingerprint, _)) => fingerprint,
@@ -294,11 +328,6 @@ where
} }
} }
pub(crate) trait DerivedDescriptorMeta {
fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths;
fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins;
}
pub(crate) trait DescriptorMeta { pub(crate) trait DescriptorMeta {
fn is_witness(&self) -> bool; fn is_witness(&self) -> bool;
fn is_taproot(&self) -> bool; fn is_taproot(&self) -> bool;
@@ -307,63 +336,23 @@ pub(crate) trait DescriptorMeta {
&self, &self,
hd_keypaths: &HdKeyPaths, hd_keypaths: &HdKeyPaths,
secp: &'s SecpCtx, secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>; ) -> Option<DerivedDescriptor>;
fn derive_from_tap_key_origins<'s>( fn derive_from_tap_key_origins<'s>(
&self, &self,
tap_key_origins: &TapKeyOrigins, tap_key_origins: &TapKeyOrigins,
secp: &'s SecpCtx, secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>; ) -> Option<DerivedDescriptor>;
fn derive_from_psbt_key_origins<'s>( fn derive_from_psbt_key_origins<'s>(
&self, &self,
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>, key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
secp: &'s SecpCtx, secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>; ) -> Option<DerivedDescriptor>;
fn derive_from_psbt_input<'s>( fn derive_from_psbt_input<'s>(
&self, &self,
psbt_input: &psbt::Input, psbt_input: &psbt::Input,
utxo: Option<TxOut>, utxo: Option<TxOut>,
secp: &'s SecpCtx, secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>; ) -> Option<DerivedDescriptor>;
}
pub(crate) trait DescriptorScripts {
fn psbt_redeem_script(&self) -> Option<Script>;
fn psbt_witness_script(&self) -> Option<Script>;
}
impl<'s> DescriptorScripts for DerivedDescriptor<'s> {
fn psbt_redeem_script(&self) -> Option<Script> {
match self.desc_type() {
DescriptorType::ShWpkh => Some(self.explicit_script().unwrap()),
DescriptorType::ShWsh => Some(self.explicit_script().unwrap().to_v0_p2wsh()),
DescriptorType::Sh => Some(self.explicit_script().unwrap()),
DescriptorType::Bare => Some(self.explicit_script().unwrap()),
DescriptorType::ShSortedMulti => Some(self.explicit_script().unwrap()),
DescriptorType::ShWshSortedMulti => Some(self.explicit_script().unwrap().to_v0_p2wsh()),
DescriptorType::Pkh
| DescriptorType::Wpkh
| DescriptorType::Tr
| DescriptorType::Wsh
| DescriptorType::WshSortedMulti => None,
}
}
fn psbt_witness_script(&self) -> Option<Script> {
match self.desc_type() {
DescriptorType::Wsh => Some(self.explicit_script().unwrap()),
DescriptorType::ShWsh => Some(self.explicit_script().unwrap()),
DescriptorType::WshSortedMulti | DescriptorType::ShWshSortedMulti => {
Some(self.explicit_script().unwrap())
}
DescriptorType::Bare
| DescriptorType::Sh
| DescriptorType::Pkh
| DescriptorType::Wpkh
| DescriptorType::ShSortedMulti
| DescriptorType::Tr
| DescriptorType::ShWpkh => None,
}
}
} }
impl DescriptorMeta for ExtendedDescriptor { impl DescriptorMeta for ExtendedDescriptor {
@@ -387,7 +376,7 @@ impl DescriptorMeta for ExtendedDescriptor {
let mut answer = Vec::new(); let mut answer = Vec::new();
self.for_each_key(|pk| { self.for_each_key(|pk| {
if let DescriptorPublicKey::XPub(xpub) = pk.as_key() { if let DescriptorPublicKey::XPub(xpub) = pk {
answer.push(xpub.clone()); answer.push(xpub.clone());
} }
@@ -401,7 +390,7 @@ impl DescriptorMeta for ExtendedDescriptor {
&self, &self,
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>, key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
secp: &'s SecpCtx, secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> { ) -> Option<DerivedDescriptor> {
// Ensure that deriving `xpub` with `path` yields `expected` // Ensure that deriving `xpub` with `path` yields `expected`
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>, let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
path: &DerivationPath, path: &DerivationPath,
@@ -423,7 +412,7 @@ impl DescriptorMeta for ExtendedDescriptor {
// using `for_any_key` should make this stop as soon as we return `true` // using `for_any_key` should make this stop as soon as we return `true`
self.for_any_key(|key| { self.for_any_key(|key| {
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() { if let DescriptorPublicKey::XPub(xpub) = key {
// Check if the key matches one entry in our `key_origins`. If it does, `matches()` will // Check if the key matches one entry in our `key_origins`. If it does, `matches()` will
// return the "prefix" that matched, so we remove that prefix from the full path // return the "prefix" that matched, so we remove that prefix from the full path
// found in `key_origins` and save it in `derive_path`. We expect this to be a derivation // found in `key_origins` and save it in `derive_path`. We expect this to be a derivation
@@ -481,14 +470,14 @@ impl DescriptorMeta for ExtendedDescriptor {
false false
}); });
path_found.map(|path| self.as_derived(path, secp)) path_found.map(|path| self.at_derivation_index(path))
} }
fn derive_from_hd_keypaths<'s>( fn derive_from_hd_keypaths<'s>(
&self, &self,
hd_keypaths: &HdKeyPaths, hd_keypaths: &HdKeyPaths,
secp: &'s SecpCtx, secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> { ) -> Option<DerivedDescriptor> {
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins` // "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
let key_origins = hd_keypaths let key_origins = hd_keypaths
.iter() .iter()
@@ -506,7 +495,7 @@ impl DescriptorMeta for ExtendedDescriptor {
&self, &self,
tap_key_origins: &TapKeyOrigins, tap_key_origins: &TapKeyOrigins,
secp: &'s SecpCtx, secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> { ) -> Option<DerivedDescriptor> {
// "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins` // "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
let key_origins = tap_key_origins let key_origins = tap_key_origins
.iter() .iter()
@@ -520,19 +509,19 @@ impl DescriptorMeta for ExtendedDescriptor {
psbt_input: &psbt::Input, psbt_input: &psbt::Input,
utxo: Option<TxOut>, utxo: Option<TxOut>,
secp: &'s SecpCtx, secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> { ) -> Option<DerivedDescriptor> {
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) { if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
return Some(derived); return Some(derived);
} }
if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) { if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) {
return Some(derived); return Some(derived);
} }
if self.is_deriveable() { if self.has_wildcard() {
// We can't try to bruteforce the derivation index, exit here // We can't try to bruteforce the derivation index, exit here
return None; return None;
} }
let descriptor = self.as_derived_fixed(secp); let descriptor = self.at_derivation_index(0);
match descriptor.desc_type() { match descriptor.desc_type() {
// TODO: add pk() here // TODO: add pk() here
DescriptorType::Pkh DescriptorType::Pkh
@@ -566,86 +555,6 @@ impl DescriptorMeta for ExtendedDescriptor {
} }
} }
impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths {
let mut answer = BTreeMap::new();
self.for_each_key(|key| {
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
let derived_pubkey = xpub
.xkey
.derive_pub(secp, &xpub.derivation_path)
.expect("Derivation can't fail");
answer.insert(
derived_pubkey.public_key,
(xpub.root_fingerprint(secp), xpub.full_path(&[])),
);
}
true
});
answer
}
fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins {
use miniscript::ToPublicKey;
let mut answer = BTreeMap::new();
let mut insert_path = |pk: &DerivedDescriptorKey<'_>, lh| {
let key_origin = match pk.deref() {
DescriptorPublicKey::XPub(xpub) => {
Some((xpub.root_fingerprint(secp), xpub.full_path(&[])))
}
DescriptorPublicKey::SinglePub(_) => None,
};
// If this is the internal key, we only insert the key origin if it's not None.
// For keys found in the tap tree we always insert a key origin (because the signer
// looks for it to know which leaves to sign for), even though it may be None
match (lh, key_origin) {
(None, Some(ko)) => {
answer
.entry(pk.to_x_only_pubkey())
.or_insert_with(|| (vec![], ko));
}
(Some(lh), origin) => {
answer
.entry(pk.to_x_only_pubkey())
.or_insert_with(|| (vec![], origin.unwrap_or_default()))
.0
.push(lh);
}
_ => {}
}
};
if let Descriptor::Tr(tr) = &self {
// Internal key first, then iterate the scripts
insert_path(tr.internal_key(), None);
for (_, ms) in tr.iter_scripts() {
// Assume always the same leaf version
let leaf_hash = taproot::TapLeafHash::from_script(
&ms.encode(),
taproot::LeafVersion::TapScript,
);
for key in ms.iter_pk_pkh() {
let key = match key {
miniscript::miniscript::iter::PkPkh::PlainPubkey(pk) => pk,
miniscript::miniscript::iter::PkPkh::HashedPubkey(pk) => pk,
};
insert_path(&key, Some(leaf_hash));
}
}
}
answer
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::str::FromStr; use std::str::FromStr;
@@ -917,7 +826,7 @@ mod test {
#[test] #[test]
fn test_sh_wsh_sortedmulti_redeemscript() { fn test_sh_wsh_sortedmulti_redeemscript() {
use super::{AsDerived, DescriptorScripts}; use miniscript::psbt::PsbtInputExt;
let secp = Secp256k1::new(); let secp = Secp256k1::new();
@@ -925,11 +834,16 @@ mod test {
let (descriptor, _) = let (descriptor, _) =
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap(); into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
let descriptor = descriptor.as_derived(0, &secp); let descriptor = descriptor.at_derivation_index(0);
let script = Script::from_str("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap(); let script = Script::from_str("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
assert_eq!(descriptor.psbt_redeem_script(), Some(script.to_v0_p2wsh())); let mut psbt_input = psbt::Input::default();
assert_eq!(descriptor.psbt_witness_script(), Some(script)); psbt_input
.update_with_descriptor_unchecked(&descriptor)
.unwrap();
assert_eq!(psbt_input.redeem_script, Some(script.to_v0_p2wsh()));
assert_eq!(psbt_input.witness_script, Some(script));
} }
} }

View File

@@ -43,14 +43,17 @@ use std::fmt;
use serde::ser::SerializeMap; use serde::ser::SerializeMap;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use bitcoin::hashes::*; use bitcoin::hashes::{hash160, ripemd160, sha256};
use bitcoin::util::bip32::Fingerprint; use bitcoin::util::bip32::Fingerprint;
use bitcoin::{PublicKey, XOnlyPublicKey}; use bitcoin::{LockTime, PublicKey, Sequence, XOnlyPublicKey};
use miniscript::descriptor::{ use miniscript::descriptor::{
DescriptorPublicKey, DescriptorSinglePub, ShInner, SinglePubKey, SortedMultiVec, WshInner, DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner,
};
use miniscript::hash256;
use miniscript::{
Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey,
}; };
use miniscript::{Descriptor, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal};
#[allow(unused_imports)] #[allow(unused_imports)]
use log::{debug, error, info, trace}; use log::{debug, error, info, trace};
@@ -58,9 +61,9 @@ use log::{debug, error, info, trace};
use crate::descriptor::ExtractPolicy; use crate::descriptor::ExtractPolicy;
use crate::keys::ExtScriptContext; use crate::keys::ExtScriptContext;
use crate::wallet::signer::{SignerId, SignersContainer}; use crate::wallet::signer::{SignerId, SignersContainer};
use crate::wallet::utils::{self, After, Older, SecpCtx}; use crate::wallet::utils::{After, Older, SecpCtx};
use super::checksum::get_checksum; use super::checksum::calc_checksum;
use super::error::Error; use super::error::Error;
use super::XKeyUtils; use super::XKeyUtils;
use bitcoin::util::psbt::{Input as PsbtInput, PartiallySignedTransaction as Psbt}; use bitcoin::util::psbt::{Input as PsbtInput, PartiallySignedTransaction as Psbt};
@@ -81,11 +84,11 @@ pub enum PkOrF {
impl PkOrF { impl PkOrF {
fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self { fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
match k { match k {
DescriptorPublicKey::SinglePub(DescriptorSinglePub { DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::FullKey(pk), key: SinglePubKey::FullKey(pk),
.. ..
}) => PkOrF::Pubkey(*pk), }) => PkOrF::Pubkey(*pk),
DescriptorPublicKey::SinglePub(DescriptorSinglePub { DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::XOnly(pk), key: SinglePubKey::XOnly(pk),
.. ..
}) => PkOrF::XOnlyPubkey(*pk), }) => PkOrF::XOnlyPubkey(*pk),
@@ -111,7 +114,7 @@ pub enum SatisfiableItem {
/// Double SHA256 preimage hash /// Double SHA256 preimage hash
Hash256Preimage { Hash256Preimage {
/// The digest value /// The digest value
hash: sha256d::Hash, hash: hash256::Hash,
}, },
/// RIPEMD160 preimage hash /// RIPEMD160 preimage hash
Ripemd160Preimage { Ripemd160Preimage {
@@ -125,13 +128,13 @@ pub enum SatisfiableItem {
}, },
/// Absolute timeclock timestamp /// Absolute timeclock timestamp
AbsoluteTimelock { AbsoluteTimelock {
/// The timestamp value /// The timelock value
value: u32, value: LockTime,
}, },
/// Relative timelock locktime /// Relative timelock locktime
RelativeTimelock { RelativeTimelock {
/// The locktime value /// The timelock value
value: u32, value: Sequence,
}, },
/// Multi-signature public keys with threshold count /// Multi-signature public keys with threshold count
Multisig { Multisig {
@@ -165,7 +168,7 @@ impl SatisfiableItem {
/// Returns a unique id for the [`SatisfiableItem`] /// Returns a unique id for the [`SatisfiableItem`]
pub fn id(&self) -> String { pub fn id(&self) -> String {
get_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem")) calc_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem"))
.expect("Failed to compute a SatisfiableItem id") .expect("Failed to compute a SatisfiableItem id")
} }
} }
@@ -438,32 +441,30 @@ pub struct Policy {
} }
/// An extra condition that must be satisfied but that is out of control of the user /// An extra condition that must be satisfied but that is out of control of the user
#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Serialize)] /// TODO: use `bitcoin::LockTime` and `bitcoin::Sequence`
#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Default, Serialize)]
pub struct Condition { pub struct Condition {
/// Optional CheckSequenceVerify condition /// Optional CheckSequenceVerify condition
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub csv: Option<u32>, pub csv: Option<Sequence>,
/// Optional timelock condition /// Optional timelock condition
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub timelock: Option<u32>, pub timelock: Option<LockTime>,
} }
impl Condition { impl Condition {
fn merge_nlocktime(a: u32, b: u32) -> Result<u32, PolicyError> { fn merge_nlocktime(a: LockTime, b: LockTime) -> Result<LockTime, PolicyError> {
if (a < utils::BLOCKS_TIMELOCK_THRESHOLD) != (b < utils::BLOCKS_TIMELOCK_THRESHOLD) { if !a.is_same_unit(b) {
Err(PolicyError::MixedTimelockUnits) Err(PolicyError::MixedTimelockUnits)
} else if a > b {
Ok(a)
} else { } else {
Ok(max(a, b)) Ok(b)
} }
} }
fn merge_nsequence(a: u32, b: u32) -> Result<u32, PolicyError> { fn merge_nsequence(a: Sequence, b: Sequence) -> Result<Sequence, PolicyError> {
let mask = utils::SEQUENCE_LOCKTIME_TYPE_FLAG | utils::SEQUENCE_LOCKTIME_MASK; if a.is_time_locked() != b.is_time_locked() {
let a = a & mask;
let b = b & mask;
if (a < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) != (b < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) {
Err(PolicyError::MixedTimelockUnits) Err(PolicyError::MixedTimelockUnits)
} else { } else {
Ok(max(a, b)) Ok(max(a, b))
@@ -720,15 +721,18 @@ impl From<SatisfiableItem> for Policy {
} }
fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId { fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
// For consistency we always compute the key hash in "ecdsa" form (with the leading sign
// prefix) even if we are in a taproot descriptor. We just want some kind of unique identifier
// for a key, so it doesn't really matter how the identifier is computed.
match key { match key {
DescriptorPublicKey::SinglePub(DescriptorSinglePub { DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::FullKey(pk), key: SinglePubKey::FullKey(pk),
.. ..
}) => pk.to_pubkeyhash().into(), }) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
DescriptorPublicKey::SinglePub(DescriptorSinglePub { DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::XOnly(pk), key: SinglePubKey::XOnly(pk),
.. ..
}) => pk.to_pubkeyhash().into(), }) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(), DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
} }
} }
@@ -779,7 +783,7 @@ fn generic_sig_in_psbt<
) -> bool { ) -> bool {
//TODO check signature validity //TODO check signature validity
psbt.inputs.iter().all(|input| match key { psbt.inputs.iter().all(|input| match key {
DescriptorPublicKey::SinglePub(DescriptorSinglePub { key, .. }) => check(input, key), DescriptorPublicKey::Single(SinglePub { key, .. }) => check(input, key),
DescriptorPublicKey::XPub(xpub) => { DescriptorPublicKey::XPub(xpub) => {
//TODO check actual derivation matches //TODO check actual derivation matches
match extract(input, xpub.root_fingerprint(secp)) { match extract(input, xpub.root_fingerprint(secp)) {
@@ -891,10 +895,13 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp)) Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp))
} }
Terminal::After(value) => { Terminal::After(value) => {
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into(); let mut policy: Policy = SatisfiableItem::AbsoluteTimelock {
value: value.into(),
}
.into();
policy.contribution = Satisfaction::Complete { policy.contribution = Satisfaction::Complete {
condition: Condition { condition: Condition {
timelock: Some(*value), timelock: Some(value.into()),
csv: None, csv: None,
}, },
}; };
@@ -905,9 +912,11 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
} = build_sat } = build_sat
{ {
let after = After::new(Some(current_height), false); let after = After::new(Some(current_height), false);
let after_sat = Satisfier::<bitcoin::PublicKey>::check_after(&after, *value); let after_sat =
let inputs_sat = psbt_inputs_sat(psbt) Satisfier::<bitcoin::PublicKey>::check_after(&after, value.into());
.all(|sat| Satisfier::<bitcoin::PublicKey>::check_after(&sat, *value)); let inputs_sat = psbt_inputs_sat(psbt).all(|sat| {
Satisfier::<bitcoin::PublicKey>::check_after(&sat, value.into())
});
if after_sat && inputs_sat { if after_sat && inputs_sat {
policy.satisfaction = policy.contribution.clone(); policy.satisfaction = policy.contribution.clone();
} }
@@ -999,6 +1008,9 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
Policy::make_thresh(mapped, threshold)? Policy::make_thresh(mapped, threshold)?
} }
// Unsupported
Terminal::RawPkH(_) => None,
}) })
} }
} }
@@ -1124,14 +1136,12 @@ mod test {
use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor}; use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor};
use super::*; use super::*;
use crate::descriptor::derived::AsDerived;
use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh}; use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
use crate::keys::{DescriptorKey, IntoDescriptorKey}; use crate::keys::{DescriptorKey, IntoDescriptorKey};
use crate::wallet::signer::SignersContainer; use crate::wallet::signer::SignersContainer;
use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::Secp256k1;
use bitcoin::util::bip32; use bitcoin::util::bip32;
use bitcoin::Network; use bitcoin::Network;
use miniscript::DescriptorTrait;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
@@ -1329,9 +1339,8 @@ mod test {
let (wallet_desc, keymap) = desc let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet) .into_wallet_descriptor(&secp, Network::Testnet)
.unwrap(); .unwrap();
let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = single_key let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -1343,16 +1352,15 @@ mod test {
let (wallet_desc, keymap) = desc let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet) .into_wallet_descriptor(&secp, Network::Testnet)
.unwrap(); .unwrap();
let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = single_key let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint)); assert!(matches!(policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == fingerprint));
assert!( assert!(
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None) matches!(policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
); );
} }
@@ -1368,21 +1376,20 @@ mod test {
let (wallet_desc, keymap) = desc let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet) .into_wallet_descriptor(&secp, Network::Testnet)
.unwrap(); .unwrap();
let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let policy = single_key let policy = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::None, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert!( assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1 matches!(policy.item, Multisig { keys, threshold } if threshold == 1
&& keys[0] == PkOrF::Fingerprint(fingerprint0) && keys[0] == PkOrF::Fingerprint(fingerprint0)
&& keys[1] == PkOrF::Fingerprint(fingerprint1)) && keys[1] == PkOrF::Fingerprint(fingerprint1))
); );
assert!( assert!(
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2 matches!(policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == 2
&& m == &1 && m == 1
&& items.len() == 2 && items.len() == 2
&& conditions.contains_key(&vec![0]) && conditions.contains_key(&vec![0])
&& conditions.contains_key(&vec![1]) && conditions.contains_key(&vec![1])
@@ -1427,8 +1434,8 @@ mod test {
&& m == &2 && m == &2
&& items.len() == 3 && items.len() == 3
&& conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none() && conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none()
&& conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(sequence) && conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
&& conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(sequence) && conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
) )
); );
} }
@@ -1574,7 +1581,7 @@ mod test {
.unwrap(); .unwrap();
let addr = wallet_desc let addr = wallet_desc
.as_derived(0, &secp) .at_derivation_index(0)
.address(Network::Testnet) .address(Network::Testnet)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -1646,7 +1653,7 @@ mod test {
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let addr = wallet_desc let addr = wallet_desc
.as_derived(0, &secp) .at_derivation_index(0)
.address(Network::Testnet) .address(Network::Testnet)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(

View File

@@ -468,12 +468,10 @@ mod test {
use std::str::FromStr; use std::str::FromStr;
use super::*; use super::*;
use crate::descriptor::derived::AsDerived;
use crate::descriptor::{DescriptorError, DescriptorMeta}; use crate::descriptor::{DescriptorError, DescriptorMeta};
use crate::keys::ValidNetworks; use crate::keys::ValidNetworks;
use bitcoin::network::constants::Network::Regtest; use bitcoin::network::constants::Network::Regtest;
use bitcoin::secp256k1::Secp256k1; use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
use miniscript::Descriptor; use miniscript::Descriptor;
// BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)` // BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
@@ -517,17 +515,15 @@ mod test {
is_fixed: bool, is_fixed: bool,
expected: &[&str], expected: &[&str],
) { ) {
let secp = Secp256k1::new();
let (desc, _key_map, _networks) = desc.unwrap(); let (desc, _key_map, _networks) = desc.unwrap();
assert_eq!(desc.is_witness(), is_witness); assert_eq!(desc.is_witness(), is_witness);
assert_eq!(!desc.is_deriveable(), is_fixed); assert_eq!(!desc.has_wildcard(), is_fixed);
for i in 0..expected.len() { for i in 0..expected.len() {
let index = i as u32; let index = i as u32;
let child_desc = if !desc.is_deriveable() { let child_desc = if !desc.has_wildcard() {
desc.as_derived_fixed(&secp) desc.at_derivation_index(0)
} else { } else {
desc.as_derived(index, &secp) desc.at_derivation_index(index)
}; };
let address = child_desc.address(Regtest).unwrap(); let address = child_desc.address(Regtest).unwrap();
assert_eq!(address.to_string(), *expected.get(i).unwrap()); assert_eq!(address.to_string(), *expected.get(i).unwrap());

View File

@@ -12,7 +12,7 @@
use std::fmt; use std::fmt;
use crate::bitcoin::Network; use crate::bitcoin::Network;
use crate::{descriptor, wallet, wallet::address_validator}; use crate::{descriptor, wallet};
use bitcoin::{OutPoint, Txid}; use bitcoin::{OutPoint, Txid};
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet) /// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
@@ -99,12 +99,12 @@ pub enum Error {
/// Error related to the parsing and usage of descriptors /// Error related to the parsing and usage of descriptors
Descriptor(crate::descriptor::error::Error), Descriptor(crate::descriptor::error::Error),
/// Error that can be returned to fail the validation of an address
AddressValidator(crate::wallet::address_validator::AddressValidatorError),
/// Encoding error /// Encoding error
Encode(bitcoin::consensus::encode::Error), Encode(bitcoin::consensus::encode::Error),
/// Miniscript error /// Miniscript error
Miniscript(miniscript::Error), Miniscript(miniscript::Error),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
/// BIP32 error /// BIP32 error
Bip32(bitcoin::util::bip32::Error), Bip32(bitcoin::util::bip32::Error),
/// An ECDSA error /// An ECDSA error
@@ -149,6 +149,14 @@ pub enum Error {
Rusqlite(rusqlite::Error), Rusqlite(rusqlite::Error),
} }
/// Errors returned by miniscript when updating inconsistent PSBTs
#[derive(Debug, Clone)]
pub enum MiniscriptPsbtError {
Conversion(miniscript::descriptor::ConversionError),
UtxoUpdate(miniscript::psbt::UtxoUpdateError),
OutputUpdate(miniscript::psbt::OutputUpdateError),
}
/// Represents the last failed [`crate::blockchain::WalletSync`] sync attempt in which we were short /// Represents the last failed [`crate::blockchain::WalletSync`] sync attempt in which we were short
/// on cached `scriptPubKey`s. /// on cached `scriptPubKey`s.
#[derive(Debug)] #[derive(Debug)]
@@ -181,7 +189,6 @@ macro_rules! impl_error {
} }
impl_error!(descriptor::error::Error, Descriptor); impl_error!(descriptor::error::Error, Descriptor);
impl_error!(address_validator::AddressValidatorError, AddressValidator);
impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError); impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError);
impl_error!(wallet::signer::SignerError, Signer); impl_error!(wallet::signer::SignerError, Signer);
@@ -198,6 +205,7 @@ impl From<crate::keys::KeyError> for Error {
impl_error!(bitcoin::consensus::encode::Error, Encode); impl_error!(bitcoin::consensus::encode::Error, Encode);
impl_error!(miniscript::Error, Miniscript); impl_error!(miniscript::Error, Miniscript);
impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
impl_error!(bitcoin::util::bip32::Error, Bip32); impl_error!(bitcoin::util::bip32::Error, Bip32);
impl_error!(bitcoin::secp256k1::Error, Secp256k1); impl_error!(bitcoin::secp256k1::Error, Secp256k1);
impl_error!(serde_json::Error, Json); impl_error!(serde_json::Error, Json);

View File

@@ -24,8 +24,8 @@ use bitcoin::{Network, PrivateKey, PublicKey, XOnlyPublicKey};
use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard}; use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
pub use miniscript::descriptor::{ pub use miniscript::descriptor::{
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, KeyMap, DescriptorPublicKey, DescriptorSecretKey, KeyMap, SinglePriv, SinglePub, SinglePubKey,
SinglePubKey, SortedMultiVec, SortedMultiVec,
}; };
pub use miniscript::ScriptContext; pub use miniscript::ScriptContext;
use miniscript::{Miniscript, Terminal}; use miniscript::{Miniscript, Terminal};
@@ -110,7 +110,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
let mut key_map = KeyMap::with_capacity(1); let mut key_map = KeyMap::with_capacity(1);
let public = secret let public = secret
.as_public(secp) .to_public(secp)
.map_err(|e| miniscript::Error::Unexpected(e.to_string()))?; .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
key_map.insert(public.clone(), secret); key_map.insert(public.clone(), secret);
@@ -224,8 +224,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// use bdk::bitcoin::PublicKey; /// use bdk::bitcoin::PublicKey;
/// ///
/// use bdk::keys::{ /// use bdk::keys::{
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub, /// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
/// IntoDescriptorKey, KeyError, ScriptContext, SinglePubKey, /// ScriptContext, SinglePub, SinglePubKey,
/// }; /// };
/// ///
/// pub struct MyKeyType { /// pub struct MyKeyType {
@@ -235,7 +235,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType { /// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> { /// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
/// Ok(DescriptorKey::from_public( /// Ok(DescriptorKey::from_public(
/// DescriptorPublicKey::SinglePub(DescriptorSinglePub { /// DescriptorPublicKey::Single(SinglePub {
/// origin: None, /// origin: None,
/// key: SinglePubKey::FullKey(self.pubkey), /// key: SinglePubKey::FullKey(self.pubkey),
/// }), /// }),
@@ -842,7 +842,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey { impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> { fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
let networks = match self { let networks = match self {
DescriptorPublicKey::SinglePub(_) => any_network(), DescriptorPublicKey::Single(_) => any_network(),
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. }) DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
if xkey.network == Network::Bitcoin => if xkey.network == Network::Bitcoin =>
{ {
@@ -857,7 +857,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey { impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> { fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
DescriptorPublicKey::SinglePub(DescriptorSinglePub { DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::FullKey(self), key: SinglePubKey::FullKey(self),
origin: None, origin: None,
}) })
@@ -867,7 +867,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey { impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> { fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
DescriptorPublicKey::SinglePub(DescriptorSinglePub { DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::XOnly(self), key: SinglePubKey::XOnly(self),
origin: None, origin: None,
}) })
@@ -878,7 +878,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey { impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> { fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
let networks = match &self { let networks = match &self {
DescriptorSecretKey::SinglePriv(sk) if sk.key.network == Network::Bitcoin => { DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => {
mainnet_network() mainnet_network()
} }
DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. }) DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. })
@@ -903,7 +903,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for &'_ str {
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey { impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> { fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
DescriptorSecretKey::SinglePriv(DescriptorSinglePriv { DescriptorSecretKey::Single(SinglePriv {
key: self, key: self,
origin: None, origin: None,
}) })

View File

@@ -203,6 +203,8 @@ pub extern crate miniscript;
extern crate serde; extern crate serde;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
#[cfg(feature = "hardware-signer")]
pub extern crate hwi;
#[cfg(all(feature = "reqwest", feature = "ureq"))] #[cfg(all(feature = "reqwest", feature = "ureq"))]
compile_error!("Features reqwest and ureq are mutually exclusive and cannot be enabled together"); compile_error!("Features reqwest and ureq are mutually exclusive and cannot be enabled together");
@@ -263,7 +265,7 @@ pub mod descriptor;
#[cfg(feature = "test-md-docs")] #[cfg(feature = "test-md-docs")]
mod doctest; mod doctest;
pub mod keys; pub mod keys;
pub(crate) mod psbt; pub mod psbt;
pub(crate) mod types; pub(crate) mod types;
pub mod wallet; pub mod wallet;
@@ -271,7 +273,6 @@ pub use descriptor::template;
pub use descriptor::HdKeyPaths; pub use descriptor::HdKeyPaths;
pub use error::Error; pub use error::Error;
pub use types::*; pub use types::*;
pub use wallet::address_validator;
pub use wallet::signer; pub use wallet::signer;
pub use wallet::signer::SignOptions; pub use wallet::signer::SignOptions;
pub use wallet::tx_builder::TxBuilder; pub use wallet::tx_builder::TxBuilder;

View File

@@ -9,11 +9,17 @@
// You may not use this file except in accordance with one or both of these // You may not use this file except in accordance with one or both of these
// licenses. // licenses.
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
use crate::FeeRate; use crate::FeeRate;
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt; use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use bitcoin::TxOut; use bitcoin::TxOut;
// TODO upstream the functions here to `rust-bitcoin`?
/// Trait to add functions to extract utxos and calculate fees.
pub trait PsbtUtils { pub trait PsbtUtils {
/// Get the `TxOut` for the specified input index, if it doesn't exist in the PSBT `None` is returned.
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>; fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in Sats. /// The total transaction fee amount, sum of input amounts minus sum of output amounts, in Sats.

View File

@@ -1,8 +1,18 @@
// Bitcoin Dev Kit
//
// 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.
use crate::testutils::TestIncomingTx; use crate::testutils::TestIncomingTx;
use bitcoin::consensus::encode::{deserialize, serialize}; use bitcoin::consensus::encode::{deserialize, serialize};
use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::sha256d; use bitcoin::hashes::sha256d;
use bitcoin::{Address, Amount, Script, Transaction, Txid, Witness}; use bitcoin::{Address, Amount, PackedLockTime, Script, Sequence, Transaction, Txid, Witness};
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType; pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi}; pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
use core::str::FromStr; use core::str::FromStr;
@@ -110,7 +120,7 @@ impl TestClient {
if let Some(true) = meta_tx.replaceable { if let Some(true) = meta_tx.replaceable {
// for some reason core doesn't set this field right // for some reason core doesn't set this field right
for input in &mut tx.input { for input in &mut tx.input {
input.sequence = 0xFFFFFFFD; input.sequence = Sequence(0xFFFFFFFD);
} }
} }
@@ -164,6 +174,7 @@ impl TestClient {
use bitcoin::blockdata::script::Builder; use bitcoin::blockdata::script::Builder;
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut}; use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
use bitcoin::hash_types::{BlockHash, TxMerkleNode}; use bitcoin::hash_types::{BlockHash, TxMerkleNode};
use bitcoin::hashes::Hash;
let block_template: serde_json::Value = self let block_template: serde_json::Value = self
.call("getblocktemplate", &[json!({"rules": ["segwit"]})]) .call("getblocktemplate", &[json!({"rules": ["segwit"]})])
@@ -176,7 +187,7 @@ impl TestClient {
block_template["previousblockhash"].as_str().unwrap(), block_template["previousblockhash"].as_str().unwrap(),
) )
.unwrap(), .unwrap(),
merkle_root: TxMerkleNode::default(), merkle_root: TxMerkleNode::all_zeros(),
time: block_template["curtime"].as_u64().unwrap() as u32, time: block_template["curtime"].as_u64().unwrap() as u32,
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(), bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
nonce: 0, nonce: 0,
@@ -184,15 +195,15 @@ impl TestClient {
debug!("header: {:#?}", header); debug!("header: {:#?}", header);
let height = block_template["height"].as_u64().unwrap() as i64; let height = block_template["height"].as_u64().unwrap() as i64;
let witness_reserved_value: Vec<u8> = sha256d::Hash::default().as_ref().into(); let witness_reserved_value: Vec<u8> = sha256d::Hash::all_zeros().as_ref().into();
// burn block subsidy and fees, not a big deal // burn block subsidy and fees, not a big deal
let mut coinbase_tx = Transaction { let mut coinbase_tx = Transaction {
version: 1, version: 1,
lock_time: 0, lock_time: PackedLockTime(0),
input: vec![TxIn { input: vec![TxIn {
previous_output: OutPoint::null(), previous_output: OutPoint::null(),
script_sig: Builder::new().push_int(height).into_script(), script_sig: Builder::new().push_int(height).into_script(),
sequence: 0xFFFFFFFF, sequence: Sequence(0xFFFFFFFF),
witness: Witness::from_vec(vec![witness_reserved_value]), witness: Witness::from_vec(vec![witness_reserved_value]),
}], }],
output: vec![], output: vec![],
@@ -1184,7 +1195,7 @@ macro_rules! bdk_blockchain_tests {
// 5. Verify 25_000 sats are received by test bitcoind node taproot wallet // 5. Verify 25_000 sats are received by test bitcoind node taproot wallet
let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap(); let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap();
assert_eq!(taproot_balance.as_sat(), 25_000, "node has incorrect taproot wallet balance"); assert_eq!(taproot_balance.to_sat(), 25_000, "node has incorrect taproot wallet balance");
} }
#[test] #[test]

View File

@@ -101,25 +101,21 @@ impl TestIncomingTx {
macro_rules! testutils { macro_rules! testutils {
( @external $descriptors:expr, $child:expr ) => ({ ( @external $descriptors:expr, $child:expr ) => ({
use $crate::bitcoin::secp256k1::Secp256k1; use $crate::bitcoin::secp256k1::Secp256k1;
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait}; use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
use $crate::descriptor::AsDerived;
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0; let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
parsed.as_derived($child, &secp).address(bitcoin::Network::Regtest).expect("No address form") parsed.at_derivation_index($child).address(bitcoin::Network::Regtest).expect("No address form")
}); });
( @internal $descriptors:expr, $child:expr ) => ({ ( @internal $descriptors:expr, $child:expr ) => ({
use $crate::bitcoin::secp256k1::Secp256k1; use $crate::bitcoin::secp256k1::Secp256k1;
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait}; use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
use $crate::descriptor::AsDerived;
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0; let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.1.expect("Missing internal descriptor")).expect("Failed to parse descriptor in `testutils!(@internal)`").0;
parsed.as_derived($child, &secp).address($crate::bitcoin::Network::Regtest).expect("No address form") parsed.at_derivation_index($child).address($crate::bitcoin::Network::Regtest).expect("No address form")
}); });
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) }); ( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) }); ( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
@@ -186,49 +182,50 @@ macro_rules! testutils {
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )? $( ( @keys $( $keys:tt )* ) )* ) => ({ ( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )? $( ( @keys $( $keys:tt )* ) )* ) => ({
use std::str::FromStr; use std::str::FromStr;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::Infallible;
use $crate::miniscript::descriptor::Descriptor; use $crate::miniscript::descriptor::Descriptor;
use $crate::miniscript::TranslatePk; use $crate::miniscript::TranslatePk;
struct Translator {
keys: HashMap<&'static str, (String, Option<String>, Option<String>)>,
is_internal: bool,
}
impl $crate::miniscript::Translator<String, String, Infallible> for Translator {
fn pk(&mut self, pk: &String) -> Result<String, Infallible> {
match self.keys.get(pk.as_str()) {
Some((key, ext_path, int_path)) => {
let path = if self.is_internal { int_path } else { ext_path };
Ok(format!("{}{}", key, path.clone().unwrap_or_default()))
}
None => Ok(pk.clone()),
}
}
fn sha256(&mut self, sha256: &String) -> Result<String, Infallible> { Ok(sha256.clone()) }
fn hash256(&mut self, hash256: &String) -> Result<String, Infallible> { Ok(hash256.clone()) }
fn ripemd160(&mut self, ripemd160: &String) -> Result<String, Infallible> { Ok(ripemd160.clone()) }
fn hash160(&mut self, hash160: &String) -> Result<String, Infallible> { Ok(hash160.clone()) }
}
#[allow(unused_assignments, unused_mut)] #[allow(unused_assignments, unused_mut)]
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new(); let mut keys = HashMap::new();
$( $(
keys = testutils!{ @keys $( $keys )* }; keys = testutils!{ @keys $( $keys )* };
)* )*
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap(); let mut translator = Translator { keys, is_internal: false };
let external: Descriptor<String> = external.translate_pk_infallible::<_, _>(|k| {
if let Some((key, ext_path, _)) = keys.get(&k.as_str()) {
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
} else {
k.clone()
}
}, |kh| {
if let Some((key, ext_path, _)) = keys.get(&kh.as_str()) {
format!("{}{}", key, ext_path.as_ref().unwrap_or(&"".into()))
} else {
kh.clone()
}
}); let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
let external = external.translate_pk(&mut translator).expect("Infallible conversion");
let external = external.to_string(); let external = external.to_string();
let internal = None::<String>$(.or({ translator.is_internal = true;
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
let string_internal: Descriptor<String> = string_internal.translate_pk_infallible::<_, _>(|k| { let internal = None::<String>$(.or({
if let Some((key, _, int_path)) = keys.get(&k.as_str()) { let internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into())) let internal = internal.translate_pk(&mut translator).expect("Infallible conversion");
} else { Some(internal.to_string())
k.clone()
}
}, |kh| {
if let Some((key, _, int_path)) = keys.get(&kh.as_str()) {
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
} else {
kh.clone()
}
});
Some(string_internal.to_string())
}))?; }))?;
(external, internal) (external, internal)

View File

@@ -166,7 +166,7 @@ pub struct LocalUtxo {
} }
/// A [`Utxo`] with its `satisfaction_weight`. /// A [`Utxo`] with its `satisfaction_weight`.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct WeightedUtxo { pub struct WeightedUtxo {
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to /// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
/// properly maintain the feerate when adding this input to a transaction during coin selection. /// properly maintain the feerate when adding this input to a transaction during coin selection.
@@ -177,7 +177,7 @@ pub struct WeightedUtxo {
pub utxo: Utxo, pub utxo: Utxo,
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
/// An unspent transaction output (UTXO). /// An unspent transaction output (UTXO).
pub enum Utxo { pub enum Utxo {
/// A UTXO owned by the local wallet. /// A UTXO owned by the local wallet.
@@ -224,7 +224,7 @@ impl Utxo {
} }
/// A wallet transaction /// A wallet transaction
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct TransactionDetails { pub struct TransactionDetails {
/// Optional transaction /// Optional transaction
pub transaction: Option<Transaction>, pub transaction: Option<Transaction>,

View File

@@ -1,158 +0,0 @@
// 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.
//! Address validation callbacks
//!
//! The typical usage of those callbacks is for displaying the newly-generated address on a
//! hardware wallet, so that the user can cross-check its correctness.
//!
//! More generally speaking though, these callbacks can also be used to "do something" every time
//! an address is generated, without necessarily checking or validating it.
//!
//! An address validator can be attached to a [`Wallet`](super::Wallet) by using the
//! [`Wallet::add_address_validator`](super::Wallet::add_address_validator) method, and
//! whenever a new address is generated (either explicitly by the user with
//! [`Wallet::get_address`](super::Wallet::get_address) or internally to create a change
//! address) all the attached validators will be polled, in sequence. All of them must complete
//! successfully to continue.
//!
//! ## Example
//!
//! ```
//! # use std::sync::Arc;
//! # use bitcoin::*;
//! # use bdk::address_validator::*;
//! # use bdk::database::*;
//! # use bdk::*;
//! # use bdk::wallet::AddressIndex::New;
//! #[derive(Debug)]
//! struct PrintAddressAndContinue;
//!
//! impl AddressValidator for PrintAddressAndContinue {
//! fn validate(
//! &self,
//! keychain: KeychainKind,
//! hd_keypaths: &HdKeyPaths,
//! script: &Script
//! ) -> Result<(), AddressValidatorError> {
//! let address = Address::from_script(script, Network::Testnet)
//! .as_ref()
//! .map(Address::to_string)
//! .unwrap_or(script.to_string());
//! println!("New address of type {:?}: {}", keychain, address);
//! println!("HD keypaths: {:#?}", hd_keypaths);
//!
//! Ok(())
//! }
//! }
//!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
//! let mut wallet = Wallet::new(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! wallet.add_address_validator(Arc::new(PrintAddressAndContinue));
//!
//! let address = wallet.get_address(New)?;
//! println!("Address: {}", address);
//! # Ok::<(), bdk::Error>(())
//! ```
use std::fmt;
use bitcoin::Script;
use crate::descriptor::HdKeyPaths;
use crate::types::KeychainKind;
/// Errors that can be returned to fail the validation of an address
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AddressValidatorError {
/// User rejected the address
UserRejected,
/// Network connection error
ConnectionError,
/// Network request timeout error
TimeoutError,
/// Invalid script
InvalidScript,
/// A custom error message
Message(String),
}
impl fmt::Display for AddressValidatorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for AddressValidatorError {}
/// Trait to build address validators
///
/// All the address validators attached to a wallet with [`Wallet::add_address_validator`](super::Wallet::add_address_validator) will be polled
/// every time an address (external or internal) is generated by the wallet. Errors returned in the
/// validator will be propagated up to the original caller that triggered the address generation.
///
/// For a usage example see [this module](crate::address_validator)'s documentation.
#[deprecated = "AddressValidator was rarely used. Address validation can occur outside of BDK"]
pub trait AddressValidator: Send + Sync + fmt::Debug {
/// Validate or inspect an address
fn validate(
&self,
keychain: KeychainKind,
hd_keypaths: &HdKeyPaths,
script: &Script,
) -> Result<(), AddressValidatorError>;
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use super::*;
use crate::wallet::AddressIndex::New;
use crate::wallet::{get_funded_wallet, test::get_test_wpkh};
#[derive(Debug)]
struct TestValidator;
#[allow(deprecated)]
impl AddressValidator for TestValidator {
fn validate(
&self,
_keychain: KeychainKind,
_hd_keypaths: &HdKeyPaths,
_script: &bitcoin::Script,
) -> Result<(), AddressValidatorError> {
Err(AddressValidatorError::InvalidScript)
}
}
#[test]
#[should_panic(expected = "InvalidScript")]
fn test_address_validator_external() {
let (mut wallet, _, _) = get_funded_wallet(get_test_wpkh());
#[allow(deprecated)]
wallet.add_address_validator(Arc::new(TestValidator));
wallet.get_address(New).unwrap();
}
#[test]
#[should_panic(expected = "InvalidScript")]
fn test_address_validator_internal() {
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
#[allow(deprecated)]
wallet.add_address_validator(Arc::new(TestValidator));
let addr = crate::testutils!(@external descriptors, 10);
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
builder.finish().unwrap();
}
}

View File

@@ -310,7 +310,7 @@ pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Sc
let drain_val = remaining_amount.saturating_sub(change_fee); let drain_val = remaining_amount.saturating_sub(change_fee);
if drain_val.is_dust(drain_script) { if drain_val.is_dust(drain_script) {
let dust_threshold = drain_script.dust_value().as_sat(); let dust_threshold = drain_script.dust_value().to_sat();
Excess::NoChange { Excess::NoChange {
dust_threshold, dust_threshold,
change_fee, change_fee,
@@ -835,7 +835,7 @@ mod test {
) )
.unwrap(), .unwrap(),
txout: TxOut { txout: TxOut {
value: rng.gen_range(0, 200000000), value: rng.gen_range(0..200000000),
script_pubkey: Script::new(), script_pubkey: Script::new(),
}, },
keychain: KeychainKind::External, keychain: KeychainKind::External,
@@ -866,7 +866,7 @@ mod test {
} }
fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 { fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
let utxos_picked_len = rng.gen_range(2, utxos.len() / 2); let utxos_picked_len = rng.gen_range(2..utxos.len() / 2);
utxos.shuffle(&mut rng); utxos.shuffle(&mut rng);
utxos[..utxos_picked_len] utxos[..utxos_picked_len]
.iter() .iter()
@@ -1226,6 +1226,7 @@ mod test {
} }
#[test] #[test]
#[ignore]
fn test_bnb_coin_selection_required_not_enough() { fn test_bnb_coin_selection_required_not_enough() {
let utxos = get_test_utxos(); let utxos = get_test_utxos();
let database = MemoryDatabase::default(); let database = MemoryDatabase::default();

View File

@@ -11,7 +11,40 @@
//! HWI Signer //! HWI Signer
//! //!
//! This module contains a simple implementation of a Custom signer for rust-hwi //! This module contains HWISigner, an implementation of a [TransactionSigner] to be
//! used with hardware wallets.
//! ```no_run
//! # use bdk::bitcoin::Network;
//! # use bdk::database::MemoryDatabase;
//! # use bdk::signer::SignerOrdering;
//! # use bdk::wallet::hardwaresigner::HWISigner;
//! # use bdk::wallet::AddressIndex::New;
//! # use bdk::{FeeRate, KeychainKind, SignOptions, SyncOptions, Wallet};
//! # use hwi::{types::HWIChain, HWIClient};
//! # use std::sync::Arc;
//! #
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let devices = HWIClient::enumerate()?;
//! let first_device = devices.first().expect("No devices found!");
//! let custom_signer = HWISigner::from_device(first_device, HWIChain::Test)?;
//!
//! # let mut wallet = Wallet::new(
//! # "",
//! # None,
//! # Network::Testnet,
//! # MemoryDatabase::default(),
//! # )?;
//! #
//! // Adding the hardware signer to the BDK wallet
//! wallet.add_signer(
//! KeychainKind::External,
//! SignerOrdering(200),
//! Arc::new(custom_signer),
//! );
//!
//! # Ok(())
//! # }
//! ```
use bitcoin::psbt::PartiallySignedTransaction; use bitcoin::psbt::PartiallySignedTransaction;
use bitcoin::secp256k1::{All, Secp256k1}; use bitcoin::secp256k1::{All, Secp256k1};

View File

@@ -24,20 +24,17 @@ use std::sync::Arc;
use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::Secp256k1;
use bitcoin::consensus::encode::serialize; use bitcoin::consensus::encode::serialize;
use bitcoin::util::{psbt, taproot}; use bitcoin::util::psbt;
use bitcoin::{ use bitcoin::{
Address, EcdsaSighashType, Network, OutPoint, SchnorrSighashType, Script, Transaction, TxOut, Address, EcdsaSighashType, LockTime, Network, OutPoint, SchnorrSighashType, Script, Sequence,
Txid, Witness, Transaction, TxOut, Txid, Witness,
}; };
use miniscript::descriptor::DescriptorTrait; use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
use miniscript::psbt::PsbtInputSatisfier;
use miniscript::ToPublicKey;
#[allow(unused_imports)] #[allow(unused_imports)]
use log::{debug, error, info, trace}; use log::{debug, error, info, trace};
pub mod address_validator;
pub mod coin_selection; pub mod coin_selection;
pub mod export; pub mod export;
pub mod signer; pub mod signer;
@@ -54,24 +51,21 @@ pub mod hardwaresigner;
pub use utils::IsDust; pub use utils::IsDust;
#[allow(deprecated)]
use address_validator::AddressValidator;
use coin_selection::DefaultCoinSelectionAlgorithm; use coin_selection::DefaultCoinSelectionAlgorithm;
use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner}; use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams}; use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx}; use utils::{check_nsequence_rbf, After, Older, SecpCtx};
use crate::blockchain::{GetHeight, NoopProgress, Progress, WalletSync}; use crate::blockchain::{GetHeight, NoopProgress, Progress, WalletSync};
use crate::database::memory::MemoryDatabase; use crate::database::memory::MemoryDatabase;
use crate::database::{AnyDatabase, BatchDatabase, BatchOperations, DatabaseUtils, SyncTime}; use crate::database::{AnyDatabase, BatchDatabase, BatchOperations, DatabaseUtils, SyncTime};
use crate::descriptor::derived::AsDerived; use crate::descriptor::checksum::calc_checksum_bytes_internal;
use crate::descriptor::policy::BuildSatisfaction; use crate::descriptor::policy::BuildSatisfaction;
use crate::descriptor::{ use crate::descriptor::{
get_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DerivedDescriptorMeta, calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta,
DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils,
Policy, XKeyUtils,
}; };
use crate::error::Error; use crate::error::{Error, MiniscriptPsbtError};
use crate::psbt::PsbtUtils; use crate::psbt::PsbtUtils;
use crate::signer::SignerError; use crate::signer::SignerError;
use crate::testutils; use crate::testutils;
@@ -100,9 +94,6 @@ pub struct Wallet<D> {
signers: Arc<SignersContainer>, signers: Arc<SignersContainer>,
change_signers: Arc<SignersContainer>, change_signers: Arc<SignersContainer>,
#[allow(deprecated)]
address_validators: Vec<Arc<dyn AddressValidator>>,
network: Network, network: Network,
database: RefCell<D>, database: RefCell<D>,
@@ -143,7 +134,7 @@ pub enum AddressIndex {
/// A derived address and the index it was found at /// A derived address and the index it was found at
/// For convenience this automatically derefs to `Address` /// For convenience this automatically derefs to `Address`
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub struct AddressInfo { pub struct AddressInfo {
/// Child index of this address /// Child index of this address
pub index: u32, pub index: u32,
@@ -203,18 +194,20 @@ where
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network)?; let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network)?;
database.check_descriptor_checksum( Self::db_checksum(
&mut database,
&descriptor.to_string(),
KeychainKind::External, KeychainKind::External,
get_checksum(&descriptor.to_string())?.as_bytes(),
)?; )?;
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp)); let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp));
let (change_descriptor, change_signers) = match change_descriptor { let (change_descriptor, change_signers) = match change_descriptor {
Some(desc) => { Some(desc) => {
let (change_descriptor, change_keymap) = let (change_descriptor, change_keymap) =
into_wallet_descriptor_checked(desc, &secp, network)?; into_wallet_descriptor_checked(desc, &secp, network)?;
database.check_descriptor_checksum( Self::db_checksum(
&mut database,
&change_descriptor.to_string(),
KeychainKind::Internal, KeychainKind::Internal,
get_checksum(&change_descriptor.to_string())?.as_bytes(),
)?; )?;
let change_signers = Arc::new(SignersContainer::build( let change_signers = Arc::new(SignersContainer::build(
@@ -222,9 +215,6 @@ where
&change_descriptor, &change_descriptor,
&secp, &secp,
)); ));
// if !parsed.same_structure(descriptor.as_ref()) {
// return Err(Error::DifferentDescriptorStructure);
// }
(Some(change_descriptor), change_signers) (Some(change_descriptor), change_signers)
} }
@@ -236,13 +226,25 @@ where
change_descriptor, change_descriptor,
signers, signers,
change_signers, change_signers,
address_validators: Vec::new(),
network, network,
database: RefCell::new(database), database: RefCell::new(database),
secp, secp,
}) })
} }
/// This checks the checksum within [`BatchDatabase`] twice (if needed). The first time with the
/// actual checksum, and the second time with the checksum of `descriptor+checksum`. The second
/// check is necessary for backwards compatibility of a checksum-inception bug.
fn db_checksum(db: &mut D, desc: &str, kind: KeychainKind) -> Result<(), Error> {
let checksum = calc_checksum_bytes_internal(desc, true)?;
if db.check_descriptor_checksum(kind, checksum).is_ok() {
return Ok(());
}
let checksum_inception = calc_checksum_bytes_internal(desc, false)?;
db.check_descriptor_checksum(kind, checksum_inception)
}
/// Get the Bitcoin network the wallet is using. /// Get the Bitcoin network the wallet is using.
pub fn network(&self) -> Network { pub fn network(&self) -> Network {
self.network self.network
@@ -254,7 +256,7 @@ where
let address_result = self let address_result = self
.get_descriptor_for_keychain(keychain) .get_descriptor_for_keychain(keychain)
.as_derived(incremented_index, &self.secp) .at_derivation_index(incremented_index)
.address(self.network); .address(self.network);
address_result address_result
@@ -273,7 +275,7 @@ where
let derived_key = self let derived_key = self
.get_descriptor_for_keychain(keychain) .get_descriptor_for_keychain(keychain)
.as_derived(current_index, &self.secp); .at_derivation_index(current_index);
let script_pubkey = derived_key.script_pubkey(); let script_pubkey = derived_key.script_pubkey();
@@ -301,7 +303,7 @@ where
// Return derived address for the descriptor of given [`KeychainKind`] at a specific index // Return derived address for the descriptor of given [`KeychainKind`] at a specific index
fn peek_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> { fn peek_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> {
self.get_descriptor_for_keychain(keychain) self.get_descriptor_for_keychain(keychain)
.as_derived(index, &self.secp) .at_derivation_index(index)
.address(self.network) .address(self.network)
.map(|address| AddressInfo { .map(|address| AddressInfo {
index, index,
@@ -317,7 +319,7 @@ where
self.set_index(keychain, index)?; self.set_index(keychain, index)?;
self.get_descriptor_for_keychain(keychain) self.get_descriptor_for_keychain(keychain)
.as_derived(index, &self.secp) .at_derivation_index(index)
.address(self.network) .address(self.network)
.map(|address| AddressInfo { .map(|address| AddressInfo {
index, index,
@@ -366,7 +368,7 @@ where
/// transaction output scripts. /// transaction output scripts.
pub fn ensure_addresses_cached(&self, max_addresses: u32) -> Result<bool, Error> { pub fn ensure_addresses_cached(&self, max_addresses: u32) -> Result<bool, Error> {
let mut new_addresses_cached = false; let mut new_addresses_cached = false;
let max_address = match self.descriptor.is_deriveable() { let max_address = match self.descriptor.has_wildcard() {
false => 0, false => 0,
true => max_addresses, true => max_addresses,
}; };
@@ -383,7 +385,7 @@ where
} }
if let Some(change_descriptor) = &self.change_descriptor { if let Some(change_descriptor) = &self.change_descriptor {
let max_address = match change_descriptor.is_deriveable() { let max_address = match change_descriptor.has_wildcard() {
false => 0, false => 0,
true => max_addresses, true => max_addresses,
}; };
@@ -552,24 +554,6 @@ where
} }
} }
/// Add an address validator
///
/// See [the `address_validator` module](address_validator) for an example.
#[deprecated]
#[allow(deprecated)]
pub fn add_address_validator(&mut self, validator: Arc<dyn AddressValidator>) {
self.address_validators.push(validator);
}
/// Get the address validators
///
/// See [the `address_validator` module](address_validator).
#[deprecated]
#[allow(deprecated)]
pub fn get_address_validators(&self) -> &[Arc<dyn AddressValidator>] {
&self.address_validators
}
/// Start building a transaction. /// Start building a transaction.
/// ///
/// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction. /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction.
@@ -684,10 +668,9 @@ where
// We use a match here instead of a map_or_else as it's way more readable :) // We use a match here instead of a map_or_else as it's way more readable :)
let current_height = match params.current_height { let current_height = match params.current_height {
// If they didn't tell us the current height, we assume it's the latest sync height. // If they didn't tell us the current height, we assume it's the latest sync height.
None => self None => self.database().get_sync_time()?.map(|sync_time| {
.database() LockTime::from_height(sync_time.block_time.height).expect("Invalid height")
.get_sync_time()? }),
.map(|sync_time| sync_time.block_time.height),
h => h, h => h,
}; };
@@ -697,24 +680,33 @@ where
// Fee sniping can be partially prevented by setting the timelock // Fee sniping can be partially prevented by setting the timelock
// to current_height. If we don't know the current_height, // to current_height. If we don't know the current_height,
// we default to 0. // we default to 0.
let fee_sniping_height = current_height.unwrap_or(0); let fee_sniping_height = current_height.unwrap_or(LockTime::ZERO);
// We choose the biggest between the required nlocktime and the fee sniping // We choose the biggest between the required nlocktime and the fee sniping
// height // height
std::cmp::max(requirements.timelock.unwrap_or(0), fee_sniping_height) match requirements.timelock {
// No requirement, just use the fee_sniping_height
None => fee_sniping_height,
// There's a block-based requirement, but the value is lower than the fee_sniping_height
Some(value @ LockTime::Blocks(_)) if value < fee_sniping_height => fee_sniping_height,
// There's a time-based requirement or a block-based requirement greater
// than the fee_sniping_height use that value
Some(value) => value,
}
} }
// Specific nLockTime required and we have no constraints, so just set to that value // Specific nLockTime required and we have no constraints, so just set to that value
Some(x) if requirements.timelock.is_none() => x, Some(x) if requirements.timelock.is_none() => x,
// Specific nLockTime required and it's compatible with the constraints // Specific nLockTime required and it's compatible with the constraints
Some(x) if check_nlocktime(x, requirements.timelock.unwrap()) => x, Some(x) if requirements.timelock.unwrap().is_same_unit(x) && x >= requirements.timelock.unwrap() => x,
// Invalid nLockTime required // Invalid nLockTime required
Some(x) => return Err(Error::Generic(format!("TxBuilder requested timelock of `{}`, but at least `{}` is required to spend from this script", x, requirements.timelock.unwrap()))) Some(x) => return Err(Error::Generic(format!("TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", x, requirements.timelock.unwrap())))
}; };
let n_sequence = match (params.rbf, requirements.csv) { let n_sequence = match (params.rbf, requirements.csv) {
// No RBF or CSV but there's an nLockTime, so the nSequence cannot be final // No RBF or CSV but there's an nLockTime, so the nSequence cannot be final
(None, None) if lock_time != 0 => 0xFFFFFFFE, (None, None) if lock_time != LockTime::ZERO => Sequence::ENABLE_LOCKTIME_NO_RBF,
// No RBF, CSV or nLockTime, make the transaction final // No RBF, CSV or nLockTime, make the transaction final
(None, None) => 0xFFFFFFFF, (None, None) => Sequence::MAX,
// No RBF requested, use the value from CSV. Note that this value is by definition // No RBF requested, use the value from CSV. Note that this value is by definition
// non-final, so even if a timelock is enabled this nSequence is fine, hence why we // non-final, so even if a timelock is enabled this nSequence is fine, hence why we
@@ -722,7 +714,7 @@ where
(None, Some(csv)) => csv, (None, Some(csv)) => csv,
// RBF with a specific value but that value is too high // RBF with a specific value but that value is too high
(Some(tx_builder::RbfValue::Value(rbf)), _) if rbf >= 0xFFFFFFFE => { (Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => {
return Err(Error::Generic( return Err(Error::Generic(
"Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into(), "Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into(),
)) ))
@@ -732,7 +724,7 @@ where
if !check_nsequence_rbf(rbf, csv) => if !check_nsequence_rbf(rbf, csv) =>
{ {
return Err(Error::Generic(format!( return Err(Error::Generic(format!(
"Cannot enable RBF with nSequence `{}` given a required OP_CSV of `{}`", "Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`",
rbf, csv rbf, csv
))) )))
} }
@@ -775,7 +767,7 @@ where
let mut tx = Transaction { let mut tx = Transaction {
version, version,
lock_time, lock_time: lock_time.into(),
input: vec![], input: vec![],
output: vec![], output: vec![],
}; };
@@ -840,7 +832,7 @@ where
params.drain_wallet, params.drain_wallet,
params.manually_selected_only, params.manually_selected_only,
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
current_height, current_height.map(LockTime::to_consensus_u32),
)?; )?;
// get drain script // get drain script
@@ -992,7 +984,11 @@ where
Some(tx) => tx, Some(tx) => tx,
}; };
let mut tx = details.transaction.take().unwrap(); let mut tx = details.transaction.take().unwrap();
if !tx.input.iter().any(|txin| txin.sequence <= 0xFFFFFFFD) { if !tx
.input
.iter()
.any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD)
{
return Err(Error::IrreplaceableTransaction); return Err(Error::IrreplaceableTransaction);
} }
@@ -1118,8 +1114,9 @@ where
psbt: &mut psbt::PartiallySignedTransaction, psbt: &mut psbt::PartiallySignedTransaction,
sign_options: SignOptions, sign_options: SignOptions,
) -> Result<bool, Error> { ) -> Result<bool, Error> {
// this helps us doing our job later // This adds all the PSBT metadata for the inputs, which will help us later figure out how
self.add_input_hd_keypaths(psbt)?; // to derive our keys
self.update_psbt_with_descriptor(psbt)?;
// If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones) // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones)
// has the `non_witness_utxo` // has the `non_witness_utxo`
@@ -1320,21 +1317,18 @@ where
} }
} }
fn get_descriptor_for_txout( fn get_descriptor_for_txout(&self, txout: &TxOut) -> Result<Option<DerivedDescriptor>, Error> {
&self,
txout: &TxOut,
) -> Result<Option<DerivedDescriptor<'_>>, Error> {
Ok(self Ok(self
.database .database
.borrow() .borrow()
.get_path_from_script_pubkey(&txout.script_pubkey)? .get_path_from_script_pubkey(&txout.script_pubkey)?
.map(|(keychain, child)| (self.get_descriptor_for_keychain(keychain), child)) .map(|(keychain, child)| (self.get_descriptor_for_keychain(keychain), child))
.map(|(desc, child)| desc.as_derived(child, &self.secp))) .map(|(desc, child)| desc.at_derivation_index(child)))
} }
fn fetch_and_increment_index(&self, keychain: KeychainKind) -> Result<u32, Error> { fn fetch_and_increment_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain); let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
let index = match descriptor.is_deriveable() { let index = match descriptor.has_wildcard() {
false => 0, false => 0,
true => self.database.borrow_mut().increment_last_index(keychain)?, true => self.database.borrow_mut().increment_last_index(keychain)?,
}; };
@@ -1348,22 +1342,12 @@ where
self.cache_addresses(keychain, index, CACHE_ADDR_BATCH_SIZE)?; self.cache_addresses(keychain, index, CACHE_ADDR_BATCH_SIZE)?;
} }
let derived_descriptor = descriptor.as_derived(index, &self.secp);
let hd_keypaths = derived_descriptor.get_hd_keypaths(&self.secp);
let script = derived_descriptor.script_pubkey();
for validator in &self.address_validators {
#[allow(deprecated)]
validator.validate(keychain, &hd_keypaths, &script)?;
}
Ok(index) Ok(index)
} }
fn fetch_index(&self, keychain: KeychainKind) -> Result<u32, Error> { fn fetch_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain); let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
let index = match descriptor.is_deriveable() { let index = match descriptor.has_wildcard() {
false => Some(0), false => Some(0),
true => self.database.borrow_mut().get_last_index(keychain)?, true => self.database.borrow_mut().get_last_index(keychain)?,
}; };
@@ -1387,7 +1371,7 @@ where
mut count: u32, mut count: u32,
) -> Result<(), Error> { ) -> Result<(), Error> {
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain); let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
if !descriptor.is_deriveable() { if !descriptor.has_wildcard() {
if from > 0 { if from > 0 {
return Ok(()); return Ok(());
} }
@@ -1400,7 +1384,7 @@ where
let start_time = time::Instant::new(); let start_time = time::Instant::new();
for i in from..(from + count) { for i in from..(from + count) {
address_batch.set_script_pubkey( address_batch.set_script_pubkey(
&descriptor.as_derived(i, &self.secp).script_pubkey(), &descriptor.at_derivation_index(i).script_pubkey(),
keychain, keychain,
i, i,
)?; )?;
@@ -1604,52 +1588,7 @@ where
} }
} }
// probably redundant but it doesn't hurt... self.update_psbt_with_descriptor(&mut psbt)?;
self.add_input_hd_keypaths(&mut psbt)?;
// add metadata for the outputs
for (psbt_output, tx_output) in psbt.outputs.iter_mut().zip(psbt.unsigned_tx.output.iter())
{
if let Some((keychain, child)) = self
.database
.borrow()
.get_path_from_script_pubkey(&tx_output.script_pubkey)?
{
let (desc, _) = self._get_descriptor_for_keychain(keychain);
let derived_descriptor = desc.as_derived(child, &self.secp);
if let miniscript::Descriptor::Tr(tr) = &derived_descriptor {
let tap_tree = if tr.taptree().is_some() {
let mut builder = taproot::TaprootBuilder::new();
for (depth, ms) in tr.iter_scripts() {
let script = ms.encode();
builder = builder.add_leaf(depth, script).expect(
"Computing spend data on a valid Tree should always succeed",
);
}
Some(
psbt::TapTree::from_builder(builder)
.expect("The tree should always be valid"),
)
} else {
None
};
psbt_output.tap_tree = tap_tree;
psbt_output
.tap_key_origins
.append(&mut derived_descriptor.get_tap_key_origins(&self.secp));
psbt_output.tap_internal_key = Some(tr.internal_key().to_x_only_pubkey());
} else {
psbt_output
.bip32_derivation
.append(&mut derived_descriptor.get_hd_keypaths(&self.secp));
}
if params.include_output_redeem_witness_script {
psbt_output.witness_script = derived_descriptor.psbt_witness_script();
psbt_output.redeem_script = derived_descriptor.psbt_redeem_script();
};
}
}
Ok(psbt) Ok(psbt)
} }
@@ -1675,29 +1614,11 @@ where
}; };
let desc = self.get_descriptor_for_keychain(keychain); let desc = self.get_descriptor_for_keychain(keychain);
let derived_descriptor = desc.as_derived(child, &self.secp); let derived_descriptor = desc.at_derivation_index(child);
if let miniscript::Descriptor::Tr(tr) = &derived_descriptor { psbt_input
psbt_input.tap_key_origins = derived_descriptor.get_tap_key_origins(&self.secp); .update_with_descriptor_unchecked(&derived_descriptor)
psbt_input.tap_internal_key = Some(tr.internal_key().to_x_only_pubkey()); .map_err(MiniscriptPsbtError::Conversion)?;
let spend_info = tr.spend_info();
psbt_input.tap_merkle_root = spend_info.merkle_root();
psbt_input.tap_scripts = spend_info
.as_script_map()
.keys()
.filter_map(|script_ver| {
spend_info
.control_block(script_ver)
.map(|cb| (cb, script_ver.clone()))
})
.collect();
} else {
psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp);
}
psbt_input.redeem_script = derived_descriptor.psbt_redeem_script();
psbt_input.witness_script = derived_descriptor.psbt_witness_script();
let prev_output = utxo.outpoint; let prev_output = utxo.outpoint;
if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? { if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
@@ -1711,38 +1632,47 @@ where
Ok(psbt_input) Ok(psbt_input)
} }
fn add_input_hd_keypaths( fn update_psbt_with_descriptor(
&self, &self,
psbt: &mut psbt::PartiallySignedTransaction, psbt: &mut psbt::PartiallySignedTransaction,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut input_utxos = Vec::with_capacity(psbt.inputs.len()); // We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all
for n in 0..psbt.inputs.len() { // the input utxos and outputs
input_utxos.push(psbt.get_utxo_for(n).clone()); //
} // Clippy complains that the collect is not required, but that's wrong
#[allow(clippy::needless_collect)]
let utxos = (0..psbt.inputs.len())
.filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo)))
.chain(
psbt.unsigned_tx
.output
.iter()
.enumerate()
.map(|(i, out)| (false, i, out.clone())),
)
.collect::<Vec<_>>();
// try to add hd_keypaths if we've already seen the output // Try to figure out the keychain and derivation for every input and output
for (psbt_input, out) in psbt.inputs.iter_mut().zip(input_utxos.iter()) { for (is_input, index, out) in utxos.into_iter() {
if let Some(out) = out { if let Some((keychain, child)) = self
if let Some((keychain, child)) = self .database
.database .borrow()
.borrow() .get_path_from_script_pubkey(&out.script_pubkey)?
.get_path_from_script_pubkey(&out.script_pubkey)? {
{ debug!(
debug!("Found descriptor {:?}/{}", keychain, child); "Found descriptor for input #{} {:?}/{}",
index, keychain, child
);
// merge hd_keypaths or tap_key_origins let desc = self.get_descriptor_for_keychain(keychain);
let desc = self.get_descriptor_for_keychain(keychain); let desc = desc.at_derivation_index(child);
if desc.is_taproot() {
let mut tap_key_origins = desc if is_input {
.as_derived(child, &self.secp) psbt.update_input_with_descriptor(index, &desc)
.get_tap_key_origins(&self.secp); .map_err(MiniscriptPsbtError::UtxoUpdate)?;
psbt_input.tap_key_origins.append(&mut tap_key_origins); } else {
} else { psbt.update_output_with_descriptor(index, &desc)
let mut hd_keypaths = desc .map_err(MiniscriptPsbtError::OutputUpdate)?;
.as_derived(child, &self.secp)
.get_hd_keypaths(&self.secp);
psbt_input.bip32_derivation.append(&mut hd_keypaths);
}
} }
} }
} }
@@ -1781,12 +1711,12 @@ where
// We need to ensure descriptor is derivable to fullfil "missing cache", otherwise we will // We need to ensure descriptor is derivable to fullfil "missing cache", otherwise we will
// end up with an infinite loop // end up with an infinite loop
let is_deriveable = self.descriptor.is_deriveable() let has_wildcard = self.descriptor.has_wildcard()
&& (self.change_descriptor.is_none() && (self.change_descriptor.is_none()
|| self.change_descriptor.as_ref().unwrap().is_deriveable()); || self.change_descriptor.as_ref().unwrap().has_wildcard());
// Restrict max rounds in case of faulty "missing cache" implementation by blockchain // Restrict max rounds in case of faulty "missing cache" implementation by blockchain
let max_rounds = if is_deriveable { 100 } else { 1 }; let max_rounds = if has_wildcard { 100 } else { 1 };
for _ in 0..max_rounds { for _ in 0..max_rounds {
let sync_res = let sync_res =
@@ -1864,14 +1794,14 @@ where
.into_wallet_descriptor(secp, network)? .into_wallet_descriptor(secp, network)?
.0 .0
.to_string(); .to_string();
let mut wallet_name = get_checksum(&descriptor[..descriptor.find('#').unwrap()])?; let mut wallet_name = calc_checksum(&descriptor[..descriptor.find('#').unwrap()])?;
if let Some(change_descriptor) = change_descriptor { if let Some(change_descriptor) = change_descriptor {
let change_descriptor = change_descriptor let change_descriptor = change_descriptor
.into_wallet_descriptor(secp, network)? .into_wallet_descriptor(secp, network)?
.0 .0
.to_string(); .to_string();
wallet_name.push_str( wallet_name.push_str(
get_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(), calc_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(),
); );
} }
@@ -1921,7 +1851,7 @@ pub fn get_funded_wallet(
#[cfg(test)] #[cfg(test)]
pub(crate) mod test { pub(crate) mod test {
use bitcoin::{util::psbt, Network}; use bitcoin::{util::psbt, Network, PackedLockTime, Sequence};
use crate::database::Database; use crate::database::Database;
use crate::types::KeychainKind; use crate::types::KeychainKind;
@@ -1943,15 +1873,38 @@ pub(crate) mod test {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let checksum = wallet.descriptor_checksum(KeychainKind::External); let checksum = wallet.descriptor_checksum(KeychainKind::External);
assert_eq!(checksum.len(), 8); assert_eq!(checksum.len(), 8);
assert_eq!(
calc_checksum(&wallet.descriptor.to_string()).unwrap(),
checksum
);
}
let raw_descriptor = wallet #[test]
.descriptor fn test_db_checksum() {
.to_string() let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
.split_once('#') let desc = wallet.descriptor.to_string();
.unwrap()
.0 let checksum = calc_checksum_bytes_internal(&desc, true).unwrap();
.to_string(); let checksum_inception = calc_checksum_bytes_internal(&desc, false).unwrap();
assert_eq!(get_checksum(&raw_descriptor).unwrap(), checksum); let checksum_invalid = [b'q'; 8];
let mut db = MemoryDatabase::new();
db.check_descriptor_checksum(KeychainKind::External, checksum)
.expect("failed to save actual checksum");
Wallet::db_checksum(&mut db, &desc, KeychainKind::External)
.expect("db that uses actual checksum should be supported");
let mut db = MemoryDatabase::new();
db.check_descriptor_checksum(KeychainKind::External, checksum_inception)
.expect("failed to save checksum inception");
Wallet::db_checksum(&mut db, &desc, KeychainKind::External)
.expect("db that uses checksum inception should be supported");
let mut db = MemoryDatabase::new();
db.check_descriptor_checksum(KeychainKind::External, checksum_invalid)
.expect("failed to save invalid checksum");
Wallet::db_checksum(&mut db, &desc, KeychainKind::External)
.expect_err("db that uses invalid checksum should fail");
} }
#[test] #[test]
@@ -2234,7 +2187,7 @@ pub(crate) mod test {
// Since we never synced the wallet we don't have a last_sync_height // Since we never synced the wallet we don't have a last_sync_height
// we could use to try to prevent fee sniping. We default to 0. // we could use to try to prevent fee sniping. We default to 0.
assert_eq!(psbt.unsigned_tx.lock_time, 0); assert_eq!(psbt.unsigned_tx.lock_time, PackedLockTime(0));
} }
#[test] #[test]
@@ -2259,7 +2212,7 @@ pub(crate) mod test {
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
// current_height will override the last sync height // current_height will override the last sync height
assert_eq!(psbt.unsigned_tx.lock_time, current_height); assert_eq!(psbt.unsigned_tx.lock_time, PackedLockTime(current_height));
} }
#[test] #[test]
@@ -2282,7 +2235,10 @@ pub(crate) mod test {
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
// If there's no current_height we're left with using the last sync height // If there's no current_height we're left with using the last sync height
assert_eq!(psbt.unsigned_tx.lock_time, sync_time.block_time.height); assert_eq!(
psbt.unsigned_tx.lock_time,
PackedLockTime(sync_time.block_time.height)
);
} }
#[test] #[test]
@@ -2293,7 +2249,7 @@ pub(crate) mod test {
builder.add_recipient(addr.script_pubkey(), 25_000); builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.lock_time, 100_000); assert_eq!(psbt.unsigned_tx.lock_time, PackedLockTime(100_000));
} }
#[test] #[test]
@@ -2304,13 +2260,13 @@ pub(crate) mod test {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.current_height(630_001) .current_height(630_001)
.nlocktime(630_000); .nlocktime(LockTime::from_height(630_000).unwrap());
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
// When we explicitly specify a nlocktime // When we explicitly specify a nlocktime
// we don't try any fee sniping prevention trick // we don't try any fee sniping prevention trick
// (we ignore the current_height) // (we ignore the current_height)
assert_eq!(psbt.unsigned_tx.lock_time, 630_000); assert_eq!(psbt.unsigned_tx.lock_time, PackedLockTime(630_000));
} }
#[test] #[test]
@@ -2320,15 +2276,15 @@ pub(crate) mod test {
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.nlocktime(630_000); .nlocktime(LockTime::from_height(630_000).unwrap());
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.lock_time, 630_000); assert_eq!(psbt.unsigned_tx.lock_time, PackedLockTime(630_000));
} }
#[test] #[test]
#[should_panic( #[should_panic(
expected = "TxBuilder requested timelock of `50000`, but at least `100000` is required to spend from this script" expected = "TxBuilder requested timelock of `Blocks(Height(50000))`, but at least `Blocks(Height(100000))` is required to spend from this script"
)] )]
fn test_create_tx_custom_locktime_incompatible_with_cltv() { fn test_create_tx_custom_locktime_incompatible_with_cltv() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
@@ -2336,7 +2292,7 @@ pub(crate) mod test {
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.nlocktime(50000); .nlocktime(LockTime::from_height(50000).unwrap());
builder.finish().unwrap(); builder.finish().unwrap();
} }
@@ -2348,7 +2304,7 @@ pub(crate) mod test {
builder.add_recipient(addr.script_pubkey(), 25_000); builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, 6); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6));
} }
#[test] #[test]
@@ -2362,12 +2318,12 @@ pub(crate) mod test {
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
// When CSV is enabled it takes precedence over the rbf value (unless forced by the user). // When CSV is enabled it takes precedence over the rbf value (unless forced by the user).
// It will be set to the OP_CSV value, in this case 6 // It will be set to the OP_CSV value, in this case 6
assert_eq!(psbt.unsigned_tx.input[0].sequence, 6); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6));
} }
#[test] #[test]
#[should_panic( #[should_panic(
expected = "Cannot enable RBF with nSequence `3` given a required OP_CSV of `6`" expected = "Cannot enable RBF with nSequence `Sequence(3)` given a required OP_CSV of `Sequence(6)`"
)] )]
fn test_create_tx_with_custom_rbf_csv() { fn test_create_tx_with_custom_rbf_csv() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
@@ -2375,7 +2331,7 @@ pub(crate) mod test {
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(3); .enable_rbf_with_sequence(Sequence(3));
builder.finish().unwrap(); builder.finish().unwrap();
} }
@@ -2387,7 +2343,7 @@ pub(crate) mod test {
builder.add_recipient(addr.script_pubkey(), 25_000); builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, 0xFFFFFFFE); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE));
} }
#[test] #[test]
@@ -2398,7 +2354,7 @@ pub(crate) mod test {
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(0xFFFFFFFE); .enable_rbf_with_sequence(Sequence(0xFFFFFFFE));
builder.finish().unwrap(); builder.finish().unwrap();
} }
@@ -2409,10 +2365,10 @@ pub(crate) mod test {
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(0xDEADBEEF); .enable_rbf_with_sequence(Sequence(0xDEADBEEF));
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, 0xDEADBEEF); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF));
} }
#[test] #[test]
@@ -2439,7 +2395,7 @@ pub(crate) mod test {
builder.add_recipient(addr.script_pubkey(), 25_000); builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, 0xFFFFFFFF); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF));
} }
#[test] #[test]
@@ -2960,7 +2916,7 @@ pub(crate) mod test {
.policy_path(path, KeychainKind::External); .policy_path(path, KeychainKind::External);
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, 0xFFFFFFFF); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF));
} }
#[test] #[test]
@@ -2979,7 +2935,7 @@ pub(crate) mod test {
.policy_path(path, KeychainKind::External); .policy_path(path, KeychainKind::External);
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, 144); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144));
} }
#[test] #[test]
@@ -4832,7 +4788,7 @@ pub(crate) mod test {
let (wallet, _, _) = get_funded_wallet(get_test_tr_repeated_key()); let (wallet, _, _) = get_funded_wallet(get_test_tr_repeated_key());
let addr = wallet.get_address(AddressIndex::New).unwrap(); let addr = wallet.get_address(AddressIndex::New).unwrap();
let path = vec![("rn4nre9c".to_string(), vec![0])] let path = vec![("e5mmg3xh".to_string(), vec![0])]
.into_iter() .into_iter()
.collect(); .collect();
@@ -4842,48 +4798,50 @@ pub(crate) mod test {
.policy_path(path, KeychainKind::External); .policy_path(path, KeychainKind::External);
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
let mut input_key_origins = psbt.inputs[0]
.tap_key_origins
.clone()
.into_iter()
.collect::<Vec<_>>();
input_key_origins.sort();
assert_eq!( assert_eq!(
psbt.inputs[0] input_key_origins,
.tap_key_origins vec![
.clone()
.into_iter()
.collect::<Vec<_>>(),
vec![(
from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
( (
vec![ from_str!("b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"),
from_str!( (
"858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e" vec![],
), (FromStr::from_str("871fd295").unwrap(), vec![].into())
from_str!( )
"f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903" ),
) (
], from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
(Default::default(), Default::default()) (
vec![
from_str!(
"858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e"
),
from_str!(
"f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903"
),
],
(FromStr::from_str("ece52657").unwrap(), vec![].into())
)
) )
)], ],
"Wrong input tap_key_origins" "Wrong input tap_key_origins"
); );
let mut output_key_origins = psbt.outputs[0]
.tap_key_origins
.clone()
.into_iter()
.collect::<Vec<_>>();
output_key_origins.sort();
assert_eq!( assert_eq!(
psbt.outputs[0] input_key_origins, output_key_origins,
.tap_key_origins
.clone()
.into_iter()
.collect::<Vec<_>>(),
vec![(
from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
(
vec![
from_str!(
"858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e"
),
from_str!(
"f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903"
)
],
(Default::default(), Default::default())
)
)],
"Wrong output tap_key_origins" "Wrong output tap_key_origins"
); );
} }
@@ -5135,7 +5093,7 @@ pub(crate) mod test {
#[test] #[test]
fn test_taproot_script_spend_sign_include_some_leaves() { fn test_taproot_script_spend_sign_include_some_leaves() {
use crate::signer::TapLeavesOptions; use crate::signer::TapLeavesOptions;
use crate::wallet::taproot::TapLeafHash; use bitcoin::util::taproot::TapLeafHash;
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
let addr = wallet.get_address(AddressIndex::New).unwrap(); let addr = wallet.get_address(AddressIndex::New).unwrap();
@@ -5177,7 +5135,7 @@ pub(crate) mod test {
#[test] #[test]
fn test_taproot_script_spend_sign_exclude_some_leaves() { fn test_taproot_script_spend_sign_exclude_some_leaves() {
use crate::signer::TapLeavesOptions; use crate::signer::TapLeavesOptions;
use crate::wallet::taproot::TapLeafHash; use bitcoin::util::taproot::TapLeafHash;
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
let addr = wallet.get_address(AddressIndex::New).unwrap(); let addr = wallet.get_address(AddressIndex::New).unwrap();
@@ -5524,6 +5482,7 @@ pub(crate) mod test {
SignOptions { SignOptions {
remove_partial_sigs: false, remove_partial_sigs: false,
try_finalize: false, try_finalize: false,
allow_grinding: false,
..Default::default() ..Default::default()
}, },
) )
@@ -5538,6 +5497,7 @@ pub(crate) mod test {
&mut psbt, &mut psbt,
SignOptions { SignOptions {
remove_partial_sigs: false, remove_partial_sigs: false,
allow_grinding: false,
..Default::default() ..Default::default()
}, },
) )
@@ -5546,6 +5506,39 @@ pub(crate) mod test {
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate); assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
} }
#[test]
fn test_fee_rate_sign_grinding_low_r() {
// Our goal is to obtain a transaction with a signature with low-R (70 bytes)
// by setting the `allow_grinding` signing option as true.
// We then check that our fee rate and fee calculation is alright and that our
// signature is 70 bytes.
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let fee_rate = FeeRate::from_sat_per_vb(1.0);
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.drain_wallet()
.fee_rate(fee_rate);
let (mut psbt, details) = builder.finish().unwrap();
wallet
.sign(
&mut psbt,
SignOptions {
remove_partial_sigs: false,
allow_grinding: true,
..Default::default()
},
)
.unwrap();
let key = psbt.inputs[0].partial_sigs.keys().next().unwrap();
let sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len();
assert_eq!(sig_len, 70);
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
}
#[cfg(feature = "test-hardware-signer")] #[cfg(feature = "test-hardware-signer")]
#[test] #[test]
fn test_create_signer() { fn test_create_signer() {

View File

@@ -96,10 +96,10 @@ use bitcoin::{secp256k1, XOnlyPublicKey};
use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script}; use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script};
use miniscript::descriptor::{ use miniscript::descriptor::{
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey, Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, KeyMap, SinglePriv,
KeyMap, SinglePubKey, SinglePubKey,
}; };
use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap}; use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
use super::utils::SecpCtx; use super::utils::SecpCtx;
use crate::descriptor::{DescriptorMeta, XKeyUtils}; use crate::descriptor::{DescriptorMeta, XKeyUtils};
@@ -369,11 +369,11 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
impl SignerCommon for SignerWrapper<PrivateKey> { impl SignerCommon for SignerWrapper<PrivateKey> {
fn id(&self, secp: &SecpCtx) -> SignerId { fn id(&self, secp: &SecpCtx) -> SignerId {
SignerId::from(self.public_key(secp).to_pubkeyhash()) SignerId::from(self.public_key(secp).to_pubkeyhash(SigType::Ecdsa))
} }
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> { fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv { Some(DescriptorSecretKey::Single(SinglePriv {
key: self.signer, key: self.signer,
origin: None, origin: None,
})) }))
@@ -472,6 +472,7 @@ impl InputSigner for SignerWrapper<PrivateKey> {
hash, hash,
hash_ty, hash_ty,
secp, secp,
sign_options.allow_grinding,
); );
Ok(()) Ok(())
@@ -485,9 +486,14 @@ fn sign_psbt_ecdsa(
hash: bitcoin::Sighash, hash: bitcoin::Sighash,
hash_ty: EcdsaSighashType, hash_ty: EcdsaSighashType,
secp: &SecpCtx, secp: &SecpCtx,
allow_grinding: bool,
) { ) {
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap(); let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
let sig = secp.sign_ecdsa(msg, secret_key); let sig = if allow_grinding {
secp.sign_ecdsa_low_r(msg, secret_key)
} else {
secp.sign_ecdsa(msg, secret_key)
};
secp.verify_ecdsa(msg, &sig, &pubkey.inner) secp.verify_ecdsa(msg, &sig, &pubkey.inner)
.expect("invalid or corrupted ecdsa signature"); .expect("invalid or corrupted ecdsa signature");
@@ -511,13 +517,13 @@ fn sign_psbt_schnorr(
let keypair = match leaf_hash { let keypair = match leaf_hash {
None => keypair None => keypair
.tap_tweak(secp, psbt_input.tap_merkle_root) .tap_tweak(secp, psbt_input.tap_merkle_root)
.into_inner(), .to_inner(),
Some(_) => keypair, // no tweak for script spend Some(_) => keypair, // no tweak for script spend
}; };
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap(); let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
let sig = secp.sign_schnorr(msg, &keypair); let sig = secp.sign_schnorr(msg, &keypair);
secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair)) secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
.expect("invalid or corrupted schnorr signature"); .expect("invalid or corrupted schnorr signature");
let final_signature = schnorr::SchnorrSig { sig, hash_ty }; let final_signature = schnorr::SchnorrSig { sig, hash_ty };
@@ -570,7 +576,7 @@ impl SignersContainer {
self.0 self.0
.values() .values()
.filter_map(|signer| signer.descriptor_secret_key()) .filter_map(|signer| signer.descriptor_secret_key())
.filter_map(|secret| secret.as_public(secp).ok().map(|public| (public, secret))) .filter_map(|secret| secret.to_public(secp).ok().map(|public| (public, secret)))
.collect() .collect()
} }
@@ -595,8 +601,13 @@ impl SignersContainer {
}; };
match secret { match secret {
DescriptorSecretKey::SinglePriv(private_key) => container.add_external( DescriptorSecretKey::Single(private_key) => container.add_external(
SignerId::from(private_key.key.public_key(secp).to_pubkeyhash()), SignerId::from(
private_key
.key
.public_key(secp)
.to_pubkeyhash(SigType::Ecdsa),
),
SignerOrdering::default(), SignerOrdering::default(),
Arc::new(SignerWrapper::new(private_key.key, ctx)), Arc::new(SignerWrapper::new(private_key.key, ctx)),
), ),
@@ -718,10 +729,15 @@ pub struct SignOptions {
/// ///
/// Defaults to `true`, i.e., we always try to sign with the taproot internal key. /// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
pub sign_with_tap_internal_key: bool, pub sign_with_tap_internal_key: bool,
/// Whether we should grind ECDSA signature to ensure signing with low r
/// or not.
/// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r.
pub allow_grinding: bool,
} }
/// Customize which taproot script-path leaves the signer should sign. /// Customize which taproot script-path leaves the signer should sign.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum TapLeavesOptions { pub enum TapLeavesOptions {
/// The signer will sign all the leaves it has a key for. /// The signer will sign all the leaves it has a key for.
All, All,
@@ -751,6 +767,7 @@ impl Default for SignOptions {
try_finalize: true, try_finalize: true,
tap_leaves_options: TapLeavesOptions::default(), tap_leaves_options: TapLeavesOptions::default(),
sign_with_tap_internal_key: true, sign_with_tap_internal_key: true,
allow_grinding: true,
} }
} }
} }

View File

@@ -42,9 +42,7 @@ use std::default::Default;
use std::marker::PhantomData; use std::marker::PhantomData;
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt}; use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{OutPoint, Script, Transaction}; use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction};
use miniscript::descriptor::DescriptorTrait;
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use crate::{database::BatchDatabase, Error, Utxo, Wallet}; use crate::{database::BatchDatabase, Error, Utxo, Wallet};
@@ -139,7 +137,7 @@ pub(crate) struct TxParams {
pub(crate) manually_selected_only: bool, pub(crate) manually_selected_only: bool,
pub(crate) sighash: Option<psbt::PsbtSighashType>, pub(crate) sighash: Option<psbt::PsbtSighashType>,
pub(crate) ordering: TxOrdering, pub(crate) ordering: TxOrdering,
pub(crate) locktime: Option<u32>, pub(crate) locktime: Option<LockTime>,
pub(crate) rbf: Option<RbfValue>, pub(crate) rbf: Option<RbfValue>,
pub(crate) version: Option<Version>, pub(crate) version: Option<Version>,
pub(crate) change_policy: ChangeSpendPolicy, pub(crate) change_policy: ChangeSpendPolicy,
@@ -147,7 +145,7 @@ pub(crate) struct TxParams {
pub(crate) add_global_xpubs: bool, pub(crate) add_global_xpubs: bool,
pub(crate) include_output_redeem_witness_script: bool, pub(crate) include_output_redeem_witness_script: bool,
pub(crate) bumping_fee: Option<PreviousFee>, pub(crate) bumping_fee: Option<PreviousFee>,
pub(crate) current_height: Option<u32>, pub(crate) current_height: Option<LockTime>,
pub(crate) allow_dust: bool, pub(crate) allow_dust: bool,
} }
@@ -426,7 +424,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
/// Use a specific nLockTime while creating the transaction /// Use a specific nLockTime while creating the transaction
/// ///
/// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator. /// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator.
pub fn nlocktime(&mut self, locktime: u32) -> &mut Self { pub fn nlocktime(&mut self, locktime: LockTime) -> &mut Self {
self.params.locktime = Some(locktime); self.params.locktime = Some(locktime);
self self
} }
@@ -541,7 +539,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
/// ///
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not /// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
/// be a valid nSequence to signal RBF. /// be a valid nSequence to signal RBF.
pub fn enable_rbf_with_sequence(&mut self, nsequence: u32) -> &mut Self { pub fn enable_rbf_with_sequence(&mut self, nsequence: Sequence) -> &mut Self {
self.params.rbf = Some(RbfValue::Value(nsequence)); self.params.rbf = Some(RbfValue::Value(nsequence));
self self
} }
@@ -558,7 +556,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
/// ///
/// In both cases, if you don't provide a current height, we use the last sync height. /// In both cases, if you don't provide a current height, we use the last sync height.
pub fn current_height(&mut self, height: u32) -> &mut Self { pub fn current_height(&mut self, height: u32) -> &mut Self {
self.params.current_height = Some(height); self.params.current_height = Some(LockTime::from_height(height).expect("Invalid height"));
self self
} }
@@ -703,7 +701,7 @@ impl TxOrdering {
#[cfg(not(test))] #[cfg(not(test))]
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
#[cfg(test)] #[cfg(test)]
let mut rng = rand::rngs::StdRng::seed_from_u64(0); let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
tx.output.shuffle(&mut rng); tx.output.shuffle(&mut rng);
} }
@@ -736,13 +734,13 @@ impl Default for Version {
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
pub(crate) enum RbfValue { pub(crate) enum RbfValue {
Default, Default,
Value(u32), Value(Sequence),
} }
impl RbfValue { impl RbfValue {
pub(crate) fn get_value(&self) -> u32 { pub(crate) fn get_value(&self) -> Sequence {
match self { match self {
RbfValue::Default => 0xFFFFFFFD, RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME,
RbfValue::Value(v) => *v, RbfValue::Value(v) => *v,
} }
} }
@@ -858,10 +856,12 @@ mod test {
} }
fn get_test_utxos() -> Vec<LocalUtxo> { fn get_test_utxos() -> Vec<LocalUtxo> {
use bitcoin::hashes::Hash;
vec![ vec![
LocalUtxo { LocalUtxo {
outpoint: OutPoint { outpoint: OutPoint {
txid: Default::default(), txid: bitcoin::Txid::from_inner([0; 32]),
vout: 0, vout: 0,
}, },
txout: Default::default(), txout: Default::default(),
@@ -870,7 +870,7 @@ mod test {
}, },
LocalUtxo { LocalUtxo {
outpoint: OutPoint { outpoint: OutPoint {
txid: Default::default(), txid: bitcoin::Txid::from_inner([0; 32]),
vout: 1, vout: 1,
}, },
txout: Default::default(), txout: Default::default(),

View File

@@ -9,23 +9,11 @@
// You may not use this file except in accordance with one or both of these // You may not use this file except in accordance with one or both of these
// licenses. // licenses.
use bitcoin::blockdata::script::Script;
use bitcoin::secp256k1::{All, Secp256k1}; use bitcoin::secp256k1::{All, Secp256k1};
use bitcoin::{LockTime, Script, Sequence};
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey}; use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
// MSB of the nSequence. If set there's no consensus-constraint, so it must be disabled when
// spending using CSV in order to enforce CSV rules
pub(crate) const SEQUENCE_LOCKTIME_DISABLE_FLAG: u32 = 1 << 31;
// When nSequence is lower than this flag the timelock is interpreted as block-height-based,
// otherwise it's time-based
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
// Mask for the bits used to express the timelock
pub(crate) const SEQUENCE_LOCKTIME_MASK: u32 = 0x0000FFFF;
// Threshold for nLockTime to be considered a block-height-based timelock rather than time-based
pub(crate) const BLOCKS_TIMELOCK_THRESHOLD: u32 = 500000000;
/// Trait to check if a value is below the dust limit. /// Trait to check if a value is below the dust limit.
/// We are performing dust value calculation for a given script public key using rust-bitcoin to /// We are performing dust value calculation for a given script public key using rust-bitcoin to
/// keep it compatible with network dust rate /// keep it compatible with network dust rate
@@ -38,7 +26,7 @@ pub trait IsDust {
impl IsDust for u64 { impl IsDust for u64 {
fn is_dust(&self, script: &Script) -> bool { fn is_dust(&self, script: &Script) -> bool {
*self < script.dust_value().as_sat() *self < script.dust_value().to_sat()
} }
} }
@@ -56,19 +44,15 @@ impl After {
} }
} }
pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool { pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool {
// This flag cannot be set in the nSequence when spending using OP_CSV // The RBF value must enable relative timelocks
if rbf & SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 { if !rbf.is_relative_lock_time() {
return false; return false;
} }
let mask = SEQUENCE_LOCKTIME_TYPE_FLAG | SEQUENCE_LOCKTIME_MASK;
let rbf = rbf & mask;
let csv = csv & mask;
// Both values should be represented in the same unit (either time-based or // Both values should be represented in the same unit (either time-based or
// block-height based) // block-height based)
if (rbf < SEQUENCE_LOCKTIME_TYPE_FLAG) != (csv < SEQUENCE_LOCKTIME_TYPE_FLAG) { if rbf.is_time_locked() != csv.is_time_locked() {
return false; return false;
} }
@@ -80,24 +64,10 @@ pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool {
true true
} }
pub(crate) fn check_nlocktime(nlocktime: u32, required: u32) -> bool {
// Both values should be expressed in the same unit
if (nlocktime < BLOCKS_TIMELOCK_THRESHOLD) != (required < BLOCKS_TIMELOCK_THRESHOLD) {
return false;
}
// The value should be at least `required`
if nlocktime < required {
return false;
}
true
}
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for After { impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for After {
fn check_after(&self, n: u32) -> bool { fn check_after(&self, n: LockTime) -> bool {
if let Some(current_height) = self.current_height { if let Some(current_height) = self.current_height {
current_height >= n current_height >= n.to_consensus_u32()
} else { } else {
self.assume_height_reached self.assume_height_reached
} }
@@ -125,10 +95,15 @@ impl Older {
} }
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older { impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
fn check_older(&self, n: u32) -> bool { fn check_older(&self, n: Sequence) -> bool {
if let Some(current_height) = self.current_height { if let Some(current_height) = self.current_height {
// TODO: test >= / > // TODO: test >= / >
current_height as u64 >= self.create_height.unwrap_or(0) as u64 + n as u64 current_height
>= self
.create_height
.unwrap_or(0)
.checked_add(n.to_consensus_u32())
.expect("Overflowing addition")
} else { } else {
self.assume_height_reached self.assume_height_reached
} }
@@ -139,11 +114,12 @@ pub(crate) type SecpCtx = Secp256k1<All>;
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{ // When nSequence is lower than this flag the timelock is interpreted as block-height-based,
check_nlocktime, check_nsequence_rbf, IsDust, BLOCKS_TIMELOCK_THRESHOLD, // otherwise it's time-based
SEQUENCE_LOCKTIME_TYPE_FLAG, pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
};
use crate::bitcoin::Address; use super::{check_nsequence_rbf, IsDust};
use crate::bitcoin::{Address, Sequence};
use std::str::FromStr; use std::str::FromStr;
#[test] #[test]
@@ -165,66 +141,40 @@ mod test {
#[test] #[test]
fn test_check_nsequence_rbf_msb_set() { fn test_check_nsequence_rbf_msb_set() {
let result = check_nsequence_rbf(0x80000000, 5000); let result = check_nsequence_rbf(Sequence(0x80000000), Sequence(5000));
assert!(!result); assert!(!result);
} }
#[test] #[test]
fn test_check_nsequence_rbf_lt_csv() { fn test_check_nsequence_rbf_lt_csv() {
let result = check_nsequence_rbf(4000, 5000); let result = check_nsequence_rbf(Sequence(4000), Sequence(5000));
assert!(!result); assert!(!result);
} }
#[test] #[test]
fn test_check_nsequence_rbf_different_unit() { fn test_check_nsequence_rbf_different_unit() {
let result = check_nsequence_rbf(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, 5000); let result =
check_nsequence_rbf(Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000), Sequence(5000));
assert!(!result); assert!(!result);
} }
#[test] #[test]
fn test_check_nsequence_rbf_mask() { fn test_check_nsequence_rbf_mask() {
let result = check_nsequence_rbf(0x3f + 10_000, 5000); let result = check_nsequence_rbf(Sequence(0x3f + 10_000), Sequence(5000));
assert!(result); assert!(result);
} }
#[test] #[test]
fn test_check_nsequence_rbf_same_unit_blocks() { fn test_check_nsequence_rbf_same_unit_blocks() {
let result = check_nsequence_rbf(10_000, 5000); let result = check_nsequence_rbf(Sequence(10_000), Sequence(5000));
assert!(result); assert!(result);
} }
#[test] #[test]
fn test_check_nsequence_rbf_same_unit_time() { fn test_check_nsequence_rbf_same_unit_time() {
let result = check_nsequence_rbf( let result = check_nsequence_rbf(
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000, Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000),
SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000),
);
assert!(result);
}
#[test]
fn test_check_nlocktime_lt_cltv() {
let result = check_nlocktime(4000, 5000);
assert!(!result);
}
#[test]
fn test_check_nlocktime_different_unit() {
let result = check_nlocktime(BLOCKS_TIMELOCK_THRESHOLD + 5000, 5000);
assert!(!result);
}
#[test]
fn test_check_nlocktime_same_unit_blocks() {
let result = check_nlocktime(10_000, 5000);
assert!(result);
}
#[test]
fn test_check_nlocktime_same_unit_time() {
let result = check_nlocktime(
BLOCKS_TIMELOCK_THRESHOLD + 10_000,
BLOCKS_TIMELOCK_THRESHOLD + 5000,
); );
assert!(result); assert!(result);
} }