Compare commits

...

78 Commits

Author SHA1 Message Date
Alekos Filini
33b45ebe82 Bump version to 0.8.0 2021-06-10 16:00:01 +02:00
Alekos Filini
2faed425ed Update CHANGELOG 2021-06-10 15:59:24 +02:00
Alekos Filini
2cc05c07a5 Bump version in src/lib.rs 2021-06-10 15:59:08 +02:00
Alekos Filini
9205295332 Merge commit 'refs/pull/365/head' of github.com:bitcoindevkit/bdk into release/0.8.0 2021-06-09 16:05:16 +02:00
Tobin Harding
3b446c9e14 Use no_run instead of ignore
We have an attribute `no_run` that builds but does not run example code
in Rustdocs, this keeps the examples building as the codebase evolves.

use `no_run` and fix example code so it builds cleanly during test run.

Some examples that require the `electrum` feature to be available have
been feature-gated to make sure they aren't accidentally compiled when
that feature is not enabled.

Co-authored-by: Alekos Filini <alekos.filini@gmail.com>
2021-06-09 11:29:57 +02:00
Alekos Filini
378167efca Remove explicit feature(external_doc)
It looks like this is now enabled by default as of `cargo 1.54.0-nightly (0cecbd673 2021-06-01)`
2021-06-09 11:27:25 +02:00
Alekos Filini
224be27aa8 Fix example/doctests format 2021-06-04 15:53:15 +02:00
Alekos Filini
4a23070cc8 [ci] Check fmt for examples/doctests 2021-06-04 15:07:02 +02:00
Alekos Filini
f8117c0f9f Bump version to 0.8.0-rc.1 2021-06-04 09:42:14 +02:00
Paul Miller
0ec064ef13 Use AddressInfo in private methods 2021-05-27 17:11:16 -04:00
Paul Miller
6b60914ca1 return AddressInfo from get_address 2021-05-27 17:11:16 -04:00
Alekos Filini
881ca8d1e3 [signer] Add an option to explicitly allow using non-ALL sighashes
Instead of blindly using the `sighash_type` set in a psbt input, we
now only sign `SIGHASH_ALL` inputs by default, and require the user to
explicitly opt-in to using other sighashes if they desire to do so.

Fixes #350
2021-05-26 10:38:15 +02:00
Alekos Filini
5633475ce8 Merge commit 'refs/pull/347/head' of github.com:bitcoindevkit/bdk 2021-05-26 08:56:38 +02:00
LLFourn
ea8488b2a7 Initialize env_logger at start of blockchain tests 2021-05-21 13:21:59 +10:00
LLFourn
d2a981efee run_blockchain_tests.sh improvements 2021-05-21 13:21:41 +10:00
LLFourn
4c92daf517 Uppercase 'Test' so that github can see what's up
It is expecting something named 'Test electrum'
2021-05-20 14:33:02 +10:00
LLFourn
aba2a05d83 Add script for running the blockchain tests locally 2021-05-19 16:45:48 +10:00
LLFourn
5b194c268d Fix clippy warnings inside testutils macro
Now that it's inside the main repo clippy is having a go at me.
2021-05-19 16:45:48 +10:00
LLFourn
00bdf08f2a Remove testutils feature so doctests worka again
I wanted to only conditionally compile testutils but it's needed in
doctests which we can't conditionally compile for:

https://github.com/rust-lang/rust/issues/67295
2021-05-19 16:45:48 +10:00
LLFourn
38b0470b14 Move blockchain related stuff to blockchain_tests 2021-05-19 16:45:48 +10:00
LLFourn
d60c5003bf Merge testutils crate into the main crate
This avoids having to keep the apis in sync between the macros and the
main project.
2021-05-19 16:45:48 +10:00
LLFourn
fcae5adabd Run blockchain tests on esplora
They were only being run on electrum before.
2021-05-19 15:47:44 +10:00
Steve Myers
9f04a9d82d Merge commit 'refs/pull/338/head' of github.com:bitcoindevkit/bdk 2021-05-18 16:41:45 -07:00
LLFourn
465ef6e674 Roll blockchain tests proc macro into normal macro
This means one less crate in the repo. Had to do a Default on TestClient
to satisfy clippy.
2021-05-18 20:02:33 +10:00
Steve Myers
aaa9943a5f Merge commit 'refs/pull/346/head' of github.com:bitcoindevkit/bdk 2021-05-14 10:49:30 -07:00
Alekos Filini
3897e29740 Fix changelog 2021-05-12 15:11:20 +02:00
Alekos Filini
8f06e45872 Bump version to 0.7.1-dev 2021-05-12 15:10:28 +02:00
Alekos Filini
766570abfd Bump version to 0.7.0 2021-05-12 14:20:58 +02:00
Alekos Filini
934ec366d9 Use the released testutils-macros 2021-05-12 14:20:23 +02:00
Alekos Filini
d0733e9496 Bump version in src/lib.rs 2021-05-12 14:19:58 +02:00
Alekos Filini
3c7a1f5918 Bump testutils-macros to v0.6.0 2021-05-12 14:19:00 +02:00
Alekos Filini
85aadaccd2 Update changelog in preparation of v0.7.0 2021-05-12 14:17:46 +02:00
Tobin Harding
fad0fe9f30 Update create transaction example code
The transaction builder changed a while ago, looks like some of the
example code did not get updated.

Update the transaction creation code to use a mutable builder.
2021-05-12 14:13:23 +02:00
Tobin Harding
6546b77c08 Remove stale comments
The two fields this comment references are not `Option` type. This
comment seems to be stale.
2021-05-11 13:29:22 +10:00
Tobin Harding
e1066e955c Remove unneeded unit expression
Clippy emits:

  warning: unneeded unit expression

As suggested, remove the unneeded unit expression.
2021-05-11 10:52:08 +10:00
Tobin Harding
7f06dc3330 Clear clippy manual_map warning
The lint `manual_map` is new so we cannot explicitly allow it and
maintain backwards comparability. Instead, allow all lints for
`get_utxo_for` with a comment explaining why.
2021-05-11 10:52:07 +10:00
Tobin Harding
de40351710 Use consistent field ordering
Clippy emits:

  warning: struct constructor field order is inconsistent with struct
  definition field order

As suggested, re-order the fields to be consistent with the struct
definition.
2021-05-11 10:51:44 +10:00
Tobin Harding
de811bea30 Use !any() instead of find()...is_none()
Clippy emits:

  warning: called `is_none()` after searching an `Iterator` with `find`

As suggested, use the construct: `!foo.iter().any(...)`
2021-05-11 10:51:44 +10:00
Tobin Harding
74cc80d127 Remove unnecessary clone
Clippy emits:

  warning: using `clone` on type `descriptor::policy::Condition` which
  implements the `Copy` trait

Remove the clone and rely on `Copy`.
2021-05-11 10:51:44 +10:00
Tobin Harding
009f68a06a Use assert!(foo) instead of assert_eq!(foo, true)
It is redundant to pass true/false to `assert_eq!` since `assert!`
already asserts true/false.

This may, however, be controversial if someone thinks that

```
    assert_eq!(foo, false);
```

is more clear than

```
    assert!(!foo);
```

Use `assert!` directly instead of `assert_eq!` with true/false argument.
2021-05-11 10:51:44 +10:00
Riccardo Casatta
47f26447da continue signing when finding already finalized inputs 2021-05-07 16:32:24 +02:00
Tobin Harding
12641b9e8f Use PsbtKey instead of PSBT
We recently converted uses of `PSBT` -> `Psbt` inline with idiomatic
Rust acronym identifiers. Do the same to `PSBTKey`.

Use `PsbtKey` instead of `PSBTKey` when aliasing the import of
`psbt::raw::Key` from `bitcoin` library.
2021-05-07 16:29:53 +02:00
Tobin Harding
aa3707b5b4 Use Psbt instead of PSBT
Idiomatic Rust uses lowercase for acronyms for all characters after the
first e.g. `std::net::TcpStream`. PSBT (Partially Signed Bitcoin
Transaction) should be rendered `Psbt` in Rust code if we want to write
idiomatic Rust.

Use `Psbt` instead of `PSBT` when aliasing the import of
`PartiallySignedTransaction` from `bitcoin` library.
2021-05-07 16:29:50 +02:00
Riccardo Casatta
f6631e35b8 continue signing when finding already finalized inputs 2021-05-07 13:52:20 +02:00
Alekos Filini
3608ff9f14 Merge commit 'refs/pull/341/head' of github.com:bitcoindevkit/bdk into release/0.7.0 2021-05-07 11:00:00 +02:00
Alekos Filini
7fdb98e147 Merge commit 'refs/pull/341/head' of github.com:bitcoindevkit/bdk 2021-05-07 10:59:32 +02:00
Tobin Harding
9aea90bd81 Use default: D mirroring Rust documentation
Currently we use `F: f` for the argument that is the default function
passed to `map_or_else` and pass a closure for the second argument. This
bent my brain while reading the documentation because the docs use
`default: D` for the first and `f: F` for the second. Although this is
totally trivial it makes deciphering the combinator chain easier if we
name the arguments the same way the Rust docs do.

Use `default: D` for the identifier of the default function passed into `map_or_else`.
2021-05-07 09:08:49 +10:00
Riccardo Casatta
898dfe6cf1 get psbt inputs with bounds check 2021-05-06 16:10:11 +02:00
Riccardo Casatta
7961ae7f8e Check index out of bound also for tx inputs not only for psbt inputs 2021-05-06 15:13:25 +02:00
Alekos Filini
8bf77c8f07 Bump version to 0.7.0-rc.1 2021-05-06 13:56:38 +02:00
Alekos Filini
3c7bae9ce9 Rewrite the non_witness_utxo check 2021-05-06 11:39:01 +02:00
Alekos Filini
17bcd8ed7d [signer] Replace force_non_witness_utxo with only_witness_utxo
Instead of providing an opt-in option to force the addition of the
`non_witness_utxo`, we will now add them by default and provide the
option to disable them when they aren't considered necessary.
2021-05-06 08:58:39 +02:00
Alekos Filini
b5e9589803 [signer] Adjust signing behavior with SignOptions 2021-05-06 08:58:38 +02:00
Alekos Filini
1d628d84b5 [signer] Fix error variant 2021-05-05 16:59:59 +02:00
Alekos Filini
b84fd6ea5c Fix import for FromStr 2021-05-05 16:59:57 +02:00
Alekos Filini
8fe4222c33 Merge commit 'refs/pull/336/head' of github.com:bitcoindevkit/bdk 2021-05-05 14:51:36 +02:00
codeShark149
e626f2e255 Added grcov based code coverage reporting in github action 2021-04-30 17:20:20 +05:30
LLFourn
5a0c150ff9 Make wallet methods take &mut psbt
Rather than consuming it because that is unergonomic.
2021-04-28 15:34:25 +10:00
Alekos Filini
00f07818f9 Merge commit 'refs/pull/321/head' of github.com:bitcoindevkit/bdk 2021-04-16 14:08:26 +02:00
Riccardo Casatta
136a4bddb2 Verify PSBT input satisfaction 2021-04-16 12:22:49 +02:00
Riccardo Casatta
ff7b74ec27 destructure tuple to improve clarity 2021-04-16 12:22:47 +02:00
Steve Myers
8c00326990 [ci] Revert fixed nightly-2021-03-23, use actual nightly 2021-04-15 10:15:13 -07:00
Riccardo Casatta
afcd26032d comment out println in tests, fix doc 2021-04-15 16:48:42 +02:00
Riccardo Casatta
8f422a1bf9 Add timelocks to policy satisfaction results
Also for signature the logic has been refactored to handle appropriately nested cases.
2021-04-15 15:57:35 +02:00
Alekos Filini
45983d2166 Merge commit 'refs/pull/322/head' of github.com:bitcoindevkit/bdk 2021-04-15 11:40:34 +02:00
Steve Myers
89cb4de7f6 [ci] Update 'test-readme-examples' job to use nightly-2021-03-23 2021-04-14 20:34:32 -07:00
Steve Myers
7ca0e0e2bd [ci] Update 'Tarpaulin to codecov.io' job to use nightly-2021-03-23 2021-04-14 20:33:42 -07:00
Alekos Filini
2bddd9baed Update CHANGELOG for v0.6.0 2021-04-14 18:49:52 +02:00
Alekos Filini
0135ba29c5 Bump version to 0.6.1-dev 2021-04-14 18:47:31 +02:00
Steve Myers
3ed44ce8cf Remove unneeded script 2021-04-09 09:19:19 -07:00
Steve Myers
8e7d8312a9 [ci] Update 'build-test' job to clippy check all-targets 2021-04-08 14:44:35 -07:00
Steve Myers
4da7488dc4 Update 'cargo-check.sh' to not check +nightly 2021-04-08 14:36:07 -07:00
Steve Myers
e37680af96 Use .flatten() instead of .filter_map(|x| x), clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#filter_map_identity
2021-04-08 14:18:07 -07:00
Steve Myers
5f873ae500 Use .any() instead of .find().is_some(), clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#search_is_some
2021-04-08 14:18:07 -07:00
Steve Myers
2380634496 Use .get(0) instead of .iter().next(), clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#iter_next_slice
2021-04-08 14:18:07 -07:00
Steve Myers
af98b8da06 Compare float equality using error margin EPSILON, clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#float_cmp
2021-04-08 14:17:59 -07:00
Steve Myers
b68ec050e2 Remove redundant clone, clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#redundant_clone
2021-04-08 11:41:58 -07:00
Steve Myers
ac7df09200 Remove needlessly taken reference of both operands, clippy warning
https://rust-lang.github.io/rust-clippy/master/index.html#op_ref
2021-04-08 11:39:38 -07:00
34 changed files with 2184 additions and 1635 deletions

View File

@@ -3,25 +3,35 @@ on: [push]
name: Code Coverage name: Code Coverage
jobs: jobs:
tarpaulin-codecov:
name: Tarpaulin to codecov.io Codecov:
name: Code Coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CARGO_INCREMENTAL: '0'
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install rustup
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Set default toolchain - name: Set default toolchain
run: rustup default nightly run: rustup default nightly
- name: Set profile - name: Set profile
run: rustup set profile minimal run: rustup set profile minimal
- name: Update toolchain
run: rustup update
- name: Test
run: cargo test --features all-keys,compiler,esplora,compact_filters --no-default-features
- id: coverage
name: Generate coverage
uses: actions-rs/grcov@v0.1.5
- name: Install tarpaulin - name: Upload coverage to Codecov
run: cargo install cargo-tarpaulin uses: codecov/codecov-action@v1
- name: Tarpaulin
run: cargo tarpaulin --features all-keys,compiler,esplora,compact_filters --run-types Tests,Doctests --exclude-files "testutils/*" --out Xml
- name: Publish to codecov.io
uses: codecov/codecov-action@v1.0.15
with: with:
fail_ci_if_error: true file: ${{ steps.coverage.outputs.report }}
file: ./cobertura.xml directory: ./coverage/reports/

View File

@@ -46,7 +46,7 @@ jobs:
- name: Build - name: Build
run: cargo build --features ${{ matrix.features }} --no-default-features run: cargo build --features ${{ matrix.features }} --no-default-features
- name: Clippy - name: Clippy
run: cargo clippy --features ${{ matrix.features }} --no-default-features -- -D warnings run: cargo clippy --all-targets --features ${{ matrix.features }} --no-default-features -- -D warnings
- name: Test - name: Test
run: cargo test --features ${{ matrix.features }} --no-default-features run: cargo test --features ${{ matrix.features }} --no-default-features
@@ -73,10 +73,19 @@ jobs:
- name: Test - name: Test
run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests
test-electrum: test-blockchains:
name: Test electrum name: Test ${{ matrix.blockchain.name }}
runs-on: ubuntu-16.04 runs-on: ubuntu-16.04
container: bitcoindevkit/electrs:0.2.0 strategy:
matrix:
blockchain:
- name: electrum
container: bitcoindevkit/electrs
start: /root/electrs --network regtest --jsonrpc-import
- name: esplora
container: bitcoindevkit/esplora
start: /root/electrs --network regtest -vvv --cookie admin:passw --jsonrpc-import --electrum-rpc-addr=0.0.0.0:60401 --http-addr 0.0.0.0:3002
container: ${{ matrix.blockchain.container }}
env: env:
BDK_RPC_AUTH: USER_PASS BDK_RPC_AUTH: USER_PASS
BDK_RPC_USER: admin BDK_RPC_USER: admin
@@ -84,6 +93,7 @@ jobs:
BDK_RPC_URL: 127.0.0.1:18443 BDK_RPC_URL: 127.0.0.1:18443
BDK_RPC_WALLET: bdk-test BDK_RPC_WALLET: bdk-test
BDK_ELECTRUM_URL: tcp://127.0.0.1:60401 BDK_ELECTRUM_URL: tcp://127.0.0.1:60401
BDK_ESPLORA_URL: http://127.0.0.1:3002
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -95,6 +105,8 @@ jobs:
~/.cargo/git ~/.cargo/git
target target
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: get pkg-config # running eslpora tests seems to need this
run: apt update && apt install -y --fix-missing pkg-config libssl-dev
- name: Install rustup - name: Install rustup
run: curl https://sh.rustup.rs -sSf | sh -s -- -y run: curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Set default toolchain - name: Set default toolchain
@@ -105,8 +117,10 @@ jobs:
run: $HOME/.cargo/bin/rustup update run: $HOME/.cargo/bin/rustup update
- name: Start core - name: Start core
run: ./ci/start-core.sh run: ./ci/start-core.sh
- name: start ${{ matrix.blockchain.name }}
run: nohup ${{ matrix.blockchain.start }} & sleep 5
- name: Test - name: Test
run: $HOME/.cargo/bin/cargo test --features test-electrum --no-default-features run: $HOME/.cargo/bin/cargo test --features ${{ matrix.blockchain.name }},test-blockchains --no-default-features ${{ matrix.blockchain.name }}::bdk_blockchain_tests
check-wasm: check-wasm:
name: Check WASM name: Check WASM
@@ -148,12 +162,12 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set default toolchain - name: Set default toolchain
run: rustup default 1.51.0 # STABLE run: rustup default nightly
- name: Set profile - name: Set profile
run: rustup set profile minimal run: rustup set profile minimal
- name: Add clippy - name: Add rustfmt
run: rustup component add rustfmt run: rustup component add rustfmt
- name: Update toolchain - name: Update toolchain
run: rustup update run: rustup update
- name: Check fmt - name: Check fmt
run: cargo fmt --all -- --check run: cargo fmt --all -- --config format_code_in_doc_comments=true --check

View File

@@ -18,7 +18,7 @@ jobs:
target target
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: Set default toolchain - name: Set default toolchain
run: rustup default nightly-2021-03-23 run: rustup default nightly
- name: Set profile - name: Set profile
run: rustup set profile minimal run: rustup set profile minimal
- name: Update toolchain - name: Update toolchain

View File

@@ -6,6 +6,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [v0.8.0] - [v0.7.0]
### Wallet
- Added an option that must be explicitly enabled to allow signing using non-`SIGHASH_ALL` sighashes (#350)
#### Changed
`get_address` now returns an `AddressInfo` struct that includes the index and derefs to `Address`.
## [v0.7.0] - [v0.6.0]
### Policy
#### Changed
Removed `fill_satisfaction` method in favor of enum parameter in `extract_policy` method
#### Added
Timelocks are considered (optionally) in building the `satisfaction` field
### Wallet
- Changed `Wallet::{sign, finalize_psbt}` now take a `&mut psbt` rather than consuming it.
- Require and validate `non_witness_utxo` for SegWit signatures by default, can be adjusted with `SignOptions`
- Replace the opt-in builder option `force_non_witness_utxo` with the opposite `only_witness_utxo`. From now on we will provide the `non_witness_utxo`, unless explicitly asked not to.
## [v0.6.0] - [v0.5.1]
### Misc ### Misc
#### Changed #### Changed
- New minimum supported rust version is 1.46.0 - New minimum supported rust version is 1.46.0
@@ -322,3 +346,5 @@ final transaction is created by calling `finish` on the builder.
[v0.4.0]: https://github.com/bitcoindevkit/bdk/compare/v0.3.0...v0.4.0 [v0.4.0]: https://github.com/bitcoindevkit/bdk/compare/v0.3.0...v0.4.0
[v0.5.0]: https://github.com/bitcoindevkit/bdk/compare/v0.4.0...v0.5.0 [v0.5.0]: https://github.com/bitcoindevkit/bdk/compare/v0.4.0...v0.5.0
[v0.5.1]: https://github.com/bitcoindevkit/bdk/compare/v0.5.0...v0.5.1 [v0.5.1]: https://github.com/bitcoindevkit/bdk/compare/v0.5.0...v0.5.1
[v0.6.0]: https://github.com/bitcoindevkit/bdk/compare/v0.5.1...v0.6.0
[v0.7.0]: https://github.com/bitcoindevkit/bdk/compare/v0.6.0...v0.7.0

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "bdk" name = "bdk"
version = "0.6.0" version = "0.8.0"
edition = "2018" edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"] authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org" homepage = "https://bitcoindevkit.org"
@@ -32,6 +32,10 @@ socks = { version = "0.3", optional = true }
lazy_static = { version = "1.4", optional = true } lazy_static = { version = "1.4", optional = true }
tiny-bip39 = { version = "^0.8", optional = true } tiny-bip39 = { version = "^0.8", optional = true }
# Needed by bdk_blockchain_tests macro
bitcoincore-rpc = { version = "0.13", optional = true }
serial_test = { version = "0.4", optional = true }
# Platform-specific dependencies # Platform-specific dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["rt"] } tokio = { version = "1", features = ["rt"] }
@@ -54,18 +58,15 @@ all-keys = ["keys-bip39"]
keys-bip39 = ["tiny-bip39"] keys-bip39 = ["tiny-bip39"]
# Debug/Test features # Debug/Test features
debug-proc-macros = ["bdk-macros/debug", "bdk-testutils-macros/debug"] test-blockchains = ["bitcoincore-rpc", "electrum-client"]
test-electrum = ["electrum"]
test-md-docs = ["electrum"] test-md-docs = ["electrum"]
[dev-dependencies] [dev-dependencies]
bdk-testutils = "0.4"
bdk-testutils-macros = "0.5"
serial_test = "0.4"
lazy_static = "1.4" lazy_static = "1.4"
env_logger = "0.7" env_logger = "0.7"
base64 = "^0.11" base64 = "^0.11"
clap = "2.33" clap = "2.33"
serial_test = "0.4"
[[example]] [[example]]
name = "address_validator" name = "address_validator"
@@ -79,10 +80,7 @@ path = "examples/compiler.rs"
required-features = ["compiler"] required-features = ["compiler"]
[workspace] [workspace]
members = ["macros", "testutils", "testutils-macros"] members = ["macros"]
# Generate docs with nightly to add the "features required" badge
# https://stackoverflow.com/questions/61417452/how-to-get-a-feature-requirement-tag-in-the-documentation-generated-by-cargo-do
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["compiler", "electrum", "esplora", "compact_filters", "key-value-db", "all-keys"] features = ["compiler", "electrum", "esplora", "compact_filters", "key-value-db", "all-keys"]
# defines the configuration attribute `docsrs` # defines the configuration attribute `docsrs`

View File

@@ -130,7 +130,7 @@ fn main() -> Result<(), bdk::Error> {
### Sign a transaction ### Sign a transaction
```rust,no_run ```rust,no_run
use bdk::{Wallet, database::MemoryDatabase}; use bdk::{Wallet, SignOptions, database::MemoryDatabase};
use bitcoin::consensus::deserialize; use bitcoin::consensus::deserialize;
@@ -143,9 +143,9 @@ fn main() -> Result<(), bdk::Error> {
)?; )?;
let psbt = "..."; let psbt = "...";
let psbt = deserialize(&base64::decode(psbt).unwrap())?; let mut psbt = deserialize(&base64::decode(psbt).unwrap())?;
let (signed_psbt, finalized) = wallet.sign(psbt, None)?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
Ok(()) Ok(())
} }
@@ -166,4 +166,4 @@ at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions. dual licensed as above, without any additional terms or conditions.

View File

@@ -11,7 +11,3 @@ done
echo "Generating 150 bitcoin blocks." echo "Generating 150 bitcoin blocks."
ADDR=$(/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS -rpcwallet=$BDK_RPC_WALLET getnewaddress) ADDR=$(/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS -rpcwallet=$BDK_RPC_WALLET getnewaddress)
/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS generatetoaddress 150 $ADDR /root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS generatetoaddress 150 $ADDR
echo "Starting electrs node."
nohup /root/electrs --network regtest --jsonrpc-import &
sleep 5

13
codecov.yaml Normal file
View File

@@ -0,0 +1,13 @@
coverage:
status:
project:
default:
target: auto
threshold: 1%
base: auto
informational: false
patch:
default:
target: auto
threshold: 100%
base: auto

64
run_blockchain_tests.sh Executable file
View File

@@ -0,0 +1,64 @@
#!/bin/sh
usage() {
cat <<'EOF'
Script for running the bdk blockchain tests for a specific blockchain by starting up the backend in docker.
Usage: ./run_blockchain_tests.sh [esplora|electrum] [test name].
EOF
}
eprintln(){
echo "$@" >&2
}
cleanup() {
if test "$id"; then
eprintln "cleaning up $blockchain docker container $id";
docker rm -fv "$id" > /dev/null;
fi
trap - EXIT INT
}
# Makes sure we clean up the container at the end or if ^C
trap 'rc=$?; cleanup; exit $rc' EXIT INT
blockchain="$1"
test_name="$2"
case "$blockchain" in
electrum)
eprintln "starting electrs docker container"
id="$(docker run -d -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp bitcoindevkit/electrs)"
;;
esplora)
eprintln "starting esplora docker container"
id="$(docker run -d -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp -p 127.0.0.1:3002:3002/tcp bitcoindevkit/esplora)"
export BDK_ESPLORA_URL=http://127.0.0.1:3002
;;
*)
usage;
exit 1;
;;
esac
# taken from https://github.com/bitcoindevkit/bitcoin-regtest-box
export BDK_RPC_AUTH=USER_PASS
export BDK_RPC_USER=admin
export BDK_RPC_PASS=passw
export BDK_RPC_URL=127.0.0.1:18443
export BDK_RPC_WALLET=bdk-test
export BDK_ELECTRUM_URL=tcp://127.0.0.1:60401
cli(){
docker exec -it "$id" /root/bitcoin-cli -regtest -rpcuser=admin -rpcpassword=passw $@
}
eprintln "running getwalletinfo until bitcoind seems to be alive"
while ! cli getwalletinfo >/dev/null; do sleep 1; done
# sleep again for good measure!
sleep 1;
cargo test --features "test-blockchains,$blockchain" --no-default-features "$blockchain::bdk_blockchain_tests::$test_name"

View File

@@ -1,31 +0,0 @@
#!/bin/bash
#
# Run various invocations of cargo check
features=( "default" "compiler" "electrum" "esplora" "compact_filters" "key-value-db" "async-interface" "all-keys" "keys-bip39" )
toolchains=( "+stable" "+1.46" "+nightly" )
main() {
check_src
check_all_targets
}
# Check with all features, with various toolchains.
check_src() {
for toolchain in "${toolchains[@]}"; do
cmd="cargo $toolchain clippy --all-targets --no-default-features"
for feature in "${features[@]}"; do
touch_files
$cmd --features "$feature"
done
done
}
# Touch files to prevent cached warnings from not showing up.
touch_files() {
touch $(find . -name *.rs)
}
main
exit 0

View File

@@ -45,13 +45,6 @@ use crate::FeeRate;
/// See the [`blockchain::electrum`](crate::blockchain::electrum) module for a usage example. /// See the [`blockchain::electrum`](crate::blockchain::electrum) module for a usage example.
pub struct ElectrumBlockchain(Client); pub struct ElectrumBlockchain(Client);
#[cfg(test)]
#[cfg(feature = "test-electrum")]
#[bdk_blockchain_tests(crate)]
fn local_electrs() -> ElectrumBlockchain {
ElectrumBlockchain::from(Client::new(&testutils::get_electrum_url()).unwrap())
}
impl std::convert::From<Client> for ElectrumBlockchain { impl std::convert::From<Client> for ElectrumBlockchain {
fn from(client: Client) -> Self { fn from(client: Client) -> Self {
ElectrumBlockchain(client) ElectrumBlockchain(client)
@@ -175,3 +168,10 @@ impl ConfigurableBlockchain for ElectrumBlockchain {
)?)) )?))
} }
} }
#[cfg(feature = "test-blockchains")]
crate::bdk_blockchain_tests! {
fn test_instance() -> ElectrumBlockchain {
ElectrumBlockchain::from(Client::new(&testutils::blockchain_tests::get_electrum_url()).unwrap())
}
}

View File

@@ -414,3 +414,10 @@ impl_error!(reqwest::Error, Reqwest, EsploraError);
impl_error!(std::num::ParseIntError, Parsing, EsploraError); impl_error!(std::num::ParseIntError, Parsing, EsploraError);
impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError); impl_error!(consensus::encode::Error, BitcoinEncoding, EsploraError);
impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError); impl_error!(bitcoin::hashes::hex::Error, Hex, EsploraError);
#[cfg(feature = "test-blockchains")]
crate::bdk_blockchain_tests! {
fn test_instance() -> EsploraBlockchain {
EsploraBlockchain::new(std::env::var("BDK_ESPLORA_URL").unwrap_or("127.0.0.1:3002".into()).as_str(), None)
}
}

View File

@@ -511,7 +511,7 @@ macro_rules! doctest_wallet {
() => {{ () => {{
use $crate::bitcoin::Network; use $crate::bitcoin::Network;
use $crate::database::MemoryDatabase; use $crate::database::MemoryDatabase;
use testutils::testutils; use $crate::testutils;
let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
let descriptors = testutils!(@descriptors (descriptor) (descriptor)); let descriptors = testutils!(@descriptors (descriptor) (descriptor));

View File

@@ -164,14 +164,14 @@ pub(crate) trait DatabaseUtils: Database {
.map(|o| o.is_some()) .map(|o| o.is_some())
} }
fn get_raw_tx_or<F>(&self, txid: &Txid, f: F) -> Result<Option<Transaction>, Error> fn get_raw_tx_or<D>(&self, txid: &Txid, default: D) -> Result<Option<Transaction>, Error>
where where
F: FnOnce() -> Result<Option<Transaction>, Error>, D: FnOnce() -> Result<Option<Transaction>, Error>,
{ {
self.get_tx(txid, true)? self.get_tx(txid, true)?
.map(|t| t.transaction) .map(|t| t.transaction)
.flatten() .flatten()
.map_or_else(f, |t| Ok(Some(t))) .map_or_else(default, |t| Ok(Some(t)))
} }
fn get_previous_output(&self, outpoint: &OutPoint) -> Result<Option<TxOut>, Error> { fn get_previous_output(&self, outpoint: &OutPoint) -> Result<Option<TxOut>, Error> {

View File

@@ -535,9 +535,7 @@ macro_rules! fragment_internal {
( @t , $( $tail:tt )* ) => ({ ( @t , $( $tail:tt )* ) => ({
$crate::fragment_internal!( @t $( $tail )* ) $crate::fragment_internal!( @t $( $tail )* )
}); });
( @t ) => ({ ( @t ) => ({});
()
});
// Fallback to calling `fragment!()` // Fallback to calling `fragment!()`
( $( $tokens:tt )* ) => ({ ( $( $tokens:tt )* ) => ({

View File

@@ -27,6 +27,8 @@ use miniscript::descriptor::{DescriptorPublicKey, DescriptorType, DescriptorXKey
pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0}; pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0};
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk}; use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
use crate::descriptor::policy::BuildSatisfaction;
pub mod checksum; pub mod checksum;
pub(crate) mod derived; pub(crate) mod derived;
#[doc(hidden)] #[doc(hidden)]
@@ -255,6 +257,7 @@ pub trait ExtractPolicy {
fn extract_policy( fn extract_policy(
&self, &self,
signers: &SignersContainer, signers: &SignersContainer,
psbt: BuildSatisfaction,
secp: &SecpCtx, secp: &SecpCtx,
) -> Result<Option<Policy>, DescriptorError>; ) -> Result<Option<Policy>, DescriptorError>;
} }

View File

@@ -22,6 +22,7 @@
//! # use std::sync::Arc; //! # use std::sync::Arc;
//! # use bdk::descriptor::*; //! # use bdk::descriptor::*;
//! # use bdk::bitcoin::secp256k1::Secp256k1; //! # use bdk::bitcoin::secp256k1::Secp256k1;
//! use bdk::descriptor::policy::BuildSatisfaction;
//! let secp = Secp256k1::new(); //! let secp = Secp256k1::new();
//! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))"; //! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))";
//! //!
@@ -29,7 +30,7 @@
//! println!("{:?}", extended_desc); //! println!("{:?}", extended_desc);
//! //!
//! let signers = Arc::new(key_map.into()); //! let signers = Arc::new(key_map.into());
//! let policy = extended_desc.extract_policy(&signers, &secp)?; //! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
//! println!("policy: {}", serde_json::to_string(&policy)?); //! println!("policy: {}", serde_json::to_string(&policy)?);
//! # Ok::<(), bdk::Error>(()) //! # Ok::<(), bdk::Error>(())
//! ``` //! ```
@@ -47,24 +48,20 @@ use bitcoin::PublicKey;
use miniscript::descriptor::{DescriptorPublicKey, ShInner, SortedMultiVec, WshInner}; use miniscript::descriptor::{DescriptorPublicKey, ShInner, SortedMultiVec, WshInner};
use miniscript::{ use miniscript::{
Descriptor, ForEachKey, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal, Descriptor, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal, ToPublicKey,
ToPublicKey,
}; };
#[allow(unused_imports)] #[allow(unused_imports)]
use log::{debug, error, info, trace}; use log::{debug, error, info, trace};
use crate::descriptor::{ use crate::descriptor::{DerivedDescriptorKey, ExtractPolicy};
DerivedDescriptor, DerivedDescriptorKey, DescriptorMeta, ExtendedDescriptor, ExtractPolicy,
};
use crate::psbt::PsbtUtils;
use crate::wallet::signer::{SignerId, SignersContainer}; use crate::wallet::signer::{SignerId, SignersContainer};
use crate::wallet::utils::{self, SecpCtx}; use crate::wallet::utils::{self, After, Older, SecpCtx};
use super::checksum::get_checksum; use super::checksum::get_checksum;
use super::error::Error; use super::error::Error;
use super::XKeyUtils; use super::XKeyUtils;
use bitcoin::util::psbt::PartiallySignedTransaction as PSBT; use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use miniscript::psbt::PsbtInputSatisfier; use miniscript::psbt::PsbtInputSatisfier;
/// Raw public key or extended key fingerprint /// Raw public key or extended key fingerprint
@@ -563,13 +560,18 @@ impl Policy {
conditions: Default::default(), conditions: Default::default(),
sorted: None, sorted: None,
}; };
let mut satisfaction = contribution.clone();
for (index, item) in items.iter().enumerate() { for (index, item) in items.iter().enumerate() {
contribution.add(&item.contribution, index)?; contribution.add(&item.contribution, index)?;
satisfaction.add(&item.satisfaction, index)?;
} }
contribution.finalize(); contribution.finalize();
satisfaction.finalize();
let mut policy: Policy = SatisfiableItem::Thresh { items, threshold }.into(); let mut policy: Policy = SatisfiableItem::Thresh { items, threshold }.into();
policy.contribution = contribution; policy.contribution = contribution;
policy.satisfaction = satisfaction;
Ok(Some(policy)) Ok(Some(policy))
} }
@@ -577,6 +579,7 @@ impl Policy {
fn make_multisig( fn make_multisig(
keys: &[DescriptorPublicKey], keys: &[DescriptorPublicKey],
signers: &SignersContainer, signers: &SignersContainer,
build_sat: BuildSatisfaction,
threshold: usize, threshold: usize,
sorted: bool, sorted: bool,
secp: &SecpCtx, secp: &SecpCtx,
@@ -594,6 +597,8 @@ impl Policy {
conditions: Default::default(), conditions: Default::default(),
sorted: Some(sorted), sorted: Some(sorted),
}; };
let mut satisfaction = contribution.clone();
for (index, key) in keys.iter().enumerate() { for (index, key) in keys.iter().enumerate() {
if signers.find(signer_id(key, secp)).is_some() { if signers.find(signer_id(key, secp)).is_some() {
contribution.add( contribution.add(
@@ -603,7 +608,19 @@ impl Policy {
index, index,
)?; )?;
} }
if let Some(psbt) = build_sat.psbt() {
if signature_in_psbt(psbt, key, secp) {
satisfaction.add(
&Satisfaction::Complete {
condition: Default::default(),
},
index,
)?;
}
}
} }
satisfaction.finalize();
contribution.finalize(); contribution.finalize();
let mut policy: Policy = SatisfiableItem::Multisig { let mut policy: Policy = SatisfiableItem::Multisig {
@@ -612,6 +629,7 @@ impl Policy {
} }
.into(); .into();
policy.contribution = contribution; policy.contribution = contribution;
policy.satisfaction = satisfaction;
Ok(Some(policy)) Ok(Some(policy))
} }
@@ -698,52 +716,6 @@ impl Policy {
_ => Ok(Condition::default()), _ => Ok(Condition::default()),
} }
} }
/// fill `self.satisfaction` with the signatures we already have in the PSBT
pub fn fill_satisfactions(
&mut self,
psbt: &PSBT,
desc: &ExtendedDescriptor, // can't put in self because non Serialize
secp: &SecpCtx,
) -> Result<(), Error> {
// Start from an empty Satisfaction (like a contribution without signers)
let policy = desc.extract_policy(&SignersContainer::default(), &secp)?;
if let Some(policy) = policy {
self.satisfaction = policy.contribution;
for (i, input) in psbt.inputs.iter().enumerate() {
let s = PsbtInputSatisfier::new(psbt, i);
let derived_desc = desc.derive_from_psbt_input(input, psbt.get_utxo_for(i), secp);
self.add_satisfaction(s, derived_desc);
}
self.satisfaction.finalize();
}
Ok(())
}
fn add_satisfaction<S: Satisfier<bitcoin::PublicKey>>(
&mut self,
satisfier: S,
derived_desc: Option<DerivedDescriptor>,
) {
if let Some(derived_desc) = derived_desc {
let mut index = 0;
derived_desc.for_each_key(|k| {
if satisfier.lookup_sig(&k.as_key().to_public_key()).is_some() {
//TODO check signature verifies
let _ = self.satisfaction.add(
&Satisfaction::Complete {
condition: Default::default(),
},
index,
);
}
index += 1;
true
});
}
}
} }
impl From<SatisfiableItem> for Policy { impl From<SatisfiableItem> for Policy {
@@ -759,7 +731,12 @@ fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
} }
} }
fn signature(key: &DescriptorPublicKey, signers: &SignersContainer, secp: &SecpCtx) -> Policy { fn signature(
key: &DescriptorPublicKey,
signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx,
) -> Policy {
let mut policy: Policy = SatisfiableItem::Signature(PkOrF::from_key(key, secp)).into(); let mut policy: Policy = SatisfiableItem::Signature(PkOrF::from_key(key, secp)).into();
policy.contribution = if signers.find(signer_id(key, secp)).is_some() { policy.contribution = if signers.find(signer_id(key, secp)).is_some() {
@@ -770,9 +747,38 @@ fn signature(key: &DescriptorPublicKey, signers: &SignersContainer, secp: &SecpC
Satisfaction::None Satisfaction::None
}; };
if let Some(psbt) = build_sat.psbt() {
policy.satisfaction = if signature_in_psbt(psbt, key, secp) {
Satisfaction::Complete {
condition: Default::default(),
}
} else {
Satisfaction::None
};
}
policy policy
} }
fn signature_in_psbt(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool {
//TODO check signature validity
psbt.inputs.iter().all(|input| match key {
DescriptorPublicKey::SinglePub(key) => input.partial_sigs.contains_key(&key.key),
DescriptorPublicKey::XPub(xpub) => {
let pubkey = input
.bip32_derivation
.iter()
.find(|(_, (f, _))| *f == xpub.root_fingerprint(secp))
.map(|(p, _)| p);
//TODO check actual derivation matches
match pubkey {
Some(pubkey) => input.partial_sigs.contains_key(pubkey),
None => false,
}
}
})
}
fn signature_key( fn signature_key(
key: &<DescriptorPublicKey as MiniscriptKey>::Hash, key: &<DescriptorPublicKey as MiniscriptKey>::Hash,
signers: &SignersContainer, signers: &SignersContainer,
@@ -796,12 +802,13 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
fn extract_policy( fn extract_policy(
&self, &self,
signers: &SignersContainer, signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx, secp: &SecpCtx,
) -> Result<Option<Policy>, Error> { ) -> Result<Option<Policy>, Error> {
Ok(match &self.node { Ok(match &self.node {
// Leaves // Leaves
Terminal::True | Terminal::False => None, Terminal::True | Terminal::False => None,
Terminal::PkK(pubkey) => Some(signature(pubkey, signers, secp)), Terminal::PkK(pubkey) => Some(signature(pubkey, signers, build_sat, secp)),
Terminal::PkH(pubkey_hash) => Some(signature_key(pubkey_hash, signers, secp)), Terminal::PkH(pubkey_hash) => Some(signature_key(pubkey_hash, signers, secp)),
Terminal::After(value) => { Terminal::After(value) => {
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into(); let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into();
@@ -811,6 +818,20 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
csv: None, csv: None,
}, },
}; };
if let BuildSatisfaction::PsbtTimelocks {
current_height,
psbt,
..
} = 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));
if after_sat && inputs_sat {
policy.satisfaction = policy.contribution.clone();
}
}
Some(policy) Some(policy)
} }
@@ -822,6 +843,20 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
csv: Some(*value), csv: Some(*value),
}, },
}; };
if let BuildSatisfaction::PsbtTimelocks {
current_height,
input_max_height,
psbt,
} = build_sat
{
let older = Older::new(Some(current_height), Some(input_max_height), false);
let older_sat = Satisfier::<bitcoin::PublicKey>::check_older(&older, *value);
let inputs_sat = psbt_inputs_sat(psbt)
.all(|sat| Satisfier::<bitcoin::PublicKey>::check_older(&sat, *value));
if older_sat && inputs_sat {
policy.satisfaction = policy.contribution.clone();
}
}
Some(policy) Some(policy)
} }
@@ -835,7 +870,9 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
Terminal::Hash160(hash) => { Terminal::Hash160(hash) => {
Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into()) Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into())
} }
Terminal::Multi(k, pks) => Policy::make_multisig(pks, signers, *k, false, secp)?, Terminal::Multi(k, pks) => {
Policy::make_multisig(pks, signers, build_sat, *k, false, secp)?
}
// Identities // Identities
Terminal::Alt(inner) Terminal::Alt(inner)
| Terminal::Swap(inner) | Terminal::Swap(inner)
@@ -843,34 +880,34 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
| Terminal::DupIf(inner) | Terminal::DupIf(inner)
| Terminal::Verify(inner) | Terminal::Verify(inner)
| Terminal::NonZero(inner) | Terminal::NonZero(inner)
| Terminal::ZeroNotEqual(inner) => inner.extract_policy(signers, secp)?, | Terminal::ZeroNotEqual(inner) => inner.extract_policy(signers, build_sat, secp)?,
// Complex policies // Complex policies
Terminal::AndV(a, b) | Terminal::AndB(a, b) => Policy::make_and( Terminal::AndV(a, b) | Terminal::AndB(a, b) => Policy::make_and(
a.extract_policy(signers, secp)?, a.extract_policy(signers, build_sat, secp)?,
b.extract_policy(signers, secp)?, b.extract_policy(signers, build_sat, secp)?,
)?, )?,
Terminal::AndOr(x, y, z) => Policy::make_or( Terminal::AndOr(x, y, z) => Policy::make_or(
Policy::make_and( Policy::make_and(
x.extract_policy(signers, secp)?, x.extract_policy(signers, build_sat, secp)?,
y.extract_policy(signers, secp)?, y.extract_policy(signers, build_sat, secp)?,
)?, )?,
z.extract_policy(signers, secp)?, z.extract_policy(signers, build_sat, secp)?,
)?, )?,
Terminal::OrB(a, b) Terminal::OrB(a, b)
| Terminal::OrD(a, b) | Terminal::OrD(a, b)
| Terminal::OrC(a, b) | Terminal::OrC(a, b)
| Terminal::OrI(a, b) => Policy::make_or( | Terminal::OrI(a, b) => Policy::make_or(
a.extract_policy(signers, secp)?, a.extract_policy(signers, build_sat, secp)?,
b.extract_policy(signers, secp)?, b.extract_policy(signers, build_sat, secp)?,
)?, )?,
Terminal::Thresh(k, nodes) => { Terminal::Thresh(k, nodes) => {
let mut threshold = *k; let mut threshold = *k;
let mapped: Vec<_> = nodes let mapped: Vec<_> = nodes
.iter() .iter()
.map(|n| n.extract_policy(signers, secp)) .map(|n| n.extract_policy(signers, build_sat, secp))
.collect::<Result<Vec<_>, _>>()? .collect::<Result<Vec<_>, _>>()?
.into_iter() .into_iter()
.filter_map(|x| x) .flatten()
.collect(); .collect();
if mapped.len() < nodes.len() { if mapped.len() < nodes.len() {
@@ -886,20 +923,55 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
} }
} }
fn psbt_inputs_sat(psbt: &Psbt) -> impl Iterator<Item = PsbtInputSatisfier> {
(0..psbt.inputs.len()).map(move |i| PsbtInputSatisfier::new(psbt, i))
}
/// Options to build the satisfaction field in the policy
#[derive(Debug, Clone, Copy)]
pub enum BuildSatisfaction<'a> {
/// Don't generate `satisfaction` field
None,
/// Analyze the given PSBT to check for existing signatures
Psbt(&'a Psbt),
/// Like `Psbt` variant and also check for expired timelocks
PsbtTimelocks {
/// Given PSBT
psbt: &'a Psbt,
/// Current blockchain height
current_height: u32,
/// The highest confirmation height between the inputs
/// CSV should consider different inputs, but we consider the worst condition for the tx as whole
input_max_height: u32,
},
}
impl<'a> BuildSatisfaction<'a> {
fn psbt(&self) -> Option<&'a Psbt> {
match self {
BuildSatisfaction::None => None,
BuildSatisfaction::Psbt(psbt) => Some(psbt),
BuildSatisfaction::PsbtTimelocks { psbt, .. } => Some(psbt),
}
}
}
impl ExtractPolicy for Descriptor<DescriptorPublicKey> { impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
fn extract_policy( fn extract_policy(
&self, &self,
signers: &SignersContainer, signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx, secp: &SecpCtx,
) -> Result<Option<Policy>, Error> { ) -> Result<Option<Policy>, Error> {
fn make_sortedmulti<Ctx: ScriptContext>( fn make_sortedmulti<Ctx: ScriptContext>(
keys: &SortedMultiVec<DescriptorPublicKey, Ctx>, keys: &SortedMultiVec<DescriptorPublicKey, Ctx>,
signers: &SignersContainer, signers: &SignersContainer,
build_sat: BuildSatisfaction,
secp: &SecpCtx, secp: &SecpCtx,
) -> Result<Option<Policy>, Error> { ) -> Result<Option<Policy>, Error> {
Ok(Policy::make_multisig( Ok(Policy::make_multisig(
keys.pks.as_ref(), keys.pks.as_ref(),
signers, signers,
build_sat,
keys.k, keys.k,
true, true,
secp, secp,
@@ -907,22 +979,24 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
} }
match self { match self {
Descriptor::Pkh(pk) => Ok(Some(signature(pk.as_inner(), signers, secp))), Descriptor::Pkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
Descriptor::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, secp))), Descriptor::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
Descriptor::Sh(sh) => match sh.as_inner() { Descriptor::Sh(sh) => match sh.as_inner() {
ShInner::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, secp))), ShInner::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
ShInner::Ms(ms) => Ok(ms.extract_policy(signers, secp)?), ShInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, secp), ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
ShInner::Wsh(wsh) => match wsh.as_inner() { ShInner::Wsh(wsh) => match wsh.as_inner() {
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, secp)?), WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, secp), WshInner::SortedMulti(ref keys) => {
make_sortedmulti(keys, signers, build_sat, secp)
}
}, },
}, },
Descriptor::Wsh(wsh) => match wsh.as_inner() { Descriptor::Wsh(wsh) => match wsh.as_inner() {
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, secp)?), WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, secp), WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
}, },
Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, secp)?), Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?),
} }
} }
} }
@@ -978,7 +1052,7 @@ mod test {
.unwrap(); .unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc let policy = wallet_desc
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -993,7 +1067,7 @@ mod test {
.unwrap(); .unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc let policy = wallet_desc
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -1017,14 +1091,14 @@ mod test {
.unwrap(); .unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc let policy = wallet_desc
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert!( assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
&& &keys[0].fingerprint.unwrap() == &fingerprint0 && keys[0].fingerprint.unwrap() == fingerprint0
&& &keys[1].fingerprint.unwrap() == &fingerprint1) && keys[1].fingerprint.unwrap() == fingerprint1)
); );
// TODO should this be "Satisfaction::None" since we have no prv keys? // TODO should this be "Satisfaction::None" since we have no prv keys?
// TODO should items and conditions not be empty? // TODO should items and conditions not be empty?
@@ -1049,13 +1123,13 @@ mod test {
.unwrap(); .unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc let policy = wallet_desc
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert!( assert!(
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
&& &keys[0].fingerprint.unwrap() == &fingerprint0 && keys[0].fingerprint.unwrap() == fingerprint0
&& &keys[1].fingerprint.unwrap() == &fingerprint1) && keys[1].fingerprint.unwrap() == fingerprint1)
); );
assert!( assert!(
@@ -1081,7 +1155,7 @@ mod test {
.unwrap(); .unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc let policy = wallet_desc
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -1113,7 +1187,7 @@ mod test {
.unwrap(); .unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc let policy = wallet_desc
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -1146,7 +1220,7 @@ mod test {
let single_key = wallet_desc.derive(0); let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = single_key let policy = single_key
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -1162,7 +1236,7 @@ mod test {
let single_key = wallet_desc.derive(0); let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = single_key let policy = single_key
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -1189,7 +1263,7 @@ mod test {
let single_key = wallet_desc.derive(0); let single_key = wallet_desc.derive(0);
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = single_key let policy = single_key
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -1232,7 +1306,7 @@ mod test {
.unwrap(); .unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc let policy = wallet_desc
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -1271,7 +1345,7 @@ mod test {
.unwrap(); .unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc let policy = wallet_desc
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
println!("desc policy = {:?}", policy); // TODO remove println!("desc policy = {:?}", policy); // TODO remove
@@ -1296,7 +1370,7 @@ mod test {
.unwrap(); .unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc let policy = wallet_desc
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
println!("desc policy = {:?}", policy); // TODO remove println!("desc policy = {:?}", policy); // TODO remove
@@ -1314,7 +1388,7 @@ mod test {
.unwrap(); .unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let policy = wallet_desc let policy = wallet_desc
.extract_policy(&signers_container, &secp) .extract_policy(&signers_container, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -1337,7 +1411,7 @@ mod test {
let signers = keymap.into(); let signers = keymap.into();
let policy = wallet_desc let policy = wallet_desc
.extract_policy(&signers, &secp) .extract_policy(&signers, BuildSatisfaction::None, &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
@@ -1369,11 +1443,12 @@ mod test {
assert_eq!(out_of_range, Err(PolicyError::IndexOutOfRange(5))); assert_eq!(out_of_range, Err(PolicyError::IndexOutOfRange(5)));
} }
const ALICE_TPRV_STR:&str = "tprv8ZgxMBicQKsPf6T5X327efHnvJDr45Xnb8W4JifNWtEoqXu9MRYS4v1oYe6DFcMVETxy5w3bqpubYRqvcVTqovG1LifFcVUuJcbwJwrhYzP";
const BOB_TPRV_STR:&str = "tprv8ZgxMBicQKsPeinZ155cJAn117KYhbaN6MV3WeG6sWhxWzcvX1eg1awd4C9GpUN1ncLEM2rzEvunAg3GizdZD4QPPCkisTz99tXXB4wZArp";
const ALICE_BOB_PATH: &str = "m/0'";
#[test] #[test]
fn test_extract_satisfaction() { fn test_extract_satisfaction() {
const ALICE_TPRV_STR:&str = "tprv8ZgxMBicQKsPf6T5X327efHnvJDr45Xnb8W4JifNWtEoqXu9MRYS4v1oYe6DFcMVETxy5w3bqpubYRqvcVTqovG1LifFcVUuJcbwJwrhYzP";
const BOB_TPRV_STR:&str = "tprv8ZgxMBicQKsPeinZ155cJAn117KYhbaN6MV3WeG6sWhxWzcvX1eg1awd4C9GpUN1ncLEM2rzEvunAg3GizdZD4QPPCkisTz99tXXB4wZArp";
const ALICE_BOB_PATH: &str = "m/0'";
const ALICE_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA"; const ALICE_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
const BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBAQVHUiEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZsshAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIUq4iBgL4YT/L4um0jzGaJHqw5733wgbPJujHRB/Pj4FCXWeZiAwcLu4+AAAAgAAAAAAiBgN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmywzJEXwuAAAAgAAAAAAAAA=="; const BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBAQVHUiEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZsshAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIUq4iBgL4YT/L4um0jzGaJHqw5733wgbPJujHRB/Pj4FCXWeZiAwcLu4+AAAAgAAAAAAiBgN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmywzJEXwuAAAAgAAAAAAAAA==";
const ALICE_BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAEHAAEI2wQARzBEAiAY9Iy41HlWFzUOnKgfoG7b7ijI1eeMEoFpZtXH3IKR1QIgWtw7QvZf9TLeCAwr0e5psEHd3gD/5ufvvNXroSTUq4EBSDBFAiEA+cw7TOTMJJbq8CeWlu+kbDt+iKsrvurjHVZYS+sLNhkCIHrAIs+HWyku1JoQ7Av3NXs7tKOoadNFFLbAjH1GeGp2AUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSrgAA"; const ALICE_BOB_SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAZb0njwT2wRS3AumaaP3yb7T4MxOePpSWih4Nq+jWChMAQAAAAD/////Af4lAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASuJJgAAAAAAACIAIERw5kTLo9DUH9QDJSClHQwPpC7VGJ+ZMDpa8U+2fzcYIgIC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhIMEUCIQD5zDtM5MwklurwJ5aW76RsO36Iqyu+6uMdVlhL6ws2GQIgesAiz4dbKS7UmhDsC/c1ezu0o6hp00UUtsCMfUZ4anYBIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstHMEQCIBj0jLjUeVYXNQ6cqB+gbtvuKMjV54wSgWlm1cfcgpHVAiBa3DtC9l/1Mt4IDCvR7mmwQd3eAP/m5++81euhJNSrgQEBBUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSriIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAEHAAEI2wQARzBEAiAY9Iy41HlWFzUOnKgfoG7b7ijI1eeMEoFpZtXH3IKR1QIgWtw7QvZf9TLeCAwr0e5psEHd3gD/5ufvvNXroSTUq4EBSDBFAiEA+cw7TOTMJJbq8CeWlu+kbDt+iKsrvurjHVZYS+sLNhkCIHrAIs+HWyku1JoQ7Av3NXs7tKOoadNFFLbAjH1GeGp2AUdSIQN4C2NhCT9V+7h1vb7ryHIwqNzfz6RaXmw/lAfwvjZmyyEC+GE/y+LptI8xmiR6sOe998IGzybox0Qfz4+BQl1nmYhSrgAA";
@@ -1400,45 +1475,135 @@ mod test {
let signers_container = Arc::new(SignersContainer::from(keymap)); let signers_container = Arc::new(SignersContainer::from(keymap));
let original_policy = wallet_desc let psbt: Psbt = deserialize(&base64::decode(ALICE_SIGNED_PSBT).unwrap()).unwrap();
.extract_policy(&signers_container, &secp)
let policy_alice_psbt = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
.unwrap() .unwrap()
.unwrap(); .unwrap();
//println!("{}", serde_json::to_string(&policy_alice_psbt).unwrap());
let psbt: PSBT = deserialize(&base64::decode(ALICE_SIGNED_PSBT).unwrap()).unwrap();
let mut policy_clone = original_policy.clone();
policy_clone
.fill_satisfactions(&psbt, &wallet_desc, &secp)
.unwrap();
assert!( assert!(
matches!(&policy_clone.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2 matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
&& m == &2 && m == &2
&& items == &vec![0] && items == &vec![0]
) )
); );
let mut policy_clone = original_policy.clone(); let psbt: Psbt = deserialize(&base64::decode(BOB_SIGNED_PSBT).unwrap()).unwrap();
let psbt: PSBT = deserialize(&base64::decode(BOB_SIGNED_PSBT).unwrap()).unwrap(); let policy_bob_psbt = wallet_desc
policy_clone .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
.fill_satisfactions(&psbt, &wallet_desc, &secp) .unwrap()
.unwrap(); .unwrap();
//println!("{}", serde_json::to_string(&policy_bob_psbt).unwrap());
assert!( assert!(
matches!(&policy_clone.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2 matches!(&policy_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
&& m == &2 && m == &2
&& items == &vec![1] && items == &vec![1]
) )
); );
let mut policy_clone = original_policy.clone(); let psbt: Psbt = deserialize(&base64::decode(ALICE_BOB_SIGNED_PSBT).unwrap()).unwrap();
let psbt: PSBT = deserialize(&base64::decode(ALICE_BOB_SIGNED_PSBT).unwrap()).unwrap(); let policy_alice_bob_psbt = wallet_desc
policy_clone .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
.fill_satisfactions(&psbt, &wallet_desc, &secp) .unwrap()
.unwrap(); .unwrap();
assert!( assert!(
matches!(&policy_clone.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2 matches!(&policy_alice_bob_psbt.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2
&& m == &2 && m == &2
&& items == &vec![0, 1] && items == &vec![0, 1]
) )
); );
} }
#[test]
fn test_extract_satisfaction_timelock() {
//const PSBT_POLICY_CONSIDER_TIMELOCK_NOT_EXPIRED: &str = "cHNidP8BAFMBAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAD/////ATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
const PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED: &str = "cHNidP8BAFMCAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAACAAAAATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAAA";
const PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED: &str ="cHNidP8BAFMCAAAAAdld52uJFGT7Yde0YZdSVh2vL020Zm2exadH5R4GSNScAAAAAAACAAAAATrcAAAAAAAAF6kUXv2Fn+YemPP4PUpNR1ZbU16/eRCHAAAAAAABASvI3AAAAAAAACIAILhzvvcBzw/Zfnc9ispRK0PCahxn1F6RHXTZAmw5tqNPIgIDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42ZstIMEUCIQCtZxNm6H3Ux3pnc64DSpgohMdBj+57xhFHcURYt2BpPAIgG3OnI7bcj/3GtWX1HHyYGSI7QGa/zq5YnsmK1Cw29NABAQVSdmNSsmlofCEDeAtjYQk/Vfu4db2+68hyMKjc38+kWl5sP5QH8L42Zsusk3whAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIrJNShyIGAvhhP8vi6bSPMZokerDnvffCBs8m6MdEH8+PgUJdZ5mIDBwu7j4AAACAAAAAACIGA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLDMkRfC4AAACAAAAAAAEHAAEIoAQASDBFAiEArWcTZuh91Md6Z3OuA0qYKITHQY/ue8YRR3FEWLdgaTwCIBtzpyO23I/9xrVl9Rx8mBkiO0Bmv86uWJ7JitQsNvTQAQEBUnZjUrJpaHwhA3gLY2EJP1X7uHW9vuvIcjCo3N/PpFpebD+UB/C+NmbLrJN8IQL4YT/L4um0jzGaJHqw5733wgbPJujHRB/Pj4FCXWeZiKyTUocAAA==";
let secp = Secp256k1::new();
let (prvkey_alice, _, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp);
let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
let desc =
descriptor!(wsh(thresh(2,d:v:older(2),s:pk(prvkey_alice),s:pk(prvkey_bob)))).unwrap();
let (wallet_desc, keymap) = desc
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
let signers_container = Arc::new(SignersContainer::from(keymap));
let addr = wallet_desc
.as_derived(0, &secp)
.address(Network::Testnet)
.unwrap();
assert_eq!(
"tb1qhpemaacpeu8ajlnh8k9v55ftg0px58r8630fz8t5mypxcwdk5d8sum522g",
addr.to_string()
);
let psbt: Psbt =
deserialize(&base64::decode(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED).unwrap()).unwrap();
let build_sat = BuildSatisfaction::PsbtTimelocks {
psbt: &psbt,
current_height: 10,
input_max_height: 9,
};
let policy = wallet_desc
.extract_policy(&signers_container, build_sat, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
&& m == &2
&& items.is_empty()
)
);
//println!("{}", serde_json::to_string(&policy).unwrap());
let build_sat_expired = BuildSatisfaction::PsbtTimelocks {
psbt: &psbt,
current_height: 12,
input_max_height: 9,
};
let policy_expired = wallet_desc
.extract_policy(&signers_container, build_sat_expired, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy_expired.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
&& m == &2
&& items == &vec![0]
)
);
//println!("{}", serde_json::to_string(&policy_expired).unwrap());
let psbt_signed: Psbt =
deserialize(&base64::decode(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED).unwrap())
.unwrap();
let build_sat_expired_signed = BuildSatisfaction::PsbtTimelocks {
psbt: &psbt_signed,
current_height: 12,
input_max_height: 9,
};
let policy_expired_signed = wallet_desc
.extract_policy(&signers_container, build_sat_expired_signed, &secp)
.unwrap()
.unwrap();
assert!(
matches!(&policy_expired_signed.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &3
&& m == &2
&& items == &vec![0, 1]
)
);
//println!("{}", serde_json::to_string(&policy_expired_signed).unwrap());
}
} }

View File

@@ -37,7 +37,7 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
/// ///
/// ``` /// ```
/// use bdk::descriptor::error::Error as DescriptorError; /// use bdk::descriptor::error::Error as DescriptorError;
/// use bdk::keys::{KeyError, IntoDescriptorKey}; /// use bdk::keys::{IntoDescriptorKey, KeyError};
/// use bdk::miniscript::Legacy; /// use bdk::miniscript::Legacy;
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut}; /// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
/// ///
@@ -455,11 +455,12 @@ expand_make_bipxx!(segwit_v0, Segwitv0);
mod test { mod test {
// test existing descriptor templates, make sure they are expanded to the right descriptors // test existing descriptor templates, make sure they are expanded to the right descriptors
use std::str::FromStr;
use super::*; use super::*;
use crate::descriptor::derived::AsDerived; use crate::descriptor::derived::AsDerived;
use crate::descriptor::{DescriptorError, DescriptorMeta}; use crate::descriptor::{DescriptorError, DescriptorMeta};
use crate::keys::ValidNetworks; use crate::keys::ValidNetworks;
use bitcoin::hashes::core::str::FromStr;
use bitcoin::network::constants::Network::Regtest; use bitcoin::network::constants::Network::Regtest;
use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap}; use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};

View File

@@ -192,7 +192,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// ``` /// ```
/// use bdk::bitcoin::PublicKey; /// use bdk::bitcoin::PublicKey;
/// ///
/// use bdk::keys::{DescriptorKey, KeyError, ScriptContext, IntoDescriptorKey}; /// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext};
/// ///
/// pub struct MyKeyType { /// pub struct MyKeyType {
/// pubkey: PublicKey, /// pubkey: PublicKey,
@@ -211,8 +211,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// use bdk::bitcoin::PublicKey; /// use bdk::bitcoin::PublicKey;
/// ///
/// use bdk::keys::{ /// use bdk::keys::{
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub, KeyError, /// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
/// ScriptContext, IntoDescriptorKey, /// IntoDescriptorKey, KeyError, ScriptContext,
/// }; /// };
/// ///
/// pub struct MyKeyType { /// pub struct MyKeyType {
@@ -237,7 +237,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// ``` /// ```
/// use bdk::bitcoin::PublicKey; /// use bdk::bitcoin::PublicKey;
/// ///
/// use bdk::keys::{DescriptorKey, ExtScriptContext, KeyError, ScriptContext, IntoDescriptorKey}; /// use bdk::keys::{DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext};
/// ///
/// pub struct MyKeyType { /// pub struct MyKeyType {
/// is_legacy: bool, /// is_legacy: bool,
@@ -266,7 +266,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
/// use bdk::bitcoin::PublicKey; /// use bdk::bitcoin::PublicKey;
/// use std::str::FromStr; /// use std::str::FromStr;
/// ///
/// use bdk::keys::{DescriptorKey, KeyError, IntoDescriptorKey}; /// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
/// ///
/// pub struct MySegwitOnlyKeyType { /// pub struct MySegwitOnlyKeyType {
/// pubkey: PublicKey, /// pubkey: PublicKey,

View File

@@ -14,9 +14,6 @@
// only enables the `doc_cfg` feature when // only enables the `doc_cfg` feature when
// the `docsrs` configuration attribute is defined // the `docsrs` configuration attribute is defined
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
// only enables the nightly `external_doc` feature when
// `test-md-docs` is enabled
#![cfg_attr(feature = "test-md-docs", feature(external_doc))]
//! A modern, lightweight, descriptor-based wallet library written in Rust. //! A modern, lightweight, descriptor-based wallet library written in Rust.
//! //!
@@ -43,36 +40,39 @@
//! interact with the bitcoin P2P network. //! interact with the bitcoin P2P network.
//! //!
//! ```toml //! ```toml
//! bdk = "0.6.0" //! bdk = "0.8.0"
//! ```
//!
//! ## Sync the balance of a descriptor
//!
//! ### Example
//! ```ignore
//! use bdk::Wallet;
//! use bdk::database::MemoryDatabase;
//! use bdk::blockchain::{noop_progress, ElectrumBlockchain};
//!
//! use bdk::electrum_client::Client;
//!
//! fn main() -> Result<(), bdk::Error> {
//! let client = Client::new("ssl://electrum.blockstream.info:60002")?;
//! let wallet = Wallet::new(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
//! bitcoin::Network::Testnet,
//! MemoryDatabase::default(),
//! ElectrumBlockchain::from(client)
//! )?;
//!
//! wallet.sync(noop_progress(), None)?;
//!
//! println!("Descriptor balance: {} SAT", wallet.get_balance()?);
//!
//! Ok(())
//! }
//! ``` //! ```
#![cfg_attr(
feature = "electrum",
doc = r##"
## Sync the balance of a descriptor
### Example
```no_run
use bdk::Wallet;
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
use bdk::electrum_client::Client;
fn main() -> Result<(), bdk::Error> {
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
let wallet = Wallet::new(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
bitcoin::Network::Testnet,
MemoryDatabase::default(),
ElectrumBlockchain::from(client)
)?;
wallet.sync(noop_progress(), None)?;
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
Ok(())
}
```
"##
)]
//! //!
//! ## Generate a few addresses //! ## Generate a few addresses
//! //!
@@ -97,58 +97,65 @@
//! Ok(()) //! Ok(())
//! } //! }
//! ``` //! ```
//! #![cfg_attr(
//! ## Create a transaction feature = "electrum",
//! doc = r##"
//! ### Example ## Create a transaction
//! ```ignore
//! use base64::decode; ### Example
//! use bdk::{FeeRate, Wallet}; ```no_run
//! use bdk::database::MemoryDatabase; use base64::decode;
//! use bdk::blockchain::{noop_progress, ElectrumBlockchain};
//! use bdk::{FeeRate, Wallet};
//! use bdk::electrum_client::Client; use bdk::database::MemoryDatabase;
//! use bdk::blockchain::{noop_progress, ElectrumBlockchain};
//! use bitcoin::consensus::serialize; use bdk::electrum_client::Client;
//! use bdk::wallet::AddressIndex::New;
//! use bitcoin::consensus::serialize;
//! fn main() -> Result<(), bdk::Error> { use bdk::wallet::AddressIndex::New;
//! let client = Client::new("ssl://electrum.blockstream.info:60002")?;
//! let wallet = Wallet::new( fn main() -> Result<(), bdk::Error> {
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", let client = Client::new("ssl://electrum.blockstream.info:60002")?;
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), let wallet = Wallet::new(
//! bitcoin::Network::Testnet, "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
//! MemoryDatabase::default(), Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
//! ElectrumBlockchain::from(client) bitcoin::Network::Testnet,
//! )?; MemoryDatabase::default(),
//! ElectrumBlockchain::from(client)
//! wallet.sync(noop_progress(), None)?; )?;
//!
//! let send_to = wallet.get_address(New)?; wallet.sync(noop_progress(), None)?;
//! let (psbt, details) = wallet.build_tx()
//! .add_recipient(send_to.script_pubkey(), 50_000) let send_to = wallet.get_address(New)?;
//! .enable_rbf() let (psbt, details) = {
//! .do_not_spend_change() let mut builder = wallet.build_tx();
//! .fee_rate(FeeRate::from_sat_per_vb(5.0)) builder
//! .finish()?; .add_recipient(send_to.script_pubkey(), 50_000)
//! .enable_rbf()
//! println!("Transaction details: {:#?}", details); .do_not_spend_change()
//! println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt))); .fee_rate(FeeRate::from_sat_per_vb(5.0));
//! builder.finish()?
//! Ok(()) };
//! }
//! ``` println!("Transaction details: {:#?}", details);
println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt)));
Ok(())
}
```
"##
)]
//! //!
//! ## Sign a transaction //! ## Sign a transaction
//! //!
//! ### Example //! ### Example
//! ```ignore //! ```no_run
//! use base64::decode; //! use base64::decode;
//! use bdk::{Wallet};
//! use bdk::database::MemoryDatabase;
//!
//! use bitcoin::consensus::deserialize; //! use bitcoin::consensus::deserialize;
//! //!
//! use bdk::{Wallet, SignOptions};
//! use bdk::database::MemoryDatabase;
//!
//! fn main() -> Result<(), bdk::Error> { //! fn main() -> Result<(), bdk::Error> {
//! let wallet = Wallet::new_offline( //! let wallet = Wallet::new_offline(
//! "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", //! "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
@@ -158,9 +165,9 @@
//! )?; //! )?;
//! //!
//! let psbt = "..."; //! let psbt = "...";
//! let psbt = deserialize(&base64::decode(psbt).unwrap())?; //! let mut psbt = deserialize(&base64::decode(psbt).unwrap())?;
//! //!
//! let (signed_psbt, finalized) = wallet.sign(psbt, None)?; //! let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
//! //!
//! Ok(()) //! Ok(())
//! } //! }
@@ -227,16 +234,10 @@ pub extern crate sled;
#[allow(unused_imports)] #[allow(unused_imports)]
#[cfg(test)] #[cfg(test)]
#[macro_use]
extern crate testutils;
#[allow(unused_imports)] #[allow(unused_imports)]
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]
extern crate testutils_macros; pub extern crate serial_test;
#[allow(unused_imports)]
#[cfg(test)]
#[macro_use]
extern crate serial_test;
#[macro_use] #[macro_use]
pub(crate) mod error; pub(crate) mod error;
@@ -256,6 +257,7 @@ pub use error::Error;
pub use types::*; pub use types::*;
pub use wallet::address_validator; pub use wallet::address_validator;
pub use wallet::signer; pub use wallet::signer;
pub use wallet::signer::SignOptions;
pub use wallet::tx_builder::TxBuilder; pub use wallet::tx_builder::TxBuilder;
pub use wallet::Wallet; pub use wallet::Wallet;
@@ -263,3 +265,10 @@ pub use wallet::Wallet;
pub fn version() -> &'static str { pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION", "unknown") env!("CARGO_PKG_VERSION", "unknown")
} }
// We should consider putting this under a feature flag but we need the macro in doctets so we need
// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed.
//
// Stuff in here is too rough to document atm
#[doc(hidden)]
pub mod testutils;

View File

@@ -9,14 +9,15 @@
// You may not use this file except in accordance with one or both of these // You may not use this file except in accordance with one or both of these
// licenses. // licenses.
use bitcoin::util::psbt::PartiallySignedTransaction as PSBT; use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use bitcoin::TxOut; use bitcoin::TxOut;
pub trait PsbtUtils { pub trait PsbtUtils {
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>; fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
} }
impl PsbtUtils for PSBT { impl PsbtUtils for Psbt {
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> { fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
let tx = &self.global.unsigned_tx; let tx = &self.global.unsigned_tx;
@@ -37,3 +38,85 @@ impl PsbtUtils for PSBT {
} }
} }
} }
#[cfg(test)]
mod test {
use crate::bitcoin::consensus::deserialize;
use crate::bitcoin::TxIn;
use crate::psbt::Psbt;
use crate::wallet::test::{get_funded_wallet, get_test_wpkh};
use crate::wallet::AddressIndex;
use crate::SignOptions;
// from bip 174
const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA";
#[test]
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_psbt_input_legacy() {
let psbt_bip: Psbt = deserialize(&base64::decode(PSBT_STR).unwrap()).unwrap();
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
psbt.inputs.push(psbt_bip.inputs[0].clone());
let options = SignOptions {
trust_witness_utxo: true,
..Default::default()
};
let _ = wallet.sign(&mut psbt, options).unwrap();
}
#[test]
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_psbt_input_segwit() {
let psbt_bip: Psbt = deserialize(&base64::decode(PSBT_STR).unwrap()).unwrap();
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
psbt.inputs.push(psbt_bip.inputs[1].clone());
let options = SignOptions {
trust_witness_utxo: true,
..Default::default()
};
let _ = wallet.sign(&mut psbt, options).unwrap();
}
#[test]
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_tx_input() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
psbt.global.unsigned_tx.input.push(TxIn::default());
let options = SignOptions {
trust_witness_utxo: true,
..Default::default()
};
let _ = wallet.sign(&mut psbt, options).unwrap();
}
#[test]
fn test_psbt_sign_with_finalized() {
let psbt_bip: Psbt = deserialize(&base64::decode(PSBT_STR).unwrap()).unwrap();
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(send_to.script_pubkey(), 10_000);
let (mut psbt, _) = builder.finish().unwrap();
// add a finalized input
psbt.inputs.push(psbt_bip.inputs[0].clone());
psbt.global
.unsigned_tx
.input
.push(psbt_bip.global.unsigned_tx.input[0].clone());
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
}
}

View File

@@ -0,0 +1,830 @@
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};
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
use core::str::FromStr;
pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use std::collections::HashMap;
use std::env;
use std::ops::Deref;
use std::path::PathBuf;
use std::time::Duration;
pub struct TestClient {
client: RpcClient,
electrum: ElectrumClient,
}
impl TestClient {
pub fn new(rpc_host_and_wallet: String, rpc_wallet_name: String) -> Self {
let client = RpcClient::new(
format!("http://{}/wallet/{}", rpc_host_and_wallet, rpc_wallet_name),
get_auth(),
)
.unwrap();
let electrum = ElectrumClient::new(&get_electrum_url()).unwrap();
TestClient { client, electrum }
}
fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) {
// wait for electrs to index the tx
exponential_backoff_poll(|| {
trace!("wait_for_tx {}", txid);
self.electrum
.script_get_history(monitor_script)
.unwrap()
.iter()
.position(|entry| entry.tx_hash == txid)
});
}
fn wait_for_block(&mut self, min_height: usize) {
self.electrum.block_headers_subscribe().unwrap();
loop {
let header = exponential_backoff_poll(|| {
self.electrum.ping().unwrap();
self.electrum.block_headers_pop().unwrap()
});
if header.height >= min_height {
break;
}
}
}
pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid {
assert!(
!meta_tx.output.is_empty(),
"can't create a transaction with no outputs"
);
let mut map = HashMap::new();
let mut required_balance = 0;
for out in &meta_tx.output {
required_balance += out.value;
map.insert(out.to_address.clone(), Amount::from_sat(out.value));
}
if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
}
// FIXME: core can't create a tx with two outputs to the same address
let tx = self
.create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
.unwrap();
let tx = self.fund_raw_transaction(tx, None, None).unwrap();
let mut tx: Transaction = deserialize(&tx.hex).unwrap();
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;
}
}
let tx = self
.sign_raw_transaction_with_wallet(&serialize(&tx), None, None)
.unwrap();
// broadcast through electrum so that it caches the tx immediately
let txid = self
.electrum
.transaction_broadcast(&deserialize(&tx.hex).unwrap())
.unwrap();
if let Some(num) = meta_tx.min_confirmations {
self.generate(num, None);
}
let monitor_script = Address::from_str(&meta_tx.output[0].to_address)
.unwrap()
.script_pubkey();
self.wait_for_tx(txid, &monitor_script);
debug!("Sent tx: {}", txid);
txid
}
pub fn bump_fee(&mut self, txid: &Txid) -> Txid {
let tx = self.get_raw_transaction_info(txid, None).unwrap();
assert!(
tx.confirmations.is_none(),
"Can't bump tx {} because it's already confirmed",
txid
);
let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap();
let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap();
let monitor_script =
tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey();
self.wait_for_tx(new_txid, &monitor_script);
debug!("Bumped {}, new txid {}", txid, new_txid);
new_txid
}
pub fn generate_manually(&mut self, txs: Vec<Transaction>) -> String {
use bitcoin::blockdata::block::{Block, BlockHeader};
use bitcoin::blockdata::script::Builder;
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
let block_template: serde_json::Value = self
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
.unwrap();
trace!("getblocktemplate: {:#?}", block_template);
let header = BlockHeader {
version: block_template["version"].as_i64().unwrap() as i32,
prev_blockhash: BlockHash::from_hex(
block_template["previousblockhash"].as_str().unwrap(),
)
.unwrap(),
merkle_root: TxMerkleNode::default(),
time: block_template["curtime"].as_u64().unwrap() as u32,
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
nonce: 0,
};
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();
// burn block subsidy and fees, not a big deal
let mut coinbase_tx = Transaction {
version: 1,
lock_time: 0,
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: Builder::new().push_int(height).into_script(),
sequence: 0xFFFFFFFF,
witness: vec![witness_reserved_value],
}],
output: vec![],
};
let mut txdata = vec![coinbase_tx.clone()];
txdata.extend_from_slice(&txs);
let mut block = Block { header, txdata };
let witness_root = block.witness_root();
let witness_commitment =
Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]);
// now update and replace the coinbase tx
let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
coinbase_tx.output.push(TxOut {
value: 0,
script_pubkey: coinbase_witness_commitment_script.into(),
});
block.txdata[0] = coinbase_tx;
// set merkle root
let merkle_root = block.merkle_root();
block.header.merkle_root = merkle_root;
assert!(block.check_merkle_root());
assert!(block.check_witness_commitment());
// now do PoW :)
let target = block.header.target();
while block.header.validate_pow(&target).is_err() {
block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces
}
let block_hex: String = serialize(&block).to_hex();
debug!("generated block hex: {}", block_hex);
self.electrum.block_headers_subscribe().unwrap();
let submit_result: serde_json::Value =
self.call("submitblock", &[block_hex.into()]).unwrap();
debug!("submitblock: {:?}", submit_result);
assert!(
submit_result.is_null(),
"submitblock error: {:?}",
submit_result.as_str()
);
self.wait_for_block(height as usize);
block.header.block_hash().to_hex()
}
pub fn generate(&mut self, num_blocks: u64, address: Option<Address>) {
let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap());
let hashes = self.generate_to_address(num_blocks, &address).unwrap();
let best_hash = hashes.last().unwrap();
let height = self.get_block_info(best_hash).unwrap().height;
self.wait_for_block(height);
debug!("Generated blocks to new height {}", height);
}
pub fn invalidate(&mut self, num_blocks: u64) {
self.electrum.block_headers_subscribe().unwrap();
let best_hash = self.get_best_block_hash().unwrap();
let initial_height = self.get_block_info(&best_hash).unwrap().height;
let mut to_invalidate = best_hash;
for i in 1..=num_blocks {
trace!(
"Invalidating block {}/{} ({})",
i,
num_blocks,
to_invalidate
);
self.invalidate_block(&to_invalidate).unwrap();
to_invalidate = self.get_best_block_hash().unwrap();
}
self.wait_for_block(initial_height - num_blocks as usize);
debug!(
"Invalidated {} blocks to new height of {}",
num_blocks,
initial_height - num_blocks as usize
);
}
pub fn reorg(&mut self, num_blocks: u64) {
self.invalidate(num_blocks);
self.generate(num_blocks, None);
}
pub fn get_node_address(&self, address_type: Option<AddressType>) -> Address {
Address::from_str(
&self
.get_new_address(None, address_type)
.unwrap()
.to_string(),
)
.unwrap()
}
}
pub fn get_electrum_url() -> String {
env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string())
}
impl Deref for TestClient {
type Target = RpcClient;
fn deref(&self) -> &Self::Target {
&self.client
}
}
impl Default for TestClient {
fn default() -> Self {
let rpc_host_and_port =
env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string());
Self::new(rpc_host_and_port, wallet)
}
}
fn exponential_backoff_poll<T, F>(mut poll: F) -> T
where
F: FnMut() -> Option<T>,
{
let mut delay = Duration::from_millis(64);
loop {
match poll() {
Some(data) => break data,
None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
None => {}
}
std::thread::sleep(delay);
}
}
// TODO: we currently only support env vars, we could also parse a toml file
fn get_auth() -> Auth {
match env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) {
Ok("USER_PASS") => Auth::UserPass(
env::var("BDK_RPC_USER").unwrap(),
env::var("BDK_RPC_PASS").unwrap(),
),
_ => Auth::CookieFile(PathBuf::from(
env::var("BDK_RPC_COOKIEFILE")
.unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()),
)),
}
}
/// This macro runs blockchain tests against a `Blockchain` implementation. It requires access to a
/// Bitcoin core wallet via RPC. At the moment you have to dig into the code yourself and look at
/// the setup required to run the tests yourself.
#[macro_export]
macro_rules! bdk_blockchain_tests {
(
fn test_instance() -> $blockchain:ty $block:block) => {
#[cfg(test)]
mod bdk_blockchain_tests {
use $crate::bitcoin::Network;
use $crate::testutils::blockchain_tests::TestClient;
use $crate::blockchain::noop_progress;
use $crate::database::MemoryDatabase;
use $crate::types::KeychainKind;
use $crate::{Wallet, FeeRate};
use $crate::wallet::AddressIndex::New;
use $crate::testutils;
use $crate::serial_test::serial;
use super::*;
fn get_blockchain() -> $blockchain {
$block
}
fn get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<$blockchain, MemoryDatabase> {
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap()
}
fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option<String>), TestClient) {
let _ = env_logger::try_init();
let descriptors = testutils! {
@descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
};
let test_client = TestClient::default();
let wallet = get_wallet_from_descriptors(&descriptors);
(wallet, descriptors, test_client)
}
#[test]
#[serial]
fn test_sync_simple() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let tx = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
};
println!("{:?}", tx);
let txid = test_client.receive(tx);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
}
#[test]
#[serial]
fn test_sync_stop_gap_20() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 5) => 50_000 )
});
test_client.receive(testutils! {
@tx ( (@external descriptors, 25) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
}
#[test]
#[serial]
fn test_sync_before_and_after_receive() {
let (wallet, descriptors, mut test_client) = init_single_sig();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
}
#[test]
#[serial]
fn test_sync_multiple_outputs_same_tx() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 105_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 3);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 105_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
}
#[test]
#[serial]
fn test_sync_receive_multi() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
test_client.receive(testutils! {
@tx ( (@external descriptors, 5) => 25_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
assert_eq!(wallet.list_unspent().unwrap().len(), 2);
}
#[test]
#[serial]
fn test_sync_address_reuse() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 25_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
}
#[test]
#[serial]
fn test_sync_receive_rbf_replaced() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
let new_txid = test_client.bump_fee(&txid);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, new_txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
}
// FIXME: I would like this to be cfg_attr(not(feature = "test-esplora"), ignore) but it
// doesn't work for some reason.
#[cfg(not(feature = "esplora"))]
#[test]
#[serial]
fn test_sync_reorg_block() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert!(list_tx_item.height.is_some());
// Invalidate 1 block
test_client.invalidate(1);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.height, None);
}
#[test]
#[serial]
fn test_sync_after_send() {
let (wallet, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
let tx = psbt.extract_tx();
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
wallet.broadcast(tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
}
#[test]
#[serial]
fn test_sync_outgoing_from_scratch() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let received_txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received);
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors);
wallet.sync(noop_progress(), None).unwrap();
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let received = tx_map.get(&received_txid).unwrap();
assert_eq!(received.received, 50_000);
assert_eq!(received.sent, 0);
let sent = tx_map.get(&sent_txid).unwrap();
assert_eq!(sent.received, details.received);
assert_eq!(sent.sent, details.sent);
assert_eq!(sent.fees, details.fees);
}
#[test]
#[serial]
fn test_sync_long_change_chain() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut total_sent = 0;
for _ in 0..5 {
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 5_000);
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
total_sent += 5_000 + details.fees;
}
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
}
#[test]
#[serial]
fn test_sync_bump_fee() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000);
assert_eq!(wallet.get_balance().unwrap(), details.received);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
let (mut new_psbt, new_details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000);
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
assert!(new_details.fees > details.fees);
}
#[test]
#[serial]
fn test_sync_bump_fee_remove_change() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees);
assert_eq!(wallet.get_balance().unwrap(), details.received);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
let (mut new_psbt, new_details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
assert_eq!(new_details.received, 0);
assert!(new_details.fees > details.fees);
}
#[test]
#[serial]
fn test_sync_bump_fee_add_input() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
assert_eq!(details.received, 1_000 - details.fees);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
let (mut new_psbt, new_details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(new_details.sent, 75_000);
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
}
#[test]
#[serial]
fn test_sync_bump_fee_add_input_no_change() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
assert_eq!(details.received, 1_000 - details.fees);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
let (mut new_psbt, new_details) = builder.finish().unwrap();
println!("{:#?}", new_details);
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(new_details.sent, 75_000);
assert_eq!(wallet.get_balance().unwrap(), 0);
assert_eq!(new_details.received, 0);
}
#[test]
#[serial]
fn test_sync_receive_coinbase() {
let (wallet, _, mut test_client) = init_single_sig();
let wallet_addr = wallet.get_address(New).unwrap().address;
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
test_client.generate(1, Some(wallet_addr));
wallet.sync(noop_progress(), None).unwrap();
assert!(wallet.get_balance().unwrap() > 0);
}
}
}
}

230
src/testutils/mod.rs Normal file
View File

@@ -0,0 +1,230 @@
// 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.
#![allow(missing_docs)]
#[cfg(feature = "test-blockchains")]
pub mod blockchain_tests;
use bitcoin::secp256k1::{Secp256k1, Verification};
use bitcoin::{Address, PublicKey};
use miniscript::descriptor::DescriptorPublicKey;
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
#[derive(Clone, Debug)]
pub struct TestIncomingOutput {
pub value: u64,
pub to_address: String,
}
impl TestIncomingOutput {
pub fn new(value: u64, to_address: Address) -> Self {
Self {
value,
to_address: to_address.to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct TestIncomingTx {
pub output: Vec<TestIncomingOutput>,
pub min_confirmations: Option<u64>,
pub locktime: Option<i64>,
pub replaceable: Option<bool>,
}
impl TestIncomingTx {
pub fn new(
output: Vec<TestIncomingOutput>,
min_confirmations: Option<u64>,
locktime: Option<i64>,
replaceable: Option<bool>,
) -> Self {
Self {
output,
min_confirmations,
locktime,
replaceable,
}
}
pub fn add_output(&mut self, output: TestIncomingOutput) {
self.output.push(output);
}
}
#[doc(hidden)]
pub trait TranslateDescriptor {
// derive and translate a `Descriptor<DescriptorPublicKey>` into a `Descriptor<PublicKey>`
fn derive_translated<C: Verification>(
&self,
secp: &Secp256k1<C>,
index: u32,
) -> Descriptor<PublicKey>;
}
impl TranslateDescriptor for Descriptor<DescriptorPublicKey> {
fn derive_translated<C: Verification>(
&self,
secp: &Secp256k1<C>,
index: u32,
) -> Descriptor<PublicKey> {
let translate = |key: &DescriptorPublicKey| -> PublicKey {
match key {
DescriptorPublicKey::XPub(xpub) => {
xpub.xkey
.derive_pub(secp, &xpub.derivation_path)
.expect("hardened derivation steps")
.public_key
}
DescriptorPublicKey::SinglePub(key) => key.key,
}
};
self.derive(index)
.translate_pk_infallible(|pk| translate(pk), |pkh| translate(pkh).to_pubkeyhash())
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! testutils {
( @external $descriptors:expr, $child:expr ) => ({
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::testutils::TranslateDescriptor;
let secp = Secp256k1::new();
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
});
( @internal $descriptors:expr, $child:expr ) => ({
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::testutils::TranslateDescriptor;
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.derive_translated(&secp, $child).address(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) });
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )? $( ( @confirmations $confirmations:expr ) )? $( ( @replaceable $replaceable:expr ) )? ) => ({
let outs = vec![$( $crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))),+];
let locktime = None::<i64>$(.or(Some($locktime)))?;
let min_confirmations = None::<u64>$(.or(Some($confirmations)))?;
let replaceable = None::<bool>$(.or(Some($replaceable)))?;
$crate::testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
});
( @literal $key:expr ) => ({
let key = $key.to_string();
(key, None::<String>, None::<String>)
});
( @generate_xprv $( $external_path:expr )? $( ,$internal_path:expr )? ) => ({
use rand::Rng;
let mut seed = [0u8; 32];
rand::thread_rng().fill(&mut seed[..]);
let key = bitcoin::util::bip32::ExtendedPrivKey::new_master(
bitcoin::Network::Testnet,
&seed,
);
let external_path = None::<String>$(.or(Some($external_path.to_string())))?;
let internal_path = None::<String>$(.or(Some($internal_path.to_string())))?;
(key.unwrap().to_string(), external_path, internal_path)
});
( @generate_wif ) => ({
use rand::Rng;
let mut key = [0u8; bitcoin::secp256k1::constants::SECRET_KEY_SIZE];
rand::thread_rng().fill(&mut key[..]);
(bitcoin::PrivateKey {
compressed: true,
network: bitcoin::Network::Testnet,
key: bitcoin::secp256k1::SecretKey::from_slice(&key).unwrap(),
}.to_string(), None::<String>, None::<String>)
});
( @keys ( $( $alias:expr => ( $( $key_type:tt )* ) ),+ ) ) => ({
let mut map = std::collections::HashMap::new();
$(
let alias: &str = $alias;
map.insert(alias, testutils!( $($key_type)* ));
)+
map
});
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )? $( ( @keys $( $keys:tt )* ) )* ) => ({
use std::str::FromStr;
use std::collections::HashMap;
use miniscript::descriptor::Descriptor;
use miniscript::TranslatePk;
#[allow(unused_assignments, unused_mut)]
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = 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 external = external.to_string();
let internal = None::<String>$(.or({
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
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())
}))?;
(external, internal)
})
}

View File

@@ -146,7 +146,7 @@ mod test {
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
wallet.add_address_validator(Arc::new(TestValidator)); wallet.add_address_validator(Arc::new(TestValidator));
let addr = testutils!(@external descriptors, 10); let addr = crate::testutils!(@external descriptors, 10);
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000); builder.add_recipient(addr.script_pubkey(), 25_000);
builder.finish().unwrap(); builder.finish().unwrap();

View File

@@ -46,17 +46,25 @@
//! let mut selected_amount = 0; //! let mut selected_amount = 0;
//! let mut additional_weight = 0; //! let mut additional_weight = 0;
//! let all_utxos_selected = required_utxos //! let all_utxos_selected = required_utxos
//! .into_iter().chain(optional_utxos) //! .into_iter()
//! .scan((&mut selected_amount, &mut additional_weight), |(selected_amount, additional_weight), weighted_utxo| { //! .chain(optional_utxos)
//! **selected_amount += weighted_utxo.utxo.txout().value; //! .scan(
//! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight; //! (&mut selected_amount, &mut additional_weight),
//! Some(weighted_utxo.utxo) //! |(selected_amount, additional_weight), weighted_utxo| {
//! }) //! **selected_amount += weighted_utxo.utxo.txout().value;
//! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight;
//! Some(weighted_utxo.utxo)
//! },
//! )
//! .collect::<Vec<_>>(); //! .collect::<Vec<_>>();
//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0; //! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0;
//! let amount_needed_with_fees = (fee_amount + additional_fees).ceil() as u64 + amount_needed; //! let amount_needed_with_fees =
//! if amount_needed_with_fees > selected_amount { //! (fee_amount + additional_fees).ceil() as u64 + amount_needed;
//! return Err(bdk::Error::InsufficientFunds{ needed: amount_needed_with_fees, available: selected_amount }); //! if amount_needed_with_fees > selected_amount {
//! return Err(bdk::Error::InsufficientFunds {
//! needed: amount_needed_with_fees,
//! available: selected_amount,
//! });
//! } //! }
//! //!
//! Ok(CoinSelectionResult { //! Ok(CoinSelectionResult {
@@ -72,8 +80,7 @@
//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); //! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
//! let (psbt, details) = { //! let (psbt, details) = {
//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything); //! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything);
//! builder //! builder.add_recipient(to_address.script_pubkey(), 50_000);
//! .add_recipient(to_address.script_pubkey(), 50_000);
//! builder.finish()? //! builder.finish()?
//! }; //! };
//! //!
@@ -255,8 +262,8 @@ impl OutputGroup {
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee.ceil() as i64; let effective_value = weighted_utxo.utxo.txout().value as i64 - fee.ceil() as i64;
OutputGroup { OutputGroup {
weighted_utxo, weighted_utxo,
effective_value,
fee, fee,
effective_value,
} }
} }
} }
@@ -654,7 +661,7 @@ mod test {
assert_eq!(result.selected.len(), 3); assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount(), 300_010); assert_eq!(result.selected_amount(), 300_010);
assert_eq!(result.fee_amount, 254.0); assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
} }
#[test] #[test]
@@ -675,7 +682,7 @@ mod test {
assert_eq!(result.selected.len(), 3); assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount(), 300_010); assert_eq!(result.selected_amount(), 300_010);
assert_eq!(result.fee_amount, 254.0); assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
} }
#[test] #[test]
@@ -696,7 +703,7 @@ mod test {
assert_eq!(result.selected.len(), 1); assert_eq!(result.selected.len(), 1);
assert_eq!(result.selected_amount(), 200_000); assert_eq!(result.selected_amount(), 200_000);
assert_eq!(result.fee_amount, 118.0); assert!((result.fee_amount - 118.0).abs() < f32::EPSILON);
} }
#[test] #[test]
@@ -756,7 +763,7 @@ mod test {
assert_eq!(result.selected.len(), 3); assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount(), 300_000); assert_eq!(result.selected_amount(), 300_000);
assert_eq!(result.fee_amount, 254.0); assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
} }
#[test] #[test]
@@ -777,7 +784,7 @@ mod test {
assert_eq!(result.selected.len(), 3); assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount(), 300_010); assert_eq!(result.selected_amount(), 300_010);
assert_eq!(result.fee_amount, 254.0); assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
} }
#[test] #[test]
@@ -798,7 +805,7 @@ mod test {
assert_eq!(result.selected.len(), 3); assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount(), 300010); assert_eq!(result.selected_amount(), 300010);
assert_eq!(result.fee_amount, 254.0); assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
} }
#[test] #[test]
@@ -968,7 +975,7 @@ mod test {
cost_of_change, cost_of_change,
) )
.unwrap(); .unwrap();
assert_eq!(result.fee_amount, 186.0); assert!((result.fee_amount - 186.0).abs() < f32::EPSILON);
assert_eq!(result.selected_amount(), 100_000); assert_eq!(result.selected_amount(), 100_000);
} }
@@ -1031,9 +1038,8 @@ mod test {
); );
assert!(result.selected_amount() > target_amount); assert!(result.selected_amount() > target_amount);
assert_eq!( assert!(
result.fee_amount, (result.fee_amount - (50.0 + result.selected.len() as f32 * 68.0)).abs() < f32::EPSILON
50.0 + result.selected.len() as f32 * 68.0
); );
} }
} }

View File

@@ -16,6 +16,7 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::fmt;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::sync::Arc; use std::sync::Arc;
@@ -23,9 +24,9 @@ use bitcoin::secp256k1::Secp256k1;
use bitcoin::consensus::encode::serialize; use bitcoin::consensus::encode::serialize;
use bitcoin::util::base58; use bitcoin::util::base58;
use bitcoin::util::psbt::raw::Key as PSBTKey; use bitcoin::util::psbt::raw::Key as PsbtKey;
use bitcoin::util::psbt::Input; use bitcoin::util::psbt::Input;
use bitcoin::util::psbt::PartiallySignedTransaction as PSBT; use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use bitcoin::{Address, Network, OutPoint, Script, SigHashType, Transaction, TxOut, Txid}; use bitcoin::{Address, Network, OutPoint, Script, SigHashType, Transaction, TxOut, Txid};
use miniscript::descriptor::DescriptorTrait; use miniscript::descriptor::DescriptorTrait;
@@ -46,13 +47,14 @@ pub use utils::IsDust;
use address_validator::AddressValidator; use address_validator::AddressValidator;
use coin_selection::DefaultCoinSelectionAlgorithm; use coin_selection::DefaultCoinSelectionAlgorithm;
use signer::{Signer, SignerOrdering, SignersContainer}; use signer::{SignOptions, Signer, SignerOrdering, SignersContainer};
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams}; use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx, DUST_LIMIT_SATOSHI}; use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx, DUST_LIMIT_SATOSHI};
use crate::blockchain::{Blockchain, Progress}; use crate::blockchain::{Blockchain, Progress};
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::descriptor::derived::AsDerived; use crate::descriptor::derived::AsDerived;
use crate::descriptor::policy::BuildSatisfaction;
use crate::descriptor::{ use crate::descriptor::{
get_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DerivedDescriptorMeta, get_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DerivedDescriptorMeta,
DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor,
@@ -60,6 +62,7 @@ use crate::descriptor::{
}; };
use crate::error::Error; use crate::error::Error;
use crate::psbt::PsbtUtils; use crate::psbt::PsbtUtils;
use crate::signer::SignerError;
use crate::types::*; use crate::types::*;
const CACHE_ADDR_BATCH_SIZE: u32 = 100; const CACHE_ADDR_BATCH_SIZE: u32 = 100;
@@ -190,28 +193,59 @@ pub enum AddressIndex {
/// then the returned address and subsequent addresses returned by calls to `AddressIndex::New` /// then the returned address and subsequent addresses returned by calls to `AddressIndex::New`
/// and `AddressIndex::LastUsed` may have already been used. Also if the index is reset to a /// and `AddressIndex::LastUsed` may have already been used. Also if the index is reset to a
/// value earlier than the [`crate::blockchain::Blockchain`] stop_gap (default is 20) then a /// value earlier than the [`crate::blockchain::Blockchain`] stop_gap (default is 20) then a
/// larger stop_gap should be used to monitor for all possibly used addresses. /// larger stop_gap should be used to monitor for all possibly used addresses.
Reset(u32), Reset(u32),
} }
/// A derived address and the index it was found at
/// For convenience this automatically derefs to `Address`
#[derive(Debug, PartialEq)]
pub struct AddressInfo {
/// Child index of this address
pub index: u32,
/// Address
pub address: Address,
}
impl Deref for AddressInfo {
type Target = Address;
fn deref(&self) -> &Self::Target {
&self.address
}
}
impl fmt::Display for AddressInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.address)
}
}
// offline actions, always available // offline actions, always available
impl<B, D> Wallet<B, D> impl<B, D> Wallet<B, D>
where where
D: BatchDatabase, D: BatchDatabase,
{ {
// Return a newly derived address using the external descriptor // Return a newly derived address using the external descriptor
fn get_new_address(&self) -> Result<Address, Error> { fn get_new_address(&self) -> Result<AddressInfo, Error> {
let incremented_index = self.fetch_and_increment_index(KeychainKind::External)?; let incremented_index = self.fetch_and_increment_index(KeychainKind::External)?;
self.descriptor let address_result = self
.descriptor
.as_derived(incremented_index, &self.secp) .as_derived(incremented_index, &self.secp)
.address(self.network) .address(self.network);
address_result
.map(|address| AddressInfo {
address,
index: incremented_index,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm) .map_err(|_| Error::ScriptDoesntHaveAddressForm)
} }
// Return the the last previously derived address if it has not been used in a received // Return the the last previously derived address if it has not been used in a received
// transaction. Otherwise return a new address using [`Wallet::get_new_address`]. // transaction. Otherwise return a new address using [`Wallet::get_new_address`].
fn get_unused_address(&self) -> Result<Address, Error> { fn get_unused_address(&self) -> Result<AddressInfo, Error> {
let current_index = self.fetch_index(KeychainKind::External)?; let current_index = self.fetch_index(KeychainKind::External)?;
let derived_key = self.descriptor.as_derived(current_index, &self.secp); let derived_key = self.descriptor.as_derived(current_index, &self.secp);
@@ -230,33 +264,39 @@ where
} else { } else {
derived_key derived_key
.address(self.network) .address(self.network)
.map(|address| AddressInfo {
address,
index: current_index,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm) .map_err(|_| Error::ScriptDoesntHaveAddressForm)
} }
} }
// Return derived address for the external descriptor at a specific index // Return derived address for the external descriptor at a specific index
fn peek_address(&self, index: u32) -> Result<Address, Error> { fn peek_address(&self, index: u32) -> Result<AddressInfo, Error> {
self.descriptor self.descriptor
.as_derived(index, &self.secp) .as_derived(index, &self.secp)
.address(self.network) .address(self.network)
.map(|address| AddressInfo { address, index })
.map_err(|_| Error::ScriptDoesntHaveAddressForm) .map_err(|_| Error::ScriptDoesntHaveAddressForm)
} }
// Return derived address for the external descriptor at a specific index and reset current // Return derived address for the external descriptor at a specific index and reset current
// address index // address index
fn reset_address(&self, index: u32) -> Result<Address, Error> { fn reset_address(&self, index: u32) -> Result<AddressInfo, Error> {
self.set_index(KeychainKind::External, index)?; self.set_index(KeychainKind::External, index)?;
self.descriptor self.descriptor
.as_derived(index, &self.secp) .as_derived(index, &self.secp)
.address(self.network) .address(self.network)
.map(|address| AddressInfo { address, index })
.map_err(|_| Error::ScriptDoesntHaveAddressForm) .map_err(|_| Error::ScriptDoesntHaveAddressForm)
} }
/// Return a derived address using the external descriptor, see [`AddressIndex`] for /// Return a derived address using the external descriptor, see [`AddressIndex`] for
/// available address index selection strategies. If none of the keys in the descriptor are derivable /// available address index selection strategies. If none of the keys in the descriptor are derivable
/// (ie. does not end with /*) then the same address will always be returned for any [`AddressIndex`]. /// (ie. does not end with /*) then the same address will always be returned for any [`AddressIndex`].
pub fn get_address(&self, address_index: AddressIndex) -> Result<Address, Error> { pub fn get_address(&self, address_index: AddressIndex) -> Result<AddressInfo, Error> {
match address_index { match address_index {
AddressIndex::New => self.get_new_address(), AddressIndex::New => self.get_new_address(),
AddressIndex::LastUnused => self.get_unused_address(), AddressIndex::LastUnused => self.get_unused_address(),
@@ -369,17 +409,17 @@ where
&self, &self,
coin_selection: Cs, coin_selection: Cs,
params: TxParams, params: TxParams,
) -> Result<(PSBT, TransactionDetails), Error> { ) -> Result<(Psbt, TransactionDetails), Error> {
let external_policy = self let external_policy = self
.descriptor .descriptor
.extract_policy(&self.signers, &self.secp)? .extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)?
.unwrap(); .unwrap();
let internal_policy = self let internal_policy = self
.change_descriptor .change_descriptor
.as_ref() .as_ref()
.map(|desc| { .map(|desc| {
Ok::<_, Error>( Ok::<_, Error>(
desc.extract_policy(&self.change_signers, &self.secp)? desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)?
.unwrap(), .unwrap(),
) )
}) })
@@ -422,9 +462,8 @@ where
}) })
.transpose()?; .transpose()?;
let requirements = external_requirements let requirements =
.clone() external_requirements.merge(&internal_requirements.unwrap_or_default())?;
.merge(&internal_requirements.unwrap_or_default())?;
debug!("Policy requirements: {:?}", requirements); debug!("Policy requirements: {:?}", requirements);
let version = match params.version { let version = match params.version {
@@ -698,24 +737,24 @@ where
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let wallet = doctest_wallet!(); /// # let wallet = doctest_wallet!();
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
/// let (psbt, _) = { /// let (mut psbt, _) = {
/// let mut builder = wallet.build_tx(); /// let mut builder = wallet.build_tx();
/// builder /// builder
/// .add_recipient(to_address.script_pubkey(), 50_000) /// .add_recipient(to_address.script_pubkey(), 50_000)
/// .enable_rbf(); /// .enable_rbf();
/// builder.finish()? /// builder.finish()?
/// }; /// };
/// let (psbt, _) = wallet.sign(psbt, None)?; /// let _ = wallet.sign(&mut psbt, SignOptions::default())?;
/// let tx = psbt.extract_tx(); /// let tx = psbt.extract_tx();
/// // broadcast tx but it's taking too long to confirm so we want to bump the fee /// // broadcast tx but it's taking too long to confirm so we want to bump the fee
/// let (psbt, _) = { /// let (mut psbt, _) = {
/// let mut builder = wallet.build_fee_bump(tx.txid())?; /// let mut builder = wallet.build_fee_bump(tx.txid())?;
/// builder /// builder
/// .fee_rate(FeeRate::from_sat_per_vb(5.0)); /// .fee_rate(FeeRate::from_sat_per_vb(5.0));
/// builder.finish()? /// builder.finish()?
/// }; /// };
/// ///
/// let (psbt, _) = wallet.sign(psbt, None)?; /// let _ = wallet.sign(&mut psbt, SignOptions::default())?;
/// let fee_bumped_tx = psbt.extract_tx(); /// let fee_bumped_tx = psbt.extract_tx();
/// // broadcast fee_bumped_tx to replace original /// // broadcast fee_bumped_tx to replace original
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(), bdk::Error>(())
@@ -832,6 +871,11 @@ where
/// Sign a transaction with all the wallet's signers, in the order specified by every signer's /// Sign a transaction with all the wallet's signers, in the order specified by every signer's
/// [`SignerOrdering`] /// [`SignerOrdering`]
/// ///
/// The [`SignOptions`] can be used to tweak the behavior of the software signers, and the way
/// the transaction is finalized at the end. Note that it can't be guaranteed that *every*
/// signers will follow the options, but the "software signers" (WIF keys and `xprv`) defined
/// in this library will.
///
/// ## Example /// ## Example
/// ///
/// ``` /// ```
@@ -842,17 +886,40 @@ where
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let wallet = doctest_wallet!(); /// # let wallet = doctest_wallet!();
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
/// let (psbt, _) = { /// let (mut psbt, _) = {
/// let mut builder = wallet.build_tx(); /// let mut builder = wallet.build_tx();
/// builder.add_recipient(to_address.script_pubkey(), 50_000); /// builder.add_recipient(to_address.script_pubkey(), 50_000);
/// builder.finish()? /// builder.finish()?
/// }; /// };
/// let (signed_psbt, finalized) = wallet.sign(psbt, None)?; /// let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
/// assert!(finalized, "we should have signed all the inputs"); /// assert!(finalized, "we should have signed all the inputs");
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(), bdk::Error>(())
pub fn sign(&self, mut psbt: PSBT, assume_height: Option<u32>) -> Result<(PSBT, bool), Error> { pub fn sign(&self, psbt: &mut Psbt, sign_options: SignOptions) -> Result<bool, Error> {
// this helps us doing our job later // this helps us doing our job later
self.add_input_hd_keypaths(&mut psbt)?; self.add_input_hd_keypaths(psbt)?;
// If we aren't allowed to use `witness_utxo`, ensure that every input but finalized one
// has the `non_witness_utxo`
if !sign_options.trust_witness_utxo
&& psbt
.inputs
.iter()
.filter(|i| i.final_script_witness.is_none() && i.final_script_sig.is_none())
.any(|i| i.non_witness_utxo.is_none())
{
return Err(Error::Signer(signer::SignerError::MissingNonWitnessUtxo));
}
// If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input
// is using `SIGHASH_ALL`
if !sign_options.allow_all_sighashes
&& !psbt
.inputs
.iter()
.all(|i| i.sighash_type.is_none() || i.sighash_type == Some(SigHashType::All))
{
return Err(Error::Signer(signer::SignerError::NonStandardSighash));
}
for signer in self for signer in self
.signers .signers
@@ -861,28 +928,32 @@ where
.chain(self.change_signers.signers().iter()) .chain(self.change_signers.signers().iter())
{ {
if signer.sign_whole_tx() { if signer.sign_whole_tx() {
signer.sign(&mut psbt, None, &self.secp)?; signer.sign(psbt, None, &self.secp)?;
} else { } else {
for index in 0..psbt.inputs.len() { for index in 0..psbt.inputs.len() {
signer.sign(&mut psbt, Some(index), &self.secp)?; signer.sign(psbt, Some(index), &self.secp)?;
} }
} }
} }
// attempt to finalize // attempt to finalize
self.finalize_psbt(psbt, assume_height) self.finalize_psbt(psbt, sign_options)
} }
/// Return the spending policies for the wallet's descriptor /// Return the spending policies for the wallet's descriptor
pub fn policies(&self, keychain: KeychainKind) -> Result<Option<Policy>, Error> { pub fn policies(&self, keychain: KeychainKind) -> Result<Option<Policy>, Error> {
match (keychain, self.change_descriptor.as_ref()) { match (keychain, self.change_descriptor.as_ref()) {
(KeychainKind::External, _) => { (KeychainKind::External, _) => Ok(self.descriptor.extract_policy(
Ok(self.descriptor.extract_policy(&self.signers, &self.secp)?) &self.signers,
} BuildSatisfaction::None,
&self.secp,
)?),
(KeychainKind::Internal, None) => Ok(None), (KeychainKind::Internal, None) => Ok(None),
(KeychainKind::Internal, Some(desc)) => { (KeychainKind::Internal, Some(desc)) => Ok(desc.extract_policy(
Ok(desc.extract_policy(&self.change_signers, &self.secp)?) &self.change_signers,
} BuildSatisfaction::None,
&self.secp,
)?),
} }
} }
@@ -902,16 +973,17 @@ where
} }
/// Try to finalize a PSBT /// Try to finalize a PSBT
pub fn finalize_psbt( ///
&self, /// The [`SignOptions`] can be used to tweak the behavior of the finalizer.
mut psbt: PSBT, pub fn finalize_psbt(&self, psbt: &mut Psbt, sign_options: SignOptions) -> Result<bool, Error> {
assume_height: Option<u32>,
) -> Result<(PSBT, bool), Error> {
let tx = &psbt.global.unsigned_tx; let tx = &psbt.global.unsigned_tx;
let mut finished = true; let mut finished = true;
for (n, input) in tx.input.iter().enumerate() { for (n, input) in tx.input.iter().enumerate() {
let psbt_input = &psbt.inputs[n]; let psbt_input = &psbt
.inputs
.get(n)
.ok_or(Error::Signer(SignerError::InputIndexOutOfRange))?;
if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() { if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() {
continue; continue;
} }
@@ -922,7 +994,7 @@ where
.borrow() .borrow()
.get_tx(&input.previous_output.txid, false)? .get_tx(&input.previous_output.txid, false)?
.map(|tx| tx.height.unwrap_or(std::u32::MAX)); .map(|tx| tx.height.unwrap_or(std::u32::MAX));
let current_height = assume_height.or(self.current_height); let current_height = sign_options.assume_height.or(self.current_height);
debug!( debug!(
"Input #{} - {}, using `create_height` = {:?}, `current_height` = {:?}", "Input #{} - {}, using `create_height` = {:?}, `current_height` = {:?}",
@@ -979,7 +1051,7 @@ where
} }
} }
Ok((psbt, finished)) Ok(finished)
} }
/// Return the secp256k1 context used for all signing operations /// Return the secp256k1 context used for all signing operations
@@ -1143,11 +1215,11 @@ where
// must_spend <- manually selected utxos // must_spend <- manually selected utxos
// may_spend <- all other available utxos // may_spend <- all other available utxos
let mut may_spend = self.get_available_utxos()?; let mut may_spend = self.get_available_utxos()?;
may_spend.retain(|may_spend| { may_spend.retain(|may_spend| {
manually_selected !manually_selected
.iter() .iter()
.find(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint) .any(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint)
.is_none()
}); });
let mut must_spend = manually_selected; let mut must_spend = manually_selected;
@@ -1204,10 +1276,10 @@ where
tx: Transaction, tx: Transaction,
selected: Vec<Utxo>, selected: Vec<Utxo>,
params: TxParams, params: TxParams,
) -> Result<PSBT, Error> { ) -> Result<Psbt, Error> {
use bitcoin::util::psbt::serialize::Serialize; use bitcoin::util::psbt::serialize::Serialize;
let mut psbt = PSBT::from_unsigned_tx(tx)?; let mut psbt = Psbt::from_unsigned_tx(tx)?;
if params.add_global_xpubs { if params.add_global_xpubs {
let mut all_xpubs = self.descriptor.get_extended_keys()?; let mut all_xpubs = self.descriptor.get_extended_keys()?;
@@ -1218,7 +1290,7 @@ where
for xpub in all_xpubs { for xpub in all_xpubs {
let serialized_xpub = base58::from_check(&xpub.xkey.to_string()) let serialized_xpub = base58::from_check(&xpub.xkey.to_string())
.expect("Internal serialization error"); .expect("Internal serialization error");
let key = PSBTKey { let key = PsbtKey {
type_value: 0x01, type_value: 0x01,
key: serialized_xpub, key: serialized_xpub,
}; };
@@ -1253,28 +1325,23 @@ where
match utxo { match utxo {
Utxo::Local(utxo) => { Utxo::Local(utxo) => {
*psbt_input = match self.get_psbt_input( *psbt_input =
utxo, match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) {
params.sighash, Ok(psbt_input) => psbt_input,
params.force_non_witness_utxo, Err(e) => match e {
) { Error::UnknownUtxo => Input {
Ok(psbt_input) => psbt_input, sighash_type: params.sighash,
Err(e) => match e { ..Input::default()
Error::UnknownUtxo => Input { },
sighash_type: params.sighash, _ => return Err(e),
..Input::default()
}, },
_ => return Err(e), }
},
}
} }
Utxo::Foreign { Utxo::Foreign {
psbt_input: foreign_psbt_input, psbt_input: foreign_psbt_input,
outpoint, outpoint,
} => { } => {
if params.force_non_witness_utxo if !params.only_witness_utxo && foreign_psbt_input.non_witness_utxo.is_none() {
&& foreign_psbt_input.non_witness_utxo.is_none()
{
return Err(Error::Generic(format!( return Err(Error::Generic(format!(
"Missing non_witness_utxo on foreign utxo {}", "Missing non_witness_utxo on foreign utxo {}",
outpoint outpoint
@@ -1318,7 +1385,7 @@ where
&self, &self,
utxo: LocalUtxo, utxo: LocalUtxo,
sighash_type: Option<SigHashType>, sighash_type: Option<SigHashType>,
force_non_witness_utxo: bool, only_witness_utxo: bool,
) -> Result<Input, Error> { ) -> Result<Input, Error> {
// Try to find the prev_script in our db to figure out if this is internal or external, // Try to find the prev_script in our db to figure out if this is internal or external,
// and the derivation index // and the derivation index
@@ -1345,14 +1412,14 @@ where
if desc.is_witness() { if desc.is_witness() {
psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone());
} }
if !desc.is_witness() || force_non_witness_utxo { if !desc.is_witness() || !only_witness_utxo {
psbt_input.non_witness_utxo = Some(prev_tx); psbt_input.non_witness_utxo = Some(prev_tx);
} }
} }
Ok(psbt_input) Ok(psbt_input)
} }
fn add_input_hd_keypaths(&self, psbt: &mut PSBT) -> Result<(), Error> { fn add_input_hd_keypaths(&self, psbt: &mut Psbt) -> Result<(), Error> {
let mut input_utxos = Vec::with_capacity(psbt.inputs.len()); let mut input_utxos = Vec::with_capacity(psbt.inputs.len());
for n in 0..psbt.inputs.len() { for n in 0..psbt.inputs.len() {
input_utxos.push(psbt.get_utxo_for(n).clone()); input_utxos.push(psbt.get_utxo_for(n).clone());
@@ -1486,7 +1553,7 @@ where
} }
#[cfg(test)] #[cfg(test)]
mod test { pub(crate) mod test {
use std::str::FromStr; use std::str::FromStr;
use bitcoin::{util::psbt, Network}; use bitcoin::{util::psbt, Network};
@@ -1496,6 +1563,8 @@ mod test {
use crate::types::KeychainKind; use crate::types::KeychainKind;
use super::*; use super::*;
use crate::signer::{SignOptions, SignerError};
use crate::testutils;
use crate::wallet::AddressIndex::{LastUnused, New, Peek, Reset}; use crate::wallet::AddressIndex::{LastUnused, New, Peek, Reset};
#[test] #[test]
@@ -1633,7 +1702,7 @@ mod test {
.database .database
.borrow_mut() .borrow_mut()
.set_script_pubkey( .set_script_pubkey(
&bitcoin::Address::from_str(&tx_meta.output.iter().next().unwrap().to_address) &bitcoin::Address::from_str(&tx_meta.output.get(0).unwrap().to_address)
.unwrap() .unwrap()
.script_pubkey(), .script_pubkey(),
KeychainKind::External, KeychainKind::External,
@@ -2232,6 +2301,7 @@ mod test {
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder builder
.set_single_recipient(addr.script_pubkey()) .set_single_recipient(addr.script_pubkey())
.only_witness_utxo()
.drain_wallet(); .drain_wallet();
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
@@ -2250,20 +2320,18 @@ mod test {
.drain_wallet(); .drain_wallet();
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
assert!(psbt.inputs[0].non_witness_utxo.is_none());
assert!(psbt.inputs[0].witness_utxo.is_some()); assert!(psbt.inputs[0].witness_utxo.is_some());
} }
#[test] #[test]
fn test_create_tx_both_non_witness_utxo_and_witness_utxo() { fn test_create_tx_both_non_witness_utxo_and_witness_utxo_default() {
let (wallet, _, _) = let (wallet, _, _) =
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_address(New).unwrap(); let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder builder
.set_single_recipient(addr.script_pubkey()) .set_single_recipient(addr.script_pubkey())
.drain_wallet() .drain_wallet();
.force_non_witness_utxo();
let (psbt, _) = builder.finish().unwrap(); let (psbt, _) = builder.finish().unwrap();
assert!(psbt.inputs[0].non_witness_utxo.is_some()); assert!(psbt.inputs[0].non_witness_utxo.is_some());
@@ -2401,6 +2469,7 @@ mod test {
let (wallet1, _, _) = get_funded_wallet(get_test_wpkh()); let (wallet1, _, _) = get_funded_wallet(get_test_wpkh());
let (wallet2, _, _) = let (wallet2, _, _) =
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let utxo = wallet2.list_unspent().unwrap().remove(0); let utxo = wallet2.list_unspent().unwrap().remove(0);
let foreign_utxo_satisfaction = wallet2 let foreign_utxo_satisfaction = wallet2
@@ -2416,9 +2485,10 @@ mod test {
let mut builder = wallet1.build_tx(); let mut builder = wallet1.build_tx();
builder builder
.add_recipient(addr.script_pubkey(), 60_000) .add_recipient(addr.script_pubkey(), 60_000)
.only_witness_utxo()
.add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
.unwrap(); .unwrap();
let (psbt, details) = builder.finish().unwrap(); let (mut psbt, details) = builder.finish().unwrap();
assert_eq!( assert_eq!(
details.sent - details.received, details.sent - details.received,
@@ -2431,19 +2501,34 @@ mod test {
.unsigned_tx .unsigned_tx
.input .input
.iter() .iter()
.find(|input| input.previous_output == utxo.outpoint) .any(|input| input.previous_output == utxo.outpoint),
.is_some(),
"foreign_utxo should be in there" "foreign_utxo should be in there"
); );
let (psbt, finished) = wallet1.sign(psbt, None).unwrap(); let finished = wallet1
.sign(
&mut psbt,
SignOptions {
trust_witness_utxo: true,
..Default::default()
},
)
.unwrap();
assert!( assert!(
!finished, !finished,
"only one of the inputs should have been signed so far" "only one of the inputs should have been signed so far"
); );
let (_, finished) = wallet2.sign(psbt, None).unwrap(); let finished = wallet2
.sign(
&mut psbt,
SignOptions {
trust_witness_utxo: true,
..Default::default()
},
)
.unwrap();
assert!(finished, "all the inputs should have been signed now"); assert!(finished, "all the inputs should have been signed now");
} }
@@ -2521,7 +2606,7 @@ mod test {
} }
#[test] #[test]
fn test_add_foreign_utxo_force_non_witness_utxo() { fn test_add_foreign_utxo_only_witness_utxo() {
let (wallet1, _, _) = get_funded_wallet(get_test_wpkh()); let (wallet1, _, _) = get_funded_wallet(get_test_wpkh());
let (wallet2, _, txid2) = let (wallet2, _, txid2) =
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
@@ -2534,9 +2619,7 @@ mod test {
.unwrap(); .unwrap();
let mut builder = wallet1.build_tx(); let mut builder = wallet1.build_tx();
builder builder.add_recipient(addr.script_pubkey(), 60_000);
.add_recipient(addr.script_pubkey(), 60_000)
.force_non_witness_utxo();
{ {
let mut builder = builder.clone(); let mut builder = builder.clone();
@@ -2553,6 +2636,22 @@ mod test {
); );
} }
{
let mut builder = builder.clone();
let psbt_input = psbt::Input {
witness_utxo: Some(utxo2.txout.clone()),
..Default::default()
};
builder
.only_witness_utxo()
.add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
.unwrap();
assert!(
builder.finish().is_ok(),
"psbt_input with just witness_utxo should succeed when `only_witness_utxo` is enabled"
);
}
{ {
let mut builder = builder.clone(); let mut builder = builder.clone();
let tx2 = wallet2 let tx2 = wallet2
@@ -2572,7 +2671,7 @@ mod test {
.unwrap(); .unwrap();
assert!( assert!(
builder.finish().is_ok(), builder.finish().is_ok(),
"psbt_input with non_witness_utxo should succeed with force_non_witness_utxo" "psbt_input with non_witness_utxo should succeed by default"
); );
} }
} }
@@ -3462,12 +3561,12 @@ mod test {
builder builder
.set_single_recipient(addr.script_pubkey()) .set_single_recipient(addr.script_pubkey())
.drain_wallet(); .drain_wallet();
let (psbt, _) = builder.finish().unwrap(); let (mut psbt, _) = builder.finish().unwrap();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert_eq!(finalized, true); assert!(finalized);
let extracted = signed_psbt.extract_tx(); let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2); assert_eq!(extracted.input[0].witness.len(), 2);
} }
@@ -3479,12 +3578,12 @@ mod test {
builder builder
.set_single_recipient(addr.script_pubkey()) .set_single_recipient(addr.script_pubkey())
.drain_wallet(); .drain_wallet();
let (psbt, _) = builder.finish().unwrap(); let (mut psbt, _) = builder.finish().unwrap();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert_eq!(finalized, true); assert!(finalized);
let extracted = signed_psbt.extract_tx(); let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2); assert_eq!(extracted.input[0].witness.len(), 2);
} }
@@ -3496,12 +3595,12 @@ mod test {
builder builder
.set_single_recipient(addr.script_pubkey()) .set_single_recipient(addr.script_pubkey())
.drain_wallet(); .drain_wallet();
let (psbt, _) = builder.finish().unwrap(); let (mut psbt, _) = builder.finish().unwrap();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert_eq!(finalized, true); assert!(finalized);
let extracted = signed_psbt.extract_tx(); let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2); assert_eq!(extracted.input[0].witness.len(), 2);
} }
@@ -3513,12 +3612,12 @@ mod test {
builder builder
.set_single_recipient(addr.script_pubkey()) .set_single_recipient(addr.script_pubkey())
.drain_wallet(); .drain_wallet();
let (psbt, _) = builder.finish().unwrap(); let (mut psbt, _) = builder.finish().unwrap();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert_eq!(finalized, true); assert!(finalized);
let extracted = signed_psbt.extract_tx(); let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2); assert_eq!(extracted.input[0].witness.len(), 2);
} }
@@ -3531,12 +3630,12 @@ mod test {
builder builder
.set_single_recipient(addr.script_pubkey()) .set_single_recipient(addr.script_pubkey())
.drain_wallet(); .drain_wallet();
let (psbt, _) = builder.finish().unwrap(); let (mut psbt, _) = builder.finish().unwrap();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert_eq!(finalized, true); assert!(finalized);
let extracted = signed_psbt.extract_tx(); let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2); assert_eq!(extracted.input[0].witness.len(), 2);
} }
@@ -3553,10 +3652,10 @@ mod test {
psbt.inputs[0].bip32_derivation.clear(); psbt.inputs[0].bip32_derivation.clear();
assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0); assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0);
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert_eq!(finalized, true); assert!(finalized);
let extracted = signed_psbt.extract_tx(); let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2); assert_eq!(extracted.input[0].witness.len(), 2);
} }
@@ -3602,7 +3701,15 @@ mod test {
psbt.inputs.push(dud_input); psbt.inputs.push(dud_input);
psbt.global.unsigned_tx.input.push(bitcoin::TxIn::default()); psbt.global.unsigned_tx.input.push(bitcoin::TxIn::default());
let (psbt, is_final) = wallet.sign(psbt, None).unwrap(); let is_final = wallet
.sign(
&mut psbt,
SignOptions {
trust_witness_utxo: true,
..Default::default()
},
)
.unwrap();
assert!( assert!(
!is_final, !is_final,
"shouldn't be final since we can't sign one of the inputs" "shouldn't be final since we can't sign one of the inputs"
@@ -3613,10 +3720,58 @@ mod test {
) )
} }
#[test]
fn test_sign_nonstandard_sighash() {
let sighash = SigHashType::NonePlusAnyoneCanPay;
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.set_single_recipient(addr.script_pubkey())
.sighash(sighash)
.drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let result = wallet.sign(&mut psbt, Default::default());
assert!(
result.is_err(),
"Signing should have failed because the TX uses non-standard sighashes"
);
assert!(
matches!(
result.unwrap_err(),
Error::Signer(SignerError::NonStandardSighash)
),
"Signing failed with the wrong error type"
);
// try again after opting-in
let result = wallet.sign(
&mut psbt,
SignOptions {
allow_all_sighashes: true,
..Default::default()
},
);
assert!(result.is_ok(), "Signing should have worked");
assert!(
result.unwrap(),
"Should finalize the input since we can produce signatures"
);
let extracted = psbt.extract_tx();
assert_eq!(
*extracted.input[0].witness[0].last().unwrap(),
sighash.as_u32() as u8,
"The signature should have been made with the right sighash"
);
}
#[test] #[test]
fn test_unused_address() { fn test_unused_address() {
let db = MemoryDatabase::new(); let db = MemoryDatabase::new();
let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
None, Network::Testnet, db).unwrap(); None, Network::Testnet, db).unwrap();
assert_eq!( assert_eq!(
@@ -3750,4 +3905,65 @@ mod test {
"tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2" "tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2"
); );
} }
#[test]
fn test_returns_index_and_address() {
let db = MemoryDatabase::new();
let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
None, Network::Testnet, db).unwrap();
// new index 0
assert_eq!(
wallet.get_address(New).unwrap(),
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a").unwrap(),
}
);
// new index 1
assert_eq!(
wallet.get_address(New).unwrap(),
AddressInfo {
index: 1,
address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7").unwrap()
}
);
// peek index 25
assert_eq!(
wallet.get_address(Peek(25)).unwrap(),
AddressInfo {
index: 25,
address: Address::from_str("tb1qsp7qu0knx3sl6536dzs0703u2w2ag6ppl9d0c2").unwrap()
}
);
// new index 2
assert_eq!(
wallet.get_address(New).unwrap(),
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2").unwrap()
}
);
// reset index 1 again
assert_eq!(
wallet.get_address(Reset(1)).unwrap(),
AddressInfo {
index: 1,
address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7").unwrap()
}
);
// new index 2 again
assert_eq!(
wallet.get_address(New).unwrap(),
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2").unwrap()
}
);
}
} }

View File

@@ -147,6 +147,12 @@ pub enum SignerError {
MissingWitnessScript, MissingWitnessScript,
/// The fingerprint and derivation path are missing from the psbt input /// The fingerprint and derivation path are missing from the psbt input
MissingHdKeypath, MissingHdKeypath,
/// The psbt contains a non-`SIGHASH_ALL` sighash in one of its input and the user hasn't
/// explicitly allowed them
///
/// To enable signing transactions with non-standard sighashes set
/// [`SignOptions::allow_all_sighashes`] to `true`.
NonStandardSighash,
} }
impl fmt::Display for SignerError { impl fmt::Display for SignerError {
@@ -206,6 +212,12 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
return Err(SignerError::InputIndexOutOfRange); return Err(SignerError::InputIndexOutOfRange);
} }
if psbt.inputs[input_index].final_script_sig.is_some()
|| psbt.inputs[input_index].final_script_witness.is_some()
{
return Ok(());
}
let (public_key, full_path) = match psbt.inputs[input_index] let (public_key, full_path) = match psbt.inputs[input_index]
.bip32_derivation .bip32_derivation
.iter() .iter()
@@ -261,10 +273,16 @@ impl Signer for PrivateKey {
secp: &SecpCtx, secp: &SecpCtx,
) -> Result<(), SignerError> { ) -> Result<(), SignerError> {
let input_index = input_index.unwrap(); let input_index = input_index.unwrap();
if input_index >= psbt.inputs.len() { if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange); return Err(SignerError::InputIndexOutOfRange);
} }
if psbt.inputs[input_index].final_script_sig.is_some()
|| psbt.inputs[input_index].final_script_witness.is_some()
{
return Ok(());
}
let pubkey = self.public_key(&secp); let pubkey = self.public_key(&secp);
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) { if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
return Ok(()); return Ok(());
@@ -427,6 +445,50 @@ impl SignersContainer {
} }
} }
/// Options for a software signer
///
/// Adjust the behavior of our software signers and the way a transaction is finalized
#[derive(Debug, Clone)]
pub struct SignOptions {
/// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been
/// provided
///
/// Defaults to `false` to mitigate the "SegWit bug" which chould trick the wallet into
/// paying a fee larger than expected.
///
/// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for
/// SegWit transactions in the PSBT they generate: in those cases setting this to `true`
/// should correctly produce a signature, at the expense of an increased trust in the creator
/// of the PSBT.
///
/// For more details see: <https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd>
pub trust_witness_utxo: bool,
/// Whether the wallet should assume a specific height has been reached when trying to finalize
/// a transaction
///
/// The wallet will only "use" a timelock to satisfy the spending policy of an input if the
/// timelock height has already been reached. This option allows overriding the "current height" to let the
/// wallet use timelocks in the future to spend a coin.
pub assume_height: Option<u32>,
/// Whether the signer should use the `sighash_type` set in the PSBT when signing, no matter
/// what its value is
///
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
pub allow_all_sighashes: bool,
}
impl Default for SignOptions {
fn default() -> Self {
SignOptions {
trust_witness_utxo: false,
assume_height: None,
allow_all_sighashes: false,
}
}
}
pub(crate) trait ComputeSighash { pub(crate) trait ComputeSighash {
fn sighash( fn sighash(
psbt: &psbt::PartiallySignedTransaction, psbt: &psbt::PartiallySignedTransaction,
@@ -439,7 +501,7 @@ impl ComputeSighash for Legacy {
psbt: &psbt::PartiallySignedTransaction, psbt: &psbt::PartiallySignedTransaction,
input_index: usize, input_index: usize,
) -> Result<(SigHash, SigHashType), SignerError> { ) -> Result<(SigHash, SigHashType), SignerError> {
if input_index >= psbt.inputs.len() { if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange); return Err(SignerError::InputIndexOutOfRange);
} }
@@ -487,25 +549,42 @@ impl ComputeSighash for Segwitv0 {
psbt: &psbt::PartiallySignedTransaction, psbt: &psbt::PartiallySignedTransaction,
input_index: usize, input_index: usize,
) -> Result<(SigHash, SigHashType), SignerError> { ) -> Result<(SigHash, SigHashType), SignerError> {
if input_index >= psbt.inputs.len() { if input_index >= psbt.inputs.len() || input_index >= psbt.global.unsigned_tx.input.len() {
return Err(SignerError::InputIndexOutOfRange); return Err(SignerError::InputIndexOutOfRange);
} }
let psbt_input = &psbt.inputs[input_index]; let psbt_input = &psbt.inputs[input_index];
let tx_input = &psbt.global.unsigned_tx.input[input_index];
let sighash = psbt_input.sighash_type.unwrap_or(SigHashType::All); let sighash = psbt_input.sighash_type.unwrap_or(SigHashType::All);
let witness_utxo = psbt_input // Always try first with the non-witness utxo
.witness_utxo let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
.as_ref() // Check the provided prev-tx
.ok_or(SignerError::MissingNonWitnessUtxo)?; if prev_tx.txid() != tx_input.previous_output.txid {
let value = witness_utxo.value; return Err(SignerError::InvalidNonWitnessUtxo);
}
// The output should be present, if it's missing the `non_witness_utxo` is invalid
prev_tx
.output
.get(tx_input.previous_output.vout as usize)
.ok_or(SignerError::InvalidNonWitnessUtxo)?
} else if let Some(witness_utxo) = &psbt_input.witness_utxo {
// Fallback to the witness_utxo. If we aren't allowed to use it, signing should fail
// before we get to this point
witness_utxo
} else {
// Nothing has been provided
return Err(SignerError::MissingNonWitnessUtxo);
};
let value = utxo.value;
let script = match psbt_input.witness_script { let script = match psbt_input.witness_script {
Some(ref witness_script) => witness_script.clone(), Some(ref witness_script) => witness_script.clone(),
None => { None => {
if witness_utxo.script_pubkey.is_v0_p2wpkh() { if utxo.script_pubkey.is_v0_p2wpkh() {
p2wpkh_script_code(&witness_utxo.script_pubkey) p2wpkh_script_code(&utxo.script_pubkey)
} else if psbt_input } else if psbt_input
.redeem_script .redeem_script
.as_ref() .as_ref()

View File

@@ -41,7 +41,7 @@ use std::collections::HashSet;
use std::default::Default; use std::default::Default;
use std::marker::PhantomData; use std::marker::PhantomData;
use bitcoin::util::psbt::{self, PartiallySignedTransaction as PSBT}; use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{OutPoint, Script, SigHashType, Transaction}; use bitcoin::{OutPoint, Script, SigHashType, Transaction};
use miniscript::descriptor::DescriptorTrait; use miniscript::descriptor::DescriptorTrait;
@@ -87,9 +87,9 @@ impl TxBuilderContext for BumpFee {}
/// let (psbt1, details) = { /// let (psbt1, details) = {
/// let mut builder = wallet.build_tx(); /// let mut builder = wallet.build_tx();
/// builder /// builder
/// .ordering(TxOrdering::Untouched) /// .ordering(TxOrdering::Untouched)
/// .add_recipient(addr1.script_pubkey(), 50_000) /// .add_recipient(addr1.script_pubkey(), 50_000)
/// .add_recipient(addr2.script_pubkey(), 50_000); /// .add_recipient(addr2.script_pubkey(), 50_000);
/// builder.finish()? /// builder.finish()?
/// }; /// };
/// ///
@@ -103,7 +103,10 @@ impl TxBuilderContext for BumpFee {}
/// builder.finish()? /// builder.finish()?
/// }; /// };
/// ///
/// assert_eq!(psbt1.global.unsigned_tx.output[..2], psbt2.global.unsigned_tx.output[..2]); /// assert_eq!(
/// psbt1.global.unsigned_tx.output[..2],
/// psbt2.global.unsigned_tx.output[..2]
/// );
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(), bdk::Error>(())
/// ``` /// ```
/// ///
@@ -119,9 +122,6 @@ impl TxBuilderContext for BumpFee {}
#[derive(Debug)] #[derive(Debug)]
pub struct TxBuilder<'a, B, D, Cs, Ctx> { pub struct TxBuilder<'a, B, D, Cs, Ctx> {
pub(crate) wallet: &'a Wallet<B, D>, pub(crate) wallet: &'a Wallet<B, D>,
// params and coin_selection are Options not becasue they are optionally set (they are always
// there) but because `.finish()` uses `Option::take` to get an owned value from a &mut self.
// They are only `None` after `.finish()` is called.
pub(crate) params: TxParams, pub(crate) params: TxParams,
pub(crate) coin_selection: Cs, pub(crate) coin_selection: Cs,
pub(crate) phantom: PhantomData<Ctx>, pub(crate) phantom: PhantomData<Ctx>,
@@ -146,7 +146,7 @@ pub(crate) struct TxParams {
pub(crate) rbf: Option<RbfValue>, pub(crate) rbf: Option<RbfValue>,
pub(crate) version: Option<Version>, pub(crate) version: Option<Version>,
pub(crate) change_policy: ChangeSpendPolicy, pub(crate) change_policy: ChangeSpendPolicy,
pub(crate) force_non_witness_utxo: bool, pub(crate) only_witness_utxo: bool,
pub(crate) add_global_xpubs: bool, pub(crate) add_global_xpubs: bool,
pub(crate) include_output_redeem_witness_script: bool, pub(crate) include_output_redeem_witness_script: bool,
pub(crate) bumping_fee: Option<PreviousFee>, pub(crate) bumping_fee: Option<PreviousFee>,
@@ -249,7 +249,8 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
/// let mut path = BTreeMap::new(); /// let mut path = BTreeMap::new();
/// path.insert("aabbccdd".to_string(), vec![0, 1]); /// path.insert("aabbccdd".to_string(), vec![0, 1]);
/// ///
/// let builder = wallet.build_tx() /// let builder = wallet
/// .build_tx()
/// .add_recipient(to_address.script_pubkey(), 50_000) /// .add_recipient(to_address.script_pubkey(), 50_000)
/// .policy_path(path, KeychainKind::External); /// .policy_path(path, KeychainKind::External);
/// ///
@@ -336,10 +337,10 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
/// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`. /// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
/// 2. The data in `non_witness_utxo` does not match what is in `outpoint`. /// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
/// ///
/// Note if you set [`force_non_witness_utxo`] any `psbt_input` you pass to this method must /// Note unless you set [`only_witness_utxo`] any `psbt_input` you pass to this method must
/// have `non_witness_utxo` set otherwise you will get an error when [`finish`] is called. /// have `non_witness_utxo` set otherwise you will get an error when [`finish`] is called.
/// ///
/// [`force_non_witness_utxo`]: Self::force_non_witness_utxo /// [`only_witness_utxo`]: Self::only_witness_utxo
/// [`finish`]: Self::finish /// [`finish`]: Self::finish
/// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight /// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight
pub fn add_foreign_utxo( pub fn add_foreign_utxo(
@@ -464,12 +465,13 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
self self
} }
/// Fill-in the [`psbt::Input::non_witness_utxo`](bitcoin::util::psbt::Input::non_witness_utxo) field even if the wallet only has SegWit /// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::util::psbt::Input::witness_utxo) field when spending from
/// descriptors. /// SegWit descriptors.
/// ///
/// This is useful for signers which always require it, like Trezor hardware wallets. /// This reduces the size of the PSBT, but some signers might reject them due to the lack of
pub fn force_non_witness_utxo(&mut self) -> &mut Self { /// the `non_witness_utxo`.
self.params.force_non_witness_utxo = true; pub fn only_witness_utxo(&mut self) -> &mut Self {
self.params.only_witness_utxo = true;
self self
} }
@@ -520,7 +522,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction. /// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
/// ///
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
pub fn finish(self) -> Result<(PSBT, TransactionDetails), Error> { pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error> {
self.wallet.create_tx(self.coin_selection, self.params) self.wallet.create_tx(self.coin_selection, self.params)
} }

View File

@@ -201,31 +201,31 @@ mod test {
#[test] #[test]
fn test_check_nsequence_rbf_msb_set() { fn test_check_nsequence_rbf_msb_set() {
let result = check_nsequence_rbf(0x80000000, 5000); let result = check_nsequence_rbf(0x80000000, 5000);
assert_eq!(result, false); assert!(!result);
} }
#[test] #[test]
fn test_check_nsequence_rbf_lt_csv() { fn test_check_nsequence_rbf_lt_csv() {
let result = check_nsequence_rbf(4000, 5000); let result = check_nsequence_rbf(4000, 5000);
assert_eq!(result, false); assert!(!result);
} }
#[test] #[test]
fn test_check_nsequence_rbf_different_unit() { fn test_check_nsequence_rbf_different_unit() {
let result = check_nsequence_rbf(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, 5000); let result = check_nsequence_rbf(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, 5000);
assert_eq!(result, false); assert!(!result);
} }
#[test] #[test]
fn test_check_nsequence_rbf_mask() { fn test_check_nsequence_rbf_mask() {
let result = check_nsequence_rbf(0x3f + 10_000, 5000); let result = check_nsequence_rbf(0x3f + 10_000, 5000);
assert_eq!(result, true); assert!(result);
} }
#[test] #[test]
fn test_check_nsequence_rbf_same_unit_blocks() { fn test_check_nsequence_rbf_same_unit_blocks() {
let result = check_nsequence_rbf(10_000, 5000); let result = check_nsequence_rbf(10_000, 5000);
assert_eq!(result, true); assert!(result);
} }
#[test] #[test]
@@ -234,25 +234,25 @@ mod test {
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000, SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000,
SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, SEQUENCE_LOCKTIME_TYPE_FLAG + 5000,
); );
assert_eq!(result, true); assert!(result);
} }
#[test] #[test]
fn test_check_nlocktime_lt_cltv() { fn test_check_nlocktime_lt_cltv() {
let result = check_nlocktime(4000, 5000); let result = check_nlocktime(4000, 5000);
assert_eq!(result, false); assert!(!result);
} }
#[test] #[test]
fn test_check_nlocktime_different_unit() { fn test_check_nlocktime_different_unit() {
let result = check_nlocktime(BLOCKS_TIMELOCK_THRESHOLD + 5000, 5000); let result = check_nlocktime(BLOCKS_TIMELOCK_THRESHOLD + 5000, 5000);
assert_eq!(result, false); assert!(!result);
} }
#[test] #[test]
fn test_check_nlocktime_same_unit_blocks() { fn test_check_nlocktime_same_unit_blocks() {
let result = check_nlocktime(10_000, 5000); let result = check_nlocktime(10_000, 5000);
assert_eq!(result, true); assert!(result);
} }
#[test] #[test]
@@ -261,6 +261,6 @@ mod test {
BLOCKS_TIMELOCK_THRESHOLD + 10_000, BLOCKS_TIMELOCK_THRESHOLD + 10_000,
BLOCKS_TIMELOCK_THRESHOLD + 5000, BLOCKS_TIMELOCK_THRESHOLD + 5000,
); );
assert_eq!(result, true); assert!(result);
} }
} }

View File

@@ -1,25 +0,0 @@
[package]
name = "bdk-testutils-macros"
version = "0.5.0"
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
edition = "2018"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk-testutils-macros"
description = "Supporting testing macros for `bdk`"
keywords = ["bdk"]
license = "MIT OR Apache-2.0"
[lib]
proc-macro = true
name = "testutils_macros"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
syn = { version = "1.0", features = ["parsing", "full"] }
proc-macro2 = "1.0"
quote = "1.0"
[features]
debug = ["syn/extra-traits"]

View File

@@ -1,553 +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.
#[macro_use]
extern crate quote;
use proc_macro::TokenStream;
use syn::spanned::Spanned;
use syn::{parse, parse2, Ident, ReturnType};
#[proc_macro_attribute]
pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream {
let root_ident = if !attr.is_empty() {
match parse::<syn::ExprPath>(attr) {
Ok(parsed) => parsed,
Err(e) => {
let error_string = e.to_string();
return (quote! {
compile_error!("Invalid crate path: {:?}", #error_string)
})
.into();
}
}
} else {
parse2::<syn::ExprPath>(quote! { bdk }).unwrap()
};
match parse::<syn::ItemFn>(item) {
Err(_) => (quote! {
compile_error!("#[bdk_blockchain_tests] can only be used on `fn`s")
})
.into(),
Ok(parsed) => {
let parsed_sig_ident = parsed.sig.ident.clone();
let mod_name = Ident::new(
&format!("generated_tests_{}", parsed_sig_ident.to_string()),
parsed.span(),
);
let return_type = match parsed.sig.output {
ReturnType::Type(_, ref t) => t.clone(),
ReturnType::Default => {
return (quote! {
compile_error!("The tagged function must return a type that impl `Blockchain`")
}).into();
}
};
let output = quote! {
#parsed
mod #mod_name {
use bitcoin::Network;
use miniscript::Descriptor;
use testutils::{TestClient, serial};
use #root_ident::blockchain::{Blockchain, noop_progress};
use #root_ident::descriptor::ExtendedDescriptor;
use #root_ident::database::MemoryDatabase;
use #root_ident::types::KeychainKind;
use #root_ident::{Wallet, TxBuilder, FeeRate};
use #root_ident::wallet::AddressIndex::New;
use super::*;
fn get_blockchain() -> #return_type {
#parsed_sig_ident()
}
fn get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<#return_type, MemoryDatabase> {
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain()).unwrap()
}
fn init_single_sig() -> (Wallet<#return_type, MemoryDatabase>, (String, Option<String>), TestClient) {
let descriptors = testutils! {
@descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) )
};
let test_client = TestClient::new();
let wallet = get_wallet_from_descriptors(&descriptors);
(wallet, descriptors, test_client)
}
#[test]
#[serial]
fn test_sync_simple() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let tx = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
};
println!("{:?}", tx);
let txid = test_client.receive(tx);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
}
#[test]
#[serial]
fn test_sync_stop_gap_20() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 5) => 50_000 )
});
test_client.receive(testutils! {
@tx ( (@external descriptors, 25) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
}
#[test]
#[serial]
fn test_sync_before_and_after_receive() {
let (wallet, descriptors, mut test_client) = init_single_sig();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
}
#[test]
#[serial]
fn test_sync_multiple_outputs_same_tx() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 105_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 3);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 105_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
}
#[test]
#[serial]
fn test_sync_receive_multi() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
test_client.receive(testutils! {
@tx ( (@external descriptors, 5) => 25_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
assert_eq!(wallet.list_unspent().unwrap().len(), 2);
}
#[test]
#[serial]
fn test_sync_address_reuse() {
let (wallet, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 25_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
}
#[test]
#[serial]
fn test_sync_receive_rbf_replaced() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
let new_txid = test_client.bump_fee(&txid);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, new_txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
}
#[test]
#[serial]
fn test_sync_reorg_block() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert!(list_tx_item.height.is_some());
// Invalidate 1 block
test_client.invalidate(1);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.height, None);
}
#[test]
#[serial]
fn test_sync_after_send() {
let (wallet, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
let tx = psbt.extract_tx();
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
wallet.broadcast(tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
}
#[test]
#[serial]
fn test_sync_outgoing_from_scratch() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let received_txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received);
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors);
wallet.sync(noop_progress(), None).unwrap();
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let received = tx_map.get(&received_txid).unwrap();
assert_eq!(received.received, 50_000);
assert_eq!(received.sent, 0);
let sent = tx_map.get(&sent_txid).unwrap();
assert_eq!(sent.received, details.received);
assert_eq!(sent.sent, details.sent);
assert_eq!(sent.fees, details.fees);
}
#[test]
#[serial]
fn test_sync_long_change_chain() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut total_sent = 0;
for _ in 0..5 {
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 5_000);
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
total_sent += 5_000 + details.fees;
}
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
}
#[test]
#[serial]
fn test_sync_bump_fee() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000);
assert_eq!(wallet.get_balance().unwrap(), details.received);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
let (new_psbt, new_details) = builder.finish().unwrap();
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000);
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
assert!(new_details.fees > details.fees);
}
#[test]
#[serial]
fn test_sync_bump_fee_remove_change() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees);
assert_eq!(wallet.get_balance().unwrap(), details.received);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
let (new_psbt, new_details) = builder.finish().unwrap();
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
assert_eq!(new_details.received, 0);
assert!(new_details.fees > details.fees);
}
#[test]
#[serial]
fn test_sync_bump_fee_add_input() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
assert_eq!(details.received, 1_000 - details.fees);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
let (new_psbt, new_details) = builder.finish().unwrap();
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(new_details.sent, 75_000);
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
}
#[test]
#[serial]
fn test_sync_bump_fee_add_input_no_change() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
assert_eq!(details.received, 1_000 - details.fees);
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
let (new_psbt, new_details) = builder.finish().unwrap();
println!("{:#?}", new_details);
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(new_details.sent, 75_000);
assert_eq!(wallet.get_balance().unwrap(), 0);
assert_eq!(new_details.received, 0);
}
#[test]
#[serial]
fn test_sync_receive_coinbase() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let wallet_addr = wallet.get_address(New).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
test_client.generate(1, Some(wallet_addr));
wallet.sync(noop_progress(), None).unwrap();
assert!(wallet.get_balance().unwrap() > 0);
}
}
};
output.into()
}
}
}

View File

@@ -1,2 +0,0 @@
target/
Cargo.lock

View File

@@ -1,26 +0,0 @@
[package]
name = "bdk-testutils"
version = "0.4.0"
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
edition = "2018"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/bdk"
documentation = "https://docs.rs/bdk-testutils"
description = "Supporting testing utilities for `bdk`"
keywords = ["bdk"]
license = "MIT OR Apache-2.0"
[lib]
name = "testutils"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serial_test = "0.4"
bitcoin = "0.26"
bitcoincore-rpc = "0.13"
miniscript = "5.1"
electrum-client = "0.6.0"

View File

@@ -1,564 +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.
#[macro_use]
extern crate serde_json;
pub use serial_test::serial;
use std::collections::HashMap;
use std::env;
use std::ops::Deref;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
#[allow(unused_imports)]
use log::{debug, error, info, trace};
use bitcoin::consensus::encode::{deserialize, serialize};
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::sha256d;
use bitcoin::secp256k1::{Secp256k1, Verification};
use bitcoin::{Address, Amount, PublicKey, Script, Transaction, Txid};
use miniscript::descriptor::DescriptorPublicKey;
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
pub use bitcoincore_rpc::bitcoincore_rpc_json::AddressType;
pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
pub use electrum_client::{Client as ElectrumClient, ElectrumApi};
// TODO: we currently only support env vars, we could also parse a toml file
fn get_auth() -> Auth {
match env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) {
Ok("USER_PASS") => Auth::UserPass(
env::var("BDK_RPC_USER").unwrap(),
env::var("BDK_RPC_PASS").unwrap(),
),
_ => Auth::CookieFile(PathBuf::from(
env::var("BDK_RPC_COOKIEFILE")
.unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()),
)),
}
}
pub fn get_electrum_url() -> String {
env::var("BDK_ELECTRUM_URL").unwrap_or_else(|_| "tcp://127.0.0.1:50001".to_string())
}
pub struct TestClient {
client: RpcClient,
electrum: ElectrumClient,
}
#[derive(Clone, Debug)]
pub struct TestIncomingOutput {
pub value: u64,
pub to_address: String,
}
impl TestIncomingOutput {
pub fn new(value: u64, to_address: Address) -> Self {
Self {
value,
to_address: to_address.to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct TestIncomingTx {
pub output: Vec<TestIncomingOutput>,
pub min_confirmations: Option<u64>,
pub locktime: Option<i64>,
pub replaceable: Option<bool>,
}
impl TestIncomingTx {
pub fn new(
output: Vec<TestIncomingOutput>,
min_confirmations: Option<u64>,
locktime: Option<i64>,
replaceable: Option<bool>,
) -> Self {
Self {
output,
min_confirmations,
locktime,
replaceable,
}
}
pub fn add_output(&mut self, output: TestIncomingOutput) {
self.output.push(output);
}
}
#[doc(hidden)]
pub trait TranslateDescriptor {
// derive and translate a `Descriptor<DescriptorPublicKey>` into a `Descriptor<PublicKey>`
fn derive_translated<C: Verification>(
&self,
secp: &Secp256k1<C>,
index: u32,
) -> Descriptor<PublicKey>;
}
impl TranslateDescriptor for Descriptor<DescriptorPublicKey> {
fn derive_translated<C: Verification>(
&self,
secp: &Secp256k1<C>,
index: u32,
) -> Descriptor<PublicKey> {
let translate = |key: &DescriptorPublicKey| -> PublicKey {
match key {
DescriptorPublicKey::XPub(xpub) => {
xpub.xkey
.derive_pub(secp, &xpub.derivation_path)
.expect("hardened derivation steps")
.public_key
}
DescriptorPublicKey::SinglePub(key) => key.key,
}
};
self.derive(index)
.translate_pk_infallible(|pk| translate(pk), |pkh| translate(pkh).to_pubkeyhash())
}
}
#[macro_export]
macro_rules! testutils {
( @external $descriptors:expr, $child:expr ) => ({
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::TranslateDescriptor;
let secp = Secp256k1::new();
let parsed = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &$descriptors.0).expect("Failed to parse descriptor in `testutils!(@external)`").0;
parsed.derive_translated(&secp, $child).address(bitcoin::Network::Regtest).expect("No address form")
});
( @internal $descriptors:expr, $child:expr ) => ({
use bitcoin::secp256k1::Secp256k1;
use miniscript::descriptor::{Descriptor, DescriptorPublicKey, DescriptorTrait};
use $crate::TranslateDescriptor;
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.derive_translated(&secp, $child).address(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) });
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )* $( ( @confirmations $confirmations:expr ) )* $( ( @replaceable $replaceable:expr ) )* ) => ({
let mut outs = Vec::new();
$( outs.push(testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))); )+
let mut locktime = None::<i64>;
$( locktime = Some($locktime); )*
let mut min_confirmations = None::<u64>;
$( min_confirmations = Some($confirmations); )*
let mut replaceable = None::<bool>;
$( replaceable = Some($replaceable); )*
testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
});
( @literal $key:expr ) => ({
let key = $key.to_string();
(key, None::<String>, None::<String>)
});
( @generate_xprv $( $external_path:expr )* $( ,$internal_path:expr )* ) => ({
use rand::Rng;
let mut seed = [0u8; 32];
rand::thread_rng().fill(&mut seed[..]);
let key = bitcoin::util::bip32::ExtendedPrivKey::new_master(
bitcoin::Network::Testnet,
&seed,
);
let mut external_path = None::<String>;
$( external_path = Some($external_path.to_string()); )*
let mut internal_path = None::<String>;
$( internal_path = Some($internal_path.to_string()); )*
(key.unwrap().to_string(), external_path, internal_path)
});
( @generate_wif ) => ({
use rand::Rng;
let mut key = [0u8; bitcoin::secp256k1::constants::SECRET_KEY_SIZE];
rand::thread_rng().fill(&mut key[..]);
(bitcoin::PrivateKey {
compressed: true,
network: bitcoin::Network::Testnet,
key: bitcoin::secp256k1::SecretKey::from_slice(&key).unwrap(),
}.to_string(), None::<String>, None::<String>)
});
( @keys ( $( $alias:expr => ( $( $key_type:tt )* ) ),+ ) ) => ({
let mut map = std::collections::HashMap::new();
$(
let alias: &str = $alias;
map.insert(alias, testutils!( $($key_type)* ));
)+
map
});
( @descriptors ( $external_descriptor:expr ) $( ( $internal_descriptor:expr ) )* $( ( @keys $( $keys:tt )* ) )* ) => ({
use std::str::FromStr;
use std::collections::HashMap;
use std::convert::TryInto;
use miniscript::descriptor::{Descriptor, DescriptorPublicKey};
use miniscript::TranslatePk;
let mut keys: HashMap<&'static str, (String, Option<String>, Option<String>)> = 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 external = external.to_string();
let mut internal = None::<String>;
$(
let string_internal: Descriptor<String> = FromStr::from_str($internal_descriptor).unwrap();
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()
}
});
internal = Some(string_internal.to_string());
)*
(external, internal)
})
}
fn exponential_backoff_poll<T, F>(mut poll: F) -> T
where
F: FnMut() -> Option<T>,
{
let mut delay = Duration::from_millis(64);
loop {
match poll() {
Some(data) => break data,
None if delay.as_millis() < 512 => delay = delay.mul_f32(2.0),
None => {}
}
std::thread::sleep(delay);
}
}
impl TestClient {
pub fn new() -> Self {
let url = env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
let wallet = env::var("BDK_RPC_WALLET").unwrap_or_else(|_| "bdk-test".to_string());
let client =
RpcClient::new(format!("http://{}/wallet/{}", url, wallet), get_auth()).unwrap();
let electrum = ElectrumClient::new(&get_electrum_url()).unwrap();
TestClient { client, electrum }
}
fn wait_for_tx(&mut self, txid: Txid, monitor_script: &Script) {
// wait for electrs to index the tx
exponential_backoff_poll(|| {
trace!("wait_for_tx {}", txid);
self.electrum
.script_get_history(monitor_script)
.unwrap()
.iter()
.position(|entry| entry.tx_hash == txid)
});
}
fn wait_for_block(&mut self, min_height: usize) {
self.electrum.block_headers_subscribe().unwrap();
loop {
let header = exponential_backoff_poll(|| {
self.electrum.ping().unwrap();
self.electrum.block_headers_pop().unwrap()
});
if header.height >= min_height {
break;
}
}
}
pub fn receive(&mut self, meta_tx: TestIncomingTx) -> Txid {
assert!(
!meta_tx.output.is_empty(),
"can't create a transaction with no outputs"
);
let mut map = HashMap::new();
let mut required_balance = 0;
for out in &meta_tx.output {
required_balance += out.value;
map.insert(out.to_address.clone(), Amount::from_sat(out.value));
}
if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
}
// FIXME: core can't create a tx with two outputs to the same address
let tx = self
.create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
.unwrap();
let tx = self.fund_raw_transaction(tx, None, None).unwrap();
let mut tx: Transaction = deserialize(&tx.hex).unwrap();
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;
}
}
let tx = self
.sign_raw_transaction_with_wallet(&serialize(&tx), None, None)
.unwrap();
// broadcast through electrum so that it caches the tx immediately
let txid = self
.electrum
.transaction_broadcast(&deserialize(&tx.hex).unwrap())
.unwrap();
if let Some(num) = meta_tx.min_confirmations {
self.generate(num, None);
}
let monitor_script = Address::from_str(&meta_tx.output[0].to_address)
.unwrap()
.script_pubkey();
self.wait_for_tx(txid, &monitor_script);
debug!("Sent tx: {}", txid);
txid
}
pub fn bump_fee(&mut self, txid: &Txid) -> Txid {
let tx = self.get_raw_transaction_info(txid, None).unwrap();
assert!(
tx.confirmations.is_none(),
"Can't bump tx {} because it's already confirmed",
txid
);
let bumped: serde_json::Value = self.call("bumpfee", &[txid.to_string().into()]).unwrap();
let new_txid = Txid::from_str(&bumped["txid"].as_str().unwrap().to_string()).unwrap();
let monitor_script =
tx.vout[0].script_pub_key.addresses.as_ref().unwrap()[0].script_pubkey();
self.wait_for_tx(new_txid, &monitor_script);
debug!("Bumped {}, new txid {}", txid, new_txid);
new_txid
}
pub fn generate_manually(&mut self, txs: Vec<Transaction>) -> String {
use bitcoin::blockdata::block::{Block, BlockHeader};
use bitcoin::blockdata::script::Builder;
use bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
use bitcoin::hash_types::{BlockHash, TxMerkleNode};
let block_template: serde_json::Value = self
.call("getblocktemplate", &[json!({"rules": ["segwit"]})])
.unwrap();
trace!("getblocktemplate: {:#?}", block_template);
let header = BlockHeader {
version: block_template["version"].as_i64().unwrap() as i32,
prev_blockhash: BlockHash::from_hex(
block_template["previousblockhash"].as_str().unwrap(),
)
.unwrap(),
merkle_root: TxMerkleNode::default(),
time: block_template["curtime"].as_u64().unwrap() as u32,
bits: u32::from_str_radix(block_template["bits"].as_str().unwrap(), 16).unwrap(),
nonce: 0,
};
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();
// burn block subsidy and fees, not a big deal
let mut coinbase_tx = Transaction {
version: 1,
lock_time: 0,
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: Builder::new().push_int(height).into_script(),
sequence: 0xFFFFFFFF,
witness: vec![witness_reserved_value],
}],
output: vec![],
};
let mut txdata = vec![coinbase_tx.clone()];
txdata.extend_from_slice(&txs);
let mut block = Block { header, txdata };
let witness_root = block.witness_root();
let witness_commitment =
Block::compute_witness_commitment(&witness_root, &coinbase_tx.input[0].witness[0]);
// now update and replace the coinbase tx
let mut coinbase_witness_commitment_script = vec![0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed];
coinbase_witness_commitment_script.extend_from_slice(&witness_commitment);
coinbase_tx.output.push(TxOut {
value: 0,
script_pubkey: coinbase_witness_commitment_script.into(),
});
block.txdata[0] = coinbase_tx;
// set merkle root
let merkle_root = block.merkle_root();
block.header.merkle_root = merkle_root;
assert!(block.check_merkle_root());
assert!(block.check_witness_commitment());
// now do PoW :)
let target = block.header.target();
while block.header.validate_pow(&target).is_err() {
block.header.nonce = block.header.nonce.checked_add(1).unwrap(); // panic if we run out of nonces
}
let block_hex: String = serialize(&block).to_hex();
debug!("generated block hex: {}", block_hex);
self.electrum.block_headers_subscribe().unwrap();
let submit_result: serde_json::Value =
self.call("submitblock", &[block_hex.into()]).unwrap();
debug!("submitblock: {:?}", submit_result);
assert!(
submit_result.is_null(),
"submitblock error: {:?}",
submit_result.as_str()
);
self.wait_for_block(height as usize);
block.header.block_hash().to_hex()
}
pub fn generate(&mut self, num_blocks: u64, address: Option<Address>) {
let address = address.unwrap_or_else(|| self.get_new_address(None, None).unwrap());
let hashes = self.generate_to_address(num_blocks, &address).unwrap();
let best_hash = hashes.last().unwrap();
let height = self.get_block_info(best_hash).unwrap().height;
self.wait_for_block(height);
debug!("Generated blocks to new height {}", height);
}
pub fn invalidate(&mut self, num_blocks: u64) {
self.electrum.block_headers_subscribe().unwrap();
let best_hash = self.get_best_block_hash().unwrap();
let initial_height = self.get_block_info(&best_hash).unwrap().height;
let mut to_invalidate = best_hash;
for i in 1..=num_blocks {
trace!(
"Invalidating block {}/{} ({})",
i,
num_blocks,
to_invalidate
);
self.invalidate_block(&to_invalidate).unwrap();
to_invalidate = self.get_best_block_hash().unwrap();
}
self.wait_for_block(initial_height - num_blocks as usize);
debug!(
"Invalidated {} blocks to new height of {}",
num_blocks,
initial_height - num_blocks as usize
);
}
pub fn reorg(&mut self, num_blocks: u64) {
self.invalidate(num_blocks);
self.generate(num_blocks, None);
}
pub fn get_node_address(&self, address_type: Option<AddressType>) -> Address {
Address::from_str(
&self
.get_new_address(None, address_type)
.unwrap()
.to_string(),
)
.unwrap()
}
}
impl Deref for TestClient {
type Target = RpcClient;
fn deref(&self) -> &Self::Target {
&self.client
}
}