Compare commits

...

43 Commits

Author SHA1 Message Date
Steve Myers
d288cbbbbc Bump version to 0.25.0 2022-12-05 14:28:30 -06:00
Steve Myers
005447c81e Downgrade ubuntu to 20.04 for test_hardware_wallet CI job 2022-12-05 13:54:05 -06:00
Steve Myers
5168d41a93 Bump version to 0.25.0-rc.1 2022-11-30 09:08:37 -08:00
Steve Myers
4c5ceaff14 Merge bitcoindevkit/bdk#806: Ensure there are no duplicated script_pubkeys in sqlite
b5fcddcf1a Add sqlite migration to drop duplicated script_pubkeys rows (Steve Myers)
21c96c9c81 Add test for issue #801 (Alekos Filini)
c51d544932 [wip] Ensure there are no duplicated script_pubkeys in sqlite (Alekos Filini)

Pull request description:

  ### Description

  Add a `UNIQUE` constraint on the script_pubkeys table so that it doesn't grow constantly when caching new addresses.

  Fixes #801

  ### Notes to the reviewers

  Adding it to the 0.25 milestone since it's just a bugfix.

  Still in draft because I need to add extra migration queries to clean up existing dbs.

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

  * [ ] This pull request breaks the existing API
  * [x] 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:
  notmandatory:
    ACK b5fcddcf1a

Tree-SHA512: 7b10e453bb38af5c4f80f77692a56e37259680e50f9c2c9e554a0e5f04fb9cab897da6476c6c9930f1c501b455472984a1c92c4f137cff49acdc390d2e705107
2022-11-30 09:05:47 -08:00
Steve Myers
b5fcddcf1a Add sqlite migration to drop duplicated script_pubkeys rows 2022-11-29 07:20:49 -08:00
Alekos Filini
d570ff2c65 Merge bitcoindevkit/bdk#803: Bump version to 0.25.0
5e56c3b3c1 Bump version to 0.25.0 (Steve Myers)

Pull request description:

  ### Description

  Bump version to 0.25.0

ACKs for top commit:
  afilini:
    ACK 5e56c3b3c1

Tree-SHA512: b1fc49caed9676d4e3db2a625b6209fffa19694ada2e2ff4d3e5d5cbbd6816ff03924387d66bf3a8b8ec4a3b44bf5d00d95cc450427e5b669e88af60400b02c9
2022-11-28 10:55:16 +01:00
Alekos Filini
21c96c9c81 Add test for issue #801 2022-11-26 15:11:09 +01:00
Alekos Filini
c51d544932 [wip] Ensure there are no duplicated script_pubkeys in sqlite
Add a `UNIQUE` constraint on the script_pubkeys table so that it doesn't
grow constantly when caching new addresses.

Fixes #801
2022-11-26 15:04:34 +01:00
Steve Myers
5e56c3b3c1 Bump version to 0.25.0 2022-11-24 22:01:01 -08:00
Steve Myers
235961a934 Merge bitcoindevkit/bdk#746: Add mnemonic_to_descriptors example
df905a8d5e Add mnemonic to descriptors example. (Vladimir Fomene)

Pull request description:

  ### Description

  Using *bdk-cli* it is simple for a new user to generate a mnemonic phrase and descriptors. This might
  not be clear for new users when using *bdk* itself.

  ### Notes to the reviewers

  This was initially requested by one user but might be relevant for other users as well.

  ### 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
  * [ ] 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
  * [ ] I'm linking the issue being fixed by this PR

ACKs for top commit:
  notmandatory:
    ACK df905a8d5e

Tree-SHA512: ccaab775d664d1d5ad6f4cd4fb11f9552c4662fbac747ad0487a016fd4a0fa6f13fcb4dffdc6f0d2f14ee432fcb2a72a8b8837b0a977ded649ec7d3d84e1c0ed
2022-11-24 12:52:42 -08:00
Vladimir Fomene
df905a8d5e Add mnemonic to descriptors example.
This was initially requested by one user but might be
relevant for other users as well.
2022-11-24 21:18:13 +03:00
Steve Myers
8b68cf9546 Merge bitcoindevkit/bdk#800: fix: ensure the key network is updated in the KeyMap as well
150f4d6f41 fix: ensure the key network is updated in the KeyMap as well (Alekos Filini)

Pull request description:

  ### Description

  Otherwise we may have inconsistencies with keys in the descriptor that have a network and keys in the keymap that are different.

  ### Notes to the reviewers

  Adding it to the `0.25` milestone since it's just a quick fix

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

  * [ ] 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

ACKs for top commit:
  notmandatory:
    ACK 150f4d6f41

Tree-SHA512: df87323cc29cf74f54a0867bfbe9fe24543de69dc6443656bd920458c9055d4b05614430c89bb470a8a6f7d10da023a0fb107b1cfb0fcc38e50f0579b6411a33
2022-11-24 09:23:10 -08:00
Alekos Filini
150f4d6f41 fix: ensure the key network is updated in the KeyMap as well
Otherwise we may have inconsistencies with keys in the descriptor that
have a network and keys in the keymap that are different.
2022-11-23 17:58:47 +01:00
Steve Myers
1c95ca33a8 Merge bitcoindevkit/bdk#526: Add code example for each supported backend
f99a6b9f43 add `esplora_backend` example. (w0xlt)
aedbc8c97d add `electrum_backend` example. (w0xlt)

Pull request description:

  This PR adds code example for connecting to Esplora, Electrum Server, Neutrino and Bitcoin Core.
  Also shows how to retrieve balance, sign and broadcast transactions.

  To test:
  ```
  cd examples/backend/
  cargo run electrum
  cargo run esplora
  cargo run neutrino
  cargo run rpc_core
  ```

ACKs for top commit:
  rajarshimaitra:
    tACK f99a6b9f43

Tree-SHA512: 1d99129f14d83d9a833cee1587fe0eb3e5da4c83ae9008fb3e510be96a874dc86f800f203f68f5da70648a911709107cf0f286c2623808dc97dd63b7addef16b
2022-11-22 08:13:16 -08:00
Steve Myers
108edc3a6b Merge bitcoindevkit/bdk#785: Fix wallet export rescan height
e9bbb8724f Fix wallet export rescan height (LLFourn)

Pull request description:

  It would return the latest transaction height rather than the earliest as the height to rescan from.

  Found by @evanlinjin  and I while implementing `bdk_core` stuff into bdk's wallet.

  ### Changelog notice

  - Fix wallet export transaction height

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] This pull request breaks the existing API
  * [x] I've added tests to reproduce the issue which are now passing

ACKs for top commit:
  rajarshimaitra:
    tACK e9bbb8724f
  notmandatory:
    ACK e9bbb8724f

Tree-SHA512: 9b29ef0df39d26806f48b38fa5c3643bad32f58b993ffdcfc7811aca64a025bd8f163967321f874aa2ef3d29c3e7bc6e2f44d348306a37111f4def036d4c095e
2022-11-22 06:30:17 -08:00
w0xlt
f99a6b9f43 add esplora_backend example. 2022-11-01 19:59:55 -03:00
w0xlt
aedbc8c97d add electrum_backend example. 2022-11-01 19:59:46 -03: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
LLFourn
e9bbb8724f Fix wallet export rescan height
It would return the latest transaction height rather than the earliest :S
2022-10-26 12:35:21 +08: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
45 changed files with 1471 additions and 1249 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
run: rustup update
- 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:
name: Rust fmt
@@ -173,8 +173,8 @@ jobs:
- name: Check fmt
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
test_harware_wallet:
runs-on: ubuntu-latest
test_hardware_wallet:
runs-on: ubuntu-20.04
strategy:
matrix:
rust:

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk"
version = "0.22.0"
version = "0.25.0"
edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org"
@@ -14,16 +14,16 @@ license = "MIT OR Apache-2.0"
[dependencies]
bdk-macros = "^0.6"
log = "^0.4"
miniscript = { version = "7.0", features = ["use-serde"] }
bitcoin = { version = "0.28.1", features = ["use-serde", "base64", "rand"] }
miniscript = { version = "8.0", features = ["serde"] }
bitcoin = { version = "0.29.1", features = ["serde", "base64", "rand"] }
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
rand = "^0.7"
rand = "^0.8"
# Optional dependencies
sled = { version = "0.34", optional = true }
electrum-client = { version = "0.11", optional = true }
esplora-client = { version = "0.1.1", default-features = false, optional = true }
electrum-client = { version = "0.12", optional = true }
esplora-client = { version = "0.2", default-features = false, optional = true }
rusqlite = { version = "0.27.0", optional = true }
ahash = { version = "0.7.6", 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 }
cc = { version = ">=1.0.64", 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 }
bitcoinconsensus = { version = "0.19.0-3", optional = true }
# 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
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["rt"] }
tokio = { version = "1", features = ["rt", "macros"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = "0.2"
async-trait = "0.1"
js-sys = "0.3"
rand = { version = "^0.7", features = ["wasm-bindgen"] }
[features]
minimal = []
@@ -98,13 +98,18 @@ test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoi
test-md-docs = ["electrum"]
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]
lazy_static = "1.4"
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]]
name = "compact_filters_balance"
required-features = ["compact_filters"]
@@ -128,6 +133,31 @@ name = "psbt_signer"
path = "examples/psbt_signer.rs"
required-features = ["electrum"]
[[example]]
name = "hardware_signer"
path = "examples/hardware_signer.rs"
required-features = ["electrum", "hardware-signer"]
[[example]]
name = "electrum_backend"
path = "examples/electrum_backend.rs"
required-features = ["electrum"]
[[example]]
name = "esplora_backend_synchronous"
path = "examples/esplora_backend_synchronous.rs"
required-features = ["use-esplora-ureq"]
[[example]]
name = "esplora_backend_asynchronous"
path = "examples/esplora_backend_asynchronous.rs"
required-features = ["use-esplora-reqwest", "reqwest-default-tls", "async-interface"]
[[example]]
name = "mnemonic_to_descriptors"
path = "examples/mnemonic_to_descriptors.rs"
required-features = ["all-keys"]
[workspace]
members = ["macros"]
[package.metadata.docs.rs]

View File

@@ -95,7 +95,7 @@ use bdk::blockchain::ElectrumBlockchain;
use bdk::electrum_client::Client;
use bdk::wallet::AddressIndex::New;
use bitcoin::base64;
use base64;
use bitcoin::consensus::serialize;
fn main() -> Result<(), bdk::Error> {
@@ -132,7 +132,7 @@ fn main() -> Result<(), bdk::Error> {
```rust,no_run
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
use bitcoin::base64;
use base64;
use bitcoin::consensus::deserialize;
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.
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
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(())
}

View File

@@ -0,0 +1,87 @@
use std::str::FromStr;
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
use bdk::bitcoin::Network;
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
use bdk::database::MemoryDatabase;
use bdk::template::Bip84;
use bdk::wallet::export::FullyNodedExport;
use bdk::{KeychainKind, SyncOptions, Wallet};
use bdk::electrum_client::Client;
use bdk::wallet::AddressIndex;
use bitcoin::util::bip32;
pub mod utils;
use crate::utils::tx::build_signed_tx;
/// This will create a wallet from an xpriv and get the balance by connecting to an Electrum server.
/// If enough amount is available, this will send a transaction to an address.
/// Otherwise, this will display a wallet address to receive funds.
///
/// This can be run with `cargo run --example electrum_backend` in the root folder.
fn main() {
let network = Network::Testnet;
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
let electrum_url = "ssl://electrum.blockstream.info:60002";
run(&network, electrum_url, xpriv);
}
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
Wallet::new(
Bip84(*xpriv, KeychainKind::External),
Some(Bip84(*xpriv, KeychainKind::Internal)),
*network,
MemoryDatabase::default(),
)
.unwrap()
}
fn run(network: &Network, electrum_url: &str, xpriv: &str) {
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
// Apparently it works only with Electrs (not EletrumX)
let blockchain = ElectrumBlockchain::from(Client::new(electrum_url).unwrap());
let wallet = create_wallet(network, &xpriv);
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let address = wallet.get_address(AddressIndex::New).unwrap().address;
println!("address: {}", address);
let balance = wallet.get_balance().unwrap();
println!("Available coins in BDK wallet : {} sats", balance);
if balance.confirmed > 6500 {
// the wallet sends the amount to itself.
let recipient_address = wallet
.get_address(AddressIndex::New)
.unwrap()
.address
.to_string();
let amount = 5359;
let tx = build_signed_tx(&wallet, &recipient_address, amount);
blockchain.broadcast(&tx).unwrap();
println!("tx id: {}", tx.txid());
} else {
println!("Insufficient Funds. Fund the wallet with the address above");
}
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
.map_err(ToString::to_string)
.map_err(bdk::Error::Generic)
.unwrap();
println!("------\nWallet Backup: {}", export.to_string());
}

View File

@@ -0,0 +1,93 @@
use std::str::FromStr;
use bdk::blockchain::Blockchain;
use bdk::{
blockchain::esplora::EsploraBlockchain,
database::MemoryDatabase,
template::Bip84,
wallet::{export::FullyNodedExport, AddressIndex},
KeychainKind, SyncOptions, Wallet,
};
use bitcoin::{
util::bip32::{self, ExtendedPrivKey},
Network,
};
pub mod utils;
use crate::utils::tx::build_signed_tx;
/// This will create a wallet from an xpriv and get the balance by connecting to an Esplora server,
/// using non blocking asynchronous calls with `reqwest`.
/// If enough amount is available, this will send a transaction to an address.
/// Otherwise, this will display a wallet address to receive funds.
///
/// This can be run with `cargo run --no-default-features --features="use-esplora-reqwest, reqwest-default-tls, async-interface" --example esplora_backend_asynchronous`
/// in the root folder.
#[tokio::main(flavor = "current_thread")]
async fn main() {
let network = Network::Signet;
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
let esplora_url = "https://explorer.bc-2.jp/api";
run(&network, esplora_url, xpriv).await;
}
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
Wallet::new(
Bip84(*xpriv, KeychainKind::External),
Some(Bip84(*xpriv, KeychainKind::Internal)),
*network,
MemoryDatabase::default(),
)
.unwrap()
}
async fn run(network: &Network, esplora_url: &str, xpriv: &str) {
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
let blockchain = EsploraBlockchain::new(esplora_url, 20);
let wallet = create_wallet(network, &xpriv);
wallet
.sync(&blockchain, SyncOptions::default())
.await
.unwrap();
let address = wallet.get_address(AddressIndex::New).unwrap().address;
println!("address: {}", address);
let balance = wallet.get_balance().unwrap();
println!("Available coins in BDK wallet : {} sats", balance);
if balance.confirmed > 10500 {
// the wallet sends the amount to itself.
let recipient_address = wallet
.get_address(AddressIndex::New)
.unwrap()
.address
.to_string();
let amount = 9359;
let tx = build_signed_tx(&wallet, &recipient_address, amount);
let _ = blockchain.broadcast(&tx);
println!("tx id: {}", tx.txid());
} else {
println!("Insufficient Funds. Fund the wallet with the address above");
}
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
.map_err(ToString::to_string)
.map_err(bdk::Error::Generic)
.unwrap();
println!("------\nWallet Backup: {}", export.to_string());
}

View File

@@ -0,0 +1,89 @@
use std::str::FromStr;
use bdk::blockchain::Blockchain;
use bdk::{
blockchain::esplora::EsploraBlockchain,
database::MemoryDatabase,
template::Bip84,
wallet::{export::FullyNodedExport, AddressIndex},
KeychainKind, SyncOptions, Wallet,
};
use bitcoin::{
util::bip32::{self, ExtendedPrivKey},
Network,
};
pub mod utils;
use crate::utils::tx::build_signed_tx;
/// This will create a wallet from an xpriv and get the balance by connecting to an Esplora server,
/// using blocking calls with `ureq`.
/// If enough amount is available, this will send a transaction to an address.
/// Otherwise, this will display a wallet address to receive funds.
///
/// This can be run with `cargo run --features=use-esplora-ureq --example esplora_backend_synchronous`
/// in the root folder.
fn main() {
let network = Network::Signet;
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
let esplora_url = "https://explorer.bc-2.jp/api";
run(&network, esplora_url, xpriv);
}
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
Wallet::new(
Bip84(*xpriv, KeychainKind::External),
Some(Bip84(*xpriv, KeychainKind::Internal)),
*network,
MemoryDatabase::default(),
)
.unwrap()
}
fn run(network: &Network, esplora_url: &str, xpriv: &str) {
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
let blockchain = EsploraBlockchain::new(esplora_url, 20);
let wallet = create_wallet(network, &xpriv);
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let address = wallet.get_address(AddressIndex::New).unwrap().address;
println!("address: {}", address);
let balance = wallet.get_balance().unwrap();
println!("Available coins in BDK wallet : {} sats", balance);
if balance.confirmed > 10500 {
// the wallet sends the amount to itself.
let recipient_address = wallet
.get_address(AddressIndex::New)
.unwrap()
.address
.to_string();
let amount = 9359;
let tx = build_signed_tx(&wallet, &recipient_address, amount);
blockchain.broadcast(&tx).unwrap();
println!("tx id: {}", tx.txid());
} else {
println!("Insufficient Funds. Fund the wallet with the address above");
}
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
.map_err(ToString::to_string)
.map_err(bdk::Error::Generic)
.unwrap();
println!("------\nWallet Backup: {}", export.to_string());
}

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

@@ -0,0 +1,60 @@
// 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 bdk::bitcoin::secp256k1::Secp256k1;
use bdk::bitcoin::util::bip32::DerivationPath;
use bdk::bitcoin::Network;
use bdk::descriptor;
use bdk::descriptor::IntoWalletDescriptor;
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
use bdk::keys::{GeneratableKey, GeneratedKey};
use bdk::miniscript::Tap;
use bdk::Error as BDK_Error;
use std::error::Error;
use std::str::FromStr;
/// This example demonstrates how to generate a mnemonic phrase
/// using BDK and use that to generate a descriptor string.
fn main() -> Result<(), Box<dyn Error>> {
let secp = Secp256k1::new();
// In this example we are generating a 12 words mnemonic phrase
// but it is also possible generate 15, 18, 21 and 24 words
// using their respective `WordCount` variant.
let mnemonic: GeneratedKey<_, Tap> =
Mnemonic::generate((WordCount::Words12, Language::English))
.map_err(|_| BDK_Error::Generic("Mnemonic generation error".to_string()))?;
println!("Mnemonic phrase: {}", *mnemonic);
let mnemonic_with_passphrase = (mnemonic, None);
// define external and internal derivation key path
let external_path = DerivationPath::from_str("m/86h/0h/0h/0").unwrap();
let internal_path = DerivationPath::from_str("m/86h/0h/0h/1").unwrap();
// generate external and internal descriptor from mnemonic
let (external_descriptor, ext_keymap) =
descriptor!(tr((mnemonic_with_passphrase.clone(), external_path)))?
.into_wallet_descriptor(&secp, Network::Testnet)?;
let (internal_descriptor, int_keymap) =
descriptor!(tr((mnemonic_with_passphrase, internal_path)))?
.into_wallet_descriptor(&secp, Network::Testnet)?;
println!("tpub external descriptor: {}", external_descriptor);
println!("tpub internal descriptor: {}", internal_descriptor);
println!(
"tprv external descriptor: {}",
external_descriptor.to_string_with_secret(&ext_keymap)
);
println!(
"tprv internal descriptor: {}",
internal_descriptor.to_string_with_secret(&int_keymap)
);
Ok(())
}

View File

@@ -9,20 +9,38 @@
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
use bdk::database::MemoryDatabase;
use bdk::wallet::AddressIndex;
use bdk::SyncOptions;
use bdk::{descriptor, SyncOptions};
use bdk::{FeeRate, SignOptions, Wallet};
use bitcoin::secp256k1::Secp256k1;
use bitcoin::{Address, Network};
use electrum_client::Client;
use miniscript::descriptor::DescriptorSecretKey;
use std::error::Error;
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>> {
// test keys created with `bdk-cli key generate` and `bdk-cli key derive` commands
let signing_external_descriptor = "wpkh([e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/0/*)";
let signing_internal_descriptor = "wpkh([e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/1/*)";
// test key created with `bdk-cli key generate` and `bdk-cli key derive` commands
let external_secret_xkey = DescriptorSecretKey::from_str("[e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/0/*").unwrap();
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 watch_only_internal_descriptor = "wpkh([e9824965/84'/1'/0']tpubDCcguXsm6uj79fSQt4V2EF7SF5b26zCuG2ZZNsbNQuw5G9YWSJuGhg2KknQBywRq4VGTu41zYTCh3QeVFyBdbsymgRX9Mrts94SW7obEdqs/1/*)";
let secp = Secp256k1::new();
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
let blockchain =

30
examples/utils/mod.rs Normal file
View File

@@ -0,0 +1,30 @@
pub(crate) mod tx {
use std::str::FromStr;
use bdk::{database::BatchDatabase, SignOptions, Wallet};
use bitcoin::{Address, Transaction};
pub fn build_signed_tx<D: BatchDatabase>(
wallet: &Wallet<D>,
recipient_address: &str,
amount: u64,
) -> Transaction {
// Create a transaction builder
let mut tx_builder = wallet.build_tx();
let to_address = Address::from_str(recipient_address).unwrap();
// Set recipient of the transaction
tx_builder.set_recipients(vec![(to_address.script_pubkey(), amount)]);
// Finalise the transaction and extract PSBT
let (mut psbt, _) = tx_builder.finish().unwrap();
// Sign the above psbt with signing option
wallet.sign(&mut psbt, SignOptions::default()).unwrap();
// Extract the final transaction
psbt.extract_tx()
}
}

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")]
pub enum AnyBlockchainConfig {
#[cfg(feature = "electrum")]

View File

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

View File

@@ -75,7 +75,10 @@ impl Mempool {
/// Look-up a transaction in the mempool given an [`Inventory`] request
pub fn get_tx(&self, inventory: &Inventory) -> Option<Transaction> {
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::WitnessTransaction(txid) => TxIdentifier::Txid(*txid),
Inventory::WTx(wtxid) => TxIdentifier::Wtxid(*wtxid),

View File

@@ -103,42 +103,42 @@ where
}
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;
match self {
BundleStatus::Init => {
written += 0x00u8.consensus_encode(&mut e)?;
written += 0x00u8.consensus_encode(e)?;
}
BundleStatus::CfHeaders { cf_headers } => {
written += 0x01u8.consensus_encode(&mut e)?;
written += VarInt(cf_headers.len() as u64).consensus_encode(&mut e)?;
written += 0x01u8.consensus_encode(e)?;
written += VarInt(cf_headers.len() as u64).consensus_encode(e)?;
for header in cf_headers {
written += header.consensus_encode(&mut e)?;
written += header.consensus_encode(e)?;
}
}
BundleStatus::CFilters { cf_filters } => {
written += 0x02u8.consensus_encode(&mut e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
written += 0x02u8.consensus_encode(e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
for filter in cf_filters {
written += filter.consensus_encode(&mut e)?;
written += filter.consensus_encode(e)?;
}
}
BundleStatus::Processed { cf_filters } => {
written += 0x03u8.consensus_encode(&mut e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
written += 0x03u8.consensus_encode(e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
for filter in cf_filters {
written += filter.consensus_encode(&mut e)?;
written += filter.consensus_encode(e)?;
}
}
BundleStatus::Pruned => {
written += 0x04u8.consensus_encode(&mut e)?;
written += 0x04u8.consensus_encode(e)?;
}
BundleStatus::Tip { cf_filters } => {
written += 0x05u8.consensus_encode(&mut e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?;
written += 0x05u8.consensus_encode(e)?;
written += VarInt(cf_filters.len() as u64).consensus_encode(e)?;
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 {
fn consensus_decode<D: Read>(mut d: D) -> Result<Self, bitcoin::consensus::encode::Error> {
let byte_type = u8::consensus_decode(&mut d)?;
fn consensus_decode<D: Read + ?Sized>(
d: &mut D,
) -> Result<Self, bitcoin::consensus::encode::Error> {
let byte_type = u8::consensus_decode(d)?;
match byte_type {
0x00 => Ok(BundleStatus::Init),
0x01 => {
let num = VarInt::consensus_decode(&mut d)?;
let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize;
let mut cf_headers = Vec::with_capacity(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 })
}
0x02 => {
let num = VarInt::consensus_decode(&mut d)?;
let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize;
let mut cf_filters = Vec::with_capacity(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 })
}
0x03 => {
let num = VarInt::consensus_decode(&mut d)?;
let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize;
let mut cf_filters = Vec::with_capacity(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 })
}
0x04 => Ok(BundleStatus::Pruned),
0x05 => {
let num = VarInt::consensus_decode(&mut d)?;
let num = VarInt::consensus_decode(d)?;
let num = num.0 as usize;
let mut cf_filters = Vec::with_capacity(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 })
@@ -276,7 +278,11 @@ impl ChainStore<Full> {
}
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 mut write_store = self.store.write().unwrap();
@@ -647,7 +653,7 @@ impl CfStore {
&first_key,
(
BundleStatus::Init,
filter.filter_header(&FilterHeader::from_hash(Default::default())),
filter.filter_header(&FilterHeader::from_hash(Hash::all_zeros())),
)
.serialize(),
)?;

View File

@@ -14,6 +14,7 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;
use bitcoin::hash_types::{BlockHash, FilterHeader};
use bitcoin::hashes::Hash;
use bitcoin::network::message::NetworkMessage;
use bitcoin::network::message_blockdata::GetHeadersMessage;
use bitcoin::util::bip158::BlockFilter;
@@ -254,7 +255,7 @@ where
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
locators_vec,
Default::default(),
Hash::all_zeros(),
)))?;
let (mut snapshot, mut last_hash) = if let NetworkMessage::Headers(headers) = peer
.recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))?
@@ -276,7 +277,7 @@ where
while sync_height < peer.get_version().start_height as usize {
peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new(
vec![last_hash],
Default::default(),
Hash::all_zeros(),
)))?;
if let NetworkMessage::Headers(headers) = peer
.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`]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
pub struct ElectrumBlockchainConfig {
/// 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]
impl GetBlockHash for EsploraBlockchain {
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(block_header.block_hash())
Ok(await_or_block!(self
.url_client
.get_block_hash(height as u32))?)
}
}

View File

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

View File

@@ -33,7 +33,7 @@ mod blocking;
pub use self::blocking::*;
/// Configuration for an [`EsploraBlockchain`]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
pub struct EsploraBlockchainConfig {
/// 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::blockchain::*;
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::descriptor::get_checksum;
use crate::descriptor::calc_checksum;
use crate::error::MissingCachedScripts;
use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
use bitcoin::Script;
@@ -77,7 +77,7 @@ impl Deref for RpcBlockchain {
}
/// RpcBlockchain configuration options
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct RpcConfig {
/// The bitcoin node url
pub url: String,
@@ -96,7 +96,7 @@ pub struct RpcConfig {
/// 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
/// 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 {
/// The minimum number of scripts to scan for on initial sync.
pub start_script_count: usize,
@@ -167,7 +167,7 @@ impl Blockchain for RpcBlockchain {
.estimate_smart_fee(target as u16, None)?
.fee_rate
.ok_or(Error::FeeRateUnavailable)?
.as_sat() as f64;
.to_sat() as f64;
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;
TransactionDetails {
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)
if let (None, Some(new_fee)) = (db_tx.fee, tx_res.detail.fee) {
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)
@@ -603,7 +608,7 @@ impl<'a, D: BatchDatabase> DbState<'a, D> {
LocalUtxo {
outpoint: OutPoint::new(entry.txid, entry.vout),
txout: TxOut {
value: entry.amount.as_sat(),
value: entry.amount.to_sat(),
script_pubkey: entry.script_pub_key,
},
keychain,
@@ -801,7 +806,7 @@ fn is_wallet_descriptor(client: &Client) -> Result<bool, Error> {
fn descriptor_from_script_pubkey(script: &Script) -> String {
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`]
@@ -873,15 +878,13 @@ impl BlockchainFactory for RpcBlockchainFactory {
mod test {
use super::*;
use crate::{
descriptor::{into_wallet_descriptor_checked, AsDerived},
testutils::blockchain_tests::TestClient,
descriptor::into_wallet_descriptor_checked, testutils::blockchain_tests::TestClient,
wallet::utils::SecpCtx,
};
use bitcoin::{Address, Network};
use bitcoincore_rpc::RpcApi;
use log::LevelFilter;
use miniscript::DescriptorTrait;
crate::bdk_blockchain_tests! {
fn test_instance(test_client: &TestClient) -> RpcBlockchain {
@@ -958,7 +961,7 @@ mod test {
// generate scripts (1 tx per script)
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<_>>();
// import scripts and wait

View File

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

View File

@@ -57,7 +57,17 @@ static MIGRATIONS: &[&str] = &[
"CREATE TABLE utxos (value INTEGER, keychain TEXT, vout INTEGER, txid BLOB, script BLOB, is_spent BOOLEAN DEFAULT 0);",
"INSERT INTO utxos SELECT value, keychain, vout, txid, script, is_spent FROM utxos_old;",
"DROP TABLE utxos_old;",
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);"
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);",
// Fix issue https://github.com/bitcoindevkit/bdk/issues/801: drop duplicated script_pubkeys
"ALTER TABLE script_pubkeys RENAME TO script_pubkeys_old;",
"DROP INDEX idx_keychain_child;",
"DROP INDEX idx_script;",
"CREATE TABLE script_pubkeys (keychain TEXT, child INTEGER, script BLOB);",
"CREATE INDEX idx_keychain_child ON script_pubkeys(keychain, child);",
"CREATE INDEX idx_script ON script_pubkeys(script);",
"CREATE UNIQUE INDEX idx_script_pks_unique ON script_pubkeys(keychain, child);",
"INSERT OR REPLACE INTO script_pubkeys SELECT keychain, child, script FROM script_pubkeys_old;",
"DROP TABLE script_pubkeys_old;"
];
/// Sqlite database stored on filesystem
@@ -88,7 +98,7 @@ impl SqliteDatabase {
child: u32,
script: &[u8],
) -> Result<i64, Error> {
let mut statement = self.connection.prepare_cached("INSERT INTO script_pubkeys (keychain, child, script) VALUES (:keychain, :child, :script)")?;
let mut statement = self.connection.prepare_cached("INSERT OR REPLACE INTO script_pubkeys (keychain, child, script) VALUES (:keychain, :child, :script)")?;
statement.execute(named_params! {
":keychain": keychain,
":child": child,
@@ -1096,4 +1106,44 @@ pub mod test {
fn test_check_descriptor_checksum() {
crate::database::test::test_check_descriptor_checksum(get_database());
}
// Issue 801: https://github.com/bitcoindevkit/bdk/issues/801
#[test]
fn test_unique_spks() {
use crate::bitcoin::hashes::hex::FromHex;
use crate::database::*;
let mut db = get_database();
let script = Script::from(
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
);
let path = 42;
let keychain = KeychainKind::External;
for _ in 0..100 {
db.set_script_pubkey(&script, keychain, path).unwrap();
}
let mut statement = db
.connection
.prepare_cached(
"select keychain,child,count(child) from script_pubkeys group by keychain,child;",
)
.unwrap();
let mut rows = statement.query([]).unwrap();
while let Some(row) = rows.next().unwrap() {
let keychain: String = row.get(0).unwrap();
let child: u32 = row.get(1).unwrap();
let count: usize = row.get(2).unwrap();
assert!(
count == 1,
"keychain={}, child={}, count={}",
keychain,
child,
count
);
}
}
}

View File

@@ -41,12 +41,24 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
c
}
/// Computes the checksum bytes of a descriptor
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
/// Computes the checksum bytes of a descriptor.
/// `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 cls = 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() {
let pos = INPUT_CHARSET
.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];
}
// 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)
}
/// 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
#[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> {
// 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)]
mod test {
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]
fn test_get_checksum() {
fn test_calc_checksum() {
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/*)";
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]
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 invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
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]
));
}

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

View File

@@ -15,32 +15,32 @@
//! from [`miniscript`].
use std::collections::BTreeMap;
use std::ops::Deref;
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
use bitcoin::util::{psbt, taproot};
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
use bitcoin::{Network, Script, TxOut};
use bitcoin::{Network, TxOut};
use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey};
use miniscript::descriptor::{
DefiniteDescriptorKey, DescriptorSecretKey, DescriptorType, InnerXKey, SinglePubKey,
};
pub use miniscript::{
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
};
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
use miniscript::{ForEachKey, MiniscriptKey, TranslatePk};
use crate::descriptor::policy::BuildSatisfaction;
pub mod checksum;
pub mod derived;
#[doc(hidden)]
pub mod dsl;
pub mod error;
pub mod policy;
pub mod template;
pub use self::checksum::get_checksum;
pub use self::derived::{AsDerived, DerivedDescriptorKey};
pub use self::checksum::calc_checksum;
use self::checksum::calc_checksum_bytes;
pub use self::error::Error as DescriptorError;
pub use self::policy::Policy;
use self::template::DescriptorTemplateOut;
@@ -52,7 +52,7 @@ use crate::wallet::utils::SecpCtx;
pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
/// 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
/// [`psbt::Output`]
@@ -84,19 +84,15 @@ impl IntoWalletDescriptor for &str {
secp: &SecpCtx,
network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let descriptor = if self.contains('#') {
let parts: Vec<&str> = self.splitn(2, '#').collect();
if !get_checksum(parts[0])
.ok()
.map(|computed| computed == parts[1])
.unwrap_or(false)
{
return Err(DescriptorError::InvalidDescriptorChecksum);
let descriptor = match self.split_once('#') {
Some((desc, original_checksum)) => {
let checksum = calc_checksum_bytes(desc)?;
if original_checksum.as_bytes() != checksum {
return Err(DescriptorError::InvalidDescriptorChecksum);
}
desc
}
parts[0]
} else {
self
None => self,
};
ExtendedDescriptor::parse_descriptor(secp, descriptor)?
@@ -132,28 +128,76 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
use crate::keys::DescriptorKey;
let check_key = |pk: &DescriptorPublicKey| {
let (pk, _, networks) = if self.0.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)?
};
struct Translator<'s, 'd> {
secp: &'s SecpCtx,
descriptor: &'d ExtendedDescriptor,
network: Network,
}
if networks.contains(&network) {
Ok(pk)
} else {
Err(DescriptorError::Key(KeyError::InvalidNetwork))
impl<'s, 'd>
miniscript::Translator<DescriptorPublicKey, miniscript::DummyKey, DescriptorError>
for Translator<'s, 'd>
{
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
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 +207,17 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
_secp: &SecpCtx,
network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let valid_networks = &self.2;
struct Translator {
network: Network,
}
let fix_key = |pk: &DescriptorPublicKey| {
if valid_networks.contains(&network) {
impl miniscript::Translator<DescriptorPublicKey, DescriptorPublicKey, DescriptorError>
for Translator
{
fn pk(
&mut self,
pk: &DescriptorPublicKey,
) -> Result<DescriptorPublicKey, DescriptorError> {
// 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
// "mainnet", but then override the set of valid networks to specify that all of
@@ -175,7 +226,7 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
let pk = match pk {
DescriptorPublicKey::XPub(ref xpub) => {
let mut xpub = xpub.clone();
xpub.xkey.network = network;
xpub.xkey.network = self.network;
DescriptorPublicKey::XPub(xpub)
}
@@ -183,15 +234,42 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
};
Ok(pk)
} else {
Err(DescriptorError::Key(KeyError::InvalidNetwork))
}
};
miniscript::translate_hash_clone!(
DescriptorPublicKey,
DescriptorPublicKey,
DescriptorError
);
}
// fixup the network for keys that need it
let translated = self.0.translate_pk(fix_key, fix_key)?;
let (desc, keymap, networks) = self;
Ok((translated, self.1))
if !networks.contains(&network) {
return Err(DescriptorError::Key(KeyError::InvalidNetwork));
}
// fixup the network for keys that need it in the descriptor
let translated = desc.translate_pk(&mut Translator { network })?;
// ...and in the key map
let fixed_keymap = keymap
.into_iter()
.map(|(mut k, mut v)| {
match (&mut k, &mut v) {
(DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => {
xpub.xkey.network = network;
xprv.xkey.network = network;
}
(_, DescriptorSecretKey::Single(key)) => {
key.key.network = network;
}
_ => {}
}
(k, v)
})
.collect();
Ok((translated, fixed_keymap))
}
}
@@ -210,7 +288,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
derivation_path,
wildcard,
..
}) = k.as_key()
}) = k
{
return *wildcard == Wildcard::Hardened
|| derivation_path.into_iter().any(ChildNumber::is_hardened);
@@ -257,7 +335,6 @@ pub trait ExtractPolicy {
}
pub(crate) trait XKeyUtils {
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath;
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
}
@@ -265,27 +342,6 @@ impl<T> XKeyUtils for DescriptorXKey<T>
where
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 {
match self.origin {
Some((fingerprint, _)) => fingerprint,
@@ -294,11 +350,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 {
fn is_witness(&self) -> bool;
fn is_taproot(&self) -> bool;
@@ -307,63 +358,23 @@ pub(crate) trait DescriptorMeta {
&self,
hd_keypaths: &HdKeyPaths,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>;
) -> Option<DerivedDescriptor>;
fn derive_from_tap_key_origins<'s>(
&self,
tap_key_origins: &TapKeyOrigins,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>;
) -> Option<DerivedDescriptor>;
fn derive_from_psbt_key_origins<'s>(
&self,
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>;
) -> Option<DerivedDescriptor>;
fn derive_from_psbt_input<'s>(
&self,
psbt_input: &psbt::Input,
utxo: Option<TxOut>,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>>;
}
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,
}
}
) -> Option<DerivedDescriptor>;
}
impl DescriptorMeta for ExtendedDescriptor {
@@ -387,7 +398,7 @@ impl DescriptorMeta for ExtendedDescriptor {
let mut answer = Vec::new();
self.for_each_key(|pk| {
if let DescriptorPublicKey::XPub(xpub) = pk.as_key() {
if let DescriptorPublicKey::XPub(xpub) = pk {
answer.push(xpub.clone());
}
@@ -401,7 +412,7 @@ impl DescriptorMeta for ExtendedDescriptor {
&self,
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> {
) -> Option<DerivedDescriptor> {
// Ensure that deriving `xpub` with `path` yields `expected`
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
path: &DerivationPath,
@@ -423,7 +434,7 @@ impl DescriptorMeta for ExtendedDescriptor {
// using `for_any_key` should make this stop as soon as we return `true`
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
// 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
@@ -481,14 +492,14 @@ impl DescriptorMeta for ExtendedDescriptor {
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>(
&self,
hd_keypaths: &HdKeyPaths,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> {
) -> Option<DerivedDescriptor> {
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
let key_origins = hd_keypaths
.iter()
@@ -506,7 +517,7 @@ impl DescriptorMeta for ExtendedDescriptor {
&self,
tap_key_origins: &TapKeyOrigins,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> {
) -> Option<DerivedDescriptor> {
// "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
let key_origins = tap_key_origins
.iter()
@@ -520,19 +531,19 @@ impl DescriptorMeta for ExtendedDescriptor {
psbt_input: &psbt::Input,
utxo: Option<TxOut>,
secp: &'s SecpCtx,
) -> Option<DerivedDescriptor<'s>> {
) -> Option<DerivedDescriptor> {
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
return Some(derived);
}
if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) {
return Some(derived);
}
if self.is_deriveable() {
if self.has_wildcard() {
// We can't try to bruteforce the derivation index, exit here
return None;
}
let descriptor = self.as_derived_fixed(secp);
let descriptor = self.at_derivation_index(0);
match descriptor.desc_type() {
// TODO: add pk() here
DescriptorType::Pkh
@@ -566,86 +577,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)]
mod test {
use std::str::FromStr;
@@ -773,23 +704,40 @@ mod test {
let secp = Secp256k1::new();
let xpub = bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap();
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
let path = bip32::DerivationPath::from_str("m/0").unwrap();
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
// we are using an "xpub"
let key = (xpub, path).into_descriptor_key().unwrap();
let key = (xprv, path.clone()).into_descriptor_key().unwrap();
// override it with any. this happens in some key conversions, like bip39
let key = key.override_valid_networks(any_network());
// make a descriptor out of it
let desc = crate::descriptor!(wpkh(key)).unwrap();
// this should convert the key that supports "any_network" to the right network (testnet)
let (wallet_desc, _) = desc
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
assert_eq!(wallet_desc.to_string(), "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)#y8p7e8kk");
let mut xprv_testnet = xprv;
xprv_testnet.network = Network::Testnet;
let xpub_testnet = bip32::ExtendedPubKey::from_priv(&secp, &xprv_testnet);
let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
xkey: xpub_testnet,
origin: None,
derivation_path: path,
wildcard: Wildcard::Unhardened,
});
assert_eq!(wallet_desc.to_string(), "wpkh(tpubD6NzVbkrYhZ4XtJzoDja5snUjBNQRP5B3f4Hyn1T1x6PVPxzzVjvw6nJx2D8RBCxog9GEVjZoyStfepTz7TtKoBVdkCtnc7VCJh9dD4RAU9/0/*)#a3svx0ha");
assert_eq!(
keymap
.get(&desc_pubkey)
.map(|key| key.to_public(&secp).unwrap()),
Some(desc_pubkey)
);
}
// test IntoWalletDescriptor trait from &str with and without checksum appended
@@ -917,7 +865,7 @@ mod test {
#[test]
fn test_sh_wsh_sortedmulti_redeemscript() {
use super::{AsDerived, DescriptorScripts};
use miniscript::psbt::PsbtInputExt;
let secp = Secp256k1::new();
@@ -925,11 +873,16 @@ mod test {
let (descriptor, _) =
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();
assert_eq!(descriptor.psbt_redeem_script(), Some(script.to_v0_p2wsh()));
assert_eq!(descriptor.psbt_witness_script(), Some(script));
let mut psbt_input = psbt::Input::default();
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::{Serialize, Serializer};
use bitcoin::hashes::*;
use bitcoin::hashes::{hash160, ripemd160, sha256};
use bitcoin::util::bip32::Fingerprint;
use bitcoin::{PublicKey, XOnlyPublicKey};
use bitcoin::{LockTime, PublicKey, Sequence, XOnlyPublicKey};
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)]
use log::{debug, error, info, trace};
@@ -58,9 +61,9 @@ use log::{debug, error, info, trace};
use crate::descriptor::ExtractPolicy;
use crate::keys::ExtScriptContext;
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::XKeyUtils;
use bitcoin::util::psbt::{Input as PsbtInput, PartiallySignedTransaction as Psbt};
@@ -81,11 +84,11 @@ pub enum PkOrF {
impl PkOrF {
fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
match k {
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::FullKey(pk),
..
}) => PkOrF::Pubkey(*pk),
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::XOnly(pk),
..
}) => PkOrF::XOnlyPubkey(*pk),
@@ -111,7 +114,7 @@ pub enum SatisfiableItem {
/// Double SHA256 preimage hash
Hash256Preimage {
/// The digest value
hash: sha256d::Hash,
hash: hash256::Hash,
},
/// RIPEMD160 preimage hash
Ripemd160Preimage {
@@ -125,13 +128,13 @@ pub enum SatisfiableItem {
},
/// Absolute timeclock timestamp
AbsoluteTimelock {
/// The timestamp value
value: u32,
/// The timelock value
value: LockTime,
},
/// Relative timelock locktime
RelativeTimelock {
/// The locktime value
value: u32,
/// The timelock value
value: Sequence,
},
/// Multi-signature public keys with threshold count
Multisig {
@@ -165,7 +168,7 @@ impl SatisfiableItem {
/// Returns a unique id for the [`SatisfiableItem`]
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")
}
}
@@ -438,32 +441,30 @@ pub struct Policy {
}
/// 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 {
/// Optional CheckSequenceVerify condition
#[serde(skip_serializing_if = "Option::is_none")]
pub csv: Option<u32>,
pub csv: Option<Sequence>,
/// Optional timelock condition
#[serde(skip_serializing_if = "Option::is_none")]
pub timelock: Option<u32>,
pub timelock: Option<LockTime>,
}
impl Condition {
fn merge_nlocktime(a: u32, b: u32) -> Result<u32, PolicyError> {
if (a < utils::BLOCKS_TIMELOCK_THRESHOLD) != (b < utils::BLOCKS_TIMELOCK_THRESHOLD) {
fn merge_nlocktime(a: LockTime, b: LockTime) -> Result<LockTime, PolicyError> {
if !a.is_same_unit(b) {
Err(PolicyError::MixedTimelockUnits)
} else if a > b {
Ok(a)
} else {
Ok(max(a, b))
Ok(b)
}
}
fn merge_nsequence(a: u32, b: u32) -> Result<u32, PolicyError> {
let mask = utils::SEQUENCE_LOCKTIME_TYPE_FLAG | utils::SEQUENCE_LOCKTIME_MASK;
let a = a & mask;
let b = b & mask;
if (a < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) != (b < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) {
fn merge_nsequence(a: Sequence, b: Sequence) -> Result<Sequence, PolicyError> {
if a.is_time_locked() != b.is_time_locked() {
Err(PolicyError::MixedTimelockUnits)
} else {
Ok(max(a, b))
@@ -720,15 +721,18 @@ impl From<SatisfiableItem> for Policy {
}
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 {
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::FullKey(pk),
..
}) => pk.to_pubkeyhash().into(),
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
}) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
DescriptorPublicKey::Single(SinglePub {
key: SinglePubKey::XOnly(pk),
..
}) => pk.to_pubkeyhash().into(),
}) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
}
}
@@ -779,7 +783,7 @@ fn generic_sig_in_psbt<
) -> bool {
//TODO check signature validity
psbt.inputs.iter().all(|input| match key {
DescriptorPublicKey::SinglePub(DescriptorSinglePub { key, .. }) => check(input, key),
DescriptorPublicKey::Single(SinglePub { key, .. }) => check(input, key),
DescriptorPublicKey::XPub(xpub) => {
//TODO check actual derivation matches
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))
}
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 {
condition: Condition {
timelock: Some(*value),
timelock: Some(value.into()),
csv: None,
},
};
@@ -905,9 +912,11 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
} = build_sat
{
let after = After::new(Some(current_height), false);
let after_sat = Satisfier::<bitcoin::PublicKey>::check_after(&after, *value);
let inputs_sat = psbt_inputs_sat(psbt)
.all(|sat| Satisfier::<bitcoin::PublicKey>::check_after(&sat, *value));
let after_sat =
Satisfier::<bitcoin::PublicKey>::check_after(&after, value.into());
let inputs_sat = psbt_inputs_sat(psbt).all(|sat| {
Satisfier::<bitcoin::PublicKey>::check_after(&sat, value.into())
});
if after_sat && inputs_sat {
policy.satisfaction = policy.contribution.clone();
}
@@ -999,6 +1008,9 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
Policy::make_thresh(mapped, threshold)?
}
// Unsupported
Terminal::RawPkH(_) => None,
})
}
}
@@ -1124,14 +1136,12 @@ mod test {
use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor};
use super::*;
use crate::descriptor::derived::AsDerived;
use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
use crate::keys::{DescriptorKey, IntoDescriptorKey};
use crate::wallet::signer::SignersContainer;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::util::bip32;
use bitcoin::Network;
use miniscript::DescriptorTrait;
use std::str::FromStr;
use std::sync::Arc;
@@ -1329,9 +1339,8 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let single_key = wallet_desc.derive(0);
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)
.unwrap()
.unwrap();
@@ -1343,16 +1352,15 @@ mod test {
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let single_key = wallet_desc.derive(0);
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)
.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!(
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
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let single_key = wallet_desc.derive(0);
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)
.unwrap()
.unwrap();
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[1] == PkOrF::Fingerprint(fingerprint1))
);
assert!(
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
&& m == &1
matches!(policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == 2
&& m == 1
&& items.len() == 2
&& conditions.contains_key(&vec![0])
&& conditions.contains_key(&vec![1])
@@ -1427,8 +1434,8 @@ mod test {
&& m == &2
&& items.len() == 3
&& 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![1,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(sequence))
)
);
}
@@ -1574,7 +1581,7 @@ mod test {
.unwrap();
let addr = wallet_desc
.as_derived(0, &secp)
.at_derivation_index(0)
.address(Network::Testnet)
.unwrap();
assert_eq!(
@@ -1646,7 +1653,7 @@ mod test {
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
let addr = wallet_desc
.as_derived(0, &secp)
.at_derivation_index(0)
.address(Network::Testnet)
.unwrap();
assert_eq!(

View File

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

View File

@@ -12,7 +12,7 @@
use std::fmt;
use crate::bitcoin::Network;
use crate::{descriptor, wallet, wallet::address_validator};
use crate::{descriptor, wallet};
use bitcoin::{OutPoint, Txid};
/// 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
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
Encode(bitcoin::consensus::encode::Error),
/// Miniscript error
Miniscript(miniscript::Error),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
/// BIP32 error
Bip32(bitcoin::util::bip32::Error),
/// An ECDSA error
@@ -149,6 +149,14 @@ pub enum 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
/// on cached `scriptPubKey`s.
#[derive(Debug)]
@@ -181,7 +189,6 @@ macro_rules! impl_error {
}
impl_error!(descriptor::error::Error, Descriptor);
impl_error!(address_validator::AddressValidatorError, AddressValidator);
impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError);
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!(miniscript::Error, Miniscript);
impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
impl_error!(bitcoin::util::bip32::Error, Bip32);
impl_error!(bitcoin::secp256k1::Error, Secp256k1);
impl_error!(serde_json::Error, Json);

View File

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

View File

@@ -203,6 +203,8 @@ pub extern crate miniscript;
extern crate serde;
#[macro_use]
extern crate serde_json;
#[cfg(feature = "hardware-signer")]
pub extern crate hwi;
#[cfg(all(feature = "reqwest", feature = "ureq"))]
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")]
mod doctest;
pub mod keys;
pub(crate) mod psbt;
pub mod psbt;
pub(crate) mod types;
pub mod wallet;
@@ -271,7 +273,6 @@ pub use descriptor::template;
pub use descriptor::HdKeyPaths;
pub use error::Error;
pub use types::*;
pub use wallet::address_validator;
pub use wallet::signer;
pub use wallet::signer::SignOptions;
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
// licenses.
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
use crate::FeeRate;
use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use bitcoin::TxOut;
// TODO upstream the functions here to `rust-bitcoin`?
/// Trait to add functions to extract utxos and calculate fees.
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>;
/// 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 bitcoin::consensus::encode::{deserialize, serialize};
use bitcoin::hashes::hex::{FromHex, ToHex};
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::{Auth, Client as RpcClient, RpcApi};
use core::str::FromStr;
@@ -110,7 +120,7 @@ impl TestClient {
if let Some(true) = meta_tx.replaceable {
// for some reason core doesn't set this field right
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::transaction::{OutPoint, TxIn, TxOut};
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
use bitcoin::hashes::Hash;
let block_template: serde_json::Value = self
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
@@ -176,7 +187,7 @@ impl TestClient {
block_template["previousblockhash"].as_str().unwrap(),
)
.unwrap(),
merkle_root: TxMerkleNode::default(),
merkle_root: TxMerkleNode::all_zeros(),
time: block_template["curtime"].as_u64().unwrap() as u32,
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
nonce: 0,
@@ -184,15 +195,15 @@ impl TestClient {
debug!("header: {:#?}", header);
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
let mut coinbase_tx = Transaction {
version: 1,
lock_time: 0,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: Builder::new().push_int(height).into_script(),
sequence: 0xFFFFFFFF,
sequence: Sequence(0xFFFFFFFF),
witness: Witness::from_vec(vec![witness_reserved_value]),
}],
output: vec![],
@@ -1184,7 +1195,7 @@ macro_rules! bdk_blockchain_tests {
// 5. Verify 25_000 sats are received by test bitcoind node taproot wallet
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]

View File

@@ -101,25 +101,21 @@ impl TestIncomingTx {
macro_rules! testutils {
( @external $descriptors:expr, $child:expr ) => ({
use $crate::bitcoin::secp256k1::Secp256k1;
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::descriptor::AsDerived;
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
let secp = Secp256k1::new();
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 ) => ({
use $crate::bitcoin::secp256k1::Secp256k1;
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::descriptor::AsDerived;
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
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;
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) });
( @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 )* ) )* ) => ({
use std::str::FromStr;
use std::collections::HashMap;
use std::convert::Infallible;
use $crate::miniscript::descriptor::Descriptor;
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)]
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = HashMap::new();
let mut keys = HashMap::new();
$(
keys = testutils!{ @keys $( $keys )* };
)*
let external: Descriptor<String> = FromStr::from_str($external_descriptor).unwrap();
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 mut translator = Translator { keys, is_internal: false };
});
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 internal = None::<String>$(.or({
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
translator.is_internal = true;
let string_internal: Descriptor<String> = string_internal.translate_pk_infallible::<_, _>(|k| {
if let Some((key, _, int_path)) = keys.get(&k.as_str()) {
format!("{}{}", key, int_path.as_ref().unwrap_or(&"".into()))
} else {
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())
let internal = None::<String>$(.or({
let internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
let internal = internal.translate_pk(&mut translator).expect("Infallible conversion");
Some(internal.to_string())
}))?;
(external, internal)

View File

@@ -166,7 +166,7 @@ pub struct LocalUtxo {
}
/// A [`Utxo`] with its `satisfaction_weight`.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WeightedUtxo {
/// 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.
@@ -177,7 +177,7 @@ pub struct WeightedUtxo {
pub utxo: Utxo,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
/// An unspent transaction output (UTXO).
pub enum Utxo {
/// A UTXO owned by the local wallet.
@@ -224,7 +224,7 @@ impl Utxo {
}
/// A wallet transaction
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct TransactionDetails {
/// Optional 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);
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 {
dust_threshold,
change_fee,
@@ -835,7 +835,7 @@ mod test {
)
.unwrap(),
txout: TxOut {
value: rng.gen_range(0, 200000000),
value: rng.gen_range(0..200000000),
script_pubkey: Script::new(),
},
keychain: KeychainKind::External,
@@ -866,7 +866,7 @@ mod test {
}
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[..utxos_picked_len]
.iter()
@@ -1226,6 +1226,7 @@ mod test {
}
#[test]
#[ignore]
fn test_bnb_coin_selection_required_not_enough() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();

View File

@@ -134,15 +134,11 @@ impl FullyNodedExport {
let blockheight = match wallet.database.borrow().iter_txs(false) {
_ if !include_blockheight => 0,
Err(_) => 0,
Ok(txs) => {
let mut heights = txs
.into_iter()
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(0))
.collect::<Vec<_>>();
heights.sort_unstable();
*heights.last().unwrap_or(&0)
}
Ok(txs) => txs
.into_iter()
.filter_map(|tx| tx.confirmation_time.map(|c| c.height))
.min()
.unwrap_or(0),
};
let export = FullyNodedExport {
@@ -249,6 +245,22 @@ mod test {
fee: Some(500),
confirmation_time: Some(BlockTime {
timestamp: 12345678,
height: 5001,
}),
})
.unwrap();
db.set_tx(&TransactionDetails {
transaction: None,
txid: Txid::from_str(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)
.unwrap(),
received: 25_000,
sent: 0,
fee: Some(300),
confirmation_time: Some(BlockTime {
timestamp: 12345677,
height: 5000,
}),
})

View File

@@ -11,7 +11,40 @@
//! 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::secp256k1::{All, Secp256k1};

View File

@@ -24,20 +24,17 @@ use std::sync::Arc;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::consensus::encode::serialize;
use bitcoin::util::{psbt, taproot};
use bitcoin::util::psbt;
use bitcoin::{
Address, EcdsaSighashType, Network, OutPoint, SchnorrSighashType, Script, Transaction, TxOut,
Txid, Witness,
Address, EcdsaSighashType, LockTime, Network, OutPoint, SchnorrSighashType, Script, Sequence,
Transaction, TxOut, Txid, Witness,
};
use miniscript::descriptor::DescriptorTrait;
use miniscript::psbt::PsbtInputSatisfier;
use miniscript::ToPublicKey;
use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
#[allow(unused_imports)]
use log::{debug, error, info, trace};
pub mod address_validator;
pub mod coin_selection;
pub mod export;
pub mod signer;
@@ -54,24 +51,21 @@ pub mod hardwaresigner;
pub use utils::IsDust;
#[allow(deprecated)]
use address_validator::AddressValidator;
use coin_selection::DefaultCoinSelectionAlgorithm;
use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
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::database::memory::MemoryDatabase;
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::{
get_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DerivedDescriptorMeta,
DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor,
Policy, XKeyUtils,
calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta,
ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils,
};
use crate::error::Error;
use crate::error::{Error, MiniscriptPsbtError};
use crate::psbt::PsbtUtils;
use crate::signer::SignerError;
use crate::testutils;
@@ -100,9 +94,6 @@ pub struct Wallet<D> {
signers: Arc<SignersContainer>,
change_signers: Arc<SignersContainer>,
#[allow(deprecated)]
address_validators: Vec<Arc<dyn AddressValidator>>,
network: Network,
database: RefCell<D>,
@@ -143,7 +134,7 @@ pub enum AddressIndex {
/// A derived address and the index it was found at
/// For convenience this automatically derefs to `Address`
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq)]
pub struct AddressInfo {
/// Child index of this address
pub index: u32,
@@ -203,18 +194,20 @@ where
let secp = Secp256k1::new();
let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network)?;
database.check_descriptor_checksum(
Self::db_checksum(
&mut database,
&descriptor.to_string(),
KeychainKind::External,
get_checksum(&descriptor.to_string())?.as_bytes(),
)?;
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp));
let (change_descriptor, change_signers) = match change_descriptor {
Some(desc) => {
let (change_descriptor, change_keymap) =
into_wallet_descriptor_checked(desc, &secp, network)?;
database.check_descriptor_checksum(
Self::db_checksum(
&mut database,
&change_descriptor.to_string(),
KeychainKind::Internal,
get_checksum(&change_descriptor.to_string())?.as_bytes(),
)?;
let change_signers = Arc::new(SignersContainer::build(
@@ -222,9 +215,6 @@ where
&change_descriptor,
&secp,
));
// if !parsed.same_structure(descriptor.as_ref()) {
// return Err(Error::DifferentDescriptorStructure);
// }
(Some(change_descriptor), change_signers)
}
@@ -236,13 +226,25 @@ where
change_descriptor,
signers,
change_signers,
address_validators: Vec::new(),
network,
database: RefCell::new(database),
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.
pub fn network(&self) -> Network {
self.network
@@ -254,7 +256,7 @@ where
let address_result = self
.get_descriptor_for_keychain(keychain)
.as_derived(incremented_index, &self.secp)
.at_derivation_index(incremented_index)
.address(self.network);
address_result
@@ -273,7 +275,7 @@ where
let derived_key = self
.get_descriptor_for_keychain(keychain)
.as_derived(current_index, &self.secp);
.at_derivation_index(current_index);
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
fn peek_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> {
self.get_descriptor_for_keychain(keychain)
.as_derived(index, &self.secp)
.at_derivation_index(index)
.address(self.network)
.map(|address| AddressInfo {
index,
@@ -317,7 +319,7 @@ where
self.set_index(keychain, index)?;
self.get_descriptor_for_keychain(keychain)
.as_derived(index, &self.secp)
.at_derivation_index(index)
.address(self.network)
.map(|address| AddressInfo {
index,
@@ -366,7 +368,7 @@ where
/// transaction output scripts.
pub fn ensure_addresses_cached(&self, max_addresses: u32) -> Result<bool, Error> {
let mut new_addresses_cached = false;
let max_address = match self.descriptor.is_deriveable() {
let max_address = match self.descriptor.has_wildcard() {
false => 0,
true => max_addresses,
};
@@ -383,7 +385,7 @@ where
}
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,
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.
///
/// 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 :)
let current_height = match params.current_height {
// If they didn't tell us the current height, we assume it's the latest sync height.
None => self
.database()
.get_sync_time()?
.map(|sync_time| sync_time.block_time.height),
None => self.database().get_sync_time()?.map(|sync_time| {
LockTime::from_height(sync_time.block_time.height).expect("Invalid height")
}),
h => h,
};
@@ -697,24 +680,33 @@ where
// Fee sniping can be partially prevented by setting the timelock
// to current_height. If we don't know the current_height,
// 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
// 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
Some(x) if requirements.timelock.is_none() => x,
// 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
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) {
// 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
(None, None) => 0xFFFFFFFF,
(None, None) => Sequence::MAX,
// 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
@@ -722,7 +714,7 @@ where
(None, Some(csv)) => csv,
// 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(
"Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into(),
))
@@ -732,7 +724,7 @@ where
if !check_nsequence_rbf(rbf, csv) =>
{
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
)))
}
@@ -775,7 +767,7 @@ where
let mut tx = Transaction {
version,
lock_time,
lock_time: lock_time.into(),
input: vec![],
output: vec![],
};
@@ -840,7 +832,7 @@ where
params.drain_wallet,
params.manually_selected_only,
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
@@ -992,7 +984,11 @@ where
Some(tx) => tx,
};
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);
}
@@ -1118,8 +1114,9 @@ where
psbt: &mut psbt::PartiallySignedTransaction,
sign_options: SignOptions,
) -> Result<bool, Error> {
// this helps us doing our job later
self.add_input_hd_keypaths(psbt)?;
// This adds all the PSBT metadata for the inputs, which will help us later figure out how
// 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)
// has the `non_witness_utxo`
@@ -1320,21 +1317,18 @@ where
}
}
fn get_descriptor_for_txout(
&self,
txout: &TxOut,
) -> Result<Option<DerivedDescriptor<'_>>, Error> {
fn get_descriptor_for_txout(&self, txout: &TxOut) -> Result<Option<DerivedDescriptor>, Error> {
Ok(self
.database
.borrow()
.get_path_from_script_pubkey(&txout.script_pubkey)?
.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> {
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
let index = match descriptor.is_deriveable() {
let index = match descriptor.has_wildcard() {
false => 0,
true => self.database.borrow_mut().increment_last_index(keychain)?,
};
@@ -1348,22 +1342,12 @@ where
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)
}
fn fetch_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
let index = match descriptor.is_deriveable() {
let index = match descriptor.has_wildcard() {
false => Some(0),
true => self.database.borrow_mut().get_last_index(keychain)?,
};
@@ -1387,7 +1371,7 @@ where
mut count: u32,
) -> Result<(), Error> {
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
if !descriptor.is_deriveable() {
if !descriptor.has_wildcard() {
if from > 0 {
return Ok(());
}
@@ -1400,7 +1384,7 @@ where
let start_time = time::Instant::new();
for i in from..(from + count) {
address_batch.set_script_pubkey(
&descriptor.as_derived(i, &self.secp).script_pubkey(),
&descriptor.at_derivation_index(i).script_pubkey(),
keychain,
i,
)?;
@@ -1604,52 +1588,7 @@ where
}
}
// probably redundant but it doesn't hurt...
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();
};
}
}
self.update_psbt_with_descriptor(&mut psbt)?;
Ok(psbt)
}
@@ -1675,29 +1614,11 @@ where
};
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.tap_key_origins = derived_descriptor.get_tap_key_origins(&self.secp);
psbt_input.tap_internal_key = Some(tr.internal_key().to_x_only_pubkey());
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();
psbt_input
.update_with_descriptor_unchecked(&derived_descriptor)
.map_err(MiniscriptPsbtError::Conversion)?;
let prev_output = utxo.outpoint;
if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
@@ -1711,38 +1632,47 @@ where
Ok(psbt_input)
}
fn add_input_hd_keypaths(
fn update_psbt_with_descriptor(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
) -> Result<(), Error> {
let mut input_utxos = Vec::with_capacity(psbt.inputs.len());
for n in 0..psbt.inputs.len() {
input_utxos.push(psbt.get_utxo_for(n).clone());
}
// We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all
// the input utxos and outputs
//
// 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
for (psbt_input, out) in psbt.inputs.iter_mut().zip(input_utxos.iter()) {
if let Some(out) = out {
if let Some((keychain, child)) = self
.database
.borrow()
.get_path_from_script_pubkey(&out.script_pubkey)?
{
debug!("Found descriptor {:?}/{}", keychain, child);
// Try to figure out the keychain and derivation for every input and output
for (is_input, index, out) in utxos.into_iter() {
if let Some((keychain, child)) = self
.database
.borrow()
.get_path_from_script_pubkey(&out.script_pubkey)?
{
debug!(
"Found descriptor for input #{} {:?}/{}",
index, keychain, child
);
// merge hd_keypaths or tap_key_origins
let desc = self.get_descriptor_for_keychain(keychain);
if desc.is_taproot() {
let mut tap_key_origins = desc
.as_derived(child, &self.secp)
.get_tap_key_origins(&self.secp);
psbt_input.tap_key_origins.append(&mut tap_key_origins);
} else {
let mut hd_keypaths = desc
.as_derived(child, &self.secp)
.get_hd_keypaths(&self.secp);
psbt_input.bip32_derivation.append(&mut hd_keypaths);
}
let desc = self.get_descriptor_for_keychain(keychain);
let desc = desc.at_derivation_index(child);
if is_input {
psbt.update_input_with_descriptor(index, &desc)
.map_err(MiniscriptPsbtError::UtxoUpdate)?;
} else {
psbt.update_output_with_descriptor(index, &desc)
.map_err(MiniscriptPsbtError::OutputUpdate)?;
}
}
}
@@ -1781,12 +1711,12 @@ where
// We need to ensure descriptor is derivable to fullfil "missing cache", otherwise we will
// 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.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
let max_rounds = if is_deriveable { 100 } else { 1 };
let max_rounds = if has_wildcard { 100 } else { 1 };
for _ in 0..max_rounds {
let sync_res =
@@ -1864,14 +1794,14 @@ where
.into_wallet_descriptor(secp, network)?
.0
.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 {
let change_descriptor = change_descriptor
.into_wallet_descriptor(secp, network)?
.0
.to_string();
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)]
pub(crate) mod test {
use bitcoin::{util::psbt, Network};
use bitcoin::{util::psbt, Network, PackedLockTime, Sequence};
use crate::database::Database;
use crate::types::KeychainKind;
@@ -1943,15 +1873,38 @@ pub(crate) mod test {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let checksum = wallet.descriptor_checksum(KeychainKind::External);
assert_eq!(checksum.len(), 8);
assert_eq!(
calc_checksum(&wallet.descriptor.to_string()).unwrap(),
checksum
);
}
let raw_descriptor = wallet
.descriptor
.to_string()
.split_once('#')
.unwrap()
.0
.to_string();
assert_eq!(get_checksum(&raw_descriptor).unwrap(), checksum);
#[test]
fn test_db_checksum() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let desc = wallet.descriptor.to_string();
let checksum = calc_checksum_bytes_internal(&desc, true).unwrap();
let checksum_inception = calc_checksum_bytes_internal(&desc, false).unwrap();
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]
@@ -2234,7 +2187,7 @@ pub(crate) mod test {
// 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.
assert_eq!(psbt.unsigned_tx.lock_time, 0);
assert_eq!(psbt.unsigned_tx.lock_time, PackedLockTime(0));
}
#[test]
@@ -2259,7 +2212,7 @@ pub(crate) mod test {
let (psbt, _) = builder.finish().unwrap();
// 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]
@@ -2282,7 +2235,10 @@ pub(crate) mod test {
let (psbt, _) = builder.finish().unwrap();
// 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]
@@ -2293,7 +2249,7 @@ pub(crate) mod test {
builder.add_recipient(addr.script_pubkey(), 25_000);
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]
@@ -2304,13 +2260,13 @@ pub(crate) mod test {
builder
.add_recipient(addr.script_pubkey(), 25_000)
.current_height(630_001)
.nlocktime(630_000);
.nlocktime(LockTime::from_height(630_000).unwrap());
let (psbt, _) = builder.finish().unwrap();
// When we explicitly specify a nlocktime
// we don't try any fee sniping prevention trick
// (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]
@@ -2320,15 +2276,15 @@ pub(crate) mod test {
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.nlocktime(630_000);
.nlocktime(LockTime::from_height(630_000).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]
#[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() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
@@ -2336,7 +2292,7 @@ pub(crate) mod test {
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.nlocktime(50000);
.nlocktime(LockTime::from_height(50000).unwrap());
builder.finish().unwrap();
}
@@ -2348,7 +2304,7 @@ pub(crate) mod test {
builder.add_recipient(addr.script_pubkey(), 25_000);
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]
@@ -2362,12 +2318,12 @@ pub(crate) mod test {
let (psbt, _) = builder.finish().unwrap();
// 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
assert_eq!(psbt.unsigned_tx.input[0].sequence, 6);
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6));
}
#[test]
#[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() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
@@ -2375,7 +2331,7 @@ pub(crate) mod test {
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(3);
.enable_rbf_with_sequence(Sequence(3));
builder.finish().unwrap();
}
@@ -2387,7 +2343,7 @@ pub(crate) mod test {
builder.add_recipient(addr.script_pubkey(), 25_000);
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]
@@ -2398,7 +2354,7 @@ pub(crate) mod test {
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(0xFFFFFFFE);
.enable_rbf_with_sequence(Sequence(0xFFFFFFFE));
builder.finish().unwrap();
}
@@ -2409,10 +2365,10 @@ pub(crate) mod test {
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(0xDEADBEEF);
.enable_rbf_with_sequence(Sequence(0xDEADBEEF));
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]
@@ -2439,7 +2395,7 @@ pub(crate) mod test {
builder.add_recipient(addr.script_pubkey(), 25_000);
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]
@@ -2960,7 +2916,7 @@ pub(crate) mod test {
.policy_path(path, KeychainKind::External);
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]
@@ -2979,7 +2935,7 @@ pub(crate) mod test {
.policy_path(path, KeychainKind::External);
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]
@@ -4832,7 +4788,7 @@ pub(crate) mod test {
let (wallet, _, _) = get_funded_wallet(get_test_tr_repeated_key());
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()
.collect();
@@ -4842,48 +4798,50 @@ pub(crate) mod test {
.policy_path(path, KeychainKind::External);
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!(
psbt.inputs[0]
.tap_key_origins
.clone()
.into_iter()
.collect::<Vec<_>>(),
vec![(
from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
input_key_origins,
vec![
(
vec![
from_str!(
"858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e"
),
from_str!(
"f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903"
)
],
(Default::default(), Default::default())
from_str!("b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"),
(
vec![],
(FromStr::from_str("871fd295").unwrap(), vec![].into())
)
),
(
from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
(
vec![
from_str!(
"858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e"
),
from_str!(
"f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903"
),
],
(FromStr::from_str("ece52657").unwrap(), vec![].into())
)
)
)],
],
"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!(
psbt.outputs[0]
.tap_key_origins
.clone()
.into_iter()
.collect::<Vec<_>>(),
vec![(
from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
(
vec![
from_str!(
"858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e"
),
from_str!(
"f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903"
)
],
(Default::default(), Default::default())
)
)],
input_key_origins, output_key_origins,
"Wrong output tap_key_origins"
);
}
@@ -5135,7 +5093,7 @@ pub(crate) mod test {
#[test]
fn test_taproot_script_spend_sign_include_some_leaves() {
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 addr = wallet.get_address(AddressIndex::New).unwrap();
@@ -5177,7 +5135,7 @@ pub(crate) mod test {
#[test]
fn test_taproot_script_spend_sign_exclude_some_leaves() {
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 addr = wallet.get_address(AddressIndex::New).unwrap();
@@ -5524,6 +5482,7 @@ pub(crate) mod test {
SignOptions {
remove_partial_sigs: false,
try_finalize: false,
allow_grinding: false,
..Default::default()
},
)
@@ -5538,6 +5497,7 @@ pub(crate) mod test {
&mut psbt,
SignOptions {
remove_partial_sigs: false,
allow_grinding: false,
..Default::default()
},
)
@@ -5546,6 +5506,39 @@ pub(crate) mod test {
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")]
#[test]
fn test_create_signer() {

View File

@@ -96,10 +96,10 @@ use bitcoin::{secp256k1, XOnlyPublicKey};
use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script};
use miniscript::descriptor::{
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey,
KeyMap, SinglePubKey,
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, KeyMap, SinglePriv,
SinglePubKey,
};
use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap};
use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
use super::utils::SecpCtx;
use crate::descriptor::{DescriptorMeta, XKeyUtils};
@@ -369,11 +369,11 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
impl SignerCommon for SignerWrapper<PrivateKey> {
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> {
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
Some(DescriptorSecretKey::Single(SinglePriv {
key: self.signer,
origin: None,
}))
@@ -472,6 +472,7 @@ impl InputSigner for SignerWrapper<PrivateKey> {
hash,
hash_ty,
secp,
sign_options.allow_grinding,
);
Ok(())
@@ -485,9 +486,14 @@ fn sign_psbt_ecdsa(
hash: bitcoin::Sighash,
hash_ty: EcdsaSighashType,
secp: &SecpCtx,
allow_grinding: bool,
) {
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)
.expect("invalid or corrupted ecdsa signature");
@@ -511,13 +517,13 @@ fn sign_psbt_schnorr(
let keypair = match leaf_hash {
None => keypair
.tap_tweak(secp, psbt_input.tap_merkle_root)
.into_inner(),
.to_inner(),
Some(_) => keypair, // no tweak for script spend
};
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
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");
let final_signature = schnorr::SchnorrSig { sig, hash_ty };
@@ -570,7 +576,7 @@ impl SignersContainer {
self.0
.values()
.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()
}
@@ -595,8 +601,13 @@ impl SignersContainer {
};
match secret {
DescriptorSecretKey::SinglePriv(private_key) => container.add_external(
SignerId::from(private_key.key.public_key(secp).to_pubkeyhash()),
DescriptorSecretKey::Single(private_key) => container.add_external(
SignerId::from(
private_key
.key
.public_key(secp)
.to_pubkeyhash(SigType::Ecdsa),
),
SignerOrdering::default(),
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.
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.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TapLeavesOptions {
/// The signer will sign all the leaves it has a key for.
All,
@@ -751,6 +767,7 @@ impl Default for SignOptions {
try_finalize: true,
tap_leaves_options: TapLeavesOptions::default(),
sign_with_tap_internal_key: true,
allow_grinding: true,
}
}
}

View File

@@ -42,9 +42,7 @@ use std::default::Default;
use std::marker::PhantomData;
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{OutPoint, Script, Transaction};
use miniscript::descriptor::DescriptorTrait;
use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction};
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use crate::{database::BatchDatabase, Error, Utxo, Wallet};
@@ -139,7 +137,7 @@ pub(crate) struct TxParams {
pub(crate) manually_selected_only: bool,
pub(crate) sighash: Option<psbt::PsbtSighashType>,
pub(crate) ordering: TxOrdering,
pub(crate) locktime: Option<u32>,
pub(crate) locktime: Option<LockTime>,
pub(crate) rbf: Option<RbfValue>,
pub(crate) version: Option<Version>,
pub(crate) change_policy: ChangeSpendPolicy,
@@ -147,7 +145,7 @@ pub(crate) struct TxParams {
pub(crate) add_global_xpubs: bool,
pub(crate) include_output_redeem_witness_script: bool,
pub(crate) bumping_fee: Option<PreviousFee>,
pub(crate) current_height: Option<u32>,
pub(crate) current_height: Option<LockTime>,
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
///
/// 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
}
@@ -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
/// 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
}
@@ -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.
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
}
@@ -703,7 +701,7 @@ impl TxOrdering {
#[cfg(not(test))]
let mut rng = rand::thread_rng();
#[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);
}
@@ -736,13 +734,13 @@ impl Default for Version {
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
pub(crate) enum RbfValue {
Default,
Value(u32),
Value(Sequence),
}
impl RbfValue {
pub(crate) fn get_value(&self) -> u32 {
pub(crate) fn get_value(&self) -> Sequence {
match self {
RbfValue::Default => 0xFFFFFFFD,
RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME,
RbfValue::Value(v) => *v,
}
}
@@ -858,10 +856,12 @@ mod test {
}
fn get_test_utxos() -> Vec<LocalUtxo> {
use bitcoin::hashes::Hash;
vec![
LocalUtxo {
outpoint: OutPoint {
txid: Default::default(),
txid: bitcoin::Txid::from_inner([0; 32]),
vout: 0,
},
txout: Default::default(),
@@ -870,7 +870,7 @@ mod test {
},
LocalUtxo {
outpoint: OutPoint {
txid: Default::default(),
txid: bitcoin::Txid::from_inner([0; 32]),
vout: 1,
},
txout: Default::default(),

View File

@@ -9,23 +9,11 @@
// You may not use this file except in accordance with one or both of these
// licenses.
use bitcoin::blockdata::script::Script;
use bitcoin::secp256k1::{All, Secp256k1};
use bitcoin::{LockTime, Script, Sequence};
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.
/// We are performing dust value calculation for a given script public key using rust-bitcoin to
/// keep it compatible with network dust rate
@@ -38,7 +26,7 @@ pub trait IsDust {
impl IsDust for u64 {
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 {
// This flag cannot be set in the nSequence when spending using OP_CSV
if rbf & SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 {
pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool {
// The RBF value must enable relative timelocks
if !rbf.is_relative_lock_time() {
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
// 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;
}
@@ -80,24 +64,10 @@ pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool {
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 {
fn check_after(&self, n: u32) -> bool {
fn check_after(&self, n: LockTime) -> bool {
if let Some(current_height) = self.current_height {
current_height >= n
current_height >= n.to_consensus_u32()
} else {
self.assume_height_reached
}
@@ -125,10 +95,15 @@ impl 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 {
// 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 {
self.assume_height_reached
}
@@ -139,11 +114,12 @@ pub(crate) type SecpCtx = Secp256k1<All>;
#[cfg(test)]
mod test {
use super::{
check_nlocktime, check_nsequence_rbf, IsDust, BLOCKS_TIMELOCK_THRESHOLD,
SEQUENCE_LOCKTIME_TYPE_FLAG,
};
use crate::bitcoin::Address;
// 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;
use super::{check_nsequence_rbf, IsDust};
use crate::bitcoin::{Address, Sequence};
use std::str::FromStr;
#[test]
@@ -165,66 +141,40 @@ mod test {
#[test]
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);
}
#[test]
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);
}
#[test]
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);
}
#[test]
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);
}
#[test]
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);
}
#[test]
fn test_check_nsequence_rbf_same_unit_time() {
let result = check_nsequence_rbf(
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000,
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,
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000),
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000),
);
assert!(result);
}