Compare commits

...

37 Commits

Author SHA1 Message Date
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
Riccardo Casatta
47f26447da continue signing when finding already finalized inputs 2021-05-07 16:32:24 +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
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
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
19 changed files with 697 additions and 311 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-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
run: rustup update
- name: Test
run: cargo test --features all-keys,compiler,esplora,compact_filters --no-default-features
- name: Install tarpaulin - id: coverage
run: cargo install cargo-tarpaulin name: Generate coverage
- name: Tarpaulin uses: actions-rs/grcov@v0.1.5
run: cargo tarpaulin --features all-keys,compiler,esplora,compact_filters --run-types Tests,Doctests --exclude-files "testutils/*" --out Xml
- name: Publish to codecov.io - name: Upload coverage to Codecov
uses: codecov/codecov-action@v1.0.15 uses: codecov/codecov-action@v1
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
@@ -65,7 +65,7 @@ jobs:
target target
key: ${{ runner.os }}-cargo-test-md-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} key: ${{ runner.os }}-cargo-test-md-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
@@ -151,7 +151,7 @@ jobs:
run: rustup default 1.51.0 # STABLE run: rustup default 1.51.0 # STABLE
- 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

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,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [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] ## [v0.6.0] - [v0.5.1]
### Misc ### Misc
@@ -325,3 +340,4 @@ final transaction is created by calling `finish` on the builder.
[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.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.1-dev" version = "0.7.1-dev"
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"
@@ -60,7 +60,7 @@ test-md-docs = ["electrum"]
[dev-dependencies] [dev-dependencies]
bdk-testutils = "0.4" bdk-testutils = "0.4"
bdk-testutils-macros = "0.5" bdk-testutils-macros = "0.6"
serial_test = "0.4" serial_test = "0.4"
lazy_static = "1.4" lazy_static = "1.4"
env_logger = "0.7" env_logger = "0.7"

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(())
} }

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

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

@@ -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,19 +48,15 @@ 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;
@@ -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();
policy_clone let policy_bob_psbt = wallet_desc
.fill_satisfactions(&psbt, &wallet_desc, &secp) .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &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();
policy_clone let policy_alice_bob_psbt = wallet_desc
.fill_satisfactions(&psbt, &wallet_desc, &secp) .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &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

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

@@ -43,7 +43,7 @@
//! interact with the bitcoin P2P network. //! interact with the bitcoin P2P network.
//! //!
//! ```toml //! ```toml
//! bdk = "0.6.0" //! bdk = "0.7.0"
//! ``` //! ```
//! //!
//! ## Sync the balance of a descriptor //! ## Sync the balance of a descriptor
@@ -125,12 +125,15 @@
//! wallet.sync(noop_progress(), None)?; //! wallet.sync(noop_progress(), None)?;
//! //!
//! let send_to = wallet.get_address(New)?; //! let send_to = wallet.get_address(New)?;
//! let (psbt, details) = wallet.build_tx() //! let (psbt, details) = {
//! .add_recipient(send_to.script_pubkey(), 50_000) //! let mut builder = wallet.build_tx();
//! .enable_rbf() //! builder
//! .do_not_spend_change() //! .add_recipient(send_to.script_pubkey(), 50_000)
//! .fee_rate(FeeRate::from_sat_per_vb(5.0)) //! .enable_rbf()
//! .finish()?; //! .do_not_spend_change()
//! .fee_rate(FeeRate::from_sat_per_vb(5.0))
//! builder.finish()?
//! };
//! //!
//! println!("Transaction details: {:#?}", details); //! println!("Transaction details: {:#?}", details);
//! println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt))); //! println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt)));
@@ -158,9 +161,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, None)?;
//! //!
//! Ok(()) //! Ok(())
//! } //! }
@@ -256,6 +259,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;

View File

@@ -37,3 +37,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,
assume_height: None,
};
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,
assume_height: None,
};
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,
assume_height: None,
};
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

@@ -654,7 +654,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 +675,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 +696,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 +756,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 +777,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 +798,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 +968,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 +1031,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

@@ -46,13 +46,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 +61,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;
@@ -372,14 +374,14 @@ where
) -> 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(),
) )
}) })
@@ -698,24 +700,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 +834,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 +849,29 @@ 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));
}
for signer in self for signer in self
.signers .signers
@@ -861,28 +880,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 +925,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 +946,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 +1003,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
@@ -1253,28 +1277,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 +1337,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,7 +1364,7 @@ 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);
} }
} }
@@ -1486,7 +1505,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};
@@ -1633,7 +1652,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 +2251,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 +2270,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 +2419,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 +2435,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 +2451,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 +2556,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 +2569,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 +2586,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 +2621,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 +3511,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_eq!(finalized, true);
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 +3528,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_eq!(finalized, true);
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 +3545,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_eq!(finalized, true);
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 +3562,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_eq!(finalized, true);
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 +3580,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_eq!(finalized, true);
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 +3602,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_eq!(finalized, true);
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 +3651,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"

View File

@@ -206,6 +206,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 +267,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 +439,43 @@ 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>,
}
impl Default for SignOptions {
fn default() -> Self {
SignOptions {
trust_witness_utxo: false,
assume_height: None,
}
}
}
pub(crate) trait ComputeSighash { pub(crate) trait ComputeSighash {
fn sighash( fn sighash(
psbt: &psbt::PartiallySignedTransaction, psbt: &psbt::PartiallySignedTransaction,
@@ -439,7 +488,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 +536,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

@@ -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>,
@@ -336,10 +336,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 +464,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
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "bdk-testutils-macros" name = "bdk-testutils-macros"
version = "0.5.0" version = "0.6.0"
authors = ["Alekos Filini <alekos.filini@gmail.com>"] authors = ["Alekos Filini <alekos.filini@gmail.com>"]
edition = "2018" edition = "2018"
homepage = "https://bitcoindevkit.org" homepage = "https://bitcoindevkit.org"

View File

@@ -297,8 +297,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000); builder.add_recipient(node_addr.script_pubkey(), 25_000);
let (psbt, details) = builder.finish().unwrap(); let (mut psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx)); println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
@@ -326,8 +326,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000); builder.add_recipient(node_addr.script_pubkey(), 25_000);
let (psbt, details) = builder.finish().unwrap(); let (mut psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap(); let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
@@ -367,8 +367,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
for _ in 0..5 { for _ in 0..5 {
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 5_000); builder.add_recipient(node_addr.script_pubkey(), 5_000);
let (psbt, details) = builder.finish().unwrap(); let (mut psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap(); wallet.broadcast(psbt.extract_tx()).unwrap();
@@ -401,8 +401,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf(); builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap(); let (mut psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap(); wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap(); wallet.sync(noop_progress(), None).unwrap();
@@ -411,8 +411,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
let mut builder = wallet.build_fee_bump(details.txid).unwrap(); let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(2.1)); builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
let (new_psbt, new_details) = builder.finish().unwrap(); let (mut new_psbt, new_details) = builder.finish().unwrap();
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap(); let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap(); wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap(); wallet.sync(noop_progress(), None).unwrap();
@@ -437,8 +437,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap(); let (mut psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap(); wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap(); wallet.sync(noop_progress(), None).unwrap();
@@ -447,8 +447,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
let mut builder = wallet.build_fee_bump(details.txid).unwrap(); let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(5.0)); builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
let (new_psbt, new_details) = builder.finish().unwrap(); let (mut new_psbt, new_details) = builder.finish().unwrap();
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap(); let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap(); wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap(); wallet.sync(noop_progress(), None).unwrap();
@@ -473,8 +473,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap(); let (mut psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap(); wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap(); wallet.sync(noop_progress(), None).unwrap();
@@ -483,8 +483,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
let mut builder = wallet.build_fee_bump(details.txid).unwrap(); let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(10.0)); builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
let (new_psbt, new_details) = builder.finish().unwrap(); let (mut new_psbt, new_details) = builder.finish().unwrap();
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap(); let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap(); wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap(); wallet.sync(noop_progress(), None).unwrap();
@@ -507,8 +507,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf(); builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
let (psbt, details) = builder.finish().unwrap(); let (mut psbt, details) = builder.finish().unwrap();
let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap(); wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap(); wallet.sync(noop_progress(), None).unwrap();
@@ -517,10 +517,10 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
let mut builder = wallet.build_fee_bump(details.txid).unwrap(); let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(123.0)); builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
let (new_psbt, new_details) = builder.finish().unwrap(); let (mut new_psbt, new_details) = builder.finish().unwrap();
println!("{:#?}", new_details); println!("{:#?}", new_details);
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap(); let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap(); wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap(); wallet.sync(noop_progress(), None).unwrap();