Compare commits
37 Commits
release/0.
...
release/0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3897e29740 | ||
|
|
8f06e45872 | ||
|
|
766570abfd | ||
|
|
934ec366d9 | ||
|
|
d0733e9496 | ||
|
|
3c7a1f5918 | ||
|
|
85aadaccd2 | ||
|
|
fad0fe9f30 | ||
|
|
47f26447da | ||
|
|
3608ff9f14 | ||
|
|
898dfe6cf1 | ||
|
|
7961ae7f8e | ||
|
|
8bf77c8f07 | ||
|
|
3c7bae9ce9 | ||
|
|
17bcd8ed7d | ||
|
|
b5e9589803 | ||
|
|
1d628d84b5 | ||
|
|
b84fd6ea5c | ||
|
|
8fe4222c33 | ||
|
|
e626f2e255 | ||
|
|
5a0c150ff9 | ||
|
|
00f07818f9 | ||
|
|
136a4bddb2 | ||
|
|
ff7b74ec27 | ||
|
|
8c00326990 | ||
|
|
afcd26032d | ||
|
|
8f422a1bf9 | ||
|
|
45983d2166 | ||
|
|
3ed44ce8cf | ||
|
|
8e7d8312a9 | ||
|
|
4da7488dc4 | ||
|
|
e37680af96 | ||
|
|
5f873ae500 | ||
|
|
2380634496 | ||
|
|
af98b8da06 | ||
|
|
b68ec050e2 | ||
|
|
ac7df09200 |
36
.github/workflows/code_coverage.yml
vendored
36
.github/workflows/code_coverage.yml
vendored
@@ -3,25 +3,35 @@ on: [push]
|
||||
name: Code Coverage
|
||||
|
||||
jobs:
|
||||
tarpaulin-codecov:
|
||||
name: Tarpaulin to codecov.io
|
||||
|
||||
Codecov:
|
||||
name: Code Coverage
|
||||
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:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rustup
|
||||
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly-2021-03-23
|
||||
run: rustup default nightly
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Test
|
||||
run: cargo test --features all-keys,compiler,esplora,compact_filters --no-default-features
|
||||
|
||||
- id: coverage
|
||||
name: Generate coverage
|
||||
uses: actions-rs/grcov@v0.1.5
|
||||
|
||||
- name: Install tarpaulin
|
||||
run: cargo install cargo-tarpaulin
|
||||
- name: Tarpaulin
|
||||
run: cargo tarpaulin --features all-keys,compiler,esplora,compact_filters --run-types Tests,Doctests --exclude-files "testutils/*" --out Xml
|
||||
|
||||
- name: Publish to codecov.io
|
||||
uses: codecov/codecov-action@v1.0.15
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
file: ./cobertura.xml
|
||||
file: ${{ steps.coverage.outputs.report }}
|
||||
directory: ./coverage/reports/
|
||||
|
||||
6
.github/workflows/cont_integration.yml
vendored
6
.github/workflows/cont_integration.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Build
|
||||
run: cargo build --features ${{ matrix.features }} --no-default-features
|
||||
- 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
|
||||
run: cargo test --features ${{ matrix.features }} --no-default-features
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-test-md-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly-2021-03-23
|
||||
run: rustup default nightly
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
run: rustup default 1.51.0 # STABLE
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Add clippy
|
||||
- name: Add rustfmt
|
||||
run: rustup component add rustfmt
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
|
||||
2
.github/workflows/nightly_docs.yml
vendored
2
.github/workflows/nightly_docs.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
target
|
||||
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly-2021-03-23
|
||||
run: rustup default nightly
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [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]
|
||||
|
||||
### 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.1]: https://github.com/bitcoindevkit/bdk/compare/v0.5.0...v0.5.1
|
||||
[v0.6.0]: https://github.com/bitcoindevkit/bdk/compare/v0.5.1...v0.6.0
|
||||
[v0.7.0]: https://github.com/bitcoindevkit/bdk/compare/v0.6.0...v0.7.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
version = "0.6.1-dev"
|
||||
version = "0.7.1-dev"
|
||||
edition = "2018"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -60,7 +60,7 @@ test-md-docs = ["electrum"]
|
||||
|
||||
[dev-dependencies]
|
||||
bdk-testutils = "0.4"
|
||||
bdk-testutils-macros = "0.5"
|
||||
bdk-testutils-macros = "0.6"
|
||||
serial_test = "0.4"
|
||||
lazy_static = "1.4"
|
||||
env_logger = "0.7"
|
||||
|
||||
@@ -130,7 +130,7 @@ fn main() -> Result<(), bdk::Error> {
|
||||
### Sign a transaction
|
||||
|
||||
```rust,no_run
|
||||
use bdk::{Wallet, database::MemoryDatabase};
|
||||
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
||||
|
||||
use bitcoin::consensus::deserialize;
|
||||
|
||||
@@ -143,9 +143,9 @@ fn main() -> Result<(), bdk::Error> {
|
||||
)?;
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -166,4 +166,4 @@ at your option.
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
|
||||
13
codecov.yaml
Normal file
13
codecov.yaml
Normal 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
|
||||
@@ -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
|
||||
@@ -27,6 +27,8 @@ use miniscript::descriptor::{DescriptorPublicKey, DescriptorType, DescriptorXKey
|
||||
pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0};
|
||||
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
|
||||
|
||||
use crate::descriptor::policy::BuildSatisfaction;
|
||||
|
||||
pub mod checksum;
|
||||
pub(crate) mod derived;
|
||||
#[doc(hidden)]
|
||||
@@ -255,6 +257,7 @@ pub trait ExtractPolicy {
|
||||
fn extract_policy(
|
||||
&self,
|
||||
signers: &SignersContainer,
|
||||
psbt: BuildSatisfaction,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<Option<Policy>, DescriptorError>;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
//! # use std::sync::Arc;
|
||||
//! # use bdk::descriptor::*;
|
||||
//! # use bdk::bitcoin::secp256k1::Secp256k1;
|
||||
//! use bdk::descriptor::policy::BuildSatisfaction;
|
||||
//! let secp = Secp256k1::new();
|
||||
//! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))";
|
||||
//!
|
||||
@@ -29,7 +30,7 @@
|
||||
//! println!("{:?}", extended_desc);
|
||||
//!
|
||||
//! 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)?);
|
||||
//! # Ok::<(), bdk::Error>(())
|
||||
//! ```
|
||||
@@ -47,19 +48,15 @@ use bitcoin::PublicKey;
|
||||
|
||||
use miniscript::descriptor::{DescriptorPublicKey, ShInner, SortedMultiVec, WshInner};
|
||||
use miniscript::{
|
||||
Descriptor, ForEachKey, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal,
|
||||
ToPublicKey,
|
||||
Descriptor, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal, ToPublicKey,
|
||||
};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
|
||||
use crate::descriptor::{
|
||||
DerivedDescriptor, DerivedDescriptorKey, DescriptorMeta, ExtendedDescriptor, ExtractPolicy,
|
||||
};
|
||||
use crate::psbt::PsbtUtils;
|
||||
use crate::descriptor::{DerivedDescriptorKey, ExtractPolicy};
|
||||
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::error::Error;
|
||||
@@ -563,13 +560,18 @@ impl Policy {
|
||||
conditions: Default::default(),
|
||||
sorted: None,
|
||||
};
|
||||
let mut satisfaction = contribution.clone();
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
contribution.add(&item.contribution, index)?;
|
||||
satisfaction.add(&item.satisfaction, index)?;
|
||||
}
|
||||
|
||||
contribution.finalize();
|
||||
satisfaction.finalize();
|
||||
|
||||
let mut policy: Policy = SatisfiableItem::Thresh { items, threshold }.into();
|
||||
policy.contribution = contribution;
|
||||
policy.satisfaction = satisfaction;
|
||||
|
||||
Ok(Some(policy))
|
||||
}
|
||||
@@ -577,6 +579,7 @@ impl Policy {
|
||||
fn make_multisig(
|
||||
keys: &[DescriptorPublicKey],
|
||||
signers: &SignersContainer,
|
||||
build_sat: BuildSatisfaction,
|
||||
threshold: usize,
|
||||
sorted: bool,
|
||||
secp: &SecpCtx,
|
||||
@@ -594,6 +597,8 @@ impl Policy {
|
||||
conditions: Default::default(),
|
||||
sorted: Some(sorted),
|
||||
};
|
||||
let mut satisfaction = contribution.clone();
|
||||
|
||||
for (index, key) in keys.iter().enumerate() {
|
||||
if signers.find(signer_id(key, secp)).is_some() {
|
||||
contribution.add(
|
||||
@@ -603,7 +608,19 @@ impl Policy {
|
||||
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();
|
||||
|
||||
let mut policy: Policy = SatisfiableItem::Multisig {
|
||||
@@ -612,6 +629,7 @@ impl Policy {
|
||||
}
|
||||
.into();
|
||||
policy.contribution = contribution;
|
||||
policy.satisfaction = satisfaction;
|
||||
|
||||
Ok(Some(policy))
|
||||
}
|
||||
@@ -698,52 +716,6 @@ impl Policy {
|
||||
_ => 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 {
|
||||
@@ -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();
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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(
|
||||
key: &<DescriptorPublicKey as MiniscriptKey>::Hash,
|
||||
signers: &SignersContainer,
|
||||
@@ -796,12 +802,13 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
|
||||
fn extract_policy(
|
||||
&self,
|
||||
signers: &SignersContainer,
|
||||
build_sat: BuildSatisfaction,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<Option<Policy>, Error> {
|
||||
Ok(match &self.node {
|
||||
// Leaves
|
||||
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::After(value) => {
|
||||
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into();
|
||||
@@ -811,6 +818,20 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
|
||||
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)
|
||||
}
|
||||
@@ -822,6 +843,20 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
|
||||
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)
|
||||
}
|
||||
@@ -835,7 +870,9 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
|
||||
Terminal::Hash160(hash) => {
|
||||
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
|
||||
Terminal::Alt(inner)
|
||||
| Terminal::Swap(inner)
|
||||
@@ -843,34 +880,34 @@ impl<Ctx: ScriptContext> ExtractPolicy for Miniscript<DescriptorPublicKey, Ctx>
|
||||
| Terminal::DupIf(inner)
|
||||
| Terminal::Verify(inner)
|
||||
| Terminal::NonZero(inner)
|
||||
| Terminal::ZeroNotEqual(inner) => inner.extract_policy(signers, secp)?,
|
||||
| Terminal::ZeroNotEqual(inner) => inner.extract_policy(signers, build_sat, secp)?,
|
||||
// Complex policies
|
||||
Terminal::AndV(a, b) | Terminal::AndB(a, b) => Policy::make_and(
|
||||
a.extract_policy(signers, secp)?,
|
||||
b.extract_policy(signers, secp)?,
|
||||
a.extract_policy(signers, build_sat, secp)?,
|
||||
b.extract_policy(signers, build_sat, secp)?,
|
||||
)?,
|
||||
Terminal::AndOr(x, y, z) => Policy::make_or(
|
||||
Policy::make_and(
|
||||
x.extract_policy(signers, secp)?,
|
||||
y.extract_policy(signers, secp)?,
|
||||
x.extract_policy(signers, build_sat, 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::OrD(a, b)
|
||||
| Terminal::OrC(a, b)
|
||||
| Terminal::OrI(a, b) => Policy::make_or(
|
||||
a.extract_policy(signers, secp)?,
|
||||
b.extract_policy(signers, secp)?,
|
||||
a.extract_policy(signers, build_sat, secp)?,
|
||||
b.extract_policy(signers, build_sat, secp)?,
|
||||
)?,
|
||||
Terminal::Thresh(k, nodes) => {
|
||||
let mut threshold = *k;
|
||||
let mapped: Vec<_> = nodes
|
||||
.iter()
|
||||
.map(|n| n.extract_policy(signers, secp))
|
||||
.map(|n| n.extract_policy(signers, build_sat, secp))
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.filter_map(|x| x)
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
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> {
|
||||
fn extract_policy(
|
||||
&self,
|
||||
signers: &SignersContainer,
|
||||
build_sat: BuildSatisfaction,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<Option<Policy>, Error> {
|
||||
fn make_sortedmulti<Ctx: ScriptContext>(
|
||||
keys: &SortedMultiVec<DescriptorPublicKey, Ctx>,
|
||||
signers: &SignersContainer,
|
||||
build_sat: BuildSatisfaction,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<Option<Policy>, Error> {
|
||||
Ok(Policy::make_multisig(
|
||||
keys.pks.as_ref(),
|
||||
signers,
|
||||
build_sat,
|
||||
keys.k,
|
||||
true,
|
||||
secp,
|
||||
@@ -907,22 +979,24 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
|
||||
}
|
||||
|
||||
match self {
|
||||
Descriptor::Pkh(pk) => Ok(Some(signature(pk.as_inner(), signers, secp))),
|
||||
Descriptor::Wpkh(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, build_sat, secp))),
|
||||
Descriptor::Sh(sh) => match sh.as_inner() {
|
||||
ShInner::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, secp))),
|
||||
ShInner::Ms(ms) => Ok(ms.extract_policy(signers, secp)?),
|
||||
ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, secp),
|
||||
ShInner::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))),
|
||||
ShInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
|
||||
ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp),
|
||||
ShInner::Wsh(wsh) => match wsh.as_inner() {
|
||||
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, secp)?),
|
||||
WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, secp),
|
||||
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?),
|
||||
WshInner::SortedMulti(ref keys) => {
|
||||
make_sortedmulti(keys, signers, build_sat, secp)
|
||||
}
|
||||
},
|
||||
},
|
||||
Descriptor::Wsh(wsh) => match wsh.as_inner() {
|
||||
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, secp)?),
|
||||
WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, secp),
|
||||
WshInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, 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();
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -993,7 +1067,7 @@ mod test {
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -1017,14 +1091,14 @@ mod test {
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||
&& &keys[0].fingerprint.unwrap() == &fingerprint0
|
||||
&& &keys[1].fingerprint.unwrap() == &fingerprint1)
|
||||
&& keys[0].fingerprint.unwrap() == fingerprint0
|
||||
&& keys[1].fingerprint.unwrap() == fingerprint1)
|
||||
);
|
||||
// TODO should this be "Satisfaction::None" since we have no prv keys?
|
||||
// TODO should items and conditions not be empty?
|
||||
@@ -1049,13 +1123,13 @@ mod test {
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||
&& &keys[0].fingerprint.unwrap() == &fingerprint0
|
||||
&& &keys[1].fingerprint.unwrap() == &fingerprint1)
|
||||
&& keys[0].fingerprint.unwrap() == fingerprint0
|
||||
&& keys[1].fingerprint.unwrap() == fingerprint1)
|
||||
);
|
||||
|
||||
assert!(
|
||||
@@ -1081,7 +1155,7 @@ mod test {
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -1113,7 +1187,7 @@ mod test {
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -1146,7 +1220,7 @@ mod test {
|
||||
let single_key = wallet_desc.derive(0);
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = single_key
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -1162,7 +1236,7 @@ mod test {
|
||||
let single_key = wallet_desc.derive(0);
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = single_key
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -1189,7 +1263,7 @@ mod test {
|
||||
let single_key = wallet_desc.derive(0);
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = single_key
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -1232,7 +1306,7 @@ mod test {
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -1271,7 +1345,7 @@ mod test {
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
println!("desc policy = {:?}", policy); // TODO remove
|
||||
@@ -1296,7 +1370,7 @@ mod test {
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
println!("desc policy = {:?}", policy); // TODO remove
|
||||
@@ -1314,7 +1388,7 @@ mod test {
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, &secp)
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -1337,7 +1411,7 @@ mod test {
|
||||
let signers = keymap.into();
|
||||
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers, &secp)
|
||||
.extract_policy(&signers, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -1369,11 +1443,12 @@ mod test {
|
||||
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]
|
||||
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 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";
|
||||
@@ -1400,45 +1475,135 @@ mod test {
|
||||
|
||||
let signers_container = Arc::new(SignersContainer::from(keymap));
|
||||
|
||||
let original_policy = wallet_desc
|
||||
.extract_policy(&signers_container, &secp)
|
||||
let psbt: PSBT = deserialize(&base64::decode(ALICE_SIGNED_PSBT).unwrap()).unwrap();
|
||||
|
||||
let policy_alice_psbt = wallet_desc
|
||||
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
|
||||
.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!(
|
||||
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
|
||||
&& items == &vec![0]
|
||||
)
|
||||
);
|
||||
|
||||
let mut policy_clone = original_policy.clone();
|
||||
let psbt: PSBT = deserialize(&base64::decode(BOB_SIGNED_PSBT).unwrap()).unwrap();
|
||||
policy_clone
|
||||
.fill_satisfactions(&psbt, &wallet_desc, &secp)
|
||||
let policy_bob_psbt = wallet_desc
|
||||
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
//println!("{}", serde_json::to_string(&policy_bob_psbt).unwrap());
|
||||
|
||||
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
|
||||
&& items == &vec![1]
|
||||
)
|
||||
);
|
||||
|
||||
let mut policy_clone = original_policy.clone();
|
||||
let psbt: PSBT = deserialize(&base64::decode(ALICE_BOB_SIGNED_PSBT).unwrap()).unwrap();
|
||||
policy_clone
|
||||
.fill_satisfactions(&psbt, &wallet_desc, &secp)
|
||||
let policy_alice_bob_psbt = wallet_desc
|
||||
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
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
|
||||
&& 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,11 +455,12 @@ expand_make_bipxx!(segwit_v0, Segwitv0);
|
||||
mod test {
|
||||
// test existing descriptor templates, make sure they are expanded to the right descriptors
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
||||
use crate::keys::ValidNetworks;
|
||||
use bitcoin::hashes::core::str::FromStr;
|
||||
use bitcoin::network::constants::Network::Regtest;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||
|
||||
22
src/lib.rs
22
src/lib.rs
@@ -43,7 +43,7 @@
|
||||
//! interact with the bitcoin P2P network.
|
||||
//!
|
||||
//! ```toml
|
||||
//! bdk = "0.6.0"
|
||||
//! bdk = "0.7.0"
|
||||
//! ```
|
||||
//!
|
||||
//! ## Sync the balance of a descriptor
|
||||
@@ -125,12 +125,15 @@
|
||||
//! wallet.sync(noop_progress(), None)?;
|
||||
//!
|
||||
//! let send_to = wallet.get_address(New)?;
|
||||
//! let (psbt, details) = wallet.build_tx()
|
||||
//! .add_recipient(send_to.script_pubkey(), 50_000)
|
||||
//! .enable_rbf()
|
||||
//! .do_not_spend_change()
|
||||
//! .fee_rate(FeeRate::from_sat_per_vb(5.0))
|
||||
//! .finish()?;
|
||||
//! let (psbt, details) = {
|
||||
//! let mut builder = wallet.build_tx();
|
||||
//! builder
|
||||
//! .add_recipient(send_to.script_pubkey(), 50_000)
|
||||
//! .enable_rbf()
|
||||
//! .do_not_spend_change()
|
||||
//! .fee_rate(FeeRate::from_sat_per_vb(5.0))
|
||||
//! builder.finish()?
|
||||
//! };
|
||||
//!
|
||||
//! println!("Transaction details: {:#?}", details);
|
||||
//! println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt)));
|
||||
@@ -158,9 +161,9 @@
|
||||
//! )?;
|
||||
//!
|
||||
//! 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(())
|
||||
//! }
|
||||
@@ -256,6 +259,7 @@ pub use error::Error;
|
||||
pub use types::*;
|
||||
pub use wallet::address_validator;
|
||||
pub use wallet::signer;
|
||||
pub use wallet::signer::SignOptions;
|
||||
pub use wallet::tx_builder::TxBuilder;
|
||||
pub use wallet::Wallet;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,7 +654,7 @@ mod test {
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_010);
|
||||
assert_eq!(result.fee_amount, 254.0);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -675,7 +675,7 @@ mod test {
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_010);
|
||||
assert_eq!(result.fee_amount, 254.0);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -696,7 +696,7 @@ mod test {
|
||||
|
||||
assert_eq!(result.selected.len(), 1);
|
||||
assert_eq!(result.selected_amount(), 200_000);
|
||||
assert_eq!(result.fee_amount, 118.0);
|
||||
assert!((result.fee_amount - 118.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -756,7 +756,7 @@ mod test {
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_000);
|
||||
assert_eq!(result.fee_amount, 254.0);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -777,7 +777,7 @@ mod test {
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300_010);
|
||||
assert_eq!(result.fee_amount, 254.0);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -798,7 +798,7 @@ mod test {
|
||||
|
||||
assert_eq!(result.selected.len(), 3);
|
||||
assert_eq!(result.selected_amount(), 300010);
|
||||
assert_eq!(result.fee_amount, 254.0);
|
||||
assert!((result.fee_amount - 254.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -968,7 +968,7 @@ mod test {
|
||||
cost_of_change,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result.fee_amount, 186.0);
|
||||
assert!((result.fee_amount - 186.0).abs() < f32::EPSILON);
|
||||
assert_eq!(result.selected_amount(), 100_000);
|
||||
}
|
||||
|
||||
@@ -1031,9 +1031,8 @@ mod test {
|
||||
);
|
||||
|
||||
assert!(result.selected_amount() > target_amount);
|
||||
assert_eq!(
|
||||
result.fee_amount,
|
||||
50.0 + result.selected.len() as f32 * 68.0
|
||||
assert!(
|
||||
(result.fee_amount - (50.0 + result.selected.len() as f32 * 68.0)).abs() < f32::EPSILON
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,13 +46,14 @@ pub use utils::IsDust;
|
||||
|
||||
use address_validator::AddressValidator;
|
||||
use coin_selection::DefaultCoinSelectionAlgorithm;
|
||||
use signer::{Signer, SignerOrdering, SignersContainer};
|
||||
use signer::{SignOptions, Signer, SignerOrdering, SignersContainer};
|
||||
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
|
||||
use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx, DUST_LIMIT_SATOSHI};
|
||||
|
||||
use crate::blockchain::{Blockchain, Progress};
|
||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
use crate::descriptor::policy::BuildSatisfaction;
|
||||
use crate::descriptor::{
|
||||
get_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DerivedDescriptorMeta,
|
||||
DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor,
|
||||
@@ -60,6 +61,7 @@ use crate::descriptor::{
|
||||
};
|
||||
use crate::error::Error;
|
||||
use crate::psbt::PsbtUtils;
|
||||
use crate::signer::SignerError;
|
||||
use crate::types::*;
|
||||
|
||||
const CACHE_ADDR_BATCH_SIZE: u32 = 100;
|
||||
@@ -190,7 +192,7 @@ pub enum AddressIndex {
|
||||
/// then the returned address and subsequent addresses returned by calls to `AddressIndex::New`
|
||||
/// and `AddressIndex::LastUsed` may have already been used. Also if the index is reset to a
|
||||
/// value earlier than the [`crate::blockchain::Blockchain`] stop_gap (default is 20) then a
|
||||
/// larger stop_gap should be used to monitor for all possibly used addresses.
|
||||
/// larger stop_gap should be used to monitor for all possibly used addresses.
|
||||
Reset(u32),
|
||||
}
|
||||
|
||||
@@ -372,14 +374,14 @@ where
|
||||
) -> Result<(PSBT, TransactionDetails), Error> {
|
||||
let external_policy = self
|
||||
.descriptor
|
||||
.extract_policy(&self.signers, &self.secp)?
|
||||
.extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)?
|
||||
.unwrap();
|
||||
let internal_policy = self
|
||||
.change_descriptor
|
||||
.as_ref()
|
||||
.map(|desc| {
|
||||
Ok::<_, Error>(
|
||||
desc.extract_policy(&self.change_signers, &self.secp)?
|
||||
desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)?
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
@@ -698,24 +700,24 @@ where
|
||||
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
|
||||
/// # let wallet = doctest_wallet!();
|
||||
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||
/// let (psbt, _) = {
|
||||
/// let (mut psbt, _) = {
|
||||
/// let mut builder = wallet.build_tx();
|
||||
/// builder
|
||||
/// .add_recipient(to_address.script_pubkey(), 50_000)
|
||||
/// .enable_rbf();
|
||||
/// builder.finish()?
|
||||
/// };
|
||||
/// let (psbt, _) = wallet.sign(psbt, None)?;
|
||||
/// let _ = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||
/// let tx = psbt.extract_tx();
|
||||
/// // 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())?;
|
||||
/// builder
|
||||
/// .fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||
/// builder.finish()?
|
||||
/// };
|
||||
///
|
||||
/// let (psbt, _) = wallet.sign(psbt, None)?;
|
||||
/// let _ = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||
/// let fee_bumped_tx = psbt.extract_tx();
|
||||
/// // broadcast fee_bumped_tx to replace original
|
||||
/// # 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
|
||||
/// [`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
|
||||
///
|
||||
/// ```
|
||||
@@ -842,17 +849,29 @@ where
|
||||
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
|
||||
/// # let wallet = doctest_wallet!();
|
||||
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||
/// let (psbt, _) = {
|
||||
/// let (mut psbt, _) = {
|
||||
/// let mut builder = wallet.build_tx();
|
||||
/// builder.add_recipient(to_address.script_pubkey(), 50_000);
|
||||
/// 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");
|
||||
/// # 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
|
||||
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
|
||||
.signers
|
||||
@@ -861,28 +880,32 @@ where
|
||||
.chain(self.change_signers.signers().iter())
|
||||
{
|
||||
if signer.sign_whole_tx() {
|
||||
signer.sign(&mut psbt, None, &self.secp)?;
|
||||
signer.sign(psbt, None, &self.secp)?;
|
||||
} else {
|
||||
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
|
||||
self.finalize_psbt(psbt, assume_height)
|
||||
self.finalize_psbt(psbt, sign_options)
|
||||
}
|
||||
|
||||
/// Return the spending policies for the wallet's descriptor
|
||||
pub fn policies(&self, keychain: KeychainKind) -> Result<Option<Policy>, Error> {
|
||||
match (keychain, self.change_descriptor.as_ref()) {
|
||||
(KeychainKind::External, _) => {
|
||||
Ok(self.descriptor.extract_policy(&self.signers, &self.secp)?)
|
||||
}
|
||||
(KeychainKind::External, _) => Ok(self.descriptor.extract_policy(
|
||||
&self.signers,
|
||||
BuildSatisfaction::None,
|
||||
&self.secp,
|
||||
)?),
|
||||
(KeychainKind::Internal, None) => Ok(None),
|
||||
(KeychainKind::Internal, Some(desc)) => {
|
||||
Ok(desc.extract_policy(&self.change_signers, &self.secp)?)
|
||||
}
|
||||
(KeychainKind::Internal, Some(desc)) => Ok(desc.extract_policy(
|
||||
&self.change_signers,
|
||||
BuildSatisfaction::None,
|
||||
&self.secp,
|
||||
)?),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,16 +925,17 @@ where
|
||||
}
|
||||
|
||||
/// Try to finalize a PSBT
|
||||
pub fn finalize_psbt(
|
||||
&self,
|
||||
mut psbt: PSBT,
|
||||
assume_height: Option<u32>,
|
||||
) -> Result<(PSBT, bool), Error> {
|
||||
///
|
||||
/// The [`SignOptions`] can be used to tweak the behavior of the finalizer.
|
||||
pub fn finalize_psbt(&self, psbt: &mut PSBT, sign_options: SignOptions) -> Result<bool, Error> {
|
||||
let tx = &psbt.global.unsigned_tx;
|
||||
let mut finished = true;
|
||||
|
||||
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() {
|
||||
continue;
|
||||
}
|
||||
@@ -922,7 +946,7 @@ where
|
||||
.borrow()
|
||||
.get_tx(&input.previous_output.txid, false)?
|
||||
.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!(
|
||||
"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
|
||||
@@ -1253,28 +1277,23 @@ where
|
||||
|
||||
match utxo {
|
||||
Utxo::Local(utxo) => {
|
||||
*psbt_input = match self.get_psbt_input(
|
||||
utxo,
|
||||
params.sighash,
|
||||
params.force_non_witness_utxo,
|
||||
) {
|
||||
Ok(psbt_input) => psbt_input,
|
||||
Err(e) => match e {
|
||||
Error::UnknownUtxo => Input {
|
||||
sighash_type: params.sighash,
|
||||
..Input::default()
|
||||
*psbt_input =
|
||||
match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) {
|
||||
Ok(psbt_input) => psbt_input,
|
||||
Err(e) => match e {
|
||||
Error::UnknownUtxo => Input {
|
||||
sighash_type: params.sighash,
|
||||
..Input::default()
|
||||
},
|
||||
_ => return Err(e),
|
||||
},
|
||||
_ => return Err(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Utxo::Foreign {
|
||||
psbt_input: foreign_psbt_input,
|
||||
outpoint,
|
||||
} => {
|
||||
if params.force_non_witness_utxo
|
||||
&& foreign_psbt_input.non_witness_utxo.is_none()
|
||||
{
|
||||
if !params.only_witness_utxo && foreign_psbt_input.non_witness_utxo.is_none() {
|
||||
return Err(Error::Generic(format!(
|
||||
"Missing non_witness_utxo on foreign utxo {}",
|
||||
outpoint
|
||||
@@ -1318,7 +1337,7 @@ where
|
||||
&self,
|
||||
utxo: LocalUtxo,
|
||||
sighash_type: Option<SigHashType>,
|
||||
force_non_witness_utxo: bool,
|
||||
only_witness_utxo: bool,
|
||||
) -> Result<Input, Error> {
|
||||
// Try to find the prev_script in our db to figure out if this is internal or external,
|
||||
// and the derivation index
|
||||
@@ -1345,7 +1364,7 @@ where
|
||||
if desc.is_witness() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1486,7 +1505,7 @@ where
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
pub(crate) mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::{util::psbt, Network};
|
||||
@@ -1633,7 +1652,7 @@ mod test {
|
||||
.database
|
||||
.borrow_mut()
|
||||
.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()
|
||||
.script_pubkey(),
|
||||
KeychainKind::External,
|
||||
@@ -2232,6 +2251,7 @@ mod test {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.set_single_recipient(addr.script_pubkey())
|
||||
.only_witness_utxo()
|
||||
.drain_wallet();
|
||||
let (psbt, _) = builder.finish().unwrap();
|
||||
|
||||
@@ -2250,20 +2270,18 @@ mod test {
|
||||
.drain_wallet();
|
||||
let (psbt, _) = builder.finish().unwrap();
|
||||
|
||||
assert!(psbt.inputs[0].non_witness_utxo.is_none());
|
||||
assert!(psbt.inputs[0].witness_utxo.is_some());
|
||||
}
|
||||
|
||||
#[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, _, _) =
|
||||
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
|
||||
let addr = wallet.get_address(New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.set_single_recipient(addr.script_pubkey())
|
||||
.drain_wallet()
|
||||
.force_non_witness_utxo();
|
||||
.drain_wallet();
|
||||
let (psbt, _) = builder.finish().unwrap();
|
||||
|
||||
assert!(psbt.inputs[0].non_witness_utxo.is_some());
|
||||
@@ -2401,6 +2419,7 @@ mod test {
|
||||
let (wallet1, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let (wallet2, _, _) =
|
||||
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
|
||||
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let utxo = wallet2.list_unspent().unwrap().remove(0);
|
||||
let foreign_utxo_satisfaction = wallet2
|
||||
@@ -2416,9 +2435,10 @@ mod test {
|
||||
let mut builder = wallet1.build_tx();
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), 60_000)
|
||||
.only_witness_utxo()
|
||||
.add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
|
||||
.unwrap();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
details.sent - details.received,
|
||||
@@ -2431,19 +2451,34 @@ mod test {
|
||||
.unsigned_tx
|
||||
.input
|
||||
.iter()
|
||||
.find(|input| input.previous_output == utxo.outpoint)
|
||||
.is_some(),
|
||||
.any(|input| input.previous_output == utxo.outpoint),
|
||||
"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!(
|
||||
!finished,
|
||||
"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");
|
||||
}
|
||||
|
||||
@@ -2521,7 +2556,7 @@ mod 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 (wallet2, _, txid2) =
|
||||
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
|
||||
@@ -2534,9 +2569,7 @@ mod test {
|
||||
.unwrap();
|
||||
|
||||
let mut builder = wallet1.build_tx();
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), 60_000)
|
||||
.force_non_witness_utxo();
|
||||
builder.add_recipient(addr.script_pubkey(), 60_000);
|
||||
|
||||
{
|
||||
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 tx2 = wallet2
|
||||
@@ -2572,7 +2621,7 @@ mod test {
|
||||
.unwrap();
|
||||
assert!(
|
||||
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
|
||||
.set_single_recipient(addr.script_pubkey())
|
||||
.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);
|
||||
|
||||
let extracted = signed_psbt.extract_tx();
|
||||
let extracted = psbt.extract_tx();
|
||||
assert_eq!(extracted.input[0].witness.len(), 2);
|
||||
}
|
||||
|
||||
@@ -3479,12 +3528,12 @@ mod test {
|
||||
builder
|
||||
.set_single_recipient(addr.script_pubkey())
|
||||
.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);
|
||||
|
||||
let extracted = signed_psbt.extract_tx();
|
||||
let extracted = psbt.extract_tx();
|
||||
assert_eq!(extracted.input[0].witness.len(), 2);
|
||||
}
|
||||
|
||||
@@ -3496,12 +3545,12 @@ mod test {
|
||||
builder
|
||||
.set_single_recipient(addr.script_pubkey())
|
||||
.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);
|
||||
|
||||
let extracted = signed_psbt.extract_tx();
|
||||
let extracted = psbt.extract_tx();
|
||||
assert_eq!(extracted.input[0].witness.len(), 2);
|
||||
}
|
||||
|
||||
@@ -3513,12 +3562,12 @@ mod test {
|
||||
builder
|
||||
.set_single_recipient(addr.script_pubkey())
|
||||
.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);
|
||||
|
||||
let extracted = signed_psbt.extract_tx();
|
||||
let extracted = psbt.extract_tx();
|
||||
assert_eq!(extracted.input[0].witness.len(), 2);
|
||||
}
|
||||
|
||||
@@ -3531,12 +3580,12 @@ mod test {
|
||||
builder
|
||||
.set_single_recipient(addr.script_pubkey())
|
||||
.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);
|
||||
|
||||
let extracted = signed_psbt.extract_tx();
|
||||
let extracted = psbt.extract_tx();
|
||||
assert_eq!(extracted.input[0].witness.len(), 2);
|
||||
}
|
||||
|
||||
@@ -3553,10 +3602,10 @@ mod test {
|
||||
psbt.inputs[0].bip32_derivation.clear();
|
||||
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);
|
||||
|
||||
let extracted = signed_psbt.extract_tx();
|
||||
let extracted = psbt.extract_tx();
|
||||
assert_eq!(extracted.input[0].witness.len(), 2);
|
||||
}
|
||||
|
||||
@@ -3602,7 +3651,15 @@ mod test {
|
||||
|
||||
psbt.inputs.push(dud_input);
|
||||
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!(
|
||||
!is_final,
|
||||
"shouldn't be final since we can't sign one of the inputs"
|
||||
@@ -3616,7 +3673,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_unused_address() {
|
||||
let db = MemoryDatabase::new();
|
||||
let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
|
||||
let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
|
||||
None, Network::Testnet, db).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -206,6 +206,12 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
|
||||
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]
|
||||
.bip32_derivation
|
||||
.iter()
|
||||
@@ -261,10 +267,16 @@ impl Signer for PrivateKey {
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
|
||||
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 {
|
||||
fn sighash(
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
@@ -439,7 +488,7 @@ impl ComputeSighash for Legacy {
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
) -> 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);
|
||||
}
|
||||
|
||||
@@ -487,25 +536,42 @@ impl ComputeSighash for Segwitv0 {
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
) -> 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);
|
||||
}
|
||||
|
||||
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 witness_utxo = psbt_input
|
||||
.witness_utxo
|
||||
.as_ref()
|
||||
.ok_or(SignerError::MissingNonWitnessUtxo)?;
|
||||
let value = witness_utxo.value;
|
||||
// Always try first with the non-witness utxo
|
||||
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||
// Check the provided prev-tx
|
||||
if prev_tx.txid() != tx_input.previous_output.txid {
|
||||
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 {
|
||||
Some(ref witness_script) => witness_script.clone(),
|
||||
None => {
|
||||
if witness_utxo.script_pubkey.is_v0_p2wpkh() {
|
||||
p2wpkh_script_code(&witness_utxo.script_pubkey)
|
||||
if utxo.script_pubkey.is_v0_p2wpkh() {
|
||||
p2wpkh_script_code(&utxo.script_pubkey)
|
||||
} else if psbt_input
|
||||
.redeem_script
|
||||
.as_ref()
|
||||
|
||||
@@ -146,7 +146,7 @@ pub(crate) struct TxParams {
|
||||
pub(crate) rbf: Option<RbfValue>,
|
||||
pub(crate) version: Option<Version>,
|
||||
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) include_output_redeem_witness_script: bool,
|
||||
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`.
|
||||
/// 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.
|
||||
///
|
||||
/// [`force_non_witness_utxo`]: Self::force_non_witness_utxo
|
||||
/// [`only_witness_utxo`]: Self::only_witness_utxo
|
||||
/// [`finish`]: Self::finish
|
||||
/// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight
|
||||
pub fn add_foreign_utxo(
|
||||
@@ -464,12 +464,13 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
||||
self
|
||||
}
|
||||
|
||||
/// Fill-in the [`psbt::Input::non_witness_utxo`](bitcoin::util::psbt::Input::non_witness_utxo) field even if the wallet only has SegWit
|
||||
/// descriptors.
|
||||
/// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::util::psbt::Input::witness_utxo) field when spending from
|
||||
/// SegWit descriptors.
|
||||
///
|
||||
/// This is useful for signers which always require it, like Trezor hardware wallets.
|
||||
pub fn force_non_witness_utxo(&mut self) -> &mut Self {
|
||||
self.params.force_non_witness_utxo = true;
|
||||
/// This reduces the size of the PSBT, but some signers might reject them due to the lack of
|
||||
/// the `non_witness_utxo`.
|
||||
pub fn only_witness_utxo(&mut self) -> &mut Self {
|
||||
self.params.only_witness_utxo = true;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk-testutils-macros"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
|
||||
edition = "2018"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
|
||||
@@ -297,8 +297,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let tx = psbt.extract_tx();
|
||||
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
||||
@@ -326,8 +326,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
|
||||
@@ -367,8 +367,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
|
||||
for _ in 0..5 {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 5_000);
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
|
||||
@@ -401,8 +401,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
@@ -411,8 +411,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
|
||||
let (new_psbt, new_details) = builder.finish().unwrap();
|
||||
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
|
||||
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
@@ -437,8 +437,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
@@ -447,8 +447,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||
let (new_psbt, new_details) = builder.finish().unwrap();
|
||||
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
|
||||
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
@@ -473,8 +473,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
@@ -483,8 +483,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
|
||||
let (new_psbt, new_details) = builder.finish().unwrap();
|
||||
let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
|
||||
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
@@ -507,8 +507,8 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
wallet.broadcast(psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
@@ -517,10 +517,10 @@ pub fn bdk_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenStream
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
|
||||
let (new_psbt, new_details) = builder.finish().unwrap();
|
||||
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||
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");
|
||||
wallet.broadcast(new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(noop_progress(), None).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user