From: Steve Myers Date: Tue, 6 Feb 2024 14:56:31 +0000 (-0600) Subject: chore: rename bdk crate to bdk_wallet X-Git-Tag: v1.0.0-alpha.12~5^2 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/struct.CommandStringError.html?a=commitdiff_plain;h=f6781652b7a4833b220e0853cd6e290bb5fc0136;p=bdk chore: rename bdk crate to bdk_wallet --- diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 1233d7e1..7507478e 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -58,8 +58,8 @@ jobs: working-directory: ./crates/chain # TODO "--target thumbv6m-none-eabi" should work but currently does not run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,hashbrown - - name: Check bdk - working-directory: ./crates/bdk + - name: Check bdk wallet + working-directory: ./crates/wallet # TODO "--target thumbv6m-none-eabi" should work but currently does not run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown - name: Check esplora @@ -89,8 +89,8 @@ jobs: target: "wasm32-unknown-unknown" - name: Rust Cache uses: Swatinem/rust-cache@v2.2.1 - - name: Check bdk - working-directory: ./crates/bdk + - name: Check bdk wallet + working-directory: ./crates/wallet run: cargo check --target wasm32-unknown-unknown --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown,dev-getrandom-wasm - name: Check esplora working-directory: ./crates/esplora diff --git a/Cargo.toml b/Cargo.toml index 87428029..7ecc7094 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" members = [ - "crates/bdk", + "crates/wallet", "crates/chain", "crates/file_store", "crates/electrum", diff --git a/README.md b/README.md index 030ec2a4..f2e0660f 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@

- Crate Info + Crate Info MIT or Apache-2.0 Licensed CI Status - API Docs + Wallet API Docs Rustc Version 1.63.0+ Chat on Discord

@@ -22,7 +22,7 @@

Project Homepage | - Documentation + Documentation

@@ -39,7 +39,7 @@ It is built upon the excellent [`rust-bitcoin`] and [`rust-miniscript`] crates. The project is split up into several crates in the `/crates` directory: -- [`bdk`](./crates/bdk): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components +- [`wallet`](./crates/wallet): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components - [`chain`](./crates/chain): Tools for storing and indexing chain data - [`persist`](./crates/persist): Types that define data persistence of a BDK wallet - [`file_store`](./crates/file_store): A (experimental) persistence backend for storing chain data in a single file. @@ -47,10 +47,10 @@ The project is split up into several crates in the `/crates` directory: - [`electrum`](./crates/electrum): Extends the [`electrum-client`] crate with methods to fetch chain data from an electrum server in the form that [`bdk_chain`] and `Wallet` can consume. Fully working examples of how to use these components are in `/example-crates`: -- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk `Wallet`. -- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk` library. -- [`example_esplora`](./example-crates/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk` library. -- [`example_bitcoind_rpc_polling`](./example-crates/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk` library. +- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk_wallet `Wallet`. +- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk_wallet` library. +- [`example_esplora`](./example-crates/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk_wallet` library. +- [`example_bitcoind_rpc_polling`](./example-crates/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk_wallet` library. - [`wallet_esplora_blocking`](./example-crates/wallet_esplora_blocking): Uses the `Wallet` to sync and spend using the Esplora blocking interface. - [`wallet_esplora_async`](./example-crates/wallet_esplora_async): Uses the `Wallet` to sync and spend using the Esplora asynchronous interface. - [`wallet_electrum`](./example-crates/wallet_electrum): Uses the `Wallet` to sync and spend using Electrum. diff --git a/crates/bdk/Cargo.toml b/crates/bdk/Cargo.toml deleted file mode 100644 index 140ab26b..00000000 --- a/crates/bdk/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -name = "bdk" -homepage = "https://bitcoindevkit.org" -version = "1.0.0-alpha.11" -repository = "https://github.com/bitcoindevkit/bdk" -documentation = "https://docs.rs/bdk" -description = "A modern, lightweight, descriptor-based wallet library" -keywords = ["bitcoin", "wallet", "descriptor", "psbt"] -readme = "README.md" -license = "MIT OR Apache-2.0" -authors = ["Bitcoin Dev Kit Developers"] -edition = "2021" -rust-version = "1.63" - -[dependencies] -anyhow = { version = "1", default-features = false } -rand = "^0.8" -miniscript = { version = "11.0.0", features = ["serde"], default-features = false } -bitcoin = { version = "0.31.0", features = ["serde", "base64", "rand-std"], default-features = false } -serde = { version = "^1.0", features = ["derive"] } -serde_json = { version = "^1.0" } -bdk_chain = { path = "../chain", version = "0.14.0", features = ["miniscript", "serde"], default-features = false } -bdk_persist = { path = "../persist", version = "0.2.0" } - -# Optional dependencies -bip39 = { version = "2.0", optional = true } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = "0.2" -js-sys = "0.3" - -[features] -default = ["std"] -std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"] -compiler = ["miniscript/compiler"] -all-keys = ["keys-bip39"] -keys-bip39 = ["bip39"] - -# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended -# for libraries to explicitly include the "getrandom/js" feature, so we only do it when -# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support -dev-getrandom-wasm = ["getrandom/js"] - -[dev-dependencies] -lazy_static = "1.4" -assert_matches = "1.5.0" -tempfile = "3" -bdk_file_store = { path = "../file_store" } -anyhow = "1" - -[package.metadata.docs.rs] -all-features = true -rustdoc-args = ["--cfg", "docsrs"] - -[[example]] -name = "mnemonic_to_descriptors" -path = "examples/mnemonic_to_descriptors.rs" -required-features = ["all-keys"] - -[[example]] -name = "miniscriptc" -path = "examples/compiler.rs" -required-features = ["compiler"] diff --git a/crates/bdk/README.md b/crates/bdk/README.md deleted file mode 100644 index 4722c584..00000000 --- a/crates/bdk/README.md +++ /dev/null @@ -1,228 +0,0 @@ -
-

BDK

- - - -

- A modern, lightweight, descriptor-based wallet library written in Rust! -

- -

- Crate Info - MIT or Apache-2.0 Licensed - CI Status - - API Docs - Rustc Version 1.63.0+ - Chat on Discord -

- -

- Project Homepage - | - Documentation -

-
- -## `bdk` - -The `bdk` crate provides the [`Wallet`] type which is a simple, high-level -interface built from the low-level components of [`bdk_chain`]. `Wallet` is a good starting point -for many simple applications as well as a good demonstration of how to use the other mechanisms to -construct a wallet. It has two keychains (external and internal) which are defined by -[miniscript descriptors][`rust-miniscript`] and uses them to generate addresses. When you give it -chain data it also uses the descriptors to find transaction outputs owned by them. From there, you -can create and sign transactions. - -For details about the API of `Wallet` see the [module-level documentation][`Wallet`]. - -### Blockchain data - -In order to get blockchain data for `Wallet` to consume, you should configure a client from -an available chain source. Typically you make a request to the chain source and get a response -that the `Wallet` can use to update its view of the chain. - -**Blockchain Data Sources** - -* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures. -* [`bdk_electrum`]: Grabs blockchain data from Electrum for updating BDK structures. -* [`bdk_bitcoind_rpc`]: Grabs blockchain data from Bitcoin Core for updating BDK structures. - -**Examples** - -* [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async) -* [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking) -* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum) -* [`example-crates/wallet_rpc`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_rpc) - -### Persistence - -To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`] implementation. - -**Implementations** - -* [`bdk_file_store`]: A simple flat-file implementation of [`PersistBackend`]. - -**Example** - - -```rust,compile_fail -use bdk::{bitcoin::Network, wallet::{ChangeSet, Wallet}}; - -fn main() { - // Create a new file `Store`. - let db = bdk_file_store::Store::::open_or_create_new(b"magic_bytes", "path/to/my_wallet.db").expect("create store"); - - let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)"; - let mut wallet = Wallet::new_or_load(descriptor, None, db, Network::Testnet).expect("create or load wallet"); - - // Insert a single `TxOut` at `OutPoint` into the wallet. - let _ = wallet.insert_txout(outpoint, txout); - wallet.commit().expect("must write to database"); -} -``` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -## Testing - -### Unit testing - -```bash -cargo test -``` - -## License - -Licensed under either of - -* Apache License, Version 2.0, ([LICENSE-APACHE](../../LICENSE-APACHE) or ) -* MIT license ([LICENSE-MIT](../../LICENSE-MIT) or ) - -at your option. - -### Contribution - -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. - -[`Wallet`]: https://docs.rs/bdk/1.0.0-alpha.7/bdk/wallet/struct.Wallet.html -[`PersistBackend`]: https://docs.rs/bdk_persist/latest/bdk_persist/trait.PersistBackend.html -[`bdk_chain`]: https://docs.rs/bdk_chain/latest -[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest -[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest -[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest -[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest -[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html diff --git a/crates/bdk/examples/compiler.rs b/crates/bdk/examples/compiler.rs deleted file mode 100644 index 0dbe1dd2..00000000 --- a/crates/bdk/examples/compiler.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -extern crate bdk; -extern crate bitcoin; -extern crate miniscript; -extern crate serde_json; - -use std::error::Error; -use std::str::FromStr; - -use bitcoin::Network; -use miniscript::policy::Concrete; -use miniscript::Descriptor; - -use bdk::{KeychainKind, Wallet}; - -/// Miniscript policy is a high level abstraction of spending conditions. Defined in the -/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html -/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy -/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet -/// can be derived from the policy. -/// -/// This example demonstrates the interaction between a bdk wallet and miniscript policy. - -fn main() -> Result<(), Box> { - // We start with a generic miniscript policy string - let policy_str = "or(10@thresh(4,pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)),1@and(older(4209713),thresh(2,pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068))))"; - println!("Compiling policy: \n{}", policy_str); - - // Parse the string as a [`Concrete`] type miniscript policy. - let policy = Concrete::::from_str(policy_str)?; - - // Create a `wsh` type descriptor from the policy. - // `policy.compile()` returns the resulting miniscript from the policy. - let descriptor = Descriptor::new_wsh(policy.compile()?)?; - - println!("Compiled into following Descriptor: \n{}", descriptor); - - // Create a new wallet from this descriptor - let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?; - - println!( - "First derived address from the descriptor: \n{}", - wallet.next_unused_address(KeychainKind::External)?, - ); - - // BDK also has it's own `Policy` structure to represent the spending condition in a more - // human readable json format. - let spending_policy = wallet.policies(KeychainKind::External)?; - println!( - "The BDK spending policy: \n{}", - serde_json::to_string_pretty(&spending_policy)? - ); - - Ok(()) -} diff --git a/crates/bdk/examples/mnemonic_to_descriptors.rs b/crates/bdk/examples/mnemonic_to_descriptors.rs deleted file mode 100644 index d2e59fe5..00000000 --- a/crates/bdk/examples/mnemonic_to_descriptors.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -use anyhow::anyhow; -use bdk::bitcoin::bip32::DerivationPath; -use bdk::bitcoin::secp256k1::Secp256k1; -use bdk::bitcoin::Network; -use bdk::descriptor; -use bdk::descriptor::IntoWalletDescriptor; -use bdk::keys::bip39::{Language, Mnemonic, WordCount}; -use bdk::keys::{GeneratableKey, GeneratedKey}; -use bdk::miniscript::Tap; -use std::str::FromStr; - -/// This example demonstrates how to generate a mnemonic phrase -/// using BDK and use that to generate a descriptor string. -fn main() -> Result<(), anyhow::Error> { - let secp = Secp256k1::new(); - - // In this example we are generating a 12 words mnemonic phrase - // but it is also possible generate 15, 18, 21 and 24 words - // using their respective `WordCount` variant. - let mnemonic: GeneratedKey<_, Tap> = - Mnemonic::generate((WordCount::Words12, Language::English)) - .map_err(|_| anyhow!("Mnemonic generation error"))?; - - println!("Mnemonic phrase: {}", *mnemonic); - let mnemonic_with_passphrase = (mnemonic, None); - - // define external and internal derivation key path - let external_path = DerivationPath::from_str("m/86h/1h/0h/0").unwrap(); - let internal_path = DerivationPath::from_str("m/86h/1h/0h/1").unwrap(); - - // generate external and internal descriptor from mnemonic - let (external_descriptor, ext_keymap) = - descriptor!(tr((mnemonic_with_passphrase.clone(), external_path)))? - .into_wallet_descriptor(&secp, Network::Testnet)?; - let (internal_descriptor, int_keymap) = - descriptor!(tr((mnemonic_with_passphrase, internal_path)))? - .into_wallet_descriptor(&secp, Network::Testnet)?; - - println!("tpub external descriptor: {}", external_descriptor); - println!("tpub internal descriptor: {}", internal_descriptor); - println!( - "tprv external descriptor: {}", - external_descriptor.to_string_with_secret(&ext_keymap) - ); - println!( - "tprv internal descriptor: {}", - internal_descriptor.to_string_with_secret(&int_keymap) - ); - - Ok(()) -} diff --git a/crates/bdk/examples/policy.rs b/crates/bdk/examples/policy.rs deleted file mode 100644 index 0f31cccd..00000000 --- a/crates/bdk/examples/policy.rs +++ /dev/null @@ -1,60 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -extern crate bdk; -use std::error::Error; - -use bdk::bitcoin::Network; -use bdk::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor}; -use bdk::wallet::signer::SignersContainer; - -/// This example describes the use of the BDK's [`bdk::descriptor::policy`] module. -/// -/// Policy is higher abstraction representation of the wallet descriptor spending condition. -/// This is useful to express complex miniscript spending conditions into more human readable form. -/// The resulting `Policy` structure can be used to derive spending conditions the wallet is capable -/// to spend from. -/// -/// This example demos a Policy output for a 2of2 multisig between between 2 parties, where the wallet holds -/// one of the Extend Private key. - -fn main() -> Result<(), Box> { - let secp = bitcoin::secp256k1::Secp256k1::new(); - - // The descriptor used in the example - // The form is "wsh(multi(2, , ))" - let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))"; - - // Use the descriptor string to derive the full descriptor and a keymap. - // The wallet descriptor can be used to create a new bdk::wallet. - // While the `keymap` can be used to create a `SignerContainer`. - // - // The `SignerContainer` can sign for `PSBT`s. - // a bdk::wallet internally uses these to handle transaction signing. - // But they can be used as independent tools also. - let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?; - - println!("Example Descriptor for policy analysis : {}", wallet_desc); - - // Create the signer with the keymap and descriptor. - let signers_container = SignersContainer::build(keymap, &wallet_desc, &secp); - - // Extract the Policy from the given descriptor and signer. - // Note that Policy is a wallet specific structure. It depends on the the descriptor, and - // what the concerned wallet with a given signer can sign for. - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp)? - .expect("We expect a policy"); - - println!("Derived Policy for the descriptor {:#?}", policy); - - Ok(()) -} diff --git a/crates/bdk/src/descriptor/checksum.rs b/crates/bdk/src/descriptor/checksum.rs deleted file mode 100644 index 243376bc..00000000 --- a/crates/bdk/src/descriptor/checksum.rs +++ /dev/null @@ -1,147 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Descriptor checksum -//! -//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the -//! checksum of a descriptor - -use crate::descriptor::DescriptorError; -use alloc::string::String; - -const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "; -const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l"; - -fn poly_mod(mut c: u64, val: u64) -> u64 { - let c0 = c >> 35; - c = ((c & 0x7ffffffff) << 5) ^ val; - if c0 & 1 > 0 { - c ^= 0xf5dee51989 - }; - if c0 & 2 > 0 { - c ^= 0xa9fdca3312 - }; - if c0 & 4 > 0 { - c ^= 0x1bab10e32d - }; - if c0 & 8 > 0 { - c ^= 0x3706b1677a - }; - if c0 & 16 > 0 { - c ^= 0x644d626ffd - }; - - c -} - -/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation -pub fn calc_checksum_bytes(mut desc: &str) -> Result<[u8; 8], DescriptorError> { - let mut c = 1; - let mut cls = 0; - let mut clscount = 0; - - let mut original_checksum = None; - if let Some(split) = desc.split_once('#') { - desc = split.0; - original_checksum = Some(split.1); - } - - for ch in desc.as_bytes() { - let pos = INPUT_CHARSET - .iter() - .position(|b| b == ch) - .ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64; - c = poly_mod(c, pos & 31); - cls = cls * 3 + (pos >> 5); - clscount += 1; - if clscount == 3 { - c = poly_mod(c, cls); - cls = 0; - clscount = 0; - } - } - if clscount > 0 { - c = poly_mod(c, cls); - } - (0..8).for_each(|_| c = poly_mod(c, 0)); - c ^= 1; - - let mut checksum = [0_u8; 8]; - for j in 0..8 { - checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize]; - } - - // if input data already had a checksum, check calculated checksum against original checksum - if let Some(original_checksum) = original_checksum { - if original_checksum.as_bytes() != checksum { - return Err(DescriptorError::InvalidDescriptorChecksum); - } - } - - Ok(checksum) -} - -/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation -pub fn calc_checksum(desc: &str) -> Result { - // unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET` - calc_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) }) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::descriptor::calc_checksum; - use assert_matches::assert_matches; - - // test calc_checksum() function; it should return the same value as Bitcoin Core - #[test] - fn test_calc_checksum() { - let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"; - assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62"); - - let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)"; - assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs"); - } - - // test calc_checksum() function; it should return the same value as Bitcoin Core even if the - // descriptor string includes a checksum hash - #[test] - fn test_calc_checksum_with_checksum_hash() { - let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62"; - assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62"); - - let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmfs"; - assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs"); - - let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26"; - assert_matches!( - calc_checksum(desc), - Err(DescriptorError::InvalidDescriptorChecksum) - ); - - let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf"; - assert_matches!( - calc_checksum(desc), - Err(DescriptorError::InvalidDescriptorChecksum) - ); - } - - #[test] - fn test_calc_checksum_invalid_character() { - let sparkle_heart = unsafe { core::str::from_utf8_unchecked(&[240, 159, 146, 150]) }; - let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart); - - assert_matches!( - calc_checksum(&invalid_desc), - Err(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0] - ); - } -} diff --git a/crates/bdk/src/descriptor/dsl.rs b/crates/bdk/src/descriptor/dsl.rs deleted file mode 100644 index aa52ff27..00000000 --- a/crates/bdk/src/descriptor/dsl.rs +++ /dev/null @@ -1,1216 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Descriptors DSL - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_top_level_sh { - // disallow `sortedmulti` in `bare()` - ( Bare, new, new, Legacy, sortedmulti $( $inner:tt )* ) => { - compile_error!("`bare()` descriptors can't contain any `sortedmulti()` operands"); - }; - ( Bare, new, new, Legacy, sortedmulti_vec $( $inner:tt )* ) => { - compile_error!("`bare()` descriptors can't contain any `sortedmulti_vec()` operands"); - }; - - ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti $( $inner:tt )* ) => {{ - use core::marker::PhantomData; - - use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey}; - use $crate::miniscript::$ctx; - - let build_desc = |k, pks| { - Ok((Descriptor::::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>)) - }; - - $crate::impl_sortedmulti!(build_desc, sortedmulti $( $inner )*) - }}; - ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti_vec $( $inner:tt )* ) => {{ - use core::marker::PhantomData; - - use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey}; - use $crate::miniscript::$ctx; - - let build_desc = |k, pks| { - Ok((Descriptor::::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>)) - }; - - $crate::impl_sortedmulti!(build_desc, sortedmulti_vec $( $inner )*) - }}; - - ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, $( $minisc:tt )* ) => {{ - use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey}; - - $crate::fragment!($( $minisc )*) - .and_then(|(minisc, keymap, networks)| Ok(($inner_struct::$constructor(minisc)?, keymap, networks))) - .and_then(|(inner, key_map, valid_networks)| Ok((Descriptor::::$inner_struct(inner), key_map, valid_networks))) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_top_level_pk { - ( $inner_type:ident, $ctx:ty, $key:expr ) => {{ - use $crate::miniscript::descriptor::$inner_type; - - #[allow(unused_imports)] - use $crate::keys::{DescriptorKey, IntoDescriptorKey}; - let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); - - $key.into_descriptor_key() - .and_then(|key: DescriptorKey<$ctx>| key.extract(&secp)) - .map_err($crate::descriptor::DescriptorError::Key) - .map(|(pk, key_map, valid_networks)| ($inner_type::new(pk), key_map, valid_networks)) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_top_level_tr { - ( $internal_key:expr, $tap_tree:expr ) => {{ - use $crate::miniscript::descriptor::{ - Descriptor, DescriptorPublicKey, KeyMap, TapTree, Tr, - }; - use $crate::miniscript::Tap; - - #[allow(unused_imports)] - use $crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks}; - - let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); - - $internal_key - .into_descriptor_key() - .and_then(|key: DescriptorKey| key.extract(&secp)) - .map_err($crate::descriptor::DescriptorError::Key) - .and_then(|(pk, mut key_map, mut valid_networks)| { - let tap_tree = $tap_tree.map( - |(tap_tree, tree_keymap, tree_networks): ( - TapTree, - KeyMap, - ValidNetworks, - )| { - key_map.extend(tree_keymap.into_iter()); - valid_networks = - $crate::keys::merge_networks(&valid_networks, &tree_networks); - - tap_tree - }, - ); - - Ok(( - Descriptor::::Tr(Tr::new(pk, tap_tree)?), - key_map, - valid_networks, - )) - }) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_leaf_opcode { - ( $terminal_variant:ident ) => {{ - use $crate::descriptor::CheckMiniscript; - - $crate::miniscript::Miniscript::from_ast( - $crate::miniscript::miniscript::decode::Terminal::$terminal_variant, - ) - .map_err($crate::descriptor::DescriptorError::Miniscript) - .and_then(|minisc| { - minisc.check_miniscript()?; - Ok(minisc) - }) - .map(|minisc| { - ( - minisc, - $crate::miniscript::descriptor::KeyMap::default(), - $crate::keys::any_network(), - ) - }) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_leaf_opcode_value { - ( $terminal_variant:ident, $value:expr ) => {{ - use $crate::descriptor::CheckMiniscript; - - $crate::miniscript::Miniscript::from_ast( - $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($value), - ) - .map_err($crate::descriptor::DescriptorError::Miniscript) - .and_then(|minisc| { - minisc.check_miniscript()?; - Ok(minisc) - }) - .map(|minisc| { - ( - minisc, - $crate::miniscript::descriptor::KeyMap::default(), - $crate::keys::any_network(), - ) - }) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_leaf_opcode_value_two { - ( $terminal_variant:ident, $one:expr, $two:expr ) => {{ - use $crate::descriptor::CheckMiniscript; - - $crate::miniscript::Miniscript::from_ast( - $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($one, $two), - ) - .map_err($crate::descriptor::DescriptorError::Miniscript) - .and_then(|minisc| { - minisc.check_miniscript()?; - Ok(minisc) - }) - .map(|minisc| { - ( - minisc, - $crate::miniscript::descriptor::KeyMap::default(), - $crate::keys::any_network(), - ) - }) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_node_opcode_two { - ( $terminal_variant:ident, $( $inner:tt )* ) => ({ - use $crate::descriptor::CheckMiniscript; - - let inner = $crate::fragment_internal!( @t $( $inner )* ); - let (a, b) = $crate::descriptor::dsl::TupleTwo::from(inner).flattened(); - - a - .and_then(|a| Ok((a, b?))) - .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks))| { - // join key_maps - a_keymap.extend(b_keymap.into_iter()); - - let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant( - $crate::alloc::sync::Arc::new(a_minisc), - $crate::alloc::sync::Arc::new(b_minisc), - ))?; - - minisc.check_miniscript()?; - - Ok((minisc, a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks))) - }) - }); -} - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_node_opcode_three { - ( $terminal_variant:ident, $( $inner:tt )* ) => ({ - use $crate::descriptor::CheckMiniscript; - - let inner = $crate::fragment_internal!( @t $( $inner )* ); - let (a, b, c) = $crate::descriptor::dsl::TupleThree::from(inner).flattened(); - - a - .and_then(|a| Ok((a, b?, c?))) - .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks), (c_minisc, c_keymap, c_networks))| { - // join key_maps - a_keymap.extend(b_keymap.into_iter()); - a_keymap.extend(c_keymap.into_iter()); - - let networks = $crate::keys::merge_networks(&a_networks, &b_networks); - let networks = $crate::keys::merge_networks(&networks, &c_networks); - - let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant( - $crate::alloc::sync::Arc::new(a_minisc), - $crate::alloc::sync::Arc::new(b_minisc), - $crate::alloc::sync::Arc::new(c_minisc), - ))?; - - minisc.check_miniscript()?; - - Ok((minisc, a_keymap, networks)) - }) - }); -} - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_sortedmulti { - ( $build_desc:expr, sortedmulti_vec ( $thresh:expr, $keys:expr ) ) => ({ - let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); - $crate::keys::make_sortedmulti($thresh, $keys, $build_desc, &secp) - }); - ( $build_desc:expr, sortedmulti ( $thresh:expr $(, $key:expr )+ ) ) => ({ - use $crate::keys::IntoDescriptorKey; - let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); - - let keys = vec![ - $( - $key.into_descriptor_key(), - )* - ]; - - keys.into_iter().collect::, _>>() - .map_err($crate::descriptor::DescriptorError::Key) - .and_then(|keys| $crate::keys::make_sortedmulti($thresh, keys, $build_desc, &secp)) - }); - -} - -#[doc(hidden)] -#[macro_export] -macro_rules! parse_tap_tree { - ( @merge $tree_a:expr, $tree_b:expr) => {{ - use $crate::miniscript::descriptor::TapTree; - - $tree_a - .and_then(|tree_a| Ok((tree_a, $tree_b?))) - .and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| { - a_keymap.extend(b_keymap.into_iter()); - Ok((TapTree::combine(a_tree, b_tree), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks))) - }) - - }}; - - // Two sub-trees - ( { { $( $tree_a:tt )* }, { $( $tree_b:tt )* } } ) => {{ - let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } ); - let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } ); - - $crate::parse_tap_tree!(@merge tree_a, tree_b) - }}; - - // One leaf and a sub-tree - ( { $op_a:ident ( $( $minisc_a:tt )* ), { $( $tree_b:tt )* } } ) => {{ - let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) ); - let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } ); - - $crate::parse_tap_tree!(@merge tree_a, tree_b) - }}; - ( { { $( $tree_a:tt )* }, $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{ - let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } ); - let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) ); - - $crate::parse_tap_tree!(@merge tree_a, tree_b) - }}; - - // Two leaves - ( { $op_a:ident ( $( $minisc_a:tt )* ), $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{ - let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) ); - let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) ); - - $crate::parse_tap_tree!(@merge tree_a, tree_b) - }}; - - // Single leaf - ( $op:ident ( $( $minisc:tt )* ) ) => {{ - use $crate::alloc::sync::Arc; - use $crate::miniscript::descriptor::TapTree; - - $crate::fragment!( $op ( $( $minisc )* ) ) - .map(|(a_minisc, a_keymap, a_networks)| (TapTree::Leaf(Arc::new(a_minisc)), a_keymap, a_networks)) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! apply_modifier { - ( $terminal_variant:ident, $inner:expr ) => {{ - use $crate::descriptor::CheckMiniscript; - - $inner - .map_err(|e| -> $crate::descriptor::DescriptorError { e.into() }) - .and_then(|(minisc, keymap, networks)| { - let minisc = $crate::miniscript::Miniscript::from_ast( - $crate::miniscript::miniscript::decode::Terminal::$terminal_variant( - $crate::alloc::sync::Arc::new(minisc), - ), - )?; - - minisc.check_miniscript()?; - - Ok((minisc, keymap, networks)) - }) - }}; - - ( a: $inner:expr ) => {{ - $crate::apply_modifier!(Alt, $inner) - }}; - ( s: $inner:expr ) => {{ - $crate::apply_modifier!(Swap, $inner) - }}; - ( c: $inner:expr ) => {{ - $crate::apply_modifier!(Check, $inner) - }}; - ( d: $inner:expr ) => {{ - $crate::apply_modifier!(DupIf, $inner) - }}; - ( v: $inner:expr ) => {{ - $crate::apply_modifier!(Verify, $inner) - }}; - ( j: $inner:expr ) => {{ - $crate::apply_modifier!(NonZero, $inner) - }}; - ( n: $inner:expr ) => {{ - $crate::apply_modifier!(ZeroNotEqual, $inner) - }}; - - // Modifiers expanded to other operators - ( t: $inner:expr ) => {{ - $inner.and_then(|(a_minisc, a_keymap, a_networks)| { - $crate::impl_leaf_opcode_value_two!( - AndV, - $crate::alloc::sync::Arc::new(a_minisc), - $crate::alloc::sync::Arc::new($crate::fragment!(true).unwrap().0) - ) - .map(|(minisc, _, _)| (minisc, a_keymap, a_networks)) - }) - }}; - ( l: $inner:expr ) => {{ - $inner.and_then(|(a_minisc, a_keymap, a_networks)| { - $crate::impl_leaf_opcode_value_two!( - OrI, - $crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0), - $crate::alloc::sync::Arc::new(a_minisc) - ) - .map(|(minisc, _, _)| (minisc, a_keymap, a_networks)) - }) - }}; - ( u: $inner:expr ) => {{ - $inner.and_then(|(a_minisc, a_keymap, a_networks)| { - $crate::impl_leaf_opcode_value_two!( - OrI, - $crate::alloc::sync::Arc::new(a_minisc), - $crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0) - ) - .map(|(minisc, _, _)| (minisc, a_keymap, a_networks)) - }) - }}; -} - -/// Macro to write full descriptors with code -/// -/// This macro expands to a `Result` of -/// [`DescriptorTemplateOut`](super::template::DescriptorTemplateOut) and [`DescriptorError`](crate::descriptor::DescriptorError) -/// -/// The syntax is very similar to the normal descriptor syntax, with the exception that modifiers -/// cannot be grouped together. For instance, a descriptor fragment like `sdv:older(144)` has to be -/// broken up to `s:d:v:older(144)`. -/// -/// The `pk()`, `pk_k()` and `pk_h()` operands can take as argument any type that implements -/// [`IntoDescriptorKey`]. This means that keys can also be written inline as strings, but in that -/// case they must be wrapped in quotes, which is another difference compared to the standard -/// descriptor syntax. -/// -/// [`IntoDescriptorKey`]: crate::keys::IntoDescriptorKey -/// -/// ## Example -/// -/// Signature plus timelock descriptor: -/// -/// ``` -/// # use std::str::FromStr; -/// let (my_descriptor, my_keys_map, networks) = bdk::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?; -/// # Ok::<(), Box>(()) -/// ``` -/// -/// ------- -/// -/// 2-of-3 that becomes a 1-of-3 after a timelock has expired. Both `descriptor_a` and `descriptor_b` are equivalent: the first -/// syntax is more suitable for a fixed number of items known at compile time, while the other accepts a -/// [`Vec`] of items, which makes it more suitable for writing dynamic descriptors. -/// -/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sndv:older(...)))` -/// -/// ``` -/// # use std::str::FromStr; -/// let my_key_1 = bitcoin::PublicKey::from_str( -/// "02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c", -/// )?; -/// let my_key_2 = -/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; -/// let my_timelock = 50; -/// -/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! { -/// wsh ( -/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n:d:v:older(my_timelock)) -/// ) -/// }?; -/// -/// #[rustfmt::skip] -/// let b_items = vec![ -/// bdk::fragment!(pk(my_key_1))?, -/// bdk::fragment!(s:pk(my_key_2))?, -/// bdk::fragment!(s:n:d:v:older(my_timelock))?, -/// ]; -/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?; -/// -/// assert_eq!(descriptor_a, descriptor_b); -/// assert_eq!(key_map_a.len(), key_map_b.len()); -/// # Ok::<(), Box>(()) -/// ``` -/// -/// ------ -/// -/// Simple 2-of-2 multi-signature, equivalent to: `wsh(multi(2, ...))` -/// -/// ``` -/// # use std::str::FromStr; -/// let my_key_1 = bitcoin::PublicKey::from_str( -/// "02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c", -/// )?; -/// let my_key_2 = -/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; -/// -/// let (descriptor, key_map, networks) = bdk::descriptor! { -/// wsh ( -/// multi(2, my_key_1, my_key_2) -/// ) -/// }?; -/// # Ok::<(), Box>(()) -/// ``` -/// -/// ------ -/// -/// Native-Segwit single-sig, equivalent to: `wpkh(...)` -/// -/// ``` -/// let my_key = -/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; -/// -/// let (descriptor, key_map, networks) = bdk::descriptor!(wpkh(my_key))?; -/// # Ok::<(), Box>(()) -/// ``` -/// -/// [`Vec`]: alloc::vec::Vec -#[macro_export] -macro_rules! descriptor { - ( bare ( $( $minisc:tt )* ) ) => ({ - $crate::impl_top_level_sh!(Bare, new, new, Legacy, $( $minisc )*) - }); - ( sh ( wsh ( $( $minisc:tt )* ) ) ) => ({ - $crate::descriptor!(shwsh ($( $minisc )*)) - }); - ( shwsh ( $( $minisc:tt )* ) ) => ({ - $crate::impl_top_level_sh!(Sh, new_wsh, new_wsh_sortedmulti, Segwitv0, $( $minisc )*) - }); - ( pk ( $key:expr ) ) => ({ - // `pk()` is actually implemented as `bare(pk())` - $crate::descriptor!( bare ( pk ( $key ) ) ) - }); - ( pkh ( $key:expr ) ) => ({ - use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey}; - - $crate::impl_top_level_pk!(Pkh, $crate::miniscript::Legacy, $key) - .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c))) - .map(|(a, b, c)| (Descriptor::::Pkh(a), b, c)) - }); - ( wpkh ( $key:expr ) ) => ({ - use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey}; - - $crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key) - .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c))) - .map(|(a, b, c)| (Descriptor::::Wpkh(a), b, c)) - }); - ( sh ( wpkh ( $key:expr ) ) ) => ({ - $crate::descriptor!(shwpkh ( $key )) - }); - ( shwpkh ( $key:expr ) ) => ({ - use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, Sh}; - - $crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key) - .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c))) - .and_then(|(a, b, c)| Ok((Descriptor::::Sh(Sh::new_wpkh(a.into_inner())?), b, c))) - }); - ( sh ( $( $minisc:tt )* ) ) => ({ - $crate::impl_top_level_sh!(Sh, new, new_sortedmulti, Legacy, $( $minisc )*) - }); - ( wsh ( $( $minisc:tt )* ) ) => ({ - $crate::impl_top_level_sh!(Wsh, new, new_sortedmulti, Segwitv0, $( $minisc )*) - }); - - ( tr ( $internal_key:expr ) ) => ({ - $crate::impl_top_level_tr!($internal_key, None) - }); - ( tr ( $internal_key:expr, $( $taptree:tt )* ) ) => ({ - let tap_tree = $crate::parse_tap_tree!( $( $taptree )* ); - tap_tree - .and_then(|tap_tree| $crate::impl_top_level_tr!($internal_key, Some(tap_tree))) - }); -} - -#[doc(hidden)] -pub struct TupleTwo { - pub a: A, - pub b: B, -} - -impl TupleTwo { - pub fn flattened(self) -> (A, B) { - (self.a, self.b) - } -} - -impl From<(A, (B, ()))> for TupleTwo { - fn from((a, (b, _)): (A, (B, ()))) -> Self { - TupleTwo { a, b } - } -} - -#[doc(hidden)] -pub struct TupleThree { - pub a: A, - pub b: B, - pub c: C, -} - -impl TupleThree { - pub fn flattened(self) -> (A, B, C) { - (self.a, self.b, self.c) - } -} - -impl From<(A, (B, (C, ())))> for TupleThree { - fn from((a, (b, (c, _))): (A, (B, (C, ())))) -> Self { - TupleThree { a, b, c } - } -} - -#[doc(hidden)] -#[macro_export] -macro_rules! group_multi_keys { - ( $( $key:expr ),+ ) => {{ - use $crate::keys::IntoDescriptorKey; - - let keys = vec![ - $( - $key.into_descriptor_key(), - )* - ]; - - keys.into_iter().collect::, _>>() - .map_err($crate::descriptor::DescriptorError::Key) - }}; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! fragment_internal { - // The @v prefix is used to parse a sequence of operands and return them in a vector. This is - // used by operands that take a variable number of arguments, like `thresh()` and `multi()`. - ( @v $op:ident ( $( $args:tt )* ) $( $tail:tt )* ) => ({ - let mut v = vec![$crate::fragment!( $op ( $( $args )* ) )]; - v.append(&mut $crate::fragment_internal!( @v $( $tail )* )); - - v - }); - // Match modifiers - ( @v $modif:tt : $( $tail:tt )* ) => ({ - let mut v = $crate::fragment_internal!( @v $( $tail )* ); - let first = v.drain(..1).next().unwrap(); - - let first = $crate::apply_modifier!($modif:first); - - let mut v_final = vec![first]; - v_final.append(&mut v); - - v_final - }); - // Remove commas between operands - ( @v , $( $tail:tt )* ) => ({ - $crate::fragment_internal!( @v $( $tail )* ) - }); - ( @v ) => ({ - vec![] - }); - - // The @t prefix is used to parse a sequence of operands and return them in a tuple. This - // allows checking at compile-time the number of arguments passed to an operand. For this - // reason it's used by `and_*()`, `or_*()`, etc. - // - // Unfortunately, due to the fact that concatenating tuples is pretty hard, the final result - // adds in the first spot the parsed operand and in the second spot the result of parsing - // all the following ones. For two operands the type then corresponds to: (X, (X, ())). For - // three operands it's (X, (X, (X, ()))), etc. - // - // To check that the right number of arguments has been passed we can "cast" those tuples to - // more convenient structures like `TupleTwo`. If the conversion succeeds, the right number of - // args was passed. Otherwise the compilation fails entirely. - ( @t $op:ident ( $( $args:tt )* ) $( $tail:tt )* ) => ({ - ($crate::fragment!( $op ( $( $args )* ) ), $crate::fragment_internal!( @t $( $tail )* )) - }); - // Match modifiers - ( @t $modif:tt : $( $tail:tt )* ) => ({ - let (first, tail) = $crate::fragment_internal!( @t $( $tail )* ); - ($crate::apply_modifier!($modif:first), tail) - }); - // Remove commas between operands - ( @t , $( $tail:tt )* ) => ({ - $crate::fragment_internal!( @t $( $tail )* ) - }); - ( @t ) => ({}); - - // Fallback to calling `fragment!()` - ( $( $tokens:tt )* ) => ({ - $crate::fragment!($( $tokens )*) - }); -} - -/// Macro to write descriptor fragments with code -/// -/// This macro will be expanded to an object of type `Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError>`. It allows writing -/// fragments of larger descriptors that can be pieced together using `fragment!(thresh_vec(m, ...))`. -/// -/// The syntax to write macro fragment is the same as documented for the [`descriptor`] macro. -#[macro_export] -macro_rules! fragment { - // Modifiers - ( $modif:tt : $( $tail:tt )* ) => ({ - let op = $crate::fragment!( $( $tail )* ); - $crate::apply_modifier!($modif:op) - }); - - // Miniscript - ( true ) => ({ - $crate::impl_leaf_opcode!(True) - }); - ( false ) => ({ - $crate::impl_leaf_opcode!(False) - }); - ( pk_k ( $key:expr ) ) => ({ - let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); - $crate::keys::make_pk($key, &secp) - }); - ( pk ( $key:expr ) ) => ({ - $crate::fragment!(c:pk_k ( $key )) - }); - ( pk_h ( $key:expr ) ) => ({ - let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); - $crate::keys::make_pkh($key, &secp) - }); - ( after ( $value:expr ) ) => ({ - $crate::impl_leaf_opcode_value!(After, $crate::miniscript::AbsLockTime::from_consensus($value)) - }); - ( older ( $value:expr ) ) => ({ - $crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!! - }); - ( sha256 ( $hash:expr ) ) => ({ - $crate::impl_leaf_opcode_value!(Sha256, $hash) - }); - ( hash256 ( $hash:expr ) ) => ({ - $crate::impl_leaf_opcode_value!(Hash256, $hash) - }); - ( ripemd160 ( $hash:expr ) ) => ({ - $crate::impl_leaf_opcode_value!(Ripemd160, $hash) - }); - ( hash160 ( $hash:expr ) ) => ({ - $crate::impl_leaf_opcode_value!(Hash160, $hash) - }); - ( and_v ( $( $inner:tt )* ) ) => ({ - $crate::impl_node_opcode_two!(AndV, $( $inner )*) - }); - ( and_b ( $( $inner:tt )* ) ) => ({ - $crate::impl_node_opcode_two!(AndB, $( $inner )*) - }); - ( and_or ( $( $inner:tt )* ) ) => ({ - $crate::impl_node_opcode_three!(AndOr, $( $inner )*) - }); - ( andor ( $( $inner:tt )* ) ) => ({ - $crate::impl_node_opcode_three!(AndOr, $( $inner )*) - }); - ( or_b ( $( $inner:tt )* ) ) => ({ - $crate::impl_node_opcode_two!(OrB, $( $inner )*) - }); - ( or_d ( $( $inner:tt )* ) ) => ({ - $crate::impl_node_opcode_two!(OrD, $( $inner )*) - }); - ( or_c ( $( $inner:tt )* ) ) => ({ - $crate::impl_node_opcode_two!(OrC, $( $inner )*) - }); - ( or_i ( $( $inner:tt )* ) ) => ({ - $crate::impl_node_opcode_two!(OrI, $( $inner )*) - }); - ( thresh_vec ( $thresh:expr, $items:expr ) ) => ({ - use $crate::miniscript::descriptor::KeyMap; - - let (items, key_maps_networks): ($crate::alloc::vec::Vec<_>, $crate::alloc::vec::Vec<_>) = $items.into_iter().map(|(a, b, c)| (a, (b, c))).unzip(); - let items = items.into_iter().map($crate::alloc::sync::Arc::new).collect(); - - let (key_maps, valid_networks) = key_maps_networks.into_iter().fold((KeyMap::default(), $crate::keys::any_network()), |(mut keys_acc, net_acc), (key, net)| { - keys_acc.extend(key.into_iter()); - let net_acc = $crate::keys::merge_networks(&net_acc, &net); - - (keys_acc, net_acc) - }); - - $crate::impl_leaf_opcode_value_two!(Thresh, $thresh, items) - .map(|(minisc, _, _)| (minisc, key_maps, valid_networks)) - }); - ( thresh ( $thresh:expr, $( $inner:tt )* ) ) => ({ - let items = $crate::fragment_internal!( @v $( $inner )* ); - - items.into_iter().collect::, _>>() - .and_then(|items| $crate::fragment!(thresh_vec($thresh, items))) - }); - ( multi_vec ( $thresh:expr, $keys:expr ) ) => ({ - let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); - - $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp) - }); - ( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({ - $crate::group_multi_keys!( $( $key ),* ) - .and_then(|keys| $crate::fragment!( multi_vec ( $thresh, keys ) )) - }); - ( multi_a_vec ( $thresh:expr, $keys:expr ) ) => ({ - let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); - - $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::MultiA, $keys, &secp) - }); - ( multi_a ( $thresh:expr $(, $key:expr )+ ) ) => ({ - $crate::group_multi_keys!( $( $key ),* ) - .and_then(|keys| $crate::fragment!( multi_a_vec ( $thresh, keys ) )) - }); - - // `sortedmulti()` is handled separately - ( sortedmulti ( $( $inner:tt )* ) ) => ({ - compile_error!("`sortedmulti` can only be used as the root operand of a descriptor"); - }); - ( sortedmulti_vec ( $( $inner:tt )* ) ) => ({ - compile_error!("`sortedmulti_vec` can only be used as the root operand of a descriptor"); - }); -} - -#[cfg(test)] -mod test { - use alloc::string::ToString; - use bitcoin::secp256k1::Secp256k1; - use miniscript::descriptor::{DescriptorPublicKey, KeyMap}; - use miniscript::{Descriptor, Legacy, Segwitv0}; - - use core::str::FromStr; - - use crate::descriptor::{DescriptorError, DescriptorMeta}; - use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks}; - use bitcoin::bip32; - use bitcoin::Network::{Bitcoin, Regtest, Signet, Testnet}; - use bitcoin::PrivateKey; - - // test the descriptor!() macro - - // verify descriptor generates expected script(s) (if bare or pk) or address(es) - fn check( - desc: Result<(Descriptor, KeyMap, ValidNetworks), DescriptorError>, - is_witness: bool, - is_fixed: bool, - expected: &[&str], - ) { - let (desc, _key_map, _networks) = desc.unwrap(); - assert_eq!(desc.is_witness(), is_witness); - assert_eq!(!desc.has_wildcard(), is_fixed); - for i in 0..expected.len() { - let child_desc = desc - .at_derivation_index(i as u32) - .expect("i is not hardened"); - let address = child_desc.address(Regtest); - if let Ok(address) = address { - assert_eq!(address.to_string(), *expected.get(i).unwrap()); - } else { - let script = child_desc.script_pubkey(); - assert_eq!(script.to_hex_string(), *expected.get(i).unwrap()); - } - } - } - - // - at least one of each "type" of operator; i.e. one modifier, one leaf_opcode, one leaf_opcode_value, etc. - // - mixing up key types that implement IntoDescriptorKey in multi() or thresh() - - // expected script for pk and bare manually created - // expected addresses created with `bitcoin-cli getdescriptorinfo` (for hash) and `bitcoin-cli deriveaddresses` - - #[test] - fn test_fixed_legacy_descriptors() { - let pubkey1 = bitcoin::PublicKey::from_str( - "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", - ) - .unwrap(); - let pubkey2 = bitcoin::PublicKey::from_str( - "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", - ) - .unwrap(); - - check( - descriptor!(bare(multi(1,pubkey1,pubkey2))), - false, - true, - &["512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd21032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af52ae"], - ); - check( - descriptor!(pk(pubkey1)), - false, - true, - &["2103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac"], - ); - check( - descriptor!(pkh(pubkey1)), - false, - true, - &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"], - ); - check( - descriptor!(sh(multi(1, pubkey1, pubkey2))), - false, - true, - &["2MymURoV1bzuMnWMGiXzyomDkeuxXY7Suey"], - ); - } - - #[test] - fn test_fixed_segwitv0_descriptors() { - let pubkey1 = bitcoin::PublicKey::from_str( - "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", - ) - .unwrap(); - let pubkey2 = bitcoin::PublicKey::from_str( - "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", - ) - .unwrap(); - - check( - descriptor!(wpkh(pubkey1)), - true, - true, - &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"], - ); - check( - descriptor!(sh(wpkh(pubkey1))), - true, - true, - &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"], - ); - check( - descriptor!(wsh(multi(1, pubkey1, pubkey2))), - true, - true, - &["bcrt1qgw8jvv2hsrvjfa6q66rk6har7d32lrqm5unnf5cl63q9phxfvgps5fyfqe"], - ); - check( - descriptor!(sh(wsh(multi(1, pubkey1, pubkey2)))), - true, - true, - &["2NCidRJysy7apkmE6JF5mLLaJFkrN3Ub9iy"], - ); - } - - #[test] - fn test_fixed_threeop_descriptors() { - let redeem_key = bitcoin::PublicKey::from_str( - "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", - ) - .unwrap(); - let move_key = bitcoin::PublicKey::from_str( - "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", - ) - .unwrap(); - - check( - descriptor!(sh(wsh(and_or(pk(redeem_key), older(1000), pk(move_key))))), - true, - true, - &["2MypGwr5eQWAWWJtiJgUEToVxc4zuokjQRe"], - ); - } - - #[test] - fn test_bip32_legacy_descriptors() { - let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - - let path = bip32::DerivationPath::from_str("m/0").unwrap(); - let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); - check( - descriptor!(pk(desc_key)), - false, - false, - &[ - "2102363ad03c10024e1b597a5b01b9982807fb638e00b06f3b2d4a89707de3b93c37ac", - "2102063a21fd780df370ed2fc8c4b86aa5ea642630609c203009df631feb7b480dd2ac", - "2102ba2685ad1fa5891cb100f1656b2ce3801822ccb9bac0336734a6f8c1b93ebbc0ac", - ], - ); - - let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); - check( - descriptor!(pkh(desc_key)), - false, - false, - &[ - "muvBdsVpJxpFuTHMKA47htJPdCvdt4F9DP", - "mxQSHK7DL2t1DN3xFxov1janCoXSSkrSPj", - "mfz43r15GiWo4nizmyzMNubsnkDpByFFAn", - ], - ); - - let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); - let desc_key1 = (xprv, path).into_descriptor_key().unwrap(); - let desc_key2 = (xprv, path2).into_descriptor_key().unwrap(); - - check( - descriptor!(sh(multi(1, desc_key1, desc_key2))), - false, - false, - &[ - "2MtMDXsfwefZkEEhVViEPidvcKRUtJamJJ8", - "2MwAUZ1NYyWjhVvGTethFL6n7nZhS8WE6At", - "2MuT6Bj66HLwZd7s4SoD8XbK4GwriKEA6Gr", - ], - ); - } - - #[test] - fn test_bip32_segwitv0_descriptors() { - let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - - let path = bip32::DerivationPath::from_str("m/0").unwrap(); - let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); - check( - descriptor!(wpkh(desc_key)), - true, - false, - &[ - "bcrt1qnhm8w9fhc8cxzgqsmqdf9fyjccyvc0gltnymu0", - "bcrt1qhylfd55rn75w9fj06zspctad5w4hz33rf0ttad", - "bcrt1qq5sq3a6k9av9d8cne0k9wcldy4nqey5yt6889r", - ], - ); - - let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); - check( - descriptor!(sh(wpkh(desc_key))), - true, - false, - &[ - "2MxvjQCaLqZ5QxZ7XotZDQ63hZw3NPss763", - "2NDUoevN4QMzhvHDMGhKuiT2fN9HXbFRMwn", - "2NF4BEAY2jF1Fu8vqfN3NVKoFtom77pUxrx", - ], - ); - - let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); - let desc_key1 = (xprv, path.clone()).into_descriptor_key().unwrap(); - let desc_key2 = (xprv, path2.clone()).into_descriptor_key().unwrap(); - check( - descriptor!(wsh(multi(1, desc_key1, desc_key2))), - true, - false, - &[ - "bcrt1qfxv8mxmlv5sz8q2mnuyaqdfe9jr4vvmx0csjhn092p6f4qfygfkq2hng49", - "bcrt1qerj85g243e6jlcdxpmn9spk0gefcwvu7nw7ee059d5ydzpdhkm2qwfkf5k", - "bcrt1qxkl2qss3k58q9ktc8e89pwr4gnptfpw4hju4xstxcjc0hkcae3jstluty7", - ], - ); - - let desc_key1 = (xprv, path).into_descriptor_key().unwrap(); - let desc_key2 = (xprv, path2).into_descriptor_key().unwrap(); - check( - descriptor!(sh(wsh(multi(1, desc_key1, desc_key2)))), - true, - false, - &[ - "2NFCtXvx9q4ci2kvKub17iSTgvRXGctCGhz", - "2NB2PrFPv5NxWCpygas8tPrGJG2ZFgeuwJw", - "2N79ZAGo5cMi5Jt7Wo9L5YmF5GkEw7sjWdC", - ], - ); - } - - #[test] - fn test_dsl_sortedmulti() { - let key_1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let path_1 = bip32::DerivationPath::from_str("m/0").unwrap(); - - let key_2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap(); - let path_2 = bip32::DerivationPath::from_str("m/1").unwrap(); - - let desc_key1 = (key_1, path_1); - let desc_key2 = (key_2, path_2); - - check( - descriptor!(sh(sortedmulti(1, desc_key1.clone(), desc_key2.clone()))), - false, - false, - &[ - "2MsxzPEJDBzpGffJXPaDpfXZAUNnZhaMh2N", - "2My3x3DLPK3UbGWGpxrXr1RnbD8MNC4FpgS", - "2NByEuiQT7YLqHCTNxL5KwYjvtuCYcXNBSC", - "2N1TGbP81kj2VUKTSWgrwxoMfuWjvfUdyu7", - "2N3Bomq2fpAcLRNfZnD3bCWK9quan28CxCR", - "2N9nrZaEzEFDqEAU9RPvDnXGT6AVwBDKAQb", - ], - ); - - check( - descriptor!(sh(wsh(sortedmulti( - 1, - desc_key1.clone(), - desc_key2.clone() - )))), - true, - false, - &[ - "2NCogc5YyM4N6ruv1hUa7WLMW1BPeCK7N9B", - "2N6mkSAKi1V2oaBXby7XHdvBMKEDRQcFpNe", - "2NFmTSttm9v6bXeoWaBvpMcgfPQcZhNn3Eh", - "2Mvib87RBPUHXNEpX5S5Kv1qqrhBfgBGsJM", - "2MtMv5mcK2EjcLsH8Txpx2JxLLzHr4ttczL", - "2MsWCB56rb4T6yPv8QudZGHERTwNgesE4f6", - ], - ); - - check( - descriptor!(wsh(sortedmulti_vec(1, vec![desc_key1, desc_key2]))), - true, - false, - &[ - "bcrt1qcvq0lg8q7a47ytrd7zk5y7uls7mulrenjgvflwylpppgwf8029es4vhpnj", - "bcrt1q80yn8sdt6l7pjvkz25lglyaqctlmsq9ugk80rmxt8yu0npdsj97sc7l4de", - "bcrt1qrvf6024v9s50qhffe3t2fr2q9ckdhx2g6jz32chm2pp24ymgtr5qfrdmct", - "bcrt1q6srfmra0ynypym35c7jvsxt2u4yrugeajq95kg2ps7lk6h2gaunsq9lzxn", - "bcrt1qhl8rrzzcdpu7tcup3lcg7tge52sqvwy5fcv4k78v6kxtwmqf3v6qpvyjza", - "bcrt1ql2elz9mhm9ll27ddpewhxs732xyl2fk2kpkqz9gdyh33wgcun4vstrd49k", - ], - ); - } - - // - verify the valid_networks returned is correctly computed based on the keys present in the descriptor - #[test] - fn test_valid_networks() { - let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let path = bip32::DerivationPath::from_str("m/0").unwrap(); - let desc_key = (xprv, path).into_descriptor_key().unwrap(); - - let (_desc, _key_map, valid_networks) = descriptor!(pkh(desc_key)).unwrap(); - assert_eq!( - valid_networks, - [Testnet, Regtest, Signet].iter().cloned().collect() - ); - - let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap(); - let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap(); - let desc_key = (xprv, path).into_descriptor_key().unwrap(); - - let (_desc, _key_map, valid_networks) = descriptor!(wpkh(desc_key)).unwrap(); - assert_eq!(valid_networks, [Bitcoin].iter().cloned().collect()); - } - - // - verify the key_maps are correctly merged together - #[test] - fn test_key_maps_merged() { - let secp = Secp256k1::new(); - - let xprv1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let path1 = bip32::DerivationPath::from_str("m/0").unwrap(); - let desc_key1 = (xprv1, path1.clone()).into_descriptor_key().unwrap(); - - let xprv2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap(); - let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); - let desc_key2 = (xprv2, path2.clone()).into_descriptor_key().unwrap(); - - let xprv3 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap(); - let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap(); - let desc_key3 = (xprv3, path3.clone()).into_descriptor_key().unwrap(); - - let (_desc, key_map, _valid_networks) = - descriptor!(sh(wsh(multi(2, desc_key1, desc_key2, desc_key3)))).unwrap(); - assert_eq!(key_map.len(), 3); - - let desc_key1: DescriptorKey = (xprv1, path1).into_descriptor_key().unwrap(); - let desc_key2: DescriptorKey = (xprv2, path2).into_descriptor_key().unwrap(); - let desc_key3: DescriptorKey = (xprv3, path3).into_descriptor_key().unwrap(); - - let (key1, _key_map, _valid_networks) = desc_key1.extract(&secp).unwrap(); - let (key2, _key_map, _valid_networks) = desc_key2.extract(&secp).unwrap(); - let (key3, _key_map, _valid_networks) = desc_key3.extract(&secp).unwrap(); - assert_eq!(key_map.get(&key1).unwrap().to_string(), "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*"); - assert_eq!(key_map.get(&key2).unwrap().to_string(), "tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF/2147483647'/0/*"); - assert_eq!(key_map.get(&key3).unwrap().to_string(), "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf/10/20/30/40/*"); - } - - // - verify the ScriptContext is correctly validated (i.e. passing a type that only impl IntoDescriptorKey to a pkh() descriptor should throw a compilation error - #[test] - fn test_script_context_validation() { - // this compiles - let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let path = bip32::DerivationPath::from_str("m/0").unwrap(); - let desc_key: DescriptorKey = (xprv, path).into_descriptor_key().unwrap(); - - let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap(); - assert_eq!(desc.to_string(), "pkh(tpubD6NzVbkrYhZ4WR7a4vY1VT3khMJMeAxVsfq9TBJyJWrNk247zCJtV7AWf6UJP7rAVsn8NNKdJi3gFyKPTmWZS9iukb91xbn2HbFSMQm2igY/0/*)#yrnz9pp2"); - - // as expected this does not compile due to invalid context - //let desc_key:DescriptorKey = (xprv, path.clone()).into_descriptor_key().unwrap(); - //let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap(); - } - - #[test] - fn test_dsl_modifiers() { - let private_key = - PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); - let (descriptor, _, _) = - descriptor!(wsh(thresh(2,n:d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap(); - - assert_eq!(descriptor.to_string(), "wsh(thresh(2,ndv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#zzk3ux8g") - } - - #[test] - #[should_panic(expected = "Miniscript(ContextError(UncompressedKeysNotAllowed))")] - fn test_dsl_miniscript_checks() { - let mut uncompressed_pk = - PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap(); - uncompressed_pk.compressed = false; - - descriptor!(wsh(v: pk(uncompressed_pk))).unwrap(); - } - - #[test] - fn test_dsl_tr_only_key() { - let private_key = - PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); - let (descriptor, _, _) = descriptor!(tr(private_key)).unwrap(); - - assert_eq!( - descriptor.to_string(), - "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)#heq9m95v" - ) - } - - #[test] - fn test_dsl_tr_simple_tree() { - let private_key = - PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); - let (descriptor, _, _) = - descriptor!(tr(private_key, { pk(private_key), pk(private_key) })).unwrap(); - - assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,{pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)})#xy5fjw6d") - } - - #[test] - fn test_dsl_tr_single_leaf() { - let private_key = - PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); - let (descriptor, _, _) = descriptor!(tr(private_key, pk(private_key))).unwrap(); - - assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c))#lzl2vmc7") - } -} diff --git a/crates/bdk/src/descriptor/error.rs b/crates/bdk/src/descriptor/error.rs deleted file mode 100644 index b2809f21..00000000 --- a/crates/bdk/src/descriptor/error.rs +++ /dev/null @@ -1,123 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Descriptor errors -use core::fmt; - -/// Errors related to the parsing and usage of descriptors -#[derive(Debug)] -pub enum Error { - /// Invalid HD Key path, such as having a wildcard but a length != 1 - InvalidHdKeyPath, - /// The provided descriptor doesn't match its checksum - InvalidDescriptorChecksum, - /// The descriptor contains hardened derivation steps on public extended keys - HardenedDerivationXpub, - /// The descriptor contains multipath keys - MultiPath, - - /// Error thrown while working with [`keys`](crate::keys) - Key(crate::keys::KeyError), - /// Error while extracting and manipulating policies - Policy(crate::descriptor::policy::PolicyError), - - /// Invalid byte found in the descriptor checksum - InvalidDescriptorCharacter(u8), - - /// BIP32 error - Bip32(bitcoin::bip32::Error), - /// Error during base58 decoding - Base58(bitcoin::base58::Error), - /// Key-related error - Pk(bitcoin::key::Error), - /// Miniscript error - Miniscript(miniscript::Error), - /// Hex decoding error - Hex(bitcoin::hex::HexToBytesError), -} - -impl From for Error { - fn from(key_error: crate::keys::KeyError) -> Error { - match key_error { - crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner), - crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner), - e => Error::Key(e), - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InvalidHdKeyPath => write!(f, "Invalid HD key path"), - Self::InvalidDescriptorChecksum => { - write!(f, "The provided descriptor doesn't match its checksum") - } - Self::HardenedDerivationXpub => write!( - f, - "The descriptor contains hardened derivation steps on public extended keys" - ), - Self::MultiPath => write!( - f, - "The descriptor contains multipath keys, which are not supported yet" - ), - Self::Key(err) => write!(f, "Key error: {}", err), - Self::Policy(err) => write!(f, "Policy error: {}", err), - Self::InvalidDescriptorCharacter(char) => { - write!(f, "Invalid descriptor character: {}", char) - } - Self::Bip32(err) => write!(f, "BIP32 error: {}", err), - Self::Base58(err) => write!(f, "Base58 error: {}", err), - Self::Pk(err) => write!(f, "Key-related error: {}", err), - Self::Miniscript(err) => write!(f, "Miniscript error: {}", err), - Self::Hex(err) => write!(f, "Hex decoding error: {}", err), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for Error {} - -impl From for Error { - fn from(err: bitcoin::bip32::Error) -> Self { - Error::Bip32(err) - } -} - -impl From for Error { - fn from(err: bitcoin::base58::Error) -> Self { - Error::Base58(err) - } -} - -impl From for Error { - fn from(err: bitcoin::key::Error) -> Self { - Error::Pk(err) - } -} - -impl From for Error { - fn from(err: miniscript::Error) -> Self { - Error::Miniscript(err) - } -} - -impl From for Error { - fn from(err: bitcoin::hex::HexToBytesError) -> Self { - Error::Hex(err) - } -} - -impl From for Error { - fn from(err: crate::descriptor::policy::PolicyError) -> Self { - Error::Policy(err) - } -} diff --git a/crates/bdk/src/descriptor/mod.rs b/crates/bdk/src/descriptor/mod.rs deleted file mode 100644 index 4b1135fe..00000000 --- a/crates/bdk/src/descriptor/mod.rs +++ /dev/null @@ -1,900 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Descriptors -//! -//! This module contains generic utilities to work with descriptors, plus some re-exported types -//! from [`miniscript`]. - -use crate::collections::BTreeMap; -use alloc::string::String; -use alloc::vec::Vec; - -use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, KeySource, Xpub}; -use bitcoin::{key::XOnlyPublicKey, secp256k1, PublicKey}; -use bitcoin::{psbt, taproot}; -use bitcoin::{Network, TxOut}; - -use miniscript::descriptor::{ - DefiniteDescriptorKey, DescriptorMultiXKey, DescriptorSecretKey, DescriptorType, - DescriptorXKey, InnerXKey, KeyMap, SinglePubKey, Wildcard, -}; -pub use miniscript::{ - Descriptor, DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0, -}; -use miniscript::{ForEachKey, MiniscriptKey, TranslatePk}; - -use crate::descriptor::policy::BuildSatisfaction; - -pub mod checksum; -#[doc(hidden)] -pub mod dsl; -pub mod error; -pub mod policy; -pub mod template; - -pub use self::checksum::calc_checksum; -use self::checksum::calc_checksum_bytes; -pub use self::error::Error as DescriptorError; -pub use self::policy::Policy; -use self::template::DescriptorTemplateOut; -use crate::keys::{IntoDescriptorKey, KeyError}; -use crate::wallet::signer::SignersContainer; -use crate::wallet::utils::SecpCtx; - -/// Alias for a [`Descriptor`] that can contain extended keys using [`DescriptorPublicKey`] -pub type ExtendedDescriptor = Descriptor; - -/// Alias for a [`Descriptor`] that contains extended **derived** keys -pub type DerivedDescriptor = Descriptor; - -/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or -/// [`psbt::Output`] -/// -/// [`psbt::Input`]: bitcoin::psbt::Input -/// [`psbt::Output`]: bitcoin::psbt::Output -pub type HdKeyPaths = BTreeMap; - -/// Alias for the type of maps that represent taproot key origins in a [`psbt::Input`] or -/// [`psbt::Output`] -/// -/// [`psbt::Input`]: bitcoin::psbt::Input -/// [`psbt::Output`]: bitcoin::psbt::Output -pub type TapKeyOrigins = BTreeMap, KeySource)>; - -/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`] -pub trait IntoWalletDescriptor { - /// Convert to wallet descriptor - fn into_wallet_descriptor( - self, - secp: &SecpCtx, - network: Network, - ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError>; -} - -impl IntoWalletDescriptor for &str { - fn into_wallet_descriptor( - self, - secp: &SecpCtx, - network: Network, - ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { - let descriptor = match self.split_once('#') { - Some((desc, original_checksum)) => { - let checksum = calc_checksum_bytes(desc)?; - if original_checksum.as_bytes() != checksum { - return Err(DescriptorError::InvalidDescriptorChecksum); - } - desc - } - None => self, - }; - - ExtendedDescriptor::parse_descriptor(secp, descriptor)? - .into_wallet_descriptor(secp, network) - } -} - -impl IntoWalletDescriptor for &String { - fn into_wallet_descriptor( - self, - secp: &SecpCtx, - network: Network, - ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { - self.as_str().into_wallet_descriptor(secp, network) - } -} - -impl IntoWalletDescriptor for ExtendedDescriptor { - fn into_wallet_descriptor( - self, - secp: &SecpCtx, - network: Network, - ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { - (self, KeyMap::default()).into_wallet_descriptor(secp, network) - } -} - -impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) { - fn into_wallet_descriptor( - self, - secp: &SecpCtx, - network: Network, - ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { - use crate::keys::DescriptorKey; - - struct Translator<'s, 'd> { - secp: &'s SecpCtx, - descriptor: &'d ExtendedDescriptor, - network: Network, - } - - impl<'s, 'd> miniscript::Translator - for Translator<'s, 'd> - { - fn pk(&mut self, pk: &DescriptorPublicKey) -> Result { - let secp = &self.secp; - - let (_, _, networks) = if self.descriptor.is_taproot() { - let descriptor_key: DescriptorKey = - pk.clone().into_descriptor_key()?; - descriptor_key.extract(secp)? - } else if self.descriptor.is_witness() { - let descriptor_key: DescriptorKey = - pk.clone().into_descriptor_key()?; - descriptor_key.extract(secp)? - } else { - let descriptor_key: DescriptorKey = - pk.clone().into_descriptor_key()?; - descriptor_key.extract(secp)? - }; - - if networks.contains(&self.network) { - Ok(Default::default()) - } else { - Err(DescriptorError::Key(KeyError::InvalidNetwork)) - } - } - fn sha256( - &mut self, - _sha256: &::Sha256, - ) -> Result { - Ok(Default::default()) - } - fn hash256( - &mut self, - _hash256: &::Hash256, - ) -> Result { - Ok(Default::default()) - } - fn ripemd160( - &mut self, - _ripemd160: &::Ripemd160, - ) -> Result { - Ok(Default::default()) - } - fn hash160( - &mut self, - _hash160: &::Hash160, - ) -> Result { - Ok(Default::default()) - } - } - - // check the network for the keys - use miniscript::TranslateErr; - match self.0.translate_pk(&mut Translator { - secp, - network, - descriptor: &self.0, - }) { - Ok(_) => {} - Err(TranslateErr::TranslatorErr(e)) => return Err(e), - Err(TranslateErr::OuterError(e)) => return Err(e.into()), - } - - Ok(self) - } -} - -impl IntoWalletDescriptor for DescriptorTemplateOut { - fn into_wallet_descriptor( - self, - _secp: &SecpCtx, - network: Network, - ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { - struct Translator { - network: Network, - } - - impl miniscript::Translator - for Translator - { - fn pk( - &mut self, - pk: &DescriptorPublicKey, - ) -> Result { - // workaround for xpubs generated by other key types, like bip39: since when the - // conversion is made one network has to be chosen, what we generally choose - // "mainnet", but then override the set of valid networks to specify that all of - // them are valid. here we reset the network to make sure the wallet struct gets a - // descriptor with the right network everywhere. - let pk = match pk { - DescriptorPublicKey::XPub(ref xpub) => { - let mut xpub = xpub.clone(); - xpub.xkey.network = self.network; - - DescriptorPublicKey::XPub(xpub) - } - other => other.clone(), - }; - - Ok(pk) - } - miniscript::translate_hash_clone!( - DescriptorPublicKey, - DescriptorPublicKey, - DescriptorError - ); - } - - let (desc, keymap, networks) = self; - - if !networks.contains(&network) { - return Err(DescriptorError::Key(KeyError::InvalidNetwork)); - } - - // fixup the network for keys that need it in the descriptor - use miniscript::TranslateErr; - let translated = match desc.translate_pk(&mut Translator { network }) { - Ok(descriptor) => descriptor, - Err(TranslateErr::TranslatorErr(e)) => return Err(e), - Err(TranslateErr::OuterError(e)) => return Err(e.into()), - }; - // ...and in the key map - let fixed_keymap = keymap - .into_iter() - .map(|(mut k, mut v)| { - match (&mut k, &mut v) { - (DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => { - xpub.xkey.network = network; - xprv.xkey.network = network; - } - (_, DescriptorSecretKey::Single(key)) => { - key.key.network = network; - } - _ => {} - } - - (k, v) - }) - .collect(); - - Ok((translated, fixed_keymap)) - } -} - -/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the -/// descriptor -pub(crate) fn into_wallet_descriptor_checked( - inner: T, - secp: &SecpCtx, - network: Network, -) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { - let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?; - - // Ensure the keys don't contain any hardened derivation steps or hardened wildcards - let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| { - if let DescriptorPublicKey::XPub(DescriptorXKey { - derivation_path, - wildcard, - .. - }) = k - { - return *wildcard == Wildcard::Hardened - || derivation_path.into_iter().any(ChildNumber::is_hardened); - } - - false - }); - if descriptor_contains_hardened_steps { - return Err(DescriptorError::HardenedDerivationXpub); - } - - if descriptor.is_multipath() { - return Err(DescriptorError::MultiPath); - } - - // Run miniscript's sanity check, which will look for duplicated keys and other potential - // issues - descriptor.sanity_check()?; - - Ok((descriptor, keymap)) -} - -#[doc(hidden)] -/// Used internally mainly by the `descriptor!()` and `fragment!()` macros -pub trait CheckMiniscript { - fn check_miniscript(&self) -> Result<(), miniscript::Error>; -} - -impl CheckMiniscript - for miniscript::Miniscript -{ - fn check_miniscript(&self) -> Result<(), miniscript::Error> { - Ctx::check_global_validity(self)?; - - Ok(()) - } -} - -/// Trait implemented on [`Descriptor`]s to add a method to extract the spending [`policy`] -pub trait ExtractPolicy { - /// Extract the spending [`policy`] - fn extract_policy( - &self, - signers: &SignersContainer, - psbt: BuildSatisfaction, - secp: &SecpCtx, - ) -> Result, DescriptorError>; -} - -pub(crate) trait XKeyUtils { - fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint; -} - -impl XKeyUtils for DescriptorMultiXKey -where - T: InnerXKey, -{ - fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint { - match self.origin { - Some((fingerprint, _)) => fingerprint, - None => self.xkey.xkey_fingerprint(secp), - } - } -} - -impl XKeyUtils for DescriptorXKey -where - T: InnerXKey, -{ - fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint { - match self.origin { - Some((fingerprint, _)) => fingerprint, - None => self.xkey.xkey_fingerprint(secp), - } - } -} - -pub(crate) trait DescriptorMeta { - fn is_witness(&self) -> bool; - fn is_taproot(&self) -> bool; - fn get_extended_keys(&self) -> Vec>; - fn derive_from_hd_keypaths( - &self, - hd_keypaths: &HdKeyPaths, - secp: &SecpCtx, - ) -> Option; - fn derive_from_tap_key_origins( - &self, - tap_key_origins: &TapKeyOrigins, - secp: &SecpCtx, - ) -> Option; - fn derive_from_psbt_key_origins( - &self, - key_origins: BTreeMap, - secp: &SecpCtx, - ) -> Option; - fn derive_from_psbt_input( - &self, - psbt_input: &psbt::Input, - utxo: Option, - secp: &SecpCtx, - ) -> Option; -} - -impl DescriptorMeta for ExtendedDescriptor { - fn is_witness(&self) -> bool { - matches!( - self.desc_type(), - DescriptorType::Wpkh - | DescriptorType::ShWpkh - | DescriptorType::Wsh - | DescriptorType::ShWsh - | DescriptorType::ShWshSortedMulti - | DescriptorType::WshSortedMulti - ) - } - - fn is_taproot(&self) -> bool { - self.desc_type() == DescriptorType::Tr - } - - fn get_extended_keys(&self) -> Vec> { - let mut answer = Vec::new(); - - self.for_each_key(|pk| { - if let DescriptorPublicKey::XPub(xpub) = pk { - answer.push(xpub.clone()); - } - - true - }); - - answer - } - - fn derive_from_psbt_key_origins( - &self, - key_origins: BTreeMap, - secp: &SecpCtx, - ) -> Option { - // Ensure that deriving `xpub` with `path` yields `expected` - let verify_key = - |xpub: &DescriptorXKey, path: &DerivationPath, expected: &SinglePubKey| { - let derived = xpub - .xkey - .derive_pub(secp, path) - .expect("The path should never contain hardened derivation steps") - .public_key; - - match expected { - SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true, - SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true, - _ => false, - } - }; - - let mut path_found = None; - - // using `for_any_key` should make this stop as soon as we return `true` - self.for_any_key(|key| { - if let DescriptorPublicKey::XPub(xpub) = key { - // Check if the key matches one entry in our `key_origins`. If it does, `matches()` will - // return the "prefix" that matched, so we remove that prefix from the full path - // found in `key_origins` and save it in `derive_path`. We expect this to be a derivation - // path of length 1 if the key is `wildcard` and an empty path otherwise. - let root_fingerprint = xpub.root_fingerprint(secp); - let derive_path = key_origins - .get_key_value(&root_fingerprint) - .and_then(|(fingerprint, (path, expected))| { - xpub.matches(&(*fingerprint, (*path).clone()), secp) - .zip(Some((path, expected))) - }) - .and_then(|(prefix, (full_path, expected))| { - let derive_path = full_path - .into_iter() - .skip(prefix.into_iter().count()) - .cloned() - .collect::(); - - // `derive_path` only contains the replacement index for the wildcard, if present, or - // an empty path for fixed descriptors. To verify the key we also need the normal steps - // that come before the wildcard, so we take them directly from `xpub` and then append - // the final index - if verify_key( - xpub, - &xpub.derivation_path.extend(derive_path.clone()), - expected, - ) { - Some(derive_path) - } else { - None - } - }); - - match derive_path { - Some(path) if xpub.wildcard != Wildcard::None && path.len() == 1 => { - // Ignore hardened wildcards - if let ChildNumber::Normal { index } = path[0] { - path_found = Some(index); - return true; - } - } - Some(path) if xpub.wildcard == Wildcard::None && path.is_empty() => { - path_found = Some(0); - return true; - } - _ => {} - } - } - - false - }); - - path_found.map(|path| { - self.at_derivation_index(path) - .expect("We ignore hardened wildcards") - }) - } - - fn derive_from_hd_keypaths( - &self, - hd_keypaths: &HdKeyPaths, - secp: &SecpCtx, - ) -> Option { - // "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins` - let key_origins = hd_keypaths - .iter() - .map(|(pk, (fingerprint, path))| { - ( - *fingerprint, - (path, SinglePubKey::FullKey(PublicKey::new(*pk))), - ) - }) - .collect(); - self.derive_from_psbt_key_origins(key_origins, secp) - } - - fn derive_from_tap_key_origins( - &self, - tap_key_origins: &TapKeyOrigins, - secp: &SecpCtx, - ) -> Option { - // "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins` - let key_origins = tap_key_origins - .iter() - .map(|(pk, (_, (fingerprint, path)))| (*fingerprint, (path, SinglePubKey::XOnly(*pk)))) - .collect(); - self.derive_from_psbt_key_origins(key_origins, secp) - } - - fn derive_from_psbt_input( - &self, - psbt_input: &psbt::Input, - utxo: Option, - secp: &SecpCtx, - ) -> Option { - if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) { - return Some(derived); - } - if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) { - return Some(derived); - } - if self.has_wildcard() { - // We can't try to bruteforce the derivation index, exit here - return None; - } - - let descriptor = self.at_derivation_index(0).expect("0 is not hardened"); - match descriptor.desc_type() { - // TODO: add pk() here - DescriptorType::Pkh - | DescriptorType::Wpkh - | DescriptorType::ShWpkh - | DescriptorType::Tr - if utxo.is_some() - && descriptor.script_pubkey() == utxo.as_ref().unwrap().script_pubkey => - { - Some(descriptor) - } - DescriptorType::Bare | DescriptorType::Sh | DescriptorType::ShSortedMulti - if psbt_input.redeem_script.is_some() - && &descriptor.explicit_script().unwrap() - == psbt_input.redeem_script.as_ref().unwrap() => - { - Some(descriptor) - } - DescriptorType::Wsh - | DescriptorType::ShWsh - | DescriptorType::ShWshSortedMulti - | DescriptorType::WshSortedMulti - if psbt_input.witness_script.is_some() - && &descriptor.explicit_script().unwrap() - == psbt_input.witness_script.as_ref().unwrap() => - { - Some(descriptor) - } - _ => None, - } - } -} - -#[cfg(test)] -mod test { - use alloc::string::ToString; - use core::str::FromStr; - - use assert_matches::assert_matches; - use bitcoin::hex::FromHex; - use bitcoin::secp256k1::Secp256k1; - use bitcoin::ScriptBuf; - use bitcoin::{bip32, Psbt}; - - use super::*; - use crate::psbt::PsbtUtils; - - #[test] - fn test_derive_from_psbt_input_wpkh_wif() { - let descriptor = Descriptor::::from_str( - "wpkh(02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737)", - ) - .unwrap(); - let psbt = Psbt::deserialize( - &Vec::::from_hex( - "70736274ff010052010000000162307be8e431fbaff807cdf9cdc3fde44d7402\ - 11bc8342c31ffd6ec11fe35bcc0100000000ffffffff01328601000000000016\ - 001493ce48570b55c42c2af816aeaba06cfee1224fae000000000001011fa086\ - 01000000000016001493ce48570b55c42c2af816aeaba06cfee1224fae010304\ - 010000000000", - ) - .unwrap(), - ) - .unwrap(); - - assert!(descriptor - .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) - .is_some()); - } - - #[test] - fn test_derive_from_psbt_input_pkh_tpub() { - let descriptor = Descriptor::::from_str( - "pkh([0f056943/44h/0h/0h]tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/10/*)", - ) - .unwrap(); - let psbt = Psbt::deserialize( - &Vec::::from_hex( - "70736274ff010053010000000145843b86be54a3cd8c9e38444e1162676c00df\ - e7964122a70df491ea12fd67090100000000ffffffff01c19598000000000017\ - a91432bb94283282f72b2e034709e348c44d5a4db0ef8700000000000100f902\ - 0000000001010167e99c0eb67640f3a1b6805f2d8be8238c947f8aaf49eb0a9c\ - bee6a42c984200000000171600142b29a22019cca05b9c2b2d283a4c4489e1cf\ - 9f8ffeffffff02a01dced06100000017a914e2abf033cadbd74f0f4c74946201\ - decd20d5c43c8780969800000000001976a9148b0fce5fb1264e599a65387313\ - 3c95478b902eb288ac02473044022015d9211576163fa5b001e84dfa3d44efd9\ - 86b8f3a0d3d2174369288b2b750906022048dacc0e5d73ae42512fd2b97e2071\ - a8d0bce443b390b1fe0b8128fe70ec919e01210232dad1c5a67dcb0116d407e2\ - 52584228ab7ec00e8b9779d0c3ffe8114fc1a7d2c80600000103040100000022\ - 0603433b83583f8c4879b329dd08bbc7da935e4cc02f637ff746e05f0466ffb2\ - a6a2180f0569432c00008000000080000000800a000000000000000000", - ) - .unwrap(), - ) - .unwrap(); - - assert!(descriptor - .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) - .is_some()); - } - - #[test] - fn test_derive_from_psbt_input_wsh() { - let descriptor = Descriptor::::from_str( - "wsh(and_v(v:pk(03b6633fef2397a0a9de9d7b6f23aef8368a6e362b0581f0f0af70d5ecfd254b14),older(6)))", - ) - .unwrap(); - let psbt = Psbt::deserialize( - &Vec::::from_hex( - "70736274ff01005302000000011c8116eea34408ab6529223c9a176606742207\ - 67a1ff1d46a6e3c4a88243ea6e01000000000600000001109698000000000017\ - a914ad105f61102e0d01d7af40d06d6a5c3ae2f7fde387000000000001012b80\ - 969800000000002200203ca72f106a72234754890ca7640c43f65d2174e44d33\ - 336030f9059345091044010304010000000105252103b6633fef2397a0a9de9d\ - 7b6f23aef8368a6e362b0581f0f0af70d5ecfd254b14ad56b20000", - ) - .unwrap(), - ) - .unwrap(); - - assert!(descriptor - .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) - .is_some()); - } - - #[test] - fn test_derive_from_psbt_input_sh() { - let descriptor = Descriptor::::from_str( - "sh(and_v(v:pk(021403881a5587297818fcaf17d239cefca22fce84a45b3b1d23e836c4af671dbb),after(630000)))", - ) - .unwrap(); - let psbt = Psbt::deserialize( - &Vec::::from_hex( - "70736274ff0100530100000001bc8c13df445dfadcc42afa6dc841f85d22b01d\ - a6270ebf981740f4b7b1d800390000000000feffffff01ba9598000000000017\ - a91457b148ba4d3e5fa8608a8657875124e3d1c9390887f09c0900000100e002\ - 0000000001016ba1bbe05cc93574a0d611ec7d93ad0ab6685b28d0cd80e8a82d\ - debb326643c90100000000feffffff02809698000000000017a914d9a6e8c455\ - 8e16c8253afe53ce37ad61cf4c38c487403504cf6100000017a9144044fb6e0b\ - 757dfc1b34886b6a95aef4d3db137e870247304402202a9b72d939bcde8ba2a1\ - e0980597e47af4f5c152a78499143c3d0a78ac2286a602207a45b1df9e93b8c9\ - 6f09f5c025fe3e413ca4b905fe65ee55d32a3276439a9b8f012102dc1fcc2636\ - 4da1aa718f03d8d9bd6f2ff410ed2cf1245a168aa3bcc995ac18e0a806000001\ - 03040100000001042821021403881a5587297818fcaf17d239cefca22fce84a4\ - 5b3b1d23e836c4af671dbbad03f09c09b10000", - ) - .unwrap(), - ) - .unwrap(); - - assert!(descriptor - .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) - .is_some()); - } - - #[test] - fn test_to_wallet_descriptor_fixup_networks() { - use crate::keys::{any_network, IntoDescriptorKey}; - - let secp = Secp256k1::new(); - - let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap(); - let path = bip32::DerivationPath::from_str("m/0").unwrap(); - - // here `to_descriptor_key` will set the valid networks for the key to only mainnet, since - // we are using an "xpub" - let key = (xprv, path.clone()).into_descriptor_key().unwrap(); - // override it with any. this happens in some key conversions, like bip39 - let key = key.override_valid_networks(any_network()); - - // make a descriptor out of it - let desc = crate::descriptor!(wpkh(key)).unwrap(); - // this should convert the key that supports "any_network" to the right network (testnet) - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - - let mut xprv_testnet = xprv; - xprv_testnet.network = Network::Testnet; - - let xpub_testnet = bip32::Xpub::from_priv(&secp, &xprv_testnet); - let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey { - xkey: xpub_testnet, - origin: None, - derivation_path: path, - wildcard: Wildcard::Unhardened, - }); - - assert_eq!(wallet_desc.to_string(), "wpkh(tpubD6NzVbkrYhZ4XtJzoDja5snUjBNQRP5B3f4Hyn1T1x6PVPxzzVjvw6nJx2D8RBCxog9GEVjZoyStfepTz7TtKoBVdkCtnc7VCJh9dD4RAU9/0/*)#a3svx0ha"); - assert_eq!( - keymap - .get(&desc_pubkey) - .map(|key| key.to_public(&secp).unwrap()), - Some(desc_pubkey) - ); - } - - // test IntoWalletDescriptor trait from &str with and without checksum appended - #[test] - fn test_descriptor_from_str_with_checksum() { - let secp = Secp256k1::new(); - - let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62" - .into_wallet_descriptor(&secp, Network::Testnet); - assert!(desc.is_ok()); - - let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" - .into_wallet_descriptor(&secp, Network::Testnet); - assert!(desc.is_ok()); - - let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw" - .into_wallet_descriptor(&secp, Network::Testnet); - assert!(desc.is_ok()); - - let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" - .into_wallet_descriptor(&secp, Network::Testnet); - assert!(desc.is_ok()); - - let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw" - .into_wallet_descriptor(&secp, Network::Testnet); - assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum)); - - let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw" - .into_wallet_descriptor(&secp, Network::Testnet); - assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum)); - } - - // test IntoWalletDescriptor trait from &str with keys from right and wrong network - #[test] - fn test_descriptor_from_str_with_keys_network() { - let secp = Secp256k1::new(); - - let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" - .into_wallet_descriptor(&secp, Network::Testnet); - assert!(desc.is_ok()); - - let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" - .into_wallet_descriptor(&secp, Network::Regtest); - assert!(desc.is_ok()); - - let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" - .into_wallet_descriptor(&secp, Network::Testnet); - assert!(desc.is_ok()); - - let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" - .into_wallet_descriptor(&secp, Network::Regtest); - assert!(desc.is_ok()); - - let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))" - .into_wallet_descriptor(&secp, Network::Testnet); - assert!(desc.is_ok()); - - let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))" - .into_wallet_descriptor(&secp, Network::Bitcoin); - assert!(desc.is_ok()); - - let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" - .into_wallet_descriptor(&secp, Network::Bitcoin); - assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork))); - - let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" - .into_wallet_descriptor(&secp, Network::Bitcoin); - assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork))); - } - - // test IntoWalletDescriptor trait from the output of the descriptor!() macro - #[test] - fn test_descriptor_from_str_from_output_of_macro() { - let secp = Secp256k1::new(); - - let tpub = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap(); - let path = bip32::DerivationPath::from_str("m/1/2").unwrap(); - let key = (tpub, path).into_descriptor_key().unwrap(); - - // make a descriptor out of it - let desc = crate::descriptor!(wpkh(key)).unwrap(); - - let (wallet_desc, _) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let wallet_desc_str = wallet_desc.to_string(); - assert_eq!(wallet_desc_str, "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw"); - - let (wallet_desc2, _) = wallet_desc_str - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - assert_eq!(wallet_desc, wallet_desc2) - } - - #[test] - fn test_into_wallet_descriptor_checked() { - let secp = Secp256k1::new(); - - let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)"; - let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet); - - assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub)); - - let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/<0;1>/*)"; - let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet); - - assert_matches!(result, Err(DescriptorError::MultiPath)); - - // repeated pubkeys - let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))"; - let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet); - - assert!(result.is_err()); - } - - #[test] - fn test_sh_wsh_sortedmulti_redeemscript() { - use miniscript::psbt::PsbtInputExt; - - let secp = Secp256k1::new(); - - let descriptor = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))"; - let (descriptor, _) = - into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap(); - - let descriptor = descriptor.at_derivation_index(0).unwrap(); - - let script = ScriptBuf::from_hex("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap(); - - let mut psbt_input = psbt::Input::default(); - psbt_input - .update_with_descriptor_unchecked(&descriptor) - .unwrap(); - - assert_eq!(psbt_input.redeem_script, Some(script.to_p2wsh())); - assert_eq!(psbt_input.witness_script, Some(script)); - } -} diff --git a/crates/bdk/src/descriptor/policy.rs b/crates/bdk/src/descriptor/policy.rs deleted file mode 100644 index 820bf2d2..00000000 --- a/crates/bdk/src/descriptor/policy.rs +++ /dev/null @@ -1,1905 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Descriptor policy -//! -//! This module implements the logic to extract and represent the spending policies of a descriptor -//! in a more human-readable format. -//! -//! This is an **EXPERIMENTAL** feature, API and other major changes are expected. -//! -//! ## Example -//! -//! ``` -//! # use std::sync::Arc; -//! # use bdk::descriptor::*; -//! # use bdk::wallet::signer::*; -//! # 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))))"; -//! -//! let (extended_desc, key_map) = ExtendedDescriptor::parse_descriptor(&secp, desc)?; -//! println!("{:?}", extended_desc); -//! -//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp)); -//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?; -//! println!("policy: {}", serde_json::to_string(&policy).unwrap()); -//! # Ok::<(), anyhow::Error>(()) -//! ``` - -use crate::collections::{BTreeMap, HashSet, VecDeque}; -use alloc::string::String; -use alloc::vec::Vec; -use core::cmp::max; - -use core::fmt; - -use serde::ser::SerializeMap; -use serde::{Serialize, Serializer}; - -use bitcoin::bip32::Fingerprint; -use bitcoin::hashes::{hash160, ripemd160, sha256}; -use bitcoin::{absolute, key::XOnlyPublicKey, PublicKey, Sequence}; - -use miniscript::descriptor::{ - DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner, -}; -use miniscript::hash256; -use miniscript::{ - Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey, -}; - -use crate::descriptor::ExtractPolicy; -use crate::keys::ExtScriptContext; -use crate::wallet::signer::{SignerId, SignersContainer}; -use crate::wallet::utils::{After, Older, SecpCtx}; - -use super::checksum::calc_checksum; -use super::error::Error; -use super::XKeyUtils; -use bitcoin::psbt::{self, Psbt}; -use miniscript::psbt::PsbtInputSatisfier; - -/// A unique identifier for a key -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum PkOrF { - /// A legacy public key - Pubkey(PublicKey), - /// A x-only public key - XOnlyPubkey(XOnlyPublicKey), - /// An extended key fingerprint - Fingerprint(Fingerprint), -} - -impl PkOrF { - fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self { - match k { - DescriptorPublicKey::Single(SinglePub { - key: SinglePubKey::FullKey(pk), - .. - }) => PkOrF::Pubkey(*pk), - DescriptorPublicKey::Single(SinglePub { - key: SinglePubKey::XOnly(pk), - .. - }) => PkOrF::XOnlyPubkey(*pk), - DescriptorPublicKey::XPub(xpub) => PkOrF::Fingerprint(xpub.root_fingerprint(secp)), - DescriptorPublicKey::MultiXPub(multi) => { - PkOrF::Fingerprint(multi.root_fingerprint(secp)) - } - } - } -} - -/// An item that needs to be satisfied -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -#[serde(tag = "type", rename_all = "UPPERCASE")] -pub enum SatisfiableItem { - // Leaves - /// ECDSA Signature for a raw public key - EcdsaSignature(PkOrF), - /// Schnorr Signature for a raw public key - SchnorrSignature(PkOrF), - /// SHA256 preimage hash - Sha256Preimage { - /// The digest value - hash: sha256::Hash, - }, - /// Double SHA256 preimage hash - Hash256Preimage { - /// The digest value - hash: hash256::Hash, - }, - /// RIPEMD160 preimage hash - Ripemd160Preimage { - /// The digest value - hash: ripemd160::Hash, - }, - /// SHA256 then RIPEMD160 preimage hash - Hash160Preimage { - /// The digest value - hash: hash160::Hash, - }, - /// Absolute timeclock timestamp - AbsoluteTimelock { - /// The timelock value - value: absolute::LockTime, - }, - /// Relative timelock locktime - RelativeTimelock { - /// The timelock value - value: Sequence, - }, - /// Multi-signature public keys with threshold count - Multisig { - /// The raw public key or extended key fingerprint - keys: Vec, - /// The required threshold count - threshold: usize, - }, - - // Complex item - /// Threshold items with threshold count - Thresh { - /// The policy items - items: Vec, - /// The required threshold count - threshold: usize, - }, -} - -impl SatisfiableItem { - /// Returns whether the [`SatisfiableItem`] is a leaf item - pub fn is_leaf(&self) -> bool { - !matches!( - self, - SatisfiableItem::Thresh { - items: _, - threshold: _, - } - ) - } - - /// Returns a unique id for the [`SatisfiableItem`] - pub fn id(&self) -> String { - calc_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem")) - .expect("Failed to compute a SatisfiableItem id") - } -} - -fn combinations(vec: &[usize], size: usize) -> Vec> { - assert!(vec.len() >= size); - - let mut answer = Vec::new(); - - let mut queue = VecDeque::new(); - for (index, val) in vec.iter().enumerate() { - let mut new_vec = Vec::with_capacity(size); - new_vec.push(*val); - queue.push_back((index, new_vec)); - } - - while let Some((index, vals)) = queue.pop_front() { - if vals.len() >= size { - answer.push(vals); - } else { - for (new_index, val) in vec.iter().skip(index + 1).enumerate() { - let mut cloned = vals.clone(); - cloned.push(*val); - queue.push_front((new_index, cloned)); - } - } - } - - answer -} - -fn mix(vec: Vec>) -> Vec> { - if vec.is_empty() || vec.iter().any(Vec::is_empty) { - return vec![]; - } - - let mut answer = Vec::new(); - let size = vec.len(); - - let mut queue = VecDeque::new(); - for i in &vec[0] { - let mut new_vec = Vec::with_capacity(size); - new_vec.push(i.clone()); - queue.push_back(new_vec); - } - - while let Some(vals) = queue.pop_front() { - if vals.len() >= size { - answer.push(vals); - } else { - let level = vals.len(); - for i in &vec[level] { - let mut cloned = vals.clone(); - cloned.push(i.clone()); - queue.push_front(cloned); - } - } - } - - answer -} - -/// Type for a map of sets of [`Condition`] items keyed by each set's index -pub type ConditionMap = BTreeMap>; -/// Type for a map of folded sets of [`Condition`] items keyed by a vector of the combined set's indexes -pub type FoldedConditionMap = BTreeMap, HashSet>; - -fn serialize_folded_cond_map( - input_map: &FoldedConditionMap, - serializer: S, -) -> Result -where - S: Serializer, -{ - let mut map = serializer.serialize_map(Some(input_map.len()))?; - for (k, v) in input_map { - let k_string = format!("{:?}", k); - map.serialize_entry(&k_string, v)?; - } - map.end() -} - -/// Represent if and how much a policy item is satisfied by the wallet's descriptor -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -#[serde(tag = "type", rename_all = "UPPERCASE")] -pub enum Satisfaction { - /// Only a partial satisfaction of some kind of threshold policy - Partial { - /// Total number of items - n: usize, - /// Threshold - m: usize, - /// The items that can be satisfied by the descriptor or are satisfied in the PSBT - items: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - /// Whether the items are sorted in lexicographic order (used by `sortedmulti`) - sorted: Option, - #[serde(skip_serializing_if = "BTreeMap::is_empty")] - /// Extra conditions that also need to be satisfied - conditions: ConditionMap, - }, - /// Can reach the threshold of some kind of threshold policy - PartialComplete { - /// Total number of items - n: usize, - /// Threshold - m: usize, - /// The items that can be satisfied by the descriptor - items: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - /// Whether the items are sorted in lexicographic order (used by `sortedmulti`) - sorted: Option, - #[serde( - serialize_with = "serialize_folded_cond_map", - skip_serializing_if = "BTreeMap::is_empty" - )] - /// Extra conditions that also need to be satisfied - conditions: FoldedConditionMap, - }, - - /// Can satisfy the policy item - Complete { - /// Extra conditions that also need to be satisfied - condition: Condition, - }, - /// Cannot satisfy or contribute to the policy item - None, -} - -impl Satisfaction { - /// Returns whether the [`Satisfaction`] is a leaf item - pub fn is_leaf(&self) -> bool { - match self { - Satisfaction::None | Satisfaction::Complete { .. } => true, - Satisfaction::PartialComplete { .. } | Satisfaction::Partial { .. } => false, - } - } - - // add `inner` as one of self's partial items. this only makes sense on partials - fn add(&mut self, inner: &Satisfaction, inner_index: usize) -> Result<(), PolicyError> { - match self { - Satisfaction::None | Satisfaction::Complete { .. } => Err(PolicyError::AddOnLeaf), - Satisfaction::PartialComplete { .. } => Err(PolicyError::AddOnPartialComplete), - Satisfaction::Partial { - n, - ref mut conditions, - ref mut items, - .. - } => { - if inner_index >= *n || items.contains(&inner_index) { - return Err(PolicyError::IndexOutOfRange(inner_index)); - } - - match inner { - // not relevant if not completed yet - Satisfaction::None | Satisfaction::Partial { .. } => return Ok(()), - Satisfaction::Complete { condition } => { - items.push(inner_index); - conditions.insert(inner_index, vec![*condition].into_iter().collect()); - } - Satisfaction::PartialComplete { - conditions: other_conditions, - .. - } => { - items.push(inner_index); - let conditions_set = other_conditions - .values() - .fold(HashSet::new(), |set, i| set.union(i).cloned().collect()); - conditions.insert(inner_index, conditions_set); - } - } - - Ok(()) - } - } - } - - fn finalize(&mut self) { - // if partial try to bump it to a partialcomplete - if let Satisfaction::Partial { - n, - m, - items, - conditions, - sorted, - } = self - { - if items.len() >= *m { - let mut map = BTreeMap::new(); - let indexes = combinations(items, *m); - // `indexes` at this point is a Vec>, with the "n choose k" of items (m of n) - indexes - .into_iter() - // .inspect(|x| println!("--- orig --- {:?}", x)) - // we map each of the combinations of elements into a tuple of ([chosen items], [conditions]). unfortunately, those items have potentially more than one - // condition (think about ORs), so we also use `mix` to expand those, i.e. [[0], [1, 2]] becomes [[0, 1], [0, 2]]. This is necessary to make sure that we - // consider every possible options and check whether or not they are compatible. - // since this step can turn one item of the iterator into multiple ones, we use `flat_map()` to expand them out - .flat_map(|i_vec| { - mix(i_vec - .iter() - .map(|i| { - conditions - .get(i) - .map(|set| set.clone().into_iter().collect()) - .unwrap_or_default() - }) - .collect()) - .into_iter() - .map(|x| (i_vec.clone(), x)) - .collect::, Vec)>>() - }) - // .inspect(|x| println!("flat {:?}", x)) - // try to fold all the conditions for this specific combination of indexes/options. if they are not compatible, try_fold will be Err - .map(|(key, val)| { - ( - key, - val.into_iter() - .try_fold(Condition::default(), |acc, v| acc.merge(&v)), - ) - }) - // .inspect(|x| println!("try_fold {:?}", x)) - // filter out all the incompatible combinations - .filter(|(_, val)| val.is_ok()) - // .inspect(|x| println!("filter {:?}", x)) - // push them into the map - .for_each(|(key, val)| { - map.entry(key) - .or_insert_with(HashSet::new) - .insert(val.unwrap()); - }); - // TODO: if the map is empty, the conditions are not compatible, return an error? - *self = Satisfaction::PartialComplete { - n: *n, - m: *m, - items: items.clone(), - conditions: map, - sorted: *sorted, - }; - } - } - } -} - -impl From for Satisfaction { - fn from(other: bool) -> Self { - if other { - Satisfaction::Complete { - condition: Default::default(), - } - } else { - Satisfaction::None - } - } -} - -/// Descriptor spending policy -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct Policy { - /// Identifier for this policy node - pub id: String, - - /// Type of this policy node - #[serde(flatten)] - pub item: SatisfiableItem, - /// How much a given PSBT already satisfies this policy node in terms of signatures - pub satisfaction: Satisfaction, - /// How the wallet's descriptor can satisfy this policy node - pub contribution: Satisfaction, -} - -/// An extra condition that must be satisfied but that is out of control of the user -/// TODO: use `bitcoin::LockTime` and `bitcoin::Sequence` -#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Default, Serialize)] -pub struct Condition { - /// Optional CheckSequenceVerify condition - #[serde(skip_serializing_if = "Option::is_none")] - pub csv: Option, - /// Optional timelock condition - #[serde(skip_serializing_if = "Option::is_none")] - pub timelock: Option, -} - -impl Condition { - fn merge_nlocktime( - a: absolute::LockTime, - b: absolute::LockTime, - ) -> Result { - if !a.is_same_unit(b) { - Err(PolicyError::MixedTimelockUnits) - } else if a > b { - Ok(a) - } else { - Ok(b) - } - } - - fn merge_nsequence(a: Sequence, b: Sequence) -> Result { - if a.is_time_locked() != b.is_time_locked() { - Err(PolicyError::MixedTimelockUnits) - } else { - Ok(max(a, b)) - } - } - - pub(crate) fn merge(mut self, other: &Condition) -> Result { - match (self.csv, other.csv) { - (Some(a), Some(b)) => self.csv = Some(Self::merge_nsequence(a, b)?), - (None, any) => self.csv = any, - _ => {} - } - - match (self.timelock, other.timelock) { - (Some(a), Some(b)) => self.timelock = Some(Self::merge_nlocktime(a, b)?), - (None, any) => self.timelock = any, - _ => {} - } - - Ok(self) - } - - /// Returns `true` if there are no extra conditions to verify - pub fn is_null(&self) -> bool { - self.csv.is_none() && self.timelock.is_none() - } -} - -/// Errors that can happen while extracting and manipulating policies -#[derive(Debug, PartialEq, Eq)] -pub enum PolicyError { - /// Not enough items are selected to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`] - NotEnoughItemsSelected(String), - /// Index out of range for an item to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`] - IndexOutOfRange(usize), - /// Can not add to an item that is [`Satisfaction::None`] or [`Satisfaction::Complete`] - AddOnLeaf, - /// Can not add to an item that is [`Satisfaction::PartialComplete`] - AddOnPartialComplete, - /// Can not merge CSV or timelock values unless both are less than or both are equal or greater than 500_000_000 - MixedTimelockUnits, - /// Incompatible conditions (not currently used) - IncompatibleConditions, -} - -impl fmt::Display for PolicyError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotEnoughItemsSelected(err) => write!(f, "Not enough items selected: {}", err), - Self::IndexOutOfRange(index) => write!(f, "Index out of range: {}", index), - Self::AddOnLeaf => write!(f, "Add on leaf"), - Self::AddOnPartialComplete => write!(f, "Add on partial complete"), - Self::MixedTimelockUnits => write!(f, "Mixed timelock units"), - Self::IncompatibleConditions => write!(f, "Incompatible conditions"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for PolicyError {} - -impl Policy { - fn new(item: SatisfiableItem) -> Self { - Policy { - id: item.id(), - item, - satisfaction: Satisfaction::None, - contribution: Satisfaction::None, - } - } - - fn make_and(a: Option, b: Option) -> Result, PolicyError> { - match (a, b) { - (None, None) => Ok(None), - (Some(x), None) | (None, Some(x)) => Ok(Some(x)), - (Some(a), Some(b)) => Self::make_thresh(vec![a, b], 2), - } - } - - fn make_or(a: Option, b: Option) -> Result, PolicyError> { - match (a, b) { - (None, None) => Ok(None), - (Some(x), None) | (None, Some(x)) => Ok(Some(x)), - (Some(a), Some(b)) => Self::make_thresh(vec![a, b], 1), - } - } - - fn make_thresh(items: Vec, threshold: usize) -> Result, PolicyError> { - if threshold == 0 { - return Ok(None); - } - - let mut contribution = Satisfaction::Partial { - n: items.len(), - m: threshold, - items: vec![], - 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)) - } - - fn make_multisig( - keys: &[DescriptorPublicKey], - signers: &SignersContainer, - build_sat: BuildSatisfaction, - threshold: usize, - sorted: bool, - secp: &SecpCtx, - ) -> Result, PolicyError> { - if threshold == 0 { - return Ok(None); - } - - let parsed_keys = keys.iter().map(|k| PkOrF::from_key(k, secp)).collect(); - - let mut contribution = Satisfaction::Partial { - n: keys.len(), - m: threshold, - items: vec![], - 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( - &Satisfaction::Complete { - condition: Default::default(), - }, - index, - )?; - } - - if let Some(psbt) = build_sat.psbt() { - if Ctx::find_signature(psbt, key, secp) { - satisfaction.add( - &Satisfaction::Complete { - condition: Default::default(), - }, - index, - )?; - } - } - } - satisfaction.finalize(); - contribution.finalize(); - - let mut policy: Policy = SatisfiableItem::Multisig { - keys: parsed_keys, - threshold, - } - .into(); - policy.contribution = contribution; - policy.satisfaction = satisfaction; - - Ok(Some(policy)) - } - - /// Return whether or not a specific path in the policy tree is required to unambiguously - /// create a transaction - /// - /// What this means is that for some spending policies the user should select which paths in - /// the tree it intends to satisfy while signing, because the transaction must be created differently based - /// on that. - pub fn requires_path(&self) -> bool { - self.get_condition(&BTreeMap::new()).is_err() - } - - /// Return the conditions that are set by the spending policy for a given path in the - /// policy tree - pub fn get_condition( - &self, - path: &BTreeMap>, - ) -> Result { - // if items.len() == threshold, selected can be omitted and we take all of them by default - let default = match &self.item { - SatisfiableItem::Thresh { items, threshold } if items.len() == *threshold => { - (0..*threshold).collect() - } - SatisfiableItem::Multisig { keys, .. } => (0..keys.len()).collect(), - _ => HashSet::new(), - }; - let selected: HashSet<_> = match path.get(&self.id) { - Some(arr) => arr.iter().copied().collect(), - _ => default, - }; - - match &self.item { - SatisfiableItem::Thresh { items, threshold } => { - let mapped_req = items - .iter() - .map(|i| i.get_condition(path)) - .collect::>(); - - // if all the requirements are null we don't care about `selected` because there - // are no requirements - if mapped_req - .iter() - .all(|cond| matches!(cond, Ok(c) if c.is_null())) - { - return Ok(Condition::default()); - } - - // make sure all the indexes in the `selected` list are within range - for index in &selected { - if *index >= items.len() { - return Err(PolicyError::IndexOutOfRange(*index)); - } - } - - // if we have something, make sure we have enough items. note that the user can set - // an empty value for this step in case of n-of-n, because `selected` is set to all - // the elements above - if selected.len() < *threshold { - return Err(PolicyError::NotEnoughItemsSelected(self.id.clone())); - } - - // check the selected items, see if there are conflicting requirements - mapped_req - .into_iter() - .enumerate() - .filter(|(index, _)| selected.contains(index)) - .try_fold(Condition::default(), |acc, (_, cond)| acc.merge(&cond?)) - } - SatisfiableItem::Multisig { keys, threshold } => { - if selected.len() < *threshold { - return Err(PolicyError::NotEnoughItemsSelected(self.id.clone())); - } - if let Some(item) = selected.into_iter().find(|&i| i >= keys.len()) { - return Err(PolicyError::IndexOutOfRange(item)); - } - - Ok(Condition::default()) - } - SatisfiableItem::AbsoluteTimelock { value } => Ok(Condition { - csv: None, - timelock: Some(*value), - }), - SatisfiableItem::RelativeTimelock { value } => Ok(Condition { - csv: Some(*value), - timelock: None, - }), - _ => Ok(Condition::default()), - } - } -} - -impl From for Policy { - fn from(other: SatisfiableItem) -> Self { - Self::new(other) - } -} - -fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId { - // For consistency we always compute the key hash in "ecdsa" form (with the leading sign - // prefix) even if we are in a taproot descriptor. We just want some kind of unique identifier - // for a key, so it doesn't really matter how the identifier is computed. - match key { - DescriptorPublicKey::Single(SinglePub { - key: SinglePubKey::FullKey(pk), - .. - }) => pk.to_pubkeyhash(SigType::Ecdsa).into(), - DescriptorPublicKey::Single(SinglePub { - key: SinglePubKey::XOnly(pk), - .. - }) => pk.to_pubkeyhash(SigType::Ecdsa).into(), - DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(), - DescriptorPublicKey::MultiXPub(xpub) => xpub.root_fingerprint(secp).into(), - } -} - -fn make_generic_signature SatisfiableItem, F: Fn(&Psbt) -> bool>( - key: &DescriptorPublicKey, - signers: &SignersContainer, - build_sat: BuildSatisfaction, - secp: &SecpCtx, - make_policy: M, - find_sig: F, -) -> Policy { - let mut policy: Policy = make_policy().into(); - - policy.contribution = if signers.find(signer_id(key, secp)).is_some() { - Satisfaction::Complete { - condition: Default::default(), - } - } else { - Satisfaction::None - }; - - if let Some(psbt) = build_sat.psbt() { - policy.satisfaction = if find_sig(psbt) { - Satisfaction::Complete { - condition: Default::default(), - } - } else { - Satisfaction::None - }; - } - - policy -} - -fn generic_sig_in_psbt< - // C is for "check", it's a closure we use to *check* if a psbt input contains the signature - // for a specific key - C: Fn(&psbt::Input, &SinglePubKey) -> bool, - // E is for "extract", it extracts a key from the bip32 derivations found in the psbt input - E: Fn(&psbt::Input, Fingerprint) -> Option, ->( - psbt: &Psbt, - key: &DescriptorPublicKey, - secp: &SecpCtx, - check: C, - extract: E, -) -> bool { - //TODO check signature validity - psbt.inputs.iter().all(|input| match key { - DescriptorPublicKey::Single(SinglePub { key, .. }) => check(input, key), - DescriptorPublicKey::XPub(xpub) => { - //TODO check actual derivation matches - match extract(input, xpub.root_fingerprint(secp)) { - Some(pubkey) => check(input, &pubkey), - None => false, - } - } - DescriptorPublicKey::MultiXPub(xpub) => { - //TODO check actual derivation matches - match extract(input, xpub.root_fingerprint(secp)) { - Some(pubkey) => check(input, &pubkey), - None => false, - } - } - }) -} - -trait SigExt: ScriptContext { - fn make_signature( - key: &DescriptorPublicKey, - signers: &SignersContainer, - build_sat: BuildSatisfaction, - secp: &SecpCtx, - ) -> Policy; - - fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool; -} - -impl SigExt for T { - fn make_signature( - key: &DescriptorPublicKey, - signers: &SignersContainer, - build_sat: BuildSatisfaction, - secp: &SecpCtx, - ) -> Policy { - if T::as_enum().is_taproot() { - make_generic_signature( - key, - signers, - build_sat, - secp, - || SatisfiableItem::SchnorrSignature(PkOrF::from_key(key, secp)), - |psbt| Self::find_signature(psbt, key, secp), - ) - } else { - make_generic_signature( - key, - signers, - build_sat, - secp, - || SatisfiableItem::EcdsaSignature(PkOrF::from_key(key, secp)), - |psbt| Self::find_signature(psbt, key, secp), - ) - } - } - - fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool { - if T::as_enum().is_taproot() { - generic_sig_in_psbt( - psbt, - key, - secp, - |input, pk| { - let pk = match pk { - SinglePubKey::XOnly(pk) => pk, - _ => return false, - }; - - if input.tap_internal_key == Some(*pk) && input.tap_key_sig.is_some() { - true - } else { - input.tap_script_sigs.keys().any(|(sk, _)| sk == pk) - } - }, - |input, fing| { - input - .tap_key_origins - .iter() - .find(|(_, (_, (f, _)))| f == &fing) - .map(|(pk, _)| SinglePubKey::XOnly(*pk)) - }, - ) - } else { - generic_sig_in_psbt( - psbt, - key, - secp, - |input, pk| match pk { - SinglePubKey::FullKey(pk) => input.partial_sigs.contains_key(pk), - _ => false, - }, - |input, fing| { - input - .bip32_derivation - .iter() - .find(|(_, (f, _))| f == &fing) - .map(|(pk, _)| SinglePubKey::FullKey(PublicKey::new(*pk))) - }, - ) - } - } -} - -impl ExtractPolicy for Miniscript { - fn extract_policy( - &self, - signers: &SignersContainer, - build_sat: BuildSatisfaction, - secp: &SecpCtx, - ) -> Result, Error> { - Ok(match &self.node { - // Leaves - Terminal::True | Terminal::False => None, - Terminal::PkK(pubkey) => Some(Ctx::make_signature(pubkey, signers, build_sat, secp)), - Terminal::PkH(pubkey_hash) => { - Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp)) - } - Terminal::After(value) => { - let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { - value: (*value).into(), - } - .into(); - policy.contribution = Satisfaction::Complete { - condition: Condition { - timelock: Some((*value).into()), - csv: None, - }, - }; - if let BuildSatisfaction::PsbtTimelocks { - current_height, - psbt, - .. - } = build_sat - { - let after = After::new(Some(current_height), false); - let after_sat = - Satisfier::::check_after(&after, (*value).into()); - let inputs_sat = psbt_inputs_sat(psbt).all(|sat| { - Satisfier::::check_after(&sat, (*value).into()) - }); - if after_sat && inputs_sat { - policy.satisfaction = policy.contribution.clone(); - } - } - - Some(policy) - } - Terminal::Older(value) => { - let mut policy: Policy = SatisfiableItem::RelativeTimelock { value: *value }.into(); - policy.contribution = Satisfaction::Complete { - condition: Condition { - timelock: None, - 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::::check_older(&older, *value); - let inputs_sat = psbt_inputs_sat(psbt) - .all(|sat| Satisfier::::check_older(&sat, *value)); - if older_sat && inputs_sat { - policy.satisfaction = policy.contribution.clone(); - } - } - - Some(policy) - } - Terminal::Sha256(hash) => Some(SatisfiableItem::Sha256Preimage { hash: *hash }.into()), - Terminal::Hash256(hash) => { - Some(SatisfiableItem::Hash256Preimage { hash: *hash }.into()) - } - Terminal::Ripemd160(hash) => { - Some(SatisfiableItem::Ripemd160Preimage { hash: *hash }.into()) - } - Terminal::Hash160(hash) => { - Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into()) - } - Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => { - Policy::make_multisig::(pks, signers, build_sat, *k, false, secp)? - } - // Identities - Terminal::Alt(inner) - | Terminal::Swap(inner) - | Terminal::Check(inner) - | Terminal::DupIf(inner) - | Terminal::Verify(inner) - | Terminal::NonZero(inner) - | 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, 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, build_sat, secp)?, - y.extract_policy(signers, build_sat, 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, 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, build_sat, secp)) - .collect::, _>>()? - .into_iter() - .flatten() - .collect(); - - if mapped.len() < nodes.len() { - threshold = match threshold.checked_sub(nodes.len() - mapped.len()) { - None => return Ok(None), - Some(x) => x, - }; - } - - Policy::make_thresh(mapped, threshold)? - } - - // Unsupported - Terminal::RawPkH(_) => None, - }) - } -} - -fn psbt_inputs_sat(psbt: &Psbt) -> impl Iterator { - (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 { - fn extract_policy( - &self, - signers: &SignersContainer, - build_sat: BuildSatisfaction, - secp: &SecpCtx, - ) -> Result, Error> { - fn make_sortedmulti( - keys: &SortedMultiVec, - signers: &SignersContainer, - build_sat: BuildSatisfaction, - secp: &SecpCtx, - ) -> Result, Error> { - Ok(Policy::make_multisig::( - keys.pks.as_ref(), - signers, - build_sat, - keys.k, - true, - secp, - )?) - } - - match self { - Descriptor::Pkh(pk) => Ok(Some(miniscript::Legacy::make_signature( - pk.as_inner(), - signers, - build_sat, - secp, - ))), - Descriptor::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature( - pk.as_inner(), - signers, - build_sat, - secp, - ))), - Descriptor::Sh(sh) => match sh.as_inner() { - ShInner::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_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, 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, build_sat, secp)?), - WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp), - }, - Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?), - Descriptor::Tr(tr) => { - // If there's no tap tree, treat this as a single sig, otherwise build a `Thresh` - // node with threshold = 1 and the key spend signature plus all the tree leaves - let key_spend_sig = - miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp); - - if tr.tap_tree().is_none() { - Ok(Some(key_spend_sig)) - } else { - let mut items = vec![key_spend_sig]; - items.append( - &mut tr - .iter_scripts() - .filter_map(|(_, ms)| { - ms.extract_policy(signers, build_sat, secp).transpose() - }) - .collect::, _>>()?, - ); - - Ok(Policy::make_thresh(items, 1)?) - } - } - } - } -} - -#[cfg(test)] -mod test { - use crate::descriptor; - use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor}; - - use super::*; - use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh}; - use crate::keys::{DescriptorKey, IntoDescriptorKey}; - use crate::wallet::signer::SignersContainer; - use alloc::{string::ToString, sync::Arc}; - use assert_matches::assert_matches; - use bitcoin::bip32; - use bitcoin::secp256k1::Secp256k1; - use bitcoin::Network; - use core::str::FromStr; - - const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf"; - const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N"; - - const PATH: &str = "m/44'/1'/0'/0"; - - fn setup_keys( - tprv: &str, - path: &str, - secp: &SecpCtx, - ) -> (DescriptorKey, DescriptorKey, Fingerprint) { - let path = bip32::DerivationPath::from_str(path).unwrap(); - let tprv = bip32::Xpriv::from_str(tprv).unwrap(); - let tpub = bip32::Xpub::from_priv(secp, &tprv); - let fingerprint = tprv.fingerprint(secp); - let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap(); - let pubkey = (tpub, path).into_descriptor_key().unwrap(); - - (prvkey, pubkey, fingerprint) - } - - // test ExtractPolicy trait for simple descriptors; wpkh(), sh(multi()) - - #[test] - fn test_extract_policy_for_wpkh() { - let secp = Secp256k1::new(); - - let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR, PATH, &secp); - let desc = descriptor!(wpkh(pubkey)).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint); - assert_matches!(&policy.contribution, Satisfaction::None); - - let desc = descriptor!(wpkh(prvkey)).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint); - assert_matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none()); - } - - // 2 pub keys descriptor, required 2 prv keys - #[test] - fn test_extract_policy_for_sh_multi_partial_0of2() { - let secp = Secp256k1::new(); - let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); - let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); - let desc = descriptor!(sh(multi(2, pubkey0, pubkey1))).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize - && keys[0] == PkOrF::Fingerprint(fingerprint0) - && keys[1] == PkOrF::Fingerprint(fingerprint1) - ); - // TODO should this be "Satisfaction::None" since we have no prv keys? - // TODO should items and conditions not be empty? - assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize - && m == &2usize - && items.is_empty() - && conditions.is_empty() - ); - } - - // 1 prv and 1 pub key descriptor, required 2 prv keys - #[test] - fn test_extract_policy_for_sh_multi_partial_1of2() { - let secp = Secp256k1::new(); - let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); - let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); - let desc = descriptor!(sh(multi(2, prvkey0, pubkey1))).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize - && keys[0] == PkOrF::Fingerprint(fingerprint0) - && keys[1] == PkOrF::Fingerprint(fingerprint1) - ); - - assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize - && m == &2usize - && items.len() == 1 - && conditions.contains_key(&0) - ); - } - - // 1 prv and 1 pub key descriptor, required 1 prv keys - #[test] - #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225 - fn test_extract_policy_for_sh_multi_complete_1of2() { - let secp = Secp256k1::new(); - - let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); - let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); - let desc = descriptor!(sh(multi(1, pubkey0, prvkey1))).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &1 - && keys[0] == PkOrF::Fingerprint(fingerprint0) - && keys[1] == PkOrF::Fingerprint(fingerprint1) - ); - assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2 - && m == &1 - && items.len() == 2 - && conditions.contains_key(&vec![0]) - && conditions.contains_key(&vec![1]) - ); - } - - // 2 prv keys descriptor, required 2 prv keys - #[test] - fn test_extract_policy_for_sh_multi_complete_2of2() { - let secp = Secp256k1::new(); - - let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); - let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); - let desc = descriptor!(sh(multi(2, prvkey0, prvkey1))).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2 - && keys[0] == PkOrF::Fingerprint(fingerprint0) - && keys[1] == PkOrF::Fingerprint(fingerprint1) - ); - - assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2 - && m == &2 - && items.len() == 2 - && conditions.contains_key(&vec![0,1]) - ); - } - - // test ExtractPolicy trait with extended and single keys - - #[test] - fn test_extract_policy_for_single_wpkh() { - let secp = Secp256k1::new(); - - let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR, PATH, &secp); - let desc = descriptor!(wpkh(pubkey)).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint); - assert_matches!(&policy.contribution, Satisfaction::None); - - let desc = descriptor!(wpkh(prvkey)).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - assert_matches!(policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == fingerprint); - assert_matches!(policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none()); - } - - // single key, 1 prv and 1 pub key descriptor, required 1 prv keys - #[test] - #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225 - fn test_extract_policy_for_single_wsh_multi_complete_1of2() { - let secp = Secp256k1::new(); - - let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); - let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); - let desc = descriptor!(sh(multi(1, pubkey0, prvkey1))).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - assert_matches!(policy.item, Multisig { keys, threshold } if threshold == 1 - && keys[0] == PkOrF::Fingerprint(fingerprint0) - && keys[1] == PkOrF::Fingerprint(fingerprint1) - ); - assert_matches!(policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == 2 - && m == 1 - && items.len() == 2 - && conditions.contains_key(&vec![0]) - && conditions.contains_key(&vec![1]) - ); - } - - // test ExtractPolicy trait with descriptors containing timelocks in a thresh() - - #[test] - #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225 - fn test_extract_policy_for_wsh_multi_timelock() { - let secp = Secp256k1::new(); - - let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); - let (_prvkey1, pubkey1, _fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); - let sequence = 50; - #[rustfmt::skip] - let desc = descriptor!(wsh(thresh( - 2, - pk(prvkey0), - s:pk(pubkey1), - s:d:v:older(sequence) - ))) - .unwrap(); - - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - assert_matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2); - - assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &3 - && m == &2 - && items.len() == 3 - && conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none() - && conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence)) - && conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence)) - ); - } - - // - mixed timelocks should fail - - #[test] - #[ignore] - fn test_extract_policy_for_wsh_mixed_timelocks() { - let secp = Secp256k1::new(); - let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); - let locktime_threshold = 500000000; // if less than this means block number, else block time in seconds - let locktime_blocks = 100; - let locktime_seconds = locktime_blocks + locktime_threshold; - let desc = descriptor!(sh(and_v( - v: pk(prvkey0), - and_v(v: after(locktime_seconds), after(locktime_blocks)) - ))) - .unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let _policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - // println!("desc policy = {:?}", policy); // TODO remove - // TODO how should this fail with mixed timelocks? - } - - // - multiple timelocks of the same type should be correctly merged together - #[test] - #[ignore] - fn test_extract_policy_for_multiple_same_timelocks() { - let secp = Secp256k1::new(); - let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); - let locktime_blocks0 = 100; - let locktime_blocks1 = 200; - let desc = descriptor!(sh(and_v( - v: pk(prvkey0), - and_v(v: after(locktime_blocks0), after(locktime_blocks1)) - ))) - .unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let _policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - // println!("desc policy = {:?}", policy); // TODO remove - // TODO how should this merge timelocks? - let (prvkey1, _pubkey1, _fingerprint1) = setup_keys(TPRV0_STR, PATH, &secp); - let locktime_seconds0 = 500000100; - let locktime_seconds1 = 500000200; - let desc = descriptor!(sh(and_v( - v: pk(prvkey1), - and_v(v: after(locktime_seconds0), after(locktime_seconds1)) - ))) - .unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - let _policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - // println!("desc policy = {:?}", policy); // TODO remove - - // TODO how should this merge timelocks? - } - - #[test] - fn test_get_condition_multisig() { - let secp = Secp256k1::new(); - - let (_, pk0, _) = setup_keys(TPRV0_STR, PATH, &secp); - let (_, pk1, _) = setup_keys(TPRV1_STR, PATH, &secp); - - let desc = descriptor!(wsh(multi(1, pk0, pk1))).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - // no args, choose the default - let no_args = policy.get_condition(&vec![].into_iter().collect()); - assert_eq!(no_args, Ok(Condition::default())); - - // enough args - let eq_thresh = - policy.get_condition(&vec![(policy.id.clone(), vec![0])].into_iter().collect()); - assert_eq!(eq_thresh, Ok(Condition::default())); - - // more args, it doesn't really change anything - let gt_thresh = - policy.get_condition(&vec![(policy.id.clone(), vec![0, 1])].into_iter().collect()); - assert_eq!(gt_thresh, Ok(Condition::default())); - - // not enough args, error - let lt_thresh = - policy.get_condition(&vec![(policy.id.clone(), vec![])].into_iter().collect()); - assert_eq!( - lt_thresh, - Err(PolicyError::NotEnoughItemsSelected(policy.id.clone())) - ); - - // index out of range - let out_of_range = - policy.get_condition(&vec![(policy.id.clone(), vec![5])].into_iter().collect()); - assert_eq!(out_of_range, Err(PolicyError::IndexOutOfRange(5))); - } - - const ALICE_TPRV_STR:&str = "tprv8ZgxMBicQKsPf6T5X327efHnvJDr45Xnb8W4JifNWtEoqXu9MRYS4v1oYe6DFcMVETxy5w3bqpubYRqvcVTqovG1LifFcVUuJcbwJwrhYzP"; - const BOB_TPRV_STR:&str = "tprv8ZgxMBicQKsPeinZ155cJAn117KYhbaN6MV3WeG6sWhxWzcvX1eg1awd4C9GpUN1ncLEM2rzEvunAg3GizdZD4QPPCkisTz99tXXB4wZArp"; - const CAROL_TPRV_STR:&str = "tprv8ZgxMBicQKsPdC3CicFifuLCEyVVdXVUNYorxUWj3iGZ6nimnLAYAY9SYB7ib8rKzRxrCKFcEytCt6szwd2GHnGPRCBLAEAoSVDefSNk4Bt"; - const ALICE_BOB_PATH: &str = "m/0'"; - - #[test] - fn test_extract_satisfaction() { - 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"; - - 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(multi(2, prvkey_alice, prvkey_bob))).unwrap(); - - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - - let addr = wallet_desc - .at_derivation_index(0) - .unwrap() - .address(Network::Testnet) - .unwrap(); - assert_eq!( - "tb1qg3cwv3xt50gdg875qvjjpfgaps86gtk4rz0ejvp6ttc5ldnlxuvqlcn0xk", - addr.to_string() - ); - - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - - let psbt = Psbt::from_str(ALICE_SIGNED_PSBT).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()); - - assert_matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2 - && m == &2 - && items == &vec![0] - ); - - let psbt = Psbt::from_str(BOB_SIGNED_PSBT).unwrap(); - 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_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2 - && m == &2 - && items == &vec![1] - ); - - let psbt = Psbt::from_str(ALICE_BOB_SIGNED_PSBT).unwrap(); - let policy_alice_bob_psbt = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp) - .unwrap() - .unwrap(); - assert_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,n: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::build(keymap, &wallet_desc, &secp)); - - let addr = wallet_desc - .at_derivation_index(0) - .unwrap() - .address(Network::Testnet) - .unwrap(); - assert_eq!( - "tb1qsydsey4hexagwkvercqsmes6yet0ndkyt6uzcphtqnygjd8hmzmsfxrv58", - addr.to_string() - ); - - let psbt = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED).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::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED).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()); - } - - #[test] - fn test_extract_pkh() { - 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 (prvkey_carol, _, _) = setup_keys(CAROL_TPRV_STR, ALICE_BOB_PATH, &secp); - - let desc = descriptor!(wsh(c: andor( - pk(prvkey_alice), - pk_k(prvkey_bob), - pk_h(prvkey_carol), - ))) - .unwrap(); - - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - - let policy = wallet_desc.extract_policy(&signers_container, BuildSatisfaction::None, &secp); - assert!(policy.is_ok()); - } - - #[test] - fn test_extract_tr_key_spend() { - let secp = Secp256k1::new(); - - let (prvkey, _, fingerprint) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); - - let desc = descriptor!(tr(prvkey)).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap(); - assert_eq!( - policy, - Some(Policy { - id: "48u0tz0n".to_string(), - item: SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(fingerprint)), - satisfaction: Satisfaction::None, - contribution: Satisfaction::Complete { - condition: Condition::default() - } - }) - ); - } - - #[test] - fn test_extract_tr_script_spend() { - let secp = Secp256k1::new(); - - let (alice_prv, _, alice_fing) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); - let (_, bob_pub, bob_fing) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp); - - let desc = descriptor!(tr(bob_pub, pk(alice_prv))).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); - - let policy = wallet_desc - .extract_policy(&signers_container, BuildSatisfaction::None, &secp) - .unwrap() - .unwrap(); - - assert_matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2); - assert_matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1]); - - let alice_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(alice_fing)); - let bob_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(bob_fing)); - - let thresh_items = match policy.item { - SatisfiableItem::Thresh { items, .. } => items, - _ => unreachable!(), - }; - - assert_eq!(thresh_items[0].item, bob_sig); - assert_eq!(thresh_items[1].item, alice_sig); - } - - #[test] - fn test_extract_tr_satisfaction_key_spend() { - const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSIRYnkGTDxwXMHP32fkDFoGJY28trxbkkVgR2z7jZa2pOJA0AyRF8LgAAAIADAAAAARcgJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQAAA=="; - const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSARNAIsRvARpRxuyQosVA7guRQT9vXr+S25W2tnP2xOGBsSgq7A4RL8yrbvwDmNlWw9R0Nc/6t+IsyCyy7dD/lbUGgyEWJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQNAMkRfC4AAACAAwAAAAEXICeQZMPHBcwc/fZ+QMWgYljby2vFuSRWBHbPuNlrak4kAAA="; - - let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap(); - let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap(); - - let secp = Secp256k1::new(); - - let (_, pubkey, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); - - let desc = descriptor!(tr(pubkey)).unwrap(); - let (wallet_desc, _) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - - let policy_unsigned = wallet_desc - .extract_policy( - &SignersContainer::default(), - BuildSatisfaction::Psbt(&unsigned_psbt), - &secp, - ) - .unwrap() - .unwrap(); - let policy_signed = wallet_desc - .extract_policy( - &SignersContainer::default(), - BuildSatisfaction::Psbt(&signed_psbt), - &secp, - ) - .unwrap() - .unwrap(); - - assert_eq!(policy_unsigned.satisfaction, Satisfaction::None); - assert_eq!( - policy_signed.satisfaction, - Satisfaction::Complete { - condition: Default::default() - } - ); - } - - #[test] - fn test_extract_tr_satisfaction_script_spend() { - const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2IhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA=="; - const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2AQcAAQhCAUALcP9w/+Ddly9DWdhHTnQ9uCDWLPZjR6vKbKePswW2Ee6W5KNfrklus/8z98n7BQ1U4vADHk0FbadeeL8rrbHlARNAC3D/cP/g3ZcvQ1nYR050Pbgg1iz2Y0erymynj7MFthHuluSjX65JbrP/M/fJ+wUNVOLwAx5NBW2nXni/K62x5UEUeEbK57HG1FUp69HHhjBZH9bSvss8e3qhLoMuXPK5hBr2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHUAXNmWieJ80Fs+PMa2C186YOBPZbYG/ieEUkagMwzJ788SoCucNdp5wnxfpuJVygFhglDrXGzujFtC82PrMohwuIhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA=="; - - let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap(); - let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap(); - - let secp = Secp256k1::new(); - - let (_, alice_pub, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); - let (_, bob_pub, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp); - - let desc = descriptor!(tr(bob_pub, pk(alice_pub))).unwrap(); - let (wallet_desc, _) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - - let policy_unsigned = wallet_desc - .extract_policy( - &SignersContainer::default(), - BuildSatisfaction::Psbt(&unsigned_psbt), - &secp, - ) - .unwrap() - .unwrap(); - let policy_signed = wallet_desc - .extract_policy( - &SignersContainer::default(), - BuildSatisfaction::Psbt(&signed_psbt), - &secp, - ) - .unwrap() - .unwrap(); - - assert_matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2); - assert_matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty()); - - assert_matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2); - assert_matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1]); - - let satisfied_items = match policy_signed.item { - SatisfiableItem::Thresh { items, .. } => items, - _ => unreachable!(), - }; - - assert_eq!( - satisfied_items[0].satisfaction, - Satisfaction::Complete { - condition: Default::default() - } - ); - assert_eq!( - satisfied_items[1].satisfaction, - Satisfaction::Complete { - condition: Default::default() - } - ); - } -} diff --git a/crates/bdk/src/descriptor/template.rs b/crates/bdk/src/descriptor/template.rs deleted file mode 100644 index 61f9c4f2..00000000 --- a/crates/bdk/src/descriptor/template.rs +++ /dev/null @@ -1,985 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Descriptor templates -//! -//! This module contains the definition of various common script templates that are ready to be -//! used. See the documentation of each template for an example. - -use bitcoin::bip32; -use bitcoin::Network; - -use miniscript::{Legacy, Segwitv0, Tap}; - -use super::{ExtendedDescriptor, IntoWalletDescriptor, KeyMap}; -use crate::descriptor::DescriptorError; -use crate::keys::{DerivableKey, IntoDescriptorKey, ValidNetworks}; -use crate::wallet::utils::SecpCtx; -use crate::{descriptor, KeychainKind}; - -/// Type alias for the return type of [`DescriptorTemplate`], [`descriptor!`](crate::descriptor!) and others -pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks); - -/// Trait for descriptor templates that can be built into a full descriptor -/// -/// Since [`IntoWalletDescriptor`] is implemented for any [`DescriptorTemplate`], they can also be -/// passed directly to the [`Wallet`](crate::Wallet) constructor. -/// -/// ## Example -/// -/// ``` -/// use bdk::descriptor::error::Error as DescriptorError; -/// use bdk::keys::{IntoDescriptorKey, KeyError}; -/// use bdk::miniscript::Legacy; -/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut}; -/// use bitcoin::Network; -/// -/// struct MyP2PKH>(K); -/// -/// impl> DescriptorTemplate for MyP2PKH { -/// fn build(self, network: Network) -> Result { -/// Ok(bdk::descriptor!(pkh(self.0))?) -/// } -/// } -/// ``` -pub trait DescriptorTemplate { - /// Build the complete descriptor - fn build(self, network: Network) -> Result; -} - -/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its -/// [`build`](DescriptorTemplate::build) method -impl IntoWalletDescriptor for T { - fn into_wallet_descriptor( - self, - secp: &SecpCtx, - network: Network, - ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { - self.build(network)?.into_wallet_descriptor(secp, network) - } -} - -/// P2PKH template. Expands to a descriptor `pkh(key)` -/// -/// ## Example -/// -/// ``` -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::Wallet; -/// # use bdk::KeychainKind; -/// use bdk::template::P2Pkh; -/// -/// let key = -/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; -/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet)?; -/// -/// assert_eq!( -/// wallet -/// .next_unused_address(KeychainKind::External)? -/// .to_string(), -/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT" -/// ); -/// # Ok::<_, Box>(()) -/// ``` -pub struct P2Pkh>(pub K); - -impl> DescriptorTemplate for P2Pkh { - fn build(self, _network: Network) -> Result { - descriptor!(pkh(self.0)) - } -} - -/// P2WPKH-P2SH template. Expands to a descriptor `sh(wpkh(key))` -/// -/// ## Example -/// -/// ``` -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::Wallet; -/// # use bdk::KeychainKind; -/// use bdk::template::P2Wpkh_P2Sh; -/// -/// let key = -/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; -/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet)?; -/// -/// assert_eq!( -/// wallet -/// .next_unused_address(KeychainKind::External)? -/// .to_string(), -/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5" -/// ); -/// # Ok::<_, Box>(()) -/// ``` -#[allow(non_camel_case_types)] -pub struct P2Wpkh_P2Sh>(pub K); - -impl> DescriptorTemplate for P2Wpkh_P2Sh { - fn build(self, _network: Network) -> Result { - descriptor!(sh(wpkh(self.0))) - } -} - -/// P2WPKH template. Expands to a descriptor `wpkh(key)` -/// -/// ## Example -/// -/// ``` -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::{Wallet}; -/// # use bdk::KeychainKind; -/// use bdk::template::P2Wpkh; -/// -/// let key = -/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; -/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet)?; -/// -/// assert_eq!( -/// wallet -/// .next_unused_address(KeychainKind::External)? -/// .to_string(), -/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd" -/// ); -/// # Ok::<_, Box>(()) -/// ``` -pub struct P2Wpkh>(pub K); - -impl> DescriptorTemplate for P2Wpkh { - fn build(self, _network: Network) -> Result { - descriptor!(wpkh(self.0)) - } -} - -/// P2TR template. Expands to a descriptor `tr(key)` -/// -/// ## Example -/// -/// ``` -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::Wallet; -/// # use bdk::KeychainKind; -/// use bdk::template::P2TR; -/// -/// let key = -/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; -/// let mut wallet = Wallet::new_no_persist(P2TR(key), None, Network::Testnet)?; -/// -/// assert_eq!( -/// wallet -/// .next_unused_address(KeychainKind::External)? -/// .to_string(), -/// "tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46" -/// ); -/// # Ok::<_, Box>(()) -/// ``` -pub struct P2TR>(pub K); - -impl> DescriptorTemplate for P2TR { - fn build(self, _network: Network) -> Result { - descriptor!(tr(self.0)) - } -} - -/// BIP44 template. Expands to `pkh(key/44'/{0,1}'/0'/{0,1}/*)` -/// -/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`). -/// -/// See [`Bip44Public`] for a template that can work with a `xpub`/`tpub`. -/// -/// ## Example -/// -/// ``` -/// # use std::str::FromStr; -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::{Wallet, KeychainKind}; -/// use bdk::template::Bip44; -/// -/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; -/// let mut wallet = Wallet::new_no_persist( -/// Bip44(key.clone(), KeychainKind::External), -/// Some(Bip44(key, KeychainKind::Internal)), -/// Network::Testnet, -/// )?; -/// -/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89"); -/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt"); -/// # Ok::<_, Box>(()) -/// ``` -pub struct Bip44>(pub K, pub KeychainKind); - -impl> DescriptorTemplate for Bip44 { - fn build(self, network: Network) -> Result { - P2Pkh(legacy::make_bipxx_private(44, self.0, self.1, network)?).build(network) - } -} - -/// BIP44 public template. Expands to `pkh(key/{0,1}/*)` -/// -/// This assumes that the key used has already been derived with `m/44'/0'/0'` for Mainnet or `m/44'/1'/0'` for Testnet. -/// -/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs. -/// -/// See [`Bip44`] for a template that does the full derivation, but requires private data -/// for the key. -/// -/// ## Example -/// -/// ``` -/// # use std::str::FromStr; -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::{Wallet, KeychainKind}; -/// use bdk::template::Bip44Public; -/// -/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?; -/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; -/// let mut wallet = Wallet::new_no_persist( -/// Bip44Public(key.clone(), fingerprint, KeychainKind::External), -/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)), -/// Network::Testnet, -/// )?; -/// -/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR"); -/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz"); -/// # Ok::<_, Box>(()) -/// ``` -pub struct Bip44Public>(pub K, pub bip32::Fingerprint, pub KeychainKind); - -impl> DescriptorTemplate for Bip44Public { - fn build(self, network: Network) -> Result { - P2Pkh(legacy::make_bipxx_public( - 44, self.0, self.1, self.2, network, - )?) - .build(network) - } -} - -/// BIP49 template. Expands to `sh(wpkh(key/49'/{0,1}'/0'/{0,1}/*))` -/// -/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`). -/// -/// See [`Bip49Public`] for a template that can work with a `xpub`/`tpub`. -/// -/// ## Example -/// -/// ``` -/// # use std::str::FromStr; -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::{Wallet, KeychainKind}; -/// use bdk::template::Bip49; -/// -/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; -/// let mut wallet = Wallet::new_no_persist( -/// Bip49(key.clone(), KeychainKind::External), -/// Some(Bip49(key, KeychainKind::Internal)), -/// Network::Testnet, -/// )?; -/// -/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB"); -/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e"); -/// # Ok::<_, Box>(()) -/// ``` -pub struct Bip49>(pub K, pub KeychainKind); - -impl> DescriptorTemplate for Bip49 { - fn build(self, network: Network) -> Result { - P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1, network)?).build(network) - } -} - -/// BIP49 public template. Expands to `sh(wpkh(key/{0,1}/*))` -/// -/// This assumes that the key used has already been derived with `m/49'/0'/0'` for Mainnet or `m/49'/1'/0'` for Testnet. -/// -/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs. -/// -/// See [`Bip49`] for a template that does the full derivation, but requires private data -/// for the key. -/// -/// ## Example -/// -/// ``` -/// # use std::str::FromStr; -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::{Wallet, KeychainKind}; -/// use bdk::template::Bip49Public; -/// -/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?; -/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; -/// let mut wallet = Wallet::new_no_persist( -/// Bip49Public(key.clone(), fingerprint, KeychainKind::External), -/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)), -/// Network::Testnet, -/// )?; -/// -/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt"); -/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q"); -/// # Ok::<_, Box>(()) -/// ``` -pub struct Bip49Public>(pub K, pub bip32::Fingerprint, pub KeychainKind); - -impl> DescriptorTemplate for Bip49Public { - fn build(self, network: Network) -> Result { - P2Wpkh_P2Sh(segwit_v0::make_bipxx_public( - 49, self.0, self.1, self.2, network, - )?) - .build(network) - } -} - -/// BIP84 template. Expands to `wpkh(key/84'/{0,1}'/0'/{0,1}/*)` -/// -/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`). -/// -/// See [`Bip84Public`] for a template that can work with a `xpub`/`tpub`. -/// -/// ## Example -/// -/// ``` -/// # use std::str::FromStr; -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::{Wallet, KeychainKind}; -/// use bdk::template::Bip84; -/// -/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; -/// let mut wallet = Wallet::new_no_persist( -/// Bip84(key.clone(), KeychainKind::External), -/// Some(Bip84(key, KeychainKind::Internal)), -/// Network::Testnet, -/// )?; -/// -/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n"); -/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr"); -/// # Ok::<_, Box>(()) -/// ``` -pub struct Bip84>(pub K, pub KeychainKind); - -impl> DescriptorTemplate for Bip84 { - fn build(self, network: Network) -> Result { - P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1, network)?).build(network) - } -} - -/// BIP84 public template. Expands to `wpkh(key/{0,1}/*)` -/// -/// This assumes that the key used has already been derived with `m/84'/0'/0'` for Mainnet or `m/84'/1'/0'` for Testnet. -/// -/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs. -/// -/// See [`Bip84`] for a template that does the full derivation, but requires private data -/// for the key. -/// -/// ## Example -/// -/// ``` -/// # use std::str::FromStr; -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::{Wallet, KeychainKind}; -/// use bdk::template::Bip84Public; -/// -/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?; -/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; -/// let mut wallet = Wallet::new_no_persist( -/// Bip84Public(key.clone(), fingerprint, KeychainKind::External), -/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)), -/// Network::Testnet, -/// )?; -/// -/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7"); -/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv"); -/// # Ok::<_, Box>(()) -/// ``` -pub struct Bip84Public>(pub K, pub bip32::Fingerprint, pub KeychainKind); - -impl> DescriptorTemplate for Bip84Public { - fn build(self, network: Network) -> Result { - P2Wpkh(segwit_v0::make_bipxx_public( - 84, self.0, self.1, self.2, network, - )?) - .build(network) - } -} - -/// BIP86 template. Expands to `tr(key/86'/{0,1}'/0'/{0,1}/*)` -/// -/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`). -/// -/// See [`Bip86Public`] for a template that can work with a `xpub`/`tpub`. -/// -/// ## Example -/// -/// ``` -/// # use std::str::FromStr; -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::{Wallet, KeychainKind}; -/// use bdk::template::Bip86; -/// -/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; -/// let mut wallet = Wallet::new_no_persist( -/// Bip86(key.clone(), KeychainKind::External), -/// Some(Bip86(key, KeychainKind::Internal)), -/// Network::Testnet, -/// )?; -/// -/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu"); -/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm"); -/// # Ok::<_, Box>(()) -/// ``` -pub struct Bip86>(pub K, pub KeychainKind); - -impl> DescriptorTemplate for Bip86 { - fn build(self, network: Network) -> Result { - P2TR(segwit_v1::make_bipxx_private(86, self.0, self.1, network)?).build(network) - } -} - -/// BIP86 public template. Expands to `tr(key/{0,1}/*)` -/// -/// This assumes that the key used has already been derived with `m/86'/0'/0'` for Mainnet or `m/86'/1'/0'` for Testnet. -/// -/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs. -/// -/// See [`Bip86`] for a template that does the full derivation, but requires private data -/// for the key. -/// -/// ## Example -/// -/// ``` -/// # use std::str::FromStr; -/// # use bdk::bitcoin::{PrivateKey, Network}; -/// # use bdk::{Wallet, KeychainKind}; -/// use bdk::template::Bip86Public; -/// -/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?; -/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; -/// let mut wallet = Wallet::new_no_persist( -/// Bip86Public(key.clone(), fingerprint, KeychainKind::External), -/// Some(Bip86Public(key, fingerprint, KeychainKind::Internal)), -/// Network::Testnet, -/// )?; -/// -/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37"); -/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku"); -/// # Ok::<_, Box>(()) -/// ``` -pub struct Bip86Public>(pub K, pub bip32::Fingerprint, pub KeychainKind); - -impl> DescriptorTemplate for Bip86Public { - fn build(self, network: Network) -> Result { - P2TR(segwit_v1::make_bipxx_public( - 86, self.0, self.1, self.2, network, - )?) - .build(network) - } -} - -macro_rules! expand_make_bipxx { - ( $mod_name:ident, $ctx:ty ) => { - mod $mod_name { - use super::*; - - pub(super) fn make_bipxx_private>( - bip: u32, - key: K, - keychain: KeychainKind, - network: Network, - ) -> Result, DescriptorError> { - let mut derivation_path = alloc::vec::Vec::with_capacity(4); - derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?); - - match network { - Network::Bitcoin => { - derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?); - } - _ => { - derivation_path.push(bip32::ChildNumber::from_hardened_idx(1)?); - } - } - derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?); - - match keychain { - KeychainKind::External => { - derivation_path.push(bip32::ChildNumber::from_normal_idx(0)?) - } - KeychainKind::Internal => { - derivation_path.push(bip32::ChildNumber::from_normal_idx(1)?) - } - }; - - let derivation_path: bip32::DerivationPath = derivation_path.into(); - - Ok((key, derivation_path)) - } - pub(super) fn make_bipxx_public>( - bip: u32, - key: K, - parent_fingerprint: bip32::Fingerprint, - keychain: KeychainKind, - network: Network, - ) -> Result, DescriptorError> { - let derivation_path: bip32::DerivationPath = match keychain { - KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(), - KeychainKind::Internal => vec![bip32::ChildNumber::from_normal_idx(1)?].into(), - }; - - let source_path = bip32::DerivationPath::from(vec![ - bip32::ChildNumber::from_hardened_idx(bip)?, - match network { - Network::Bitcoin => bip32::ChildNumber::from_hardened_idx(0)?, - _ => bip32::ChildNumber::from_hardened_idx(1)?, - }, - bip32::ChildNumber::from_hardened_idx(0)?, - ]); - - Ok((key, (parent_fingerprint, source_path), derivation_path)) - } - } - }; -} - -expand_make_bipxx!(legacy, Legacy); -expand_make_bipxx!(segwit_v0, Segwitv0); -expand_make_bipxx!(segwit_v1, Tap); - -#[cfg(test)] -mod test { - // test existing descriptor templates, make sure they are expanded to the right descriptors - - use alloc::{string::ToString, vec::Vec}; - use core::str::FromStr; - - use super::*; - use crate::descriptor::{DescriptorError, DescriptorMeta}; - use crate::keys::ValidNetworks; - use assert_matches::assert_matches; - use miniscript::descriptor::{DescriptorPublicKey, KeyMap}; - use miniscript::Descriptor; - - // BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)` - #[test] - fn test_bip44_template_cointype() { - use bitcoin::bip32::ChildNumber::{self, Hardened}; - - let xprvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap(); - assert_eq!(Network::Bitcoin, xprvkey.network); - let xdesc = Bip44(xprvkey, KeychainKind::Internal) - .build(Network::Bitcoin) - .unwrap(); - - if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 { - let path: Vec = pkh.into_inner().full_derivation_path().unwrap().into(); - let purpose = path.first().unwrap(); - assert_matches!(purpose, Hardened { index: 44 }); - let coin_type = path.get(1).unwrap(); - assert_matches!(coin_type, Hardened { index: 0 }); - } - - let tprvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - assert_eq!(Network::Testnet, tprvkey.network); - let tdesc = Bip44(tprvkey, KeychainKind::Internal) - .build(Network::Testnet) - .unwrap(); - - if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 { - let path: Vec = pkh.into_inner().full_derivation_path().unwrap().into(); - let purpose = path.first().unwrap(); - assert_matches!(purpose, Hardened { index: 44 }); - let coin_type = path.get(1).unwrap(); - assert_matches!(coin_type, Hardened { index: 1 }); - } - } - - // verify template descriptor generates expected address(es) - fn check( - desc: Result<(Descriptor, KeyMap, ValidNetworks), DescriptorError>, - is_witness: bool, - is_taproot: bool, - is_fixed: bool, - network: Network, - expected: &[&str], - ) { - let (desc, _key_map, _networks) = desc.unwrap(); - assert_eq!(desc.is_witness(), is_witness); - assert_eq!(desc.is_taproot(), is_taproot); - assert_eq!(!desc.has_wildcard(), is_fixed); - for i in 0..expected.len() { - let index = i as u32; - let child_desc = if !desc.has_wildcard() { - desc.at_derivation_index(0).unwrap() - } else { - desc.at_derivation_index(index).unwrap() - }; - let address = child_desc.address(network).unwrap(); - assert_eq!(address.to_string(), *expected.get(i).unwrap()); - } - } - - // P2PKH - #[test] - fn test_p2ph_template() { - let prvkey = - bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") - .unwrap(); - check( - P2Pkh(prvkey).build(Network::Bitcoin), - false, - false, - true, - Network::Regtest, - &["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"], - ); - - let pubkey = bitcoin::PublicKey::from_str( - "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", - ) - .unwrap(); - check( - P2Pkh(pubkey).build(Network::Bitcoin), - false, - false, - true, - Network::Regtest, - &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"], - ); - } - - // P2WPKH-P2SH `sh(wpkh(key))` - #[test] - fn test_p2wphp2sh_template() { - let prvkey = - bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") - .unwrap(); - check( - P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin), - true, - false, - true, - Network::Regtest, - &["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"], - ); - - let pubkey = bitcoin::PublicKey::from_str( - "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", - ) - .unwrap(); - check( - P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin), - true, - false, - true, - Network::Regtest, - &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"], - ); - } - - // P2WPKH `wpkh(key)` - #[test] - fn test_p2wph_template() { - let prvkey = - bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") - .unwrap(); - check( - P2Wpkh(prvkey).build(Network::Bitcoin), - true, - false, - true, - Network::Regtest, - &["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"], - ); - - let pubkey = bitcoin::PublicKey::from_str( - "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", - ) - .unwrap(); - check( - P2Wpkh(pubkey).build(Network::Bitcoin), - true, - false, - true, - Network::Regtest, - &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"], - ); - } - - // P2TR `tr(key)` - #[test] - fn test_p2tr_template() { - let prvkey = - bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") - .unwrap(); - check( - P2TR(prvkey).build(Network::Bitcoin), - false, - true, - true, - Network::Regtest, - &["bcrt1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xqnwtkqq"], - ); - - let pubkey = bitcoin::PublicKey::from_str( - "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", - ) - .unwrap(); - check( - P2TR(pubkey).build(Network::Bitcoin), - false, - true, - true, - Network::Regtest, - &["bcrt1pw74tdcrxlzn5r8z6ku2vztr86fgq0m245s72mjktf4afwzsf8ugs4evwdf"], - ); - } - - // BIP44 `pkh(key/44'/0'/0'/{0,1}/*)` - #[test] - fn test_bip44_template() { - let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - check( - Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin), - false, - false, - false, - Network::Regtest, - &[ - "n453VtnjDHPyDt2fDstKSu7A3YCJoHZ5g5", - "mvfrrumXgTtwFPWDNUecBBgzuMXhYM7KRP", - "mzYvhRAuQqbdSKMVVzXNYyqihgNdRadAUQ", - ], - ); - check( - Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin), - false, - false, - false, - Network::Regtest, - &[ - "muHF98X9KxEzdKrnFAX85KeHv96eXopaip", - "n4hpyLJE5ub6B5Bymv4eqFxS5KjrewSmYR", - "mgvkdv1ffmsXd2B1sRKQ5dByK3SzpG42rA", - ], - ); - } - - // BIP44 public `pkh(key/{0,1}/*)` - #[test] - fn test_bip44_public_template() { - let pubkey = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap(); - let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap(); - check( - Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin), - false, - false, - false, - Network::Regtest, - &[ - "miNG7dJTzJqNbFS19svRdTCisC65dsubtR", - "n2UqaDbCjWSFJvpC84m3FjUk5UaeibCzYg", - "muCPpS6Ue7nkzeJMWDViw7Lkwr92Yc4K8g", - ], - ); - check( - Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin), - false, - false, - false, - Network::Regtest, - &[ - "moDr3vJ8wpt5nNxSK55MPq797nXJb2Ru9H", - "ms7A1Yt4uTezT2XkefW12AvLoko8WfNJMG", - "mhYiyat2rtEnV77cFfQsW32y1m2ceCGHPo", - ], - ); - } - - // BIP49 `sh(wpkh(key/49'/0'/0'/{0,1}/*))` - #[test] - fn test_bip49_template() { - let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - check( - Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin), - true, - false, - false, - Network::Regtest, - &[ - "2N9bCAJXGm168MjVwpkBdNt6ucka3PKVoUV", - "2NDckYkqrYyDMtttEav5hB3Bfw9EGAW5HtS", - "2NAFTVtksF9T4a97M7nyCjwUBD24QevZ5Z4", - ], - ); - check( - Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin), - true, - false, - false, - Network::Regtest, - &[ - "2NB3pA8PnzJLGV8YEKNDFpbViZv3Bm1K6CG", - "2NBiX2Wzxngb5rPiWpUiJQ2uLVB4HBjFD4p", - "2NA8ek4CdQ6aMkveYF6AYuEYNrftB47QGTn", - ], - ); - } - - // BIP49 public `sh(wpkh(key/{0,1}/*))` - #[test] - fn test_bip49_public_template() { - let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap(); - let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap(); - check( - Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin), - true, - false, - false, - Network::Regtest, - &[ - "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt", - "2NCTQfJ1sZa3wQ3pPseYRHbaNEpC3AquEfX", - "2MveFxAuC8BYPzTybx7FxSzW8HSd8ATT4z7", - ], - ); - check( - Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin), - true, - false, - false, - Network::Regtest, - &[ - "2NF2vttKibwyxigxtx95Zw8K7JhDbo5zPVJ", - "2Mtmyd8taksxNVWCJ4wVvaiss7QPZGcAJuH", - "2NBs3CTVYPr1HCzjB4YFsnWCPCtNg8uMEfp", - ], - ); - } - - // BIP84 `wpkh(key/84'/0'/0'/{0,1}/*)` - #[test] - fn test_bip84_template() { - let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - check( - Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin), - true, - false, - false, - Network::Regtest, - &[ - "bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s", - "bcrt1qx0v6zgfwe50m4kqc58cqzcyem7ay2sfl3gvqhp", - "bcrt1q4h7fq9zhxst6e69p3n882nfj649l7w9g3zccfp", - ], - ); - check( - Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin), - true, - false, - false, - Network::Regtest, - &[ - "bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa", - "bcrt1qqqasfhxpkkf7zrxqnkr2sfhn74dgsrc3e3ky45", - "bcrt1qpks7n0gq74hsgsz3phn5vuazjjq0f5eqhsgyce", - ], - ); - } - - // BIP84 public `wpkh(key/{0,1}/*)` - #[test] - fn test_bip84_public_template() { - let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap(); - let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap(); - check( - Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin), - true, - false, - false, - Network::Regtest, - &[ - "bcrt1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2prcdvd0h", - "bcrt1q3lncdlwq3lgcaaeyruynjnlccr0ve0kakh6ana", - "bcrt1qt9800y6xl3922jy3uyl0z33jh5wfpycyhcylr9", - ], - ); - check( - Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin), - true, - false, - false, - Network::Regtest, - &[ - "bcrt1qm6wqukenh7guu792lj2njgw9n78cmwsy8xy3z2", - "bcrt1q694twxtjn4nnrvnyvra769j0a23rllj5c6cgwp", - "bcrt1qhlac3c5ranv5w5emlnqs7wxhkxt8maelylcarp", - ], - ); - } - - // BIP86 `tr(key/86'/0'/0'/{0,1}/*)` - // Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki - #[test] - fn test_bip86_template() { - let prvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap(); - check( - Bip86(prvkey, KeychainKind::External).build(Network::Bitcoin), - false, - true, - false, - Network::Bitcoin, - &[ - "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", - "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh", - "bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8", - ], - ); - check( - Bip86(prvkey, KeychainKind::Internal).build(Network::Bitcoin), - false, - true, - false, - Network::Bitcoin, - &[ - "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", - "bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj", - "bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5", - ], - ); - } - - // BIP86 public `tr(key/{0,1}/*)` - // Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki - #[test] - fn test_bip86_public_template() { - let pubkey = bitcoin::bip32::Xpub::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap(); - let fingerprint = bitcoin::bip32::Fingerprint::from_str("73c5da0a").unwrap(); - check( - Bip86Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin), - false, - true, - false, - Network::Bitcoin, - &[ - "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", - "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh", - "bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8", - ], - ); - check( - Bip86Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin), - false, - true, - false, - Network::Bitcoin, - &[ - "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", - "bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj", - "bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5", - ], - ); - } -} diff --git a/crates/bdk/src/keys/bip39.rs b/crates/bdk/src/keys/bip39.rs deleted file mode 100644 index 7158505f..00000000 --- a/crates/bdk/src/keys/bip39.rs +++ /dev/null @@ -1,227 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! BIP-0039 - -// TODO: maybe write our own implementation of bip39? Seems stupid to have an extra dependency for -// something that should be fairly simple to re-implement. - -use alloc::string::String; -use bitcoin::bip32; -use bitcoin::Network; - -use miniscript::ScriptContext; - -pub use bip39::{Error, Language, Mnemonic}; - -type Seed = [u8; 64]; - -/// Type describing entropy length (aka word count) in the mnemonic -pub enum WordCount { - /// 12 words mnemonic (128 bits entropy) - Words12 = 128, - /// 15 words mnemonic (160 bits entropy) - Words15 = 160, - /// 18 words mnemonic (192 bits entropy) - Words18 = 192, - /// 21 words mnemonic (224 bits entropy) - Words21 = 224, - /// 24 words mnemonic (256 bits entropy) - Words24 = 256, -} - -use super::{ - any_network, DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey, KeyError, -}; - -fn set_valid_on_any_network( - descriptor_key: DescriptorKey, -) -> DescriptorKey { - // We have to pick one network to build the xprv, but since the bip39 standard doesn't - // encode the network, the xprv we create is actually valid everywhere. So we override the - // valid networks with `any_network()`. - descriptor_key.override_valid_networks(any_network()) -} - -/// Type for a BIP39 mnemonic with an optional passphrase -pub type MnemonicWithPassphrase = (Mnemonic, Option); - -#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] -impl DerivableKey for Seed { - fn into_extended_key(self) -> Result, KeyError> { - Ok(bip32::Xpriv::new_master(Network::Bitcoin, &self[..])?.into()) - } - - fn into_descriptor_key( - self, - source: Option, - derivation_path: bip32::DerivationPath, - ) -> Result, KeyError> { - let descriptor_key = self - .into_extended_key()? - .into_descriptor_key(source, derivation_path)?; - - Ok(set_valid_on_any_network(descriptor_key)) - } -} - -#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] -impl DerivableKey for MnemonicWithPassphrase { - fn into_extended_key(self) -> Result, KeyError> { - let (mnemonic, passphrase) = self; - let seed: Seed = mnemonic.to_seed(passphrase.as_deref().unwrap_or("")); - - seed.into_extended_key() - } - - fn into_descriptor_key( - self, - source: Option, - derivation_path: bip32::DerivationPath, - ) -> Result, KeyError> { - let descriptor_key = self - .into_extended_key()? - .into_descriptor_key(source, derivation_path)?; - - Ok(set_valid_on_any_network(descriptor_key)) - } -} - -#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] -impl DerivableKey for (GeneratedKey, Option) { - fn into_extended_key(self) -> Result, KeyError> { - let (mnemonic, passphrase) = self; - (mnemonic.into_key(), passphrase).into_extended_key() - } - - fn into_descriptor_key( - self, - source: Option, - derivation_path: bip32::DerivationPath, - ) -> Result, KeyError> { - let (mnemonic, passphrase) = self; - (mnemonic.into_key(), passphrase).into_descriptor_key(source, derivation_path) - } -} - -#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] -impl DerivableKey for Mnemonic { - fn into_extended_key(self) -> Result, KeyError> { - (self, None).into_extended_key() - } - - fn into_descriptor_key( - self, - source: Option, - derivation_path: bip32::DerivationPath, - ) -> Result, KeyError> { - let descriptor_key = self - .into_extended_key()? - .into_descriptor_key(source, derivation_path)?; - - Ok(set_valid_on_any_network(descriptor_key)) - } -} - -#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] -impl GeneratableKey for Mnemonic { - type Entropy = [u8; 32]; - - type Options = (WordCount, Language); - type Error = Option; - - fn generate_with_entropy( - (word_count, language): Self::Options, - entropy: Self::Entropy, - ) -> Result, Self::Error> { - let entropy = &entropy[..(word_count as usize / 8)]; - let mnemonic = Mnemonic::from_entropy_in(language, entropy)?; - - Ok(GeneratedKey::new(mnemonic, any_network())) - } -} - -#[cfg(test)] -mod test { - use alloc::string::ToString; - use core::str::FromStr; - - use bitcoin::bip32; - - use bip39::{Language, Mnemonic}; - - use crate::keys::{any_network, GeneratableKey, GeneratedKey}; - - use super::WordCount; - - #[test] - fn test_keys_bip39_mnemonic() { - let mnemonic = - "aim bunker wash balance finish force paper analyst cabin spoon stable organ"; - let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap(); - let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap(); - - let key = (mnemonic, path); - let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap(); - assert_eq!(desc.to_string(), "wpkh([be83839f/44'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)#0r8v4nkv"); - assert_eq!(keys.len(), 1); - assert_eq!(networks.len(), 4); - } - - #[test] - fn test_keys_bip39_mnemonic_passphrase() { - let mnemonic = - "aim bunker wash balance finish force paper analyst cabin spoon stable organ"; - let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap(); - let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap(); - - let key = ((mnemonic, Some("passphrase".into())), path); - let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap(); - assert_eq!(desc.to_string(), "wpkh([8f6cb80c/44'/0'/0']xpub6DWYS8bbihFevy29M4cbw4ZR3P5E12jB8R88gBDWCTCNpYiDHhYWNywrCF9VZQYagzPmsZpxXpytzSoxynyeFr4ZyzheVjnpLKuse4fiwZw/0/*)#h0j0tg5m"); - assert_eq!(keys.len(), 1); - assert_eq!(networks.len(), 4); - } - - #[test] - fn test_keys_generate_bip39() { - let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = - Mnemonic::generate_with_entropy( - (WordCount::Words12, Language::English), - crate::keys::test::TEST_ENTROPY, - ) - .unwrap(); - assert_eq!(generated_mnemonic.valid_networks, any_network()); - assert_eq!( - generated_mnemonic.to_string(), - "primary fetch primary fetch primary fetch primary fetch primary fetch primary fever" - ); - - let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = - Mnemonic::generate_with_entropy( - (WordCount::Words24, Language::English), - crate::keys::test::TEST_ENTROPY, - ) - .unwrap(); - assert_eq!(generated_mnemonic.valid_networks, any_network()); - assert_eq!(generated_mnemonic.to_string(), "primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary foster"); - } - - #[test] - fn test_keys_generate_bip39_random() { - let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = - Mnemonic::generate((WordCount::Words12, Language::English)).unwrap(); - assert_eq!(generated_mnemonic.valid_networks, any_network()); - - let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = - Mnemonic::generate((WordCount::Words24, Language::English)).unwrap(); - assert_eq!(generated_mnemonic.valid_networks, any_network()); - } -} diff --git a/crates/bdk/src/keys/mod.rs b/crates/bdk/src/keys/mod.rs deleted file mode 100644 index 75abb3aa..00000000 --- a/crates/bdk/src/keys/mod.rs +++ /dev/null @@ -1,1006 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Key formats - -use crate::collections::HashSet; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; -use core::any::TypeId; -use core::fmt; -use core::marker::PhantomData; -use core::ops::Deref; -use core::str::FromStr; - -use bitcoin::secp256k1::{self, Secp256k1, Signing}; - -use bitcoin::bip32; -use bitcoin::{key::XOnlyPublicKey, Network, PrivateKey, PublicKey}; - -use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard}; -pub use miniscript::descriptor::{ - DescriptorPublicKey, DescriptorSecretKey, KeyMap, SinglePriv, SinglePub, SinglePubKey, - SortedMultiVec, -}; -pub use miniscript::ScriptContext; -use miniscript::{Miniscript, Terminal}; - -use crate::descriptor::{CheckMiniscript, DescriptorError}; -use crate::wallet::utils::SecpCtx; - -#[cfg(feature = "keys-bip39")] -#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] -pub mod bip39; - -/// Set of valid networks for a key -pub type ValidNetworks = HashSet; - -/// Create a set containing mainnet, testnet, signet, and regtest -pub fn any_network() -> ValidNetworks { - vec![ - Network::Bitcoin, - Network::Testnet, - Network::Regtest, - Network::Signet, - ] - .into_iter() - .collect() -} -/// Create a set only containing mainnet -pub fn mainnet_network() -> ValidNetworks { - vec![Network::Bitcoin].into_iter().collect() -} -/// Create a set containing testnet and regtest -pub fn test_networks() -> ValidNetworks { - vec![Network::Testnet, Network::Regtest, Network::Signet] - .into_iter() - .collect() -} -/// Compute the intersection of two sets -pub fn merge_networks(a: &ValidNetworks, b: &ValidNetworks) -> ValidNetworks { - a.intersection(b).cloned().collect() -} - -/// Container for public or secret keys -#[derive(Debug)] -pub enum DescriptorKey { - #[doc(hidden)] - Public(DescriptorPublicKey, ValidNetworks, PhantomData), - #[doc(hidden)] - Secret(DescriptorSecretKey, ValidNetworks, PhantomData), -} - -impl DescriptorKey { - /// Create an instance given a public key and a set of valid networks - pub fn from_public(public: DescriptorPublicKey, networks: ValidNetworks) -> Self { - DescriptorKey::Public(public, networks, PhantomData) - } - - /// Create an instance given a secret key and a set of valid networks - pub fn from_secret(secret: DescriptorSecretKey, networks: ValidNetworks) -> Self { - DescriptorKey::Secret(secret, networks, PhantomData) - } - - /// Override the computed set of valid networks - pub fn override_valid_networks(self, networks: ValidNetworks) -> Self { - match self { - DescriptorKey::Public(key, _, _) => DescriptorKey::Public(key, networks, PhantomData), - DescriptorKey::Secret(key, _, _) => DescriptorKey::Secret(key, networks, PhantomData), - } - } - - // This method is used internally by `bdk::fragment!` and `bdk::descriptor!`. It has to be - // public because it is effectively called by external crates once the macros are expanded, - // but since it is not meant to be part of the public api we hide it from the docs. - #[doc(hidden)] - pub fn extract( - self, - secp: &SecpCtx, - ) -> Result<(DescriptorPublicKey, KeyMap, ValidNetworks), KeyError> { - match self { - DescriptorKey::Public(public, valid_networks, _) => { - Ok((public, KeyMap::default(), valid_networks)) - } - DescriptorKey::Secret(secret, valid_networks, _) => { - let mut key_map = KeyMap::new(); - - let public = secret - .to_public(secp) - .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?; - key_map.insert(public.clone(), secret); - - Ok((public, key_map, valid_networks)) - } - } - } -} - -/// Enum representation of the known valid [`ScriptContext`]s -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -pub enum ScriptContextEnum { - /// Legacy scripts - Legacy, - /// Segwitv0 scripts - Segwitv0, - /// Taproot scripts - Tap, -} - -impl ScriptContextEnum { - /// Returns whether the script context is [`ScriptContextEnum::Legacy`] - pub fn is_legacy(&self) -> bool { - self == &ScriptContextEnum::Legacy - } - - /// Returns whether the script context is [`ScriptContextEnum::Segwitv0`] - pub fn is_segwit_v0(&self) -> bool { - self == &ScriptContextEnum::Segwitv0 - } - - /// Returns whether the script context is [`ScriptContextEnum::Tap`] - pub fn is_taproot(&self) -> bool { - self == &ScriptContextEnum::Tap - } -} - -/// Trait that adds extra useful methods to [`ScriptContext`]s -pub trait ExtScriptContext: ScriptContext { - /// Returns the [`ScriptContext`] as a [`ScriptContextEnum`] - fn as_enum() -> ScriptContextEnum; - - /// Returns whether the script context is [`Legacy`](miniscript::Legacy) - fn is_legacy() -> bool { - Self::as_enum().is_legacy() - } - - /// Returns whether the script context is [`Segwitv0`](miniscript::Segwitv0) - fn is_segwit_v0() -> bool { - Self::as_enum().is_segwit_v0() - } - - /// Returns whether the script context is [`Tap`](miniscript::Tap), aka Taproot or Segwit V1 - fn is_taproot() -> bool { - Self::as_enum().is_taproot() - } -} - -impl ExtScriptContext for Ctx { - fn as_enum() -> ScriptContextEnum { - match TypeId::of::() { - t if t == TypeId::of::() => ScriptContextEnum::Legacy, - t if t == TypeId::of::() => ScriptContextEnum::Segwitv0, - t if t == TypeId::of::() => ScriptContextEnum::Tap, - _ => unimplemented!("Unknown ScriptContext type"), - } - } -} - -/// Trait for objects that can be turned into a public or secret [`DescriptorKey`] -/// -/// The generic type `Ctx` is used to define the context in which the key is valid: some key -/// formats, like the mnemonics used by Electrum wallets, encode internally whether the wallet is -/// legacy or segwit. Thus, trying to turn a valid legacy mnemonic into a `DescriptorKey` -/// that would become part of a segwit descriptor should fail. -/// -/// For key types that do care about this, the [`ExtScriptContext`] trait provides some useful -/// methods that can be used to check at runtime which `Ctx` is being used. -/// -/// For key types that can do this check statically (because they can only work within a -/// single `Ctx`), the "specialized" trait can be implemented to make the compiler handle the type -/// checking. -/// -/// Keys also have control over the networks they support: constructing the return object with -/// [`DescriptorKey::from_public`] or [`DescriptorKey::from_secret`] allows to specify a set of -/// [`ValidNetworks`]. -/// -/// ## Examples -/// -/// Key type valid in any context: -/// -/// ``` -/// use bdk::bitcoin::PublicKey; -/// -/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext}; -/// -/// pub struct MyKeyType { -/// pubkey: PublicKey, -/// } -/// -/// impl IntoDescriptorKey for MyKeyType { -/// fn into_descriptor_key(self) -> Result, KeyError> { -/// self.pubkey.into_descriptor_key() -/// } -/// } -/// ``` -/// -/// Key type that is only valid on mainnet: -/// -/// ``` -/// use bdk::bitcoin::PublicKey; -/// -/// use bdk::keys::{ -/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError, -/// ScriptContext, SinglePub, SinglePubKey, -/// }; -/// -/// pub struct MyKeyType { -/// pubkey: PublicKey, -/// } -/// -/// impl IntoDescriptorKey for MyKeyType { -/// fn into_descriptor_key(self) -> Result, KeyError> { -/// Ok(DescriptorKey::from_public( -/// DescriptorPublicKey::Single(SinglePub { -/// origin: None, -/// key: SinglePubKey::FullKey(self.pubkey), -/// }), -/// mainnet_network(), -/// )) -/// } -/// } -/// ``` -/// -/// Key type that internally encodes in which context it's valid. The context is checked at runtime: -/// -/// ``` -/// use bdk::bitcoin::PublicKey; -/// -/// use bdk::keys::{DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext}; -/// -/// pub struct MyKeyType { -/// is_legacy: bool, -/// pubkey: PublicKey, -/// } -/// -/// impl IntoDescriptorKey for MyKeyType { -/// fn into_descriptor_key(self) -> Result, KeyError> { -/// if Ctx::is_legacy() == self.is_legacy { -/// self.pubkey.into_descriptor_key() -/// } else { -/// Err(KeyError::InvalidScriptContext) -/// } -/// } -/// } -/// ``` -/// -/// Key type that can only work within [`miniscript::Segwitv0`] context. Only the specialized version -/// of the trait is implemented. -/// -/// This example deliberately fails to compile, to demonstrate how the compiler can catch when keys -/// are misused. In this case, the "segwit-only" key is used to build a `pkh()` descriptor, which -/// makes the compiler (correctly) fail. -/// -/// ```compile_fail -/// use bdk::bitcoin::PublicKey; -/// use core::str::FromStr; -/// -/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError}; -/// -/// pub struct MySegwitOnlyKeyType { -/// pubkey: PublicKey, -/// } -/// -/// impl IntoDescriptorKey for MySegwitOnlyKeyType { -/// fn into_descriptor_key(self) -> Result, KeyError> { -/// self.pubkey.into_descriptor_key() -/// } -/// } -/// -/// let key = MySegwitOnlyKeyType { -/// pubkey: PublicKey::from_str("...")?, -/// }; -/// let (descriptor, _, _) = bdk::descriptor!(pkh(key))?; -/// // ^^^^^ changing this to `wpkh` would make it compile -/// -/// # Ok::<_, Box>(()) -/// ``` -pub trait IntoDescriptorKey: Sized { - /// Turn the key into a [`DescriptorKey`] within the requested [`ScriptContext`] - fn into_descriptor_key(self) -> Result, KeyError>; -} - -/// Enum for extended keys that can be either `xprv` or `xpub` -/// -/// An instance of [`ExtendedKey`] can be constructed from an [`Xpriv`](bip32::Xpriv) -/// or an [`Xpub`](bip32::Xpub) by using the `From` trait. -/// -/// Defaults to the [`Legacy`](miniscript::Legacy) context. -pub enum ExtendedKey { - /// A private extended key, aka an `xprv` - Private((bip32::Xpriv, PhantomData)), - /// A public extended key, aka an `xpub` - Public((bip32::Xpub, PhantomData)), -} - -impl ExtendedKey { - /// Return whether or not the key contains the private data - pub fn has_secret(&self) -> bool { - match self { - ExtendedKey::Private(_) => true, - ExtendedKey::Public(_) => false, - } - } - - /// Transform the [`ExtendedKey`] into an [`Xpriv`](bip32::Xpriv) for the - /// given [`Network`], if the key contains the private data - pub fn into_xprv(self, network: Network) -> Option { - match self { - ExtendedKey::Private((mut xprv, _)) => { - xprv.network = network; - Some(xprv) - } - ExtendedKey::Public(_) => None, - } - } - - /// Transform the [`ExtendedKey`] into an [`Xpub`](bip32::Xpub) for the - /// given [`Network`] - pub fn into_xpub( - self, - network: bitcoin::Network, - secp: &Secp256k1, - ) -> bip32::Xpub { - let mut xpub = match self { - ExtendedKey::Private((xprv, _)) => bip32::Xpub::from_priv(secp, &xprv), - ExtendedKey::Public((xpub, _)) => xpub, - }; - - xpub.network = network; - xpub - } -} - -impl From for ExtendedKey { - fn from(xpub: bip32::Xpub) -> Self { - ExtendedKey::Public((xpub, PhantomData)) - } -} - -impl From for ExtendedKey { - fn from(xprv: bip32::Xpriv) -> Self { - ExtendedKey::Private((xprv, PhantomData)) - } -} - -/// Trait for keys that can be derived. -/// -/// When extra metadata are provided, a [`DerivableKey`] can be transformed into a -/// [`DescriptorKey`]: the trait [`IntoDescriptorKey`] is automatically implemented -/// for `(DerivableKey, DerivationPath)` and -/// `(DerivableKey, KeySource, DerivationPath)` tuples. -/// -/// For key types that don't encode any indication about the path to use (like bip39), it's -/// generally recommended to implement this trait instead of [`IntoDescriptorKey`]. The same -/// rules regarding script context and valid networks apply. -/// -/// ## Examples -/// -/// Key types that can be directly converted into an [`Xpriv`] or -/// an [`Xpub`] can implement only the required `into_extended_key()` method. -/// -/// ``` -/// use bdk::bitcoin; -/// use bdk::bitcoin::bip32; -/// use bdk::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext}; -/// -/// struct MyCustomKeyType { -/// key_data: bitcoin::PrivateKey, -/// chain_code: [u8; 32], -/// network: bitcoin::Network, -/// } -/// -/// impl DerivableKey for MyCustomKeyType { -/// fn into_extended_key(self) -> Result, KeyError> { -/// let xprv = bip32::Xpriv { -/// network: self.network, -/// depth: 0, -/// parent_fingerprint: bip32::Fingerprint::default(), -/// private_key: self.key_data.inner, -/// chain_code: bip32::ChainCode::from(&self.chain_code), -/// child_number: bip32::ChildNumber::Normal { index: 0 }, -/// }; -/// -/// xprv.into_extended_key() -/// } -/// } -/// ``` -/// -/// Types that don't internally encode the [`Network`] in which they are valid need some extra -/// steps to override the set of valid networks, otherwise only the network specified in the -/// [`Xpriv`] or [`Xpub`] will be considered valid. -/// -/// ``` -/// use bdk::bitcoin; -/// use bdk::bitcoin::bip32; -/// use bdk::keys::{ -/// any_network, DerivableKey, DescriptorKey, ExtendedKey, KeyError, ScriptContext, -/// }; -/// -/// struct MyCustomKeyType { -/// key_data: bitcoin::PrivateKey, -/// chain_code: [u8; 32], -/// } -/// -/// impl DerivableKey for MyCustomKeyType { -/// fn into_extended_key(self) -> Result, KeyError> { -/// let xprv = bip32::Xpriv { -/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here -/// depth: 0, -/// parent_fingerprint: bip32::Fingerprint::default(), -/// private_key: self.key_data.inner, -/// chain_code: bip32::ChainCode::from(&self.chain_code), -/// child_number: bip32::ChildNumber::Normal { index: 0 }, -/// }; -/// -/// xprv.into_extended_key() -/// } -/// -/// fn into_descriptor_key( -/// self, -/// source: Option, -/// derivation_path: bip32::DerivationPath, -/// ) -> Result, KeyError> { -/// let descriptor_key = self -/// .into_extended_key()? -/// .into_descriptor_key(source, derivation_path)?; -/// -/// // Override the set of valid networks here -/// Ok(descriptor_key.override_valid_networks(any_network())) -/// } -/// } -/// ``` -/// -/// [`DerivationPath`]: (bip32::DerivationPath) -/// [`Xpriv`]: (bip32::Xpriv) -/// [`Xpub`]: (bip32::Xpub) -pub trait DerivableKey: Sized { - /// Consume `self` and turn it into an [`ExtendedKey`] - #[cfg_attr( - feature = "keys-bip39", - doc = r##" -This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait, -like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled. -```rust -use bdk::bitcoin::Network; -use bdk::keys::{DerivableKey, ExtendedKey}; -use bdk::keys::bip39::{Mnemonic, Language}; - -# fn main() -> Result<(), Box> { -let xkey: ExtendedKey = - Mnemonic::parse_in( - Language::English, - "jelly crash boy whisper mouse ecology tuna soccer memory million news short", - )? - .into_extended_key()?; -let xprv = xkey.into_xprv(Network::Bitcoin).unwrap(); -# Ok(()) } -``` -"## - )] - fn into_extended_key(self) -> Result, KeyError>; - - /// Consume `self` and turn it into a [`DescriptorKey`] by adding the extra metadata, such as - /// key origin and derivation path - fn into_descriptor_key( - self, - origin: Option, - derivation_path: bip32::DerivationPath, - ) -> Result, KeyError> { - match self.into_extended_key()? { - ExtendedKey::Private((xprv, _)) => DescriptorSecretKey::XPrv(DescriptorXKey { - origin, - xkey: xprv, - derivation_path, - wildcard: Wildcard::Unhardened, - }) - .into_descriptor_key(), - ExtendedKey::Public((xpub, _)) => DescriptorPublicKey::XPub(DescriptorXKey { - origin, - xkey: xpub, - derivation_path, - wildcard: Wildcard::Unhardened, - }) - .into_descriptor_key(), - } - } -} - -/// Identity conversion -impl DerivableKey for ExtendedKey { - fn into_extended_key(self) -> Result, KeyError> { - Ok(self) - } -} - -impl DerivableKey for bip32::Xpub { - fn into_extended_key(self) -> Result, KeyError> { - Ok(self.into()) - } -} - -impl DerivableKey for bip32::Xpriv { - fn into_extended_key(self) -> Result, KeyError> { - Ok(self.into()) - } -} - -/// Output of a [`GeneratableKey`] key generation -pub struct GeneratedKey { - key: K, - valid_networks: ValidNetworks, - phantom: PhantomData, -} - -impl GeneratedKey { - fn new(key: K, valid_networks: ValidNetworks) -> Self { - GeneratedKey { - key, - valid_networks, - phantom: PhantomData, - } - } - - /// Consumes `self` and returns the key - pub fn into_key(self) -> K { - self.key - } -} - -impl Deref for GeneratedKey { - type Target = K; - - fn deref(&self) -> &Self::Target { - &self.key - } -} - -impl Clone for GeneratedKey { - fn clone(&self) -> GeneratedKey { - GeneratedKey { - key: self.key.clone(), - valid_networks: self.valid_networks.clone(), - phantom: self.phantom, - } - } -} - -// Make generated "derivable" keys themselves "derivable". Also make sure they are assigned the -// right `valid_networks`. -impl DerivableKey for GeneratedKey -where - Ctx: ScriptContext, - K: DerivableKey, -{ - fn into_extended_key(self) -> Result, KeyError> { - self.key.into_extended_key() - } - - fn into_descriptor_key( - self, - origin: Option, - derivation_path: bip32::DerivationPath, - ) -> Result, KeyError> { - let descriptor_key = self.key.into_descriptor_key(origin, derivation_path)?; - Ok(descriptor_key.override_valid_networks(self.valid_networks)) - } -} - -// Make generated keys directly usable in descriptors, and make sure they get assigned the right -// `valid_networks`. -impl IntoDescriptorKey for GeneratedKey -where - Ctx: ScriptContext, - K: IntoDescriptorKey, -{ - fn into_descriptor_key(self) -> Result, KeyError> { - let desc_key = self.key.into_descriptor_key()?; - Ok(desc_key.override_valid_networks(self.valid_networks)) - } -} - -/// Trait for keys that can be generated -/// -/// The same rules about [`ScriptContext`] and [`ValidNetworks`] from [`IntoDescriptorKey`] apply. -/// -/// This trait is particularly useful when combined with [`DerivableKey`]: if `Self` -/// implements it, the returned [`GeneratedKey`] will also implement it. The same is true for -/// [`IntoDescriptorKey`]: the generated keys can be directly used in descriptors if `Self` is also -/// [`IntoDescriptorKey`]. -pub trait GeneratableKey: Sized { - /// Type specifying the amount of entropy required e.g. `[u8;32]` - type Entropy: AsMut<[u8]> + Default; - - /// Extra options required by the `generate_with_entropy` - type Options; - /// Returned error in case of failure - type Error: core::fmt::Debug; - - /// Generate a key given the extra options and the entropy - fn generate_with_entropy( - options: Self::Options, - entropy: Self::Entropy, - ) -> Result, Self::Error>; - - /// Generate a key given the options with a random entropy - fn generate(options: Self::Options) -> Result, Self::Error> { - use rand::{thread_rng, Rng}; - - let mut entropy = Self::Entropy::default(); - thread_rng().fill(entropy.as_mut()); - Self::generate_with_entropy(options, entropy) - } -} - -/// Trait that allows generating a key with the default options -/// -/// This trait is automatically implemented if the [`GeneratableKey::Options`] implements [`Default`]. -pub trait GeneratableDefaultOptions: GeneratableKey -where - Ctx: ScriptContext, - >::Options: Default, -{ - /// Generate a key with the default options and a given entropy - fn generate_with_entropy_default( - entropy: Self::Entropy, - ) -> Result, Self::Error> { - Self::generate_with_entropy(Default::default(), entropy) - } - - /// Generate a key with the default options and a random entropy - fn generate_default() -> Result, Self::Error> { - Self::generate(Default::default()) - } -} - -/// Automatic implementation of [`GeneratableDefaultOptions`] for [`GeneratableKey`]s where -/// `Options` implements `Default` -impl GeneratableDefaultOptions for K -where - Ctx: ScriptContext, - K: GeneratableKey, - >::Options: Default, -{ -} - -impl GeneratableKey for bip32::Xpriv { - type Entropy = [u8; 32]; - - type Options = (); - type Error = bip32::Error; - - fn generate_with_entropy( - _: Self::Options, - entropy: Self::Entropy, - ) -> Result, Self::Error> { - // pick a arbitrary network here, but say that we support all of them - let xprv = bip32::Xpriv::new_master(Network::Bitcoin, entropy.as_ref())?; - Ok(GeneratedKey::new(xprv, any_network())) - } -} - -/// Options for generating a [`PrivateKey`] -/// -/// Defaults to creating compressed keys, which save on-chain bytes and fees -#[derive(Debug, Copy, Clone)] -pub struct PrivateKeyGenerateOptions { - /// Whether the generated key should be "compressed" or not - pub compressed: bool, -} - -impl Default for PrivateKeyGenerateOptions { - fn default() -> Self { - PrivateKeyGenerateOptions { compressed: true } - } -} - -impl GeneratableKey for PrivateKey { - type Entropy = [u8; secp256k1::constants::SECRET_KEY_SIZE]; - - type Options = PrivateKeyGenerateOptions; - type Error = bip32::Error; - - fn generate_with_entropy( - options: Self::Options, - entropy: Self::Entropy, - ) -> Result, Self::Error> { - // pick a arbitrary network here, but say that we support all of them - let inner = secp256k1::SecretKey::from_slice(&entropy)?; - let private_key = PrivateKey { - compressed: options.compressed, - network: Network::Bitcoin, - inner, - }; - - Ok(GeneratedKey::new(private_key, any_network())) - } -} - -impl> IntoDescriptorKey - for (T, bip32::DerivationPath) -{ - fn into_descriptor_key(self) -> Result, KeyError> { - self.0.into_descriptor_key(None, self.1) - } -} - -impl> IntoDescriptorKey - for (T, bip32::KeySource, bip32::DerivationPath) -{ - fn into_descriptor_key(self) -> Result, KeyError> { - self.0.into_descriptor_key(Some(self.1), self.2) - } -} - -fn expand_multi_keys, Ctx: ScriptContext>( - pks: Vec, - secp: &SecpCtx, -) -> Result<(Vec, KeyMap, ValidNetworks), KeyError> { - let (pks, key_maps_networks): (Vec<_>, Vec<_>) = pks - .into_iter() - .map(|key| key.into_descriptor_key()?.extract(secp)) - .collect::, _>>()? - .into_iter() - .map(|(a, b, c)| (a, (b, c))) - .unzip(); - - let (key_map, valid_networks) = key_maps_networks.into_iter().fold( - (KeyMap::default(), any_network()), - |(mut keys_acc, net_acc), (key, net)| { - keys_acc.extend(key); - let net_acc = merge_networks(&net_acc, &net); - - (keys_acc, net_acc) - }, - ); - - Ok((pks, key_map, valid_networks)) -} - -// Used internally by `bdk::fragment!` to build `pk_k()` fragments -#[doc(hidden)] -pub fn make_pk, Ctx: ScriptContext>( - descriptor_key: Pk, - secp: &SecpCtx, -) -> Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError> { - let (key, key_map, valid_networks) = descriptor_key.into_descriptor_key()?.extract(secp)?; - let minisc = Miniscript::from_ast(Terminal::PkK(key))?; - - minisc.check_miniscript()?; - - Ok((minisc, key_map, valid_networks)) -} - -// Used internally by `bdk::fragment!` to build `pk_h()` fragments -#[doc(hidden)] -pub fn make_pkh, Ctx: ScriptContext>( - descriptor_key: Pk, - secp: &SecpCtx, -) -> Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError> { - let (key, key_map, valid_networks) = descriptor_key.into_descriptor_key()?.extract(secp)?; - let minisc = Miniscript::from_ast(Terminal::PkH(key))?; - - minisc.check_miniscript()?; - - Ok((minisc, key_map, valid_networks)) -} - -// Used internally by `bdk::fragment!` to build `multi()` fragments -#[doc(hidden)] -pub fn make_multi< - Pk: IntoDescriptorKey, - Ctx: ScriptContext, - V: Fn(usize, Vec) -> Terminal, ->( - thresh: usize, - variant: V, - pks: Vec, - secp: &SecpCtx, -) -> Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError> { - let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?; - let minisc = Miniscript::from_ast(variant(thresh, pks))?; - - minisc.check_miniscript()?; - - Ok((minisc, key_map, valid_networks)) -} - -// Used internally by `bdk::descriptor!` to build `sortedmulti()` fragments -#[doc(hidden)] -pub fn make_sortedmulti( - thresh: usize, - pks: Vec, - build_desc: F, - secp: &SecpCtx, -) -> Result<(Descriptor, KeyMap, ValidNetworks), DescriptorError> -where - Pk: IntoDescriptorKey, - Ctx: ScriptContext, - F: Fn( - usize, - Vec, - ) -> Result<(Descriptor, PhantomData), DescriptorError>, -{ - let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?; - let descriptor = build_desc(thresh, pks)?.0; - - Ok((descriptor, key_map, valid_networks)) -} - -/// The "identity" conversion is used internally by some `bdk::fragment`s -impl IntoDescriptorKey for DescriptorKey { - fn into_descriptor_key(self) -> Result, KeyError> { - Ok(self) - } -} - -impl IntoDescriptorKey for DescriptorPublicKey { - fn into_descriptor_key(self) -> Result, KeyError> { - let networks = match self { - DescriptorPublicKey::Single(_) => any_network(), - DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. }) - if xkey.network == Network::Bitcoin => - { - mainnet_network() - } - _ => test_networks(), - }; - - Ok(DescriptorKey::from_public(self, networks)) - } -} - -impl IntoDescriptorKey for PublicKey { - fn into_descriptor_key(self) -> Result, KeyError> { - DescriptorPublicKey::Single(SinglePub { - key: SinglePubKey::FullKey(self), - origin: None, - }) - .into_descriptor_key() - } -} - -impl IntoDescriptorKey for XOnlyPublicKey { - fn into_descriptor_key(self) -> Result, KeyError> { - DescriptorPublicKey::Single(SinglePub { - key: SinglePubKey::XOnly(self), - origin: None, - }) - .into_descriptor_key() - } -} - -impl IntoDescriptorKey for DescriptorSecretKey { - fn into_descriptor_key(self) -> Result, KeyError> { - let networks = match &self { - DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => { - mainnet_network() - } - DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. }) - if xkey.network == Network::Bitcoin => - { - mainnet_network() - } - _ => test_networks(), - }; - - Ok(DescriptorKey::from_secret(self, networks)) - } -} - -impl IntoDescriptorKey for &'_ str { - fn into_descriptor_key(self) -> Result, KeyError> { - DescriptorSecretKey::from_str(self) - .map_err(|e| KeyError::Message(e.to_string()))? - .into_descriptor_key() - } -} - -impl IntoDescriptorKey for PrivateKey { - fn into_descriptor_key(self) -> Result, KeyError> { - DescriptorSecretKey::Single(SinglePriv { - key: self, - origin: None, - }) - .into_descriptor_key() - } -} - -/// Errors thrown while working with [`keys`](crate::keys) -#[derive(Debug)] -pub enum KeyError { - /// The key cannot exist in the given script context - InvalidScriptContext, - /// The key is not valid for the given network - InvalidNetwork, - /// The key has an invalid checksum - InvalidChecksum, - - /// Custom error message - Message(String), - - /// BIP32 error - Bip32(bitcoin::bip32::Error), - /// Miniscript error - Miniscript(miniscript::Error), -} - -impl From for KeyError { - fn from(err: miniscript::Error) -> Self { - KeyError::Miniscript(err) - } -} - -impl From for KeyError { - fn from(err: bip32::Error) -> Self { - KeyError::Bip32(err) - } -} - -impl fmt::Display for KeyError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InvalidScriptContext => write!(f, "Invalid script context"), - Self::InvalidNetwork => write!(f, "Invalid network"), - Self::InvalidChecksum => write!(f, "Invalid checksum"), - Self::Message(err) => write!(f, "{}", err), - Self::Bip32(err) => write!(f, "BIP32 error: {}", err), - Self::Miniscript(err) => write!(f, "Miniscript error: {}", err), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for KeyError {} - -#[cfg(test)] -pub mod test { - use bitcoin::bip32; - - use super::*; - - pub const TEST_ENTROPY: [u8; 32] = [0xAA; 32]; - - #[test] - fn test_keys_generate_xprv() { - let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> = - bip32::Xpriv::generate_with_entropy_default(TEST_ENTROPY).unwrap(); - - assert_eq!(generated_xprv.valid_networks, any_network()); - assert_eq!(generated_xprv.to_string(), "xprv9s21ZrQH143K4Xr1cJyqTvuL2FWR8eicgY9boWqMBv8MDVUZ65AXHnzBrK1nyomu6wdcabRgmGTaAKawvhAno1V5FowGpTLVx3jxzE5uk3Q"); - } - - #[test] - fn test_keys_generate_wif() { - let generated_wif: GeneratedKey<_, miniscript::Segwitv0> = - bitcoin::PrivateKey::generate_with_entropy_default(TEST_ENTROPY).unwrap(); - - assert_eq!(generated_wif.valid_networks, any_network()); - assert_eq!( - generated_wif.to_string(), - "L2wTu6hQrnDMiFNWA5na6jB12ErGQqtXwqpSL7aWquJaZG8Ai3ch" - ); - } - - #[cfg(feature = "keys-bip39")] - #[test] - fn test_keys_wif_network_bip39() { - let xkey: ExtendedKey = bip39::Mnemonic::parse_in( - bip39::Language::English, - "jelly crash boy whisper mouse ecology tuna soccer memory million news short", - ) - .unwrap() - .into_extended_key() - .unwrap(); - let xprv = xkey.into_xprv(Network::Testnet).unwrap(); - - assert_eq!(xprv.network, Network::Testnet); - } -} diff --git a/crates/bdk/src/lib.rs b/crates/bdk/src/lib.rs deleted file mode 100644 index f7c6f354..00000000 --- a/crates/bdk/src/lib.rs +++ /dev/null @@ -1,47 +0,0 @@ -#![doc = include_str!("../README.md")] -// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined -#![cfg_attr(docsrs, feature(doc_cfg))] -#![cfg_attr( - docsrs, - doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png") -)] -#![no_std] -#![warn(missing_docs)] - -#[cfg(feature = "std")] -#[macro_use] -extern crate std; - -#[doc(hidden)] -#[macro_use] -pub extern crate alloc; - -pub extern crate bitcoin; -pub extern crate miniscript; -extern crate serde; -extern crate serde_json; - -#[cfg(feature = "keys-bip39")] -extern crate bip39; - -pub mod descriptor; -pub mod keys; -pub mod psbt; -pub(crate) mod types; -pub mod wallet; - -pub use descriptor::template; -pub use descriptor::HdKeyPaths; -pub use types::*; -pub use wallet::signer; -pub use wallet::signer::SignOptions; -pub use wallet::tx_builder::TxBuilder; -pub use wallet::Wallet; - -/// Get the version of BDK at runtime -pub fn version() -> &'static str { - env!("CARGO_PKG_VERSION", "unknown") -} - -pub use bdk_chain as chain; -pub(crate) use bdk_chain::collections; diff --git a/crates/bdk/src/psbt/mod.rs b/crates/bdk/src/psbt/mod.rs deleted file mode 100644 index 7a66989e..00000000 --- a/crates/bdk/src/psbt/mod.rs +++ /dev/null @@ -1,75 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Additional functions on the `rust-bitcoin` `Psbt` structure. - -use alloc::vec::Vec; -use bitcoin::Amount; -use bitcoin::FeeRate; -use bitcoin::Psbt; -use bitcoin::TxOut; - -// TODO upstream the functions here to `rust-bitcoin`? - -/// Trait to add functions to extract utxos and calculate fees. -pub trait PsbtUtils { - /// Get the `TxOut` for the specified input index, if it doesn't exist in the PSBT `None` is returned. - fn get_utxo_for(&self, input_index: usize) -> Option; - - /// The total transaction fee amount, sum of input amounts minus sum of output amounts, in sats. - /// If the PSBT is missing a TxOut for an input returns None. - fn fee_amount(&self) -> Option; - - /// The transaction's fee rate. This value will only be accurate if calculated AFTER the - /// `Psbt` is finalized and all witness/signature data is added to the - /// transaction. - /// If the PSBT is missing a TxOut for an input returns None. - fn fee_rate(&self) -> Option; -} - -impl PsbtUtils for Psbt { - fn get_utxo_for(&self, input_index: usize) -> Option { - let tx = &self.unsigned_tx; - let input = self.inputs.get(input_index)?; - - match (&input.witness_utxo, &input.non_witness_utxo) { - (Some(_), _) => input.witness_utxo.clone(), - (_, Some(_)) => input.non_witness_utxo.as_ref().map(|in_tx| { - in_tx.output[tx.input[input_index].previous_output.vout as usize].clone() - }), - _ => None, - } - } - - fn fee_amount(&self) -> Option { - let tx = &self.unsigned_tx; - let utxos: Option> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect(); - - utxos.map(|inputs| { - let input_amount: u64 = inputs.iter().map(|i| i.value.to_sat()).sum(); - let output_amount: u64 = self - .unsigned_tx - .output - .iter() - .map(|o| o.value.to_sat()) - .sum(); - input_amount - .checked_sub(output_amount) - .expect("input amount must be greater than output amount") - }) - } - - fn fee_rate(&self) -> Option { - let fee_amount = self.fee_amount(); - let weight = self.clone().extract_tx().ok()?.weight(); - fee_amount.map(|fee| Amount::from_sat(fee) / weight) - } -} diff --git a/crates/bdk/src/types.rs b/crates/bdk/src/types.rs deleted file mode 100644 index 4ce961b7..00000000 --- a/crates/bdk/src/types.rs +++ /dev/null @@ -1,135 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -use alloc::boxed::Box; -use core::convert::AsRef; - -use bdk_chain::ConfirmationTime; -use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut}; -use bitcoin::psbt; - -use serde::{Deserialize, Serialize}; - -/// Types of keychains -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub enum KeychainKind { - /// External keychain, used for deriving recipient addresses. - External = 0, - /// Internal keychain, used for deriving change addresses. - Internal = 1, -} - -impl KeychainKind { - /// Return [`KeychainKind`] as a byte - pub fn as_byte(&self) -> u8 { - match self { - KeychainKind::External => b'e', - KeychainKind::Internal => b'i', - } - } -} - -impl AsRef<[u8]> for KeychainKind { - fn as_ref(&self) -> &[u8] { - match self { - KeychainKind::External => b"e", - KeychainKind::Internal => b"i", - } - } -} - -/// An unspent output owned by a [`Wallet`]. -/// -/// [`Wallet`]: crate::Wallet -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -pub struct LocalOutput { - /// Reference to a transaction output - pub outpoint: OutPoint, - /// Transaction output - pub txout: TxOut, - /// Type of keychain - pub keychain: KeychainKind, - /// Whether this UTXO is spent or not - pub is_spent: bool, - /// The derivation index for the script pubkey in the wallet - pub derivation_index: u32, - /// The confirmation time for transaction containing this utxo - pub confirmation_time: ConfirmationTime, -} - -/// A [`Utxo`] with its `satisfaction_weight`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct WeightedUtxo { - /// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to - /// properly maintain the feerate when adding this input to a transaction during coin selection. - /// - /// [weight units]: https://en.bitcoin.it/wiki/Weight_units - pub satisfaction_weight: usize, - /// The UTXO - pub utxo: Utxo, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -/// An unspent transaction output (UTXO). -pub enum Utxo { - /// A UTXO owned by the local wallet. - Local(LocalOutput), - /// A UTXO owned by another wallet. - Foreign { - /// The location of the output. - outpoint: OutPoint, - /// The nSequence value to set for this input. - sequence: Option, - /// The information about the input we require to add it to a PSBT. - // Box it to stop the type being too big. - psbt_input: Box, - }, -} - -impl Utxo { - /// Get the location of the UTXO - pub fn outpoint(&self) -> OutPoint { - match &self { - Utxo::Local(local) => local.outpoint, - Utxo::Foreign { outpoint, .. } => *outpoint, - } - } - - /// Get the `TxOut` of the UTXO - pub fn txout(&self) -> &TxOut { - match &self { - Utxo::Local(local) => &local.txout, - Utxo::Foreign { - outpoint, - psbt_input, - .. - } => { - if let Some(prev_tx) = &psbt_input.non_witness_utxo { - return &prev_tx.output[outpoint.vout as usize]; - } - - if let Some(txout) = &psbt_input.witness_utxo { - return txout; - } - - unreachable!("Foreign UTXOs will always have one of these set") - } - } - } - - /// Get the sequence number if an explicit sequence number has to be set for this input. - pub fn sequence(&self) -> Option { - match self { - Utxo::Local(_) => None, - Utxo::Foreign { sequence, .. } => *sequence, - } - } -} diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs deleted file mode 100644 index 6be3cb97..00000000 --- a/crates/bdk/src/wallet/coin_selection.rs +++ /dev/null @@ -1,1602 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Coin selection -//! -//! This module provides the trait [`CoinSelectionAlgorithm`] that can be implemented to -//! define custom coin selection algorithms. -//! -//! You can specify a custom coin selection algorithm through the [`coin_selection`] method on -//! [`TxBuilder`]. [`DefaultCoinSelectionAlgorithm`] aliases the coin selection algorithm that will -//! be used if it is not explicitly set. -//! -//! [`TxBuilder`]: super::tx_builder::TxBuilder -//! [`coin_selection`]: super::tx_builder::TxBuilder::coin_selection -//! -//! ## Example -//! -//! ``` -//! # use std::str::FromStr; -//! # use bitcoin::*; -//! # use bdk::wallet::{self, ChangeSet, coin_selection::*, coin_selection}; -//! # use bdk::wallet::error::CreateTxError; -//! # use bdk_persist::PersistBackend; -//! # use bdk::*; -//! # use bdk::wallet::coin_selection::decide_change; -//! # use anyhow::Error; -//! #[derive(Debug)] -//! struct AlwaysSpendEverything; -//! -//! impl CoinSelectionAlgorithm for AlwaysSpendEverything { -//! fn coin_select( -//! &self, -//! required_utxos: Vec, -//! optional_utxos: Vec, -//! fee_rate: FeeRate, -//! target_amount: u64, -//! drain_script: &Script, -//! ) -> Result { -//! let mut selected_amount = 0; -//! let mut additional_weight = Weight::ZERO; -//! let all_utxos_selected = required_utxos -//! .into_iter() -//! .chain(optional_utxos) -//! .scan( -//! (&mut selected_amount, &mut additional_weight), -//! |(selected_amount, additional_weight), weighted_utxo| { -//! **selected_amount += weighted_utxo.utxo.txout().value.to_sat(); -//! **additional_weight += Weight::from_wu( -//! (TxIn::default().segwit_weight().to_wu() -//! + weighted_utxo.satisfaction_weight as u64) -//! as u64, -//! ); -//! Some(weighted_utxo.utxo) -//! }, -//! ) -//! .collect::>(); -//! let additional_fees = (fee_rate * additional_weight).to_sat(); -//! let amount_needed_with_fees = additional_fees + target_amount; -//! if selected_amount < amount_needed_with_fees { -//! return Err(coin_selection::Error::InsufficientFunds { -//! needed: amount_needed_with_fees, -//! available: selected_amount, -//! }); -//! } -//! -//! let remaining_amount = selected_amount - amount_needed_with_fees; -//! -//! let excess = decide_change(remaining_amount, fee_rate, drain_script); -//! -//! Ok(CoinSelectionResult { -//! selected: all_utxos_selected, -//! fee_amount: additional_fees, -//! excess, -//! }) -//! } -//! } -//! -//! # let mut wallet = doctest_wallet!(); -//! // create wallet, sync, ... -//! -//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") -//! .unwrap() -//! .require_network(Network::Testnet) -//! .unwrap(); -//! let psbt = { -//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything); -//! builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); -//! builder.finish()? -//! }; -//! -//! // inspect, sign, broadcast, ... -//! -//! # Ok::<(), anyhow::Error>(()) -//! ``` - -use crate::chain::collections::HashSet; -use crate::wallet::utils::IsDust; -use crate::Utxo; -use crate::WeightedUtxo; -use bitcoin::FeeRate; - -use alloc::vec::Vec; -use bitcoin::consensus::encode::serialize; -use bitcoin::OutPoint; -use bitcoin::TxIn; -use bitcoin::{Script, Weight}; - -use core::convert::TryInto; -use core::fmt::{self, Formatter}; -use rand::seq::SliceRandom; - -/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not -/// overridden -pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection; - -/// Errors that can be thrown by the [`coin_selection`](crate::wallet::coin_selection) module -#[derive(Debug)] -pub enum Error { - /// Wallet's UTXO set is not enough to cover recipient's requested plus fee - InsufficientFunds { - /// Sats needed for some transaction - needed: u64, - /// Sats available for spending - available: u64, - }, - /// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for - /// the desired outputs plus fee, if there is not such combination this error is thrown - BnBNoExactMatch, - /// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow - /// exponentially, thus a limit is set, and when hit, this error is thrown - BnBTotalTriesExceeded, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - Self::InsufficientFunds { needed, available } => write!( - f, - "Insufficient funds: {} sat available of {} sat needed", - available, needed - ), - Self::BnBTotalTriesExceeded => { - write!(f, "Branch and bound coin selection: total tries exceeded") - } - Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for Error {} - -#[derive(Debug)] -/// Remaining amount after performing coin selection -pub enum Excess { - /// It's not possible to create spendable output from excess using the current drain output - NoChange { - /// Threshold to consider amount as dust for this particular change script_pubkey - dust_threshold: u64, - /// Exceeding amount of current selection over outgoing value and fee costs - remaining_amount: u64, - /// The calculated fee for the drain TxOut with the selected script_pubkey - change_fee: u64, - }, - /// It's possible to create spendable output from excess using the current drain output - Change { - /// Effective amount available to create change after deducting the change output fee - amount: u64, - /// The deducted change output fee - fee: u64, - }, -} - -/// Result of a successful coin selection -#[derive(Debug)] -pub struct CoinSelectionResult { - /// List of outputs selected for use as inputs - pub selected: Vec, - /// Total fee amount for the selected utxos in satoshis - pub fee_amount: u64, - /// Remaining amount after deducing fees and outgoing outputs - pub excess: Excess, -} - -impl CoinSelectionResult { - /// The total value of the inputs selected. - pub fn selected_amount(&self) -> u64 { - self.selected.iter().map(|u| u.txout().value.to_sat()).sum() - } - - /// The total value of the inputs selected from the local wallet. - pub fn local_selected_amount(&self) -> u64 { - self.selected - .iter() - .filter_map(|u| match u { - Utxo::Local(_) => Some(u.txout().value.to_sat()), - _ => None, - }) - .sum() - } -} - -/// Trait for generalized coin selection algorithms -/// -/// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin -/// selection algorithm when it creates transactions. -/// -/// For an example see [this module](crate::wallet::coin_selection)'s documentation. -pub trait CoinSelectionAlgorithm: core::fmt::Debug { - /// Perform the coin selection - /// - /// - `database`: a reference to the wallet's database that can be used to lookup additional - /// details for a specific UTXO - /// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their - /// weight cost - /// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their - /// weight cost - /// - `fee_rate`: fee rate to use - /// - `target_amount`: the outgoing amount in satoshis and the fees already - /// accumulated from added outputs and transaction’s header. - /// - `drain_script`: the script to use in case of change - #[allow(clippy::too_many_arguments)] - fn coin_select( - &self, - required_utxos: Vec, - optional_utxos: Vec, - fee_rate: FeeRate, - target_amount: u64, - drain_script: &Script, - ) -> Result; -} - -/// Simple and dumb coin selection -/// -/// This coin selection algorithm sorts the available UTXOs by value and then picks them starting -/// from the largest ones until the required amount is reached. -#[derive(Debug, Default, Clone, Copy)] -pub struct LargestFirstCoinSelection; - -impl CoinSelectionAlgorithm for LargestFirstCoinSelection { - fn coin_select( - &self, - required_utxos: Vec, - mut optional_utxos: Vec, - fee_rate: FeeRate, - target_amount: u64, - drain_script: &Script, - ) -> Result { - // We put the "required UTXOs" first and make sure the optional UTXOs are sorted, - // initially smallest to largest, before being reversed with `.rev()`. - let utxos = { - optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value); - required_utxos - .into_iter() - .map(|utxo| (true, utxo)) - .chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo))) - }; - - select_sorted_utxos(utxos, fee_rate, target_amount, drain_script) - } -} - -/// OldestFirstCoinSelection always picks the utxo with the smallest blockheight to add to the selected coins next -/// -/// This coin selection algorithm sorts the available UTXOs by blockheight and then picks them starting -/// from the oldest ones until the required amount is reached. -#[derive(Debug, Default, Clone, Copy)] -pub struct OldestFirstCoinSelection; - -impl CoinSelectionAlgorithm for OldestFirstCoinSelection { - fn coin_select( - &self, - required_utxos: Vec, - mut optional_utxos: Vec, - fee_rate: FeeRate, - target_amount: u64, - drain_script: &Script, - ) -> Result { - // We put the "required UTXOs" first and make sure the optional UTXOs are sorted from - // oldest to newest according to blocktime - // For utxo that doesn't exist in DB, they will have lowest priority to be selected - let utxos = { - optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo { - Utxo::Local(local) => Some(local.confirmation_time), - Utxo::Foreign { .. } => None, - }); - - required_utxos - .into_iter() - .map(|utxo| (true, utxo)) - .chain(optional_utxos.into_iter().map(|utxo| (false, utxo))) - }; - - select_sorted_utxos(utxos, fee_rate, target_amount, drain_script) - } -} - -/// Decide if change can be created -/// -/// - `remaining_amount`: the amount in which the selected coins exceed the target amount -/// - `fee_rate`: required fee rate for the current selection -/// - `drain_script`: script to consider change creation -pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess { - // drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value) - let drain_output_len = serialize(drain_script).len() + 8usize; - let change_fee = - (fee_rate * Weight::from_vb(drain_output_len as u64).expect("overflow occurred")).to_sat(); - let drain_val = remaining_amount.saturating_sub(change_fee); - - if drain_val.is_dust(drain_script) { - let dust_threshold = drain_script.dust_value().to_sat(); - Excess::NoChange { - dust_threshold, - change_fee, - remaining_amount, - } - } else { - Excess::Change { - amount: drain_val, - fee: change_fee, - } - } -} - -fn select_sorted_utxos( - utxos: impl Iterator, - fee_rate: FeeRate, - target_amount: u64, - drain_script: &Script, -) -> Result { - let mut selected_amount = 0; - let mut fee_amount = 0; - let selected = utxos - .scan( - (&mut selected_amount, &mut fee_amount), - |(selected_amount, fee_amount), (must_use, weighted_utxo)| { - if must_use || **selected_amount < target_amount + **fee_amount { - **fee_amount += (fee_rate - * Weight::from_wu( - TxIn::default().segwit_weight().to_wu() - + weighted_utxo.satisfaction_weight as u64, - )) - .to_sat(); - **selected_amount += weighted_utxo.utxo.txout().value.to_sat(); - Some(weighted_utxo.utxo) - } else { - None - } - }, - ) - .collect::>(); - - let amount_needed_with_fees = target_amount + fee_amount; - if selected_amount < amount_needed_with_fees { - return Err(Error::InsufficientFunds { - needed: amount_needed_with_fees, - available: selected_amount, - }); - } - - let remaining_amount = selected_amount - amount_needed_with_fees; - - let excess = decide_change(remaining_amount, fee_rate, drain_script); - - Ok(CoinSelectionResult { - selected, - fee_amount, - excess, - }) -} - -#[derive(Debug, Clone)] -// Adds fee information to an UTXO. -struct OutputGroup { - weighted_utxo: WeightedUtxo, - // Amount of fees for spending a certain utxo, calculated using a certain FeeRate - fee: u64, - // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it - effective_value: i64, -} - -impl OutputGroup { - fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self { - let fee = (fee_rate - * Weight::from_wu( - TxIn::default().segwit_weight().to_wu() + weighted_utxo.satisfaction_weight as u64, - )) - .to_sat(); - let effective_value = weighted_utxo.utxo.txout().value.to_sat() as i64 - fee as i64; - OutputGroup { - weighted_utxo, - fee, - effective_value, - } - } -} - -/// Branch and bound coin selection -/// -/// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: -#[derive(Debug, Clone)] -pub struct BranchAndBoundCoinSelection { - size_of_change: u64, -} - -impl Default for BranchAndBoundCoinSelection { - fn default() -> Self { - Self { - // P2WPKH cost of change -> value (8 bytes) + script len (1 bytes) + script (22 bytes) - size_of_change: 8 + 1 + 22, - } - } -} - -impl BranchAndBoundCoinSelection { - /// Create new instance with target size for change output - pub fn new(size_of_change: u64) -> Self { - Self { size_of_change } - } -} - -const BNB_TOTAL_TRIES: usize = 100_000; - -impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { - fn coin_select( - &self, - required_utxos: Vec, - optional_utxos: Vec, - fee_rate: FeeRate, - target_amount: u64, - drain_script: &Script, - ) -> Result { - // Mapping every (UTXO, usize) to an output group - let required_utxos: Vec = required_utxos - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .collect(); - - // Mapping every (UTXO, usize) to an output group, filtering UTXOs with a negative - // effective value - let optional_utxos: Vec = optional_utxos - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .filter(|u| u.effective_value.is_positive()) - .collect(); - - let curr_value = required_utxos - .iter() - .fold(0, |acc, x| acc + x.effective_value); - - let curr_available_value = optional_utxos - .iter() - .fold(0, |acc, x| acc + x.effective_value); - - let cost_of_change = - (Weight::from_vb(self.size_of_change).expect("overflow occurred") * fee_rate).to_sat(); - - // `curr_value` and `curr_available_value` are both the sum of *effective_values* of - // the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with - // negative effective value, so it will always be positive. - // - // Since we are required to spend the required UTXOs (curr_value) we have to consider - // all their effective values, even when negative, which means that curr_value could - // be negative as well. - // - // If the sum of curr_value and curr_available_value is negative or lower than our target, - // we can immediately exit with an error, as it's guaranteed we will never find a solution - // if we actually run the BnB. - let total_value: Result = (curr_available_value + curr_value).try_into(); - match total_value { - Ok(v) if v >= target_amount => {} - _ => { - // Assume we spend all the UTXOs we can (all the required + all the optional with - // positive effective value), sum their value and their fee cost. - let (utxo_fees, utxo_value) = required_utxos - .iter() - .chain(optional_utxos.iter()) - .fold((0, 0), |(mut fees, mut value), utxo| { - fees += utxo.fee; - value += utxo.weighted_utxo.utxo.txout().value.to_sat(); - - (fees, value) - }); - - // Add to the target the fee cost of the UTXOs - return Err(Error::InsufficientFunds { - needed: target_amount + utxo_fees, - available: utxo_value, - }); - } - } - - let target_amount = target_amount - .try_into() - .expect("Bitcoin amount to fit into i64"); - - if curr_value > target_amount { - // remaining_amount can't be negative as that would mean the - // selection wasn't successful - // target_amount = amount_needed + (fee_amount - vin_fees) - let remaining_amount = (curr_value - target_amount) as u64; - - let excess = decide_change(remaining_amount, fee_rate, drain_script); - - return Ok(BranchAndBoundCoinSelection::calculate_cs_result( - vec![], - required_utxos, - excess, - )); - } - - Ok(self - .bnb( - required_utxos.clone(), - optional_utxos.clone(), - curr_value, - curr_available_value, - target_amount, - cost_of_change, - drain_script, - fee_rate, - ) - .unwrap_or_else(|_| { - self.single_random_draw( - required_utxos, - optional_utxos, - curr_value, - target_amount, - drain_script, - fee_rate, - ) - })) - } -} - -impl BranchAndBoundCoinSelection { - // TODO: make this more Rust-onic :) - // (And perhaps refactor with less arguments?) - #[allow(clippy::too_many_arguments)] - fn bnb( - &self, - required_utxos: Vec, - mut optional_utxos: Vec, - mut curr_value: i64, - mut curr_available_value: i64, - target_amount: i64, - cost_of_change: u64, - drain_script: &Script, - fee_rate: FeeRate, - ) -> Result { - // current_selection[i] will contain true if we are using optional_utxos[i], - // false otherwise. Note that current_selection.len() could be less than - // optional_utxos.len(), it just means that we still haven't decided if we should keep - // certain optional_utxos or not. - let mut current_selection: Vec = Vec::with_capacity(optional_utxos.len()); - - // Sort the utxo_pool - optional_utxos.sort_unstable_by_key(|a| a.effective_value); - optional_utxos.reverse(); - - // Contains the best selection we found - let mut best_selection = Vec::new(); - let mut best_selection_value = None; - - // Depth First search loop for choosing the UTXOs - for _ in 0..BNB_TOTAL_TRIES { - // Conditions for starting a backtrack - let mut backtrack = false; - // Cannot possibly reach target with the amount remaining in the curr_available_value, - // or the selected value is out of range. - // Go back and try other branch - if curr_value + curr_available_value < target_amount - || curr_value > target_amount + cost_of_change as i64 - { - backtrack = true; - } else if curr_value >= target_amount { - // Selected value is within range, there's no point in going forward. Start - // backtracking - backtrack = true; - - // If we found a solution better than the previous one, or if there wasn't previous - // solution, update the best solution - if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() { - best_selection.clone_from(¤t_selection); - best_selection_value = Some(curr_value); - } - - // If we found a perfect match, break here - if curr_value == target_amount { - break; - } - } - - // Backtracking, moving backwards - if backtrack { - // Walk backwards to find the last included UTXO that still needs to have its omission branch traversed. - while let Some(false) = current_selection.last() { - current_selection.pop(); - curr_available_value += optional_utxos[current_selection.len()].effective_value; - } - - if current_selection.last_mut().is_none() { - // We have walked back to the first utxo and no branch is untraversed. All solutions searched - // If best selection is empty, then there's no exact match - if best_selection.is_empty() { - return Err(Error::BnBNoExactMatch); - } - break; - } - - if let Some(c) = current_selection.last_mut() { - // Output was included on previous iterations, try excluding now. - *c = false; - } - - let utxo = &optional_utxos[current_selection.len() - 1]; - curr_value -= utxo.effective_value; - } else { - // Moving forwards, continuing down this branch - let utxo = &optional_utxos[current_selection.len()]; - - // Remove this utxo from the curr_available_value utxo amount - curr_available_value -= utxo.effective_value; - - // Inclusion branch first (Largest First Exploration) - current_selection.push(true); - curr_value += utxo.effective_value; - } - } - - // Check for solution - if best_selection.is_empty() { - return Err(Error::BnBTotalTriesExceeded); - } - - // Set output set - let selected_utxos = optional_utxos - .into_iter() - .zip(best_selection) - .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None }) - .collect::>(); - - let selected_amount = best_selection_value.unwrap(); - - // remaining_amount can't be negative as that would mean the - // selection wasn't successful - // target_amount = amount_needed + (fee_amount - vin_fees) - let remaining_amount = (selected_amount - target_amount) as u64; - - let excess = decide_change(remaining_amount, fee_rate, drain_script); - - Ok(BranchAndBoundCoinSelection::calculate_cs_result( - selected_utxos, - required_utxos, - excess, - )) - } - - #[allow(clippy::too_many_arguments)] - fn single_random_draw( - &self, - required_utxos: Vec, - mut optional_utxos: Vec, - curr_value: i64, - target_amount: i64, - drain_script: &Script, - fee_rate: FeeRate, - ) -> CoinSelectionResult { - optional_utxos.shuffle(&mut rand::thread_rng()); - let selected_utxos = optional_utxos.into_iter().fold( - (curr_value, vec![]), - |(mut amount, mut utxos), utxo| { - if amount >= target_amount { - (amount, utxos) - } else { - amount += utxo.effective_value; - utxos.push(utxo); - (amount, utxos) - } - }, - ); - - // remaining_amount can't be negative as that would mean the - // selection wasn't successful - // target_amount = amount_needed + (fee_amount - vin_fees) - let remaining_amount = (selected_utxos.0 - target_amount) as u64; - - let excess = decide_change(remaining_amount, fee_rate, drain_script); - - BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess) - } - - fn calculate_cs_result( - mut selected_utxos: Vec, - mut required_utxos: Vec, - excess: Excess, - ) -> CoinSelectionResult { - selected_utxos.append(&mut required_utxos); - let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::(); - let selected = selected_utxos - .into_iter() - .map(|u| u.weighted_utxo.utxo) - .collect::>(); - - CoinSelectionResult { - selected, - fee_amount, - excess, - } - } -} - -/// Remove duplicate UTXOs. -/// -/// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept. -pub(crate) fn filter_duplicates(required: I, optional: I) -> (I, I) -where - I: IntoIterator + FromIterator, -{ - let mut visited = HashSet::::new(); - let required = required - .into_iter() - .filter(|utxo| visited.insert(utxo.utxo.outpoint())) - .collect::(); - let optional = optional - .into_iter() - .filter(|utxo| visited.insert(utxo.utxo.outpoint())) - .collect::(); - (required, optional) -} - -#[cfg(test)] -mod test { - use assert_matches::assert_matches; - use core::str::FromStr; - - use bdk_chain::ConfirmationTime; - use bitcoin::{Amount, ScriptBuf, TxIn, TxOut}; - - use super::*; - use crate::types::*; - use crate::wallet::coin_selection::filter_duplicates; - - use rand::rngs::StdRng; - use rand::seq::SliceRandom; - use rand::{Rng, RngCore, SeedableRng}; - - // signature len (1WU) + signature and sighash (72WU) - // + pubkey len (1WU) + pubkey (33WU) - const P2WPKH_SATISFACTION_SIZE: usize = 1 + 72 + 1 + 33; - - const FEE_AMOUNT: u64 = 50; - - fn utxo(value: u64, index: u32, confirmation_time: ConfirmationTime) -> WeightedUtxo { - assert!(index < 10); - let outpoint = OutPoint::from_str(&format!( - "000000000000000000000000000000000000000000000000000000000000000{}:0", - index - )) - .unwrap(); - WeightedUtxo { - satisfaction_weight: P2WPKH_SATISFACTION_SIZE, - utxo: Utxo::Local(LocalOutput { - outpoint, - txout: TxOut { - value: Amount::from_sat(value), - script_pubkey: ScriptBuf::new(), - }, - keychain: KeychainKind::External, - is_spent: false, - derivation_index: 42, - confirmation_time, - }), - } - } - - fn get_test_utxos() -> Vec { - vec![ - utxo(100_000, 0, ConfirmationTime::Unconfirmed { last_seen: 0 }), - utxo( - FEE_AMOUNT - 40, - 1, - ConfirmationTime::Unconfirmed { last_seen: 0 }, - ), - utxo(200_000, 2, ConfirmationTime::Unconfirmed { last_seen: 0 }), - ] - } - - fn get_oldest_first_test_utxos() -> Vec { - // ensure utxos are from different tx - let utxo1 = utxo( - 120_000, - 1, - ConfirmationTime::Confirmed { - height: 1, - time: 1231006505, - }, - ); - let utxo2 = utxo( - 80_000, - 2, - ConfirmationTime::Confirmed { - height: 2, - time: 1231006505, - }, - ); - let utxo3 = utxo( - 300_000, - 3, - ConfirmationTime::Confirmed { - height: 3, - time: 1231006505, - }, - ); - vec![utxo1, utxo2, utxo3] - } - - fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec { - let mut res = Vec::new(); - for i in 0..utxos_number { - res.push(WeightedUtxo { - satisfaction_weight: P2WPKH_SATISFACTION_SIZE, - utxo: Utxo::Local(LocalOutput { - outpoint: OutPoint::from_str(&format!( - "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}", - i - )) - .unwrap(), - txout: TxOut { - value: Amount::from_sat(rng.gen_range(0..200000000)), - script_pubkey: ScriptBuf::new(), - }, - keychain: KeychainKind::External, - is_spent: false, - derivation_index: rng.next_u32(), - confirmation_time: if rng.gen_bool(0.5) { - ConfirmationTime::Confirmed { - height: rng.next_u32(), - time: rng.next_u64(), - } - } else { - ConfirmationTime::Unconfirmed { last_seen: 0 } - }, - }), - }); - } - res - } - - fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec { - (0..utxos_number) - .map(|i| WeightedUtxo { - satisfaction_weight: P2WPKH_SATISFACTION_SIZE, - utxo: Utxo::Local(LocalOutput { - outpoint: OutPoint::from_str(&format!( - "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}", - i - )) - .unwrap(), - txout: TxOut { - value: Amount::from_sat(utxos_value), - script_pubkey: ScriptBuf::new(), - }, - keychain: KeychainKind::External, - is_spent: false, - derivation_index: 42, - confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 }, - }), - }) - .collect() - } - - fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec) -> u64 { - let utxos_picked_len = rng.gen_range(2..utxos.len() / 2); - utxos.shuffle(&mut rng); - utxos[..utxos_picked_len] - .iter() - .map(|u| u.utxo.txout().value.to_sat()) - .sum() - } - - #[test] - fn test_largest_first_coin_selection_success() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 250_000 + FEE_AMOUNT; - - let result = LargestFirstCoinSelection - .coin_select( - utxos, - vec![], - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - - assert_eq!(result.selected.len(), 3); - assert_eq!(result.selected_amount(), 300_010); - assert_eq!(result.fee_amount, 204) - } - - #[test] - fn test_largest_first_coin_selection_use_all() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 20_000 + FEE_AMOUNT; - - let result = LargestFirstCoinSelection - .coin_select( - utxos, - vec![], - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - - assert_eq!(result.selected.len(), 3); - assert_eq!(result.selected_amount(), 300_010); - assert_eq!(result.fee_amount, 204); - } - - #[test] - fn test_largest_first_coin_selection_use_only_necessary() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 20_000 + FEE_AMOUNT; - - let result = LargestFirstCoinSelection - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - - assert_eq!(result.selected.len(), 1); - assert_eq!(result.selected_amount(), 200_000); - assert_eq!(result.fee_amount, 68); - } - - #[test] - #[should_panic(expected = "InsufficientFunds")] - fn test_largest_first_coin_selection_insufficient_funds() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 500_000 + FEE_AMOUNT; - - LargestFirstCoinSelection - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - } - - #[test] - #[should_panic(expected = "InsufficientFunds")] - fn test_largest_first_coin_selection_insufficient_funds_high_fees() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 250_000 + FEE_AMOUNT; - - LargestFirstCoinSelection - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1000), - target_amount, - &drain_script, - ) - .unwrap(); - } - - #[test] - fn test_oldest_first_coin_selection_success() { - let utxos = get_oldest_first_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 180_000 + FEE_AMOUNT; - - let result = OldestFirstCoinSelection - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - - assert_eq!(result.selected.len(), 2); - assert_eq!(result.selected_amount(), 200_000); - assert_eq!(result.fee_amount, 136) - } - - #[test] - fn test_oldest_first_coin_selection_use_all() { - let utxos = get_oldest_first_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 20_000 + FEE_AMOUNT; - - let result = OldestFirstCoinSelection - .coin_select( - utxos, - vec![], - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - - assert_eq!(result.selected.len(), 3); - assert_eq!(result.selected_amount(), 500_000); - assert_eq!(result.fee_amount, 204); - } - - #[test] - fn test_oldest_first_coin_selection_use_only_necessary() { - let utxos = get_oldest_first_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 20_000 + FEE_AMOUNT; - - let result = OldestFirstCoinSelection - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - - assert_eq!(result.selected.len(), 1); - assert_eq!(result.selected_amount(), 120_000); - assert_eq!(result.fee_amount, 68); - } - - #[test] - #[should_panic(expected = "InsufficientFunds")] - fn test_oldest_first_coin_selection_insufficient_funds() { - let utxos = get_oldest_first_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 600_000 + FEE_AMOUNT; - - OldestFirstCoinSelection - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - } - - #[test] - #[should_panic(expected = "InsufficientFunds")] - fn test_oldest_first_coin_selection_insufficient_funds_high_fees() { - let utxos = get_oldest_first_test_utxos(); - - let target_amount: u64 = utxos - .iter() - .map(|wu| wu.utxo.txout().value.to_sat()) - .sum::() - - 50; - let drain_script = ScriptBuf::default(); - - OldestFirstCoinSelection - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1000), - target_amount, - &drain_script, - ) - .unwrap(); - } - - #[test] - fn test_bnb_coin_selection_success() { - // In this case bnb won't find a suitable match and single random draw will - // select three outputs - let utxos = generate_same_value_utxos(100_000, 20); - - let drain_script = ScriptBuf::default(); - - let target_amount = 250_000 + FEE_AMOUNT; - - let result = BranchAndBoundCoinSelection::default() - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - - assert_eq!(result.selected.len(), 3); - assert_eq!(result.selected_amount(), 300_000); - assert_eq!(result.fee_amount, 204); - } - - #[test] - fn test_bnb_coin_selection_required_are_enough() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 20_000 + FEE_AMOUNT; - - let result = BranchAndBoundCoinSelection::default() - .coin_select( - utxos.clone(), - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - - assert_eq!(result.selected.len(), 3); - assert_eq!(result.selected_amount(), 300_010); - assert_eq!(result.fee_amount, 204); - } - - #[test] - fn test_bnb_coin_selection_optional_are_enough() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 299756 + FEE_AMOUNT; - - let result = BranchAndBoundCoinSelection::default() - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - - assert_eq!(result.selected.len(), 2); - assert_eq!(result.selected_amount(), 300000); - assert_eq!(result.fee_amount, 136); - } - - #[test] - #[ignore] - fn test_bnb_coin_selection_required_not_enough() { - let utxos = get_test_utxos(); - - let required = vec![utxos[0].clone()]; - let mut optional = utxos[1..].to_vec(); - optional.push(utxo( - 500_000, - 3, - ConfirmationTime::Unconfirmed { last_seen: 0 }, - )); - - // Defensive assertions, for sanity and in case someone changes the test utxos vector. - let amount: u64 = required.iter().map(|u| u.utxo.txout().value.to_sat()).sum(); - assert_eq!(amount, 100_000); - let amount: u64 = optional.iter().map(|u| u.utxo.txout().value.to_sat()).sum(); - assert!(amount > 150_000); - let drain_script = ScriptBuf::default(); - - let target_amount = 150_000 + FEE_AMOUNT; - - let result = BranchAndBoundCoinSelection::default() - .coin_select( - required, - optional, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - - assert_eq!(result.selected.len(), 2); - assert_eq!(result.selected_amount(), 300_000); - assert_eq!(result.fee_amount, 136); - } - - #[test] - #[should_panic(expected = "InsufficientFunds")] - fn test_bnb_coin_selection_insufficient_funds() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 500_000 + FEE_AMOUNT; - - BranchAndBoundCoinSelection::default() - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - ) - .unwrap(); - } - - #[test] - #[should_panic(expected = "InsufficientFunds")] - fn test_bnb_coin_selection_insufficient_funds_high_fees() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 250_000 + FEE_AMOUNT; - - BranchAndBoundCoinSelection::default() - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1000), - target_amount, - &drain_script, - ) - .unwrap(); - } - - #[test] - fn test_bnb_coin_selection_check_fee_rate() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - let target_amount = 99932; // first utxo's effective value - let feerate = FeeRate::BROADCAST_MIN; - - let result = BranchAndBoundCoinSelection::new(0) - .coin_select(vec![], utxos, feerate, target_amount, &drain_script) - .unwrap(); - - assert_eq!(result.selected.len(), 1); - assert_eq!(result.selected_amount(), 100_000); - let input_weight = - TxIn::default().segwit_weight().to_wu() + P2WPKH_SATISFACTION_SIZE as u64; - // the final fee rate should be exactly the same as the fee rate given - let result_feerate = Amount::from_sat(result.fee_amount) / Weight::from_wu(input_weight); - assert_eq!(result_feerate, feerate); - } - - #[test] - fn test_bnb_coin_selection_exact_match() { - let seed = [0; 32]; - let mut rng: StdRng = SeedableRng::from_seed(seed); - - for _i in 0..200 { - let mut optional_utxos = generate_random_utxos(&mut rng, 16); - let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos); - let drain_script = ScriptBuf::default(); - let result = BranchAndBoundCoinSelection::new(0) - .coin_select( - vec![], - optional_utxos, - FeeRate::ZERO, - target_amount, - &drain_script, - ) - .unwrap(); - assert_eq!(result.selected_amount(), target_amount); - } - } - - #[test] - #[should_panic(expected = "BnBNoExactMatch")] - fn test_bnb_function_no_exact_match() { - let fee_rate = FeeRate::from_sat_per_vb_unchecked(10); - let utxos: Vec = get_test_utxos() - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .collect(); - - let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); - - let size_of_change = 31; - let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat(); - - let drain_script = ScriptBuf::default(); - let target_amount = 20_000 + FEE_AMOUNT; - BranchAndBoundCoinSelection::new(size_of_change) - .bnb( - vec![], - utxos, - 0, - curr_available_value, - target_amount as i64, - cost_of_change, - &drain_script, - fee_rate, - ) - .unwrap(); - } - - #[test] - #[should_panic(expected = "BnBTotalTriesExceeded")] - fn test_bnb_function_tries_exceeded() { - let fee_rate = FeeRate::from_sat_per_vb_unchecked(10); - let utxos: Vec = generate_same_value_utxos(100_000, 100_000) - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .collect(); - - let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); - - let size_of_change = 31; - let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat(); - let target_amount = 20_000 + FEE_AMOUNT; - - let drain_script = ScriptBuf::default(); - - BranchAndBoundCoinSelection::new(size_of_change) - .bnb( - vec![], - utxos, - 0, - curr_available_value, - target_amount as i64, - cost_of_change, - &drain_script, - fee_rate, - ) - .unwrap(); - } - - // The match won't be exact but still in the range - #[test] - fn test_bnb_function_almost_exact_match_with_fees() { - let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); - let size_of_change = 31; - let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat(); - - let utxos: Vec<_> = generate_same_value_utxos(50_000, 10) - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .collect(); - - let curr_value = 0; - - let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); - - // 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) - - // cost_of_change + 5. - let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change as i64 + 5; - - let drain_script = ScriptBuf::default(); - - let result = BranchAndBoundCoinSelection::new(size_of_change) - .bnb( - vec![], - utxos, - curr_value, - curr_available_value, - target_amount, - cost_of_change, - &drain_script, - fee_rate, - ) - .unwrap(); - assert_eq!(result.selected_amount(), 100_000); - assert_eq!(result.fee_amount, 136); - } - - // TODO: bnb() function should be optimized, and this test should be done with more utxos - #[test] - fn test_bnb_function_exact_match_more_utxos() { - let seed = [0; 32]; - let mut rng: StdRng = SeedableRng::from_seed(seed); - let fee_rate = FeeRate::ZERO; - - for _ in 0..200 { - let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40) - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .collect(); - - let curr_value = 0; - - let curr_available_value = optional_utxos - .iter() - .fold(0, |acc, x| acc + x.effective_value); - - let target_amount = - optional_utxos[3].effective_value + optional_utxos[23].effective_value; - - let drain_script = ScriptBuf::default(); - - let result = BranchAndBoundCoinSelection::new(0) - .bnb( - vec![], - optional_utxos, - curr_value, - curr_available_value, - target_amount, - 0, - &drain_script, - fee_rate, - ) - .unwrap(); - assert_eq!(result.selected_amount(), target_amount as u64); - } - } - - #[test] - fn test_single_random_draw_function_success() { - let seed = [0; 32]; - let mut rng: StdRng = SeedableRng::from_seed(seed); - let mut utxos = generate_random_utxos(&mut rng, 300); - let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT; - - let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); - let utxos: Vec = utxos - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .collect(); - - let drain_script = ScriptBuf::default(); - - let result = BranchAndBoundCoinSelection::default().single_random_draw( - vec![], - utxos, - 0, - target_amount as i64, - &drain_script, - fee_rate, - ); - - assert!(result.selected_amount() > target_amount); - assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64); - } - - #[test] - fn test_bnb_exclude_negative_effective_value() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - - let selection = BranchAndBoundCoinSelection::default().coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(10), - 500_000, - &drain_script, - ); - - assert_matches!( - selection, - Err(Error::InsufficientFunds { - available: 300_000, - .. - }) - ); - } - - #[test] - fn test_bnb_include_negative_effective_value_when_required() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - - let (required, optional) = utxos.into_iter().partition( - |u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value.to_sat() < 1000), - ); - - let selection = BranchAndBoundCoinSelection::default().coin_select( - required, - optional, - FeeRate::from_sat_per_vb_unchecked(10), - 500_000, - &drain_script, - ); - - assert_matches!( - selection, - Err(Error::InsufficientFunds { - available: 300_010, - .. - }) - ); - } - - #[test] - fn test_bnb_sum_of_effective_value_negative() { - let utxos = get_test_utxos(); - let drain_script = ScriptBuf::default(); - - let selection = BranchAndBoundCoinSelection::default().coin_select( - utxos, - vec![], - FeeRate::from_sat_per_vb_unchecked(10_000), - 500_000, - &drain_script, - ); - - assert_matches!( - selection, - Err(Error::InsufficientFunds { - available: 300_010, - .. - }) - ); - } - - #[test] - fn test_filter_duplicates() { - fn utxo(txid: &str, value: u64) -> WeightedUtxo { - WeightedUtxo { - satisfaction_weight: 0, - utxo: Utxo::Local(LocalOutput { - outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0), - txout: TxOut { - value: Amount::from_sat(value), - script_pubkey: ScriptBuf::new(), - }, - keychain: KeychainKind::External, - is_spent: false, - derivation_index: 0, - confirmation_time: ConfirmationTime::Confirmed { - height: 12345, - time: 12345, - }, - }), - } - } - - fn to_utxo_vec(utxos: &[(&str, u64)]) -> Vec { - let mut v = utxos - .iter() - .map(|&(txid, value)| utxo(txid, value)) - .collect::>(); - v.sort_by_key(|u| u.utxo.outpoint()); - v - } - - struct TestCase<'a> { - name: &'a str, - required: &'a [(&'a str, u64)], - optional: &'a [(&'a str, u64)], - exp_required: &'a [(&'a str, u64)], - exp_optional: &'a [(&'a str, u64)], - } - - let test_cases = [ - TestCase { - name: "no_duplicates", - required: &[("A", 1000), ("B", 2100)], - optional: &[("C", 1000)], - exp_required: &[("A", 1000), ("B", 2100)], - exp_optional: &[("C", 1000)], - }, - TestCase { - name: "duplicate_required_utxos", - required: &[("A", 3000), ("B", 1200), ("C", 1234), ("A", 3000)], - optional: &[("D", 2100)], - exp_required: &[("A", 3000), ("B", 1200), ("C", 1234)], - exp_optional: &[("D", 2100)], - }, - TestCase { - name: "duplicate_optional_utxos", - required: &[("A", 3000), ("B", 1200)], - optional: &[("C", 5000), ("D", 1300), ("C", 5000)], - exp_required: &[("A", 3000), ("B", 1200)], - exp_optional: &[("C", 5000), ("D", 1300)], - }, - TestCase { - name: "duplicate_across_required_and_optional_utxos", - required: &[("A", 3000), ("B", 1200), ("C", 2100)], - optional: &[("A", 3000), ("D", 1200), ("E", 5000)], - exp_required: &[("A", 3000), ("B", 1200), ("C", 2100)], - exp_optional: &[("D", 1200), ("E", 5000)], - }, - ]; - - for (i, t) in test_cases.into_iter().enumerate() { - println!("Case {}: {}", i, t.name); - let (required, optional) = - filter_duplicates(to_utxo_vec(t.required), to_utxo_vec(t.optional)); - assert_eq!( - required, - to_utxo_vec(t.exp_required), - "[{}:{}] unexpected `required` result", - i, - t.name - ); - assert_eq!( - optional, - to_utxo_vec(t.exp_optional), - "[{}:{}] unexpected `optional` result", - i, - t.name - ); - } - } -} diff --git a/crates/bdk/src/wallet/error.rs b/crates/bdk/src/wallet/error.rs deleted file mode 100644 index eaf811d6..00000000 --- a/crates/bdk/src/wallet/error.rs +++ /dev/null @@ -1,291 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet) - -use crate::descriptor::policy::PolicyError; -use crate::descriptor::DescriptorError; -use crate::wallet::coin_selection; -use crate::{descriptor, KeychainKind}; -use alloc::string::String; -use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid}; -use core::fmt; - -/// Errors returned by miniscript when updating inconsistent PSBTs -#[derive(Debug, Clone)] -pub enum MiniscriptPsbtError { - /// Descriptor key conversion error - Conversion(miniscript::descriptor::ConversionError), - /// Return error type for PsbtExt::update_input_with_descriptor - UtxoUpdate(miniscript::psbt::UtxoUpdateError), - /// Return error type for PsbtExt::update_output_with_descriptor - OutputUpdate(miniscript::psbt::OutputUpdateError), -} - -impl fmt::Display for MiniscriptPsbtError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Conversion(err) => write!(f, "Conversion error: {}", err), - Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err), - Self::OutputUpdate(err) => write!(f, "Output update error: {}", err), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for MiniscriptPsbtError {} - -#[derive(Debug)] -/// Error returned from [`TxBuilder::finish`] -/// -/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish -pub enum CreateTxError { - /// There was a problem with the descriptors passed in - Descriptor(DescriptorError), - /// We were unable to load wallet data from or write wallet data to the persistence backend - Persist(anyhow::Error), - /// There was a problem while extracting and manipulating policies - Policy(PolicyError), - /// Spending policy is not compatible with this [`KeychainKind`] - SpendingPolicyRequired(KeychainKind), - /// Requested invalid transaction version '0' - Version0, - /// Requested transaction version `1`, but at least `2` is needed to use OP_CSV - Version1Csv, - /// Requested `LockTime` is less than is required to spend from this script - LockTime { - /// Requested `LockTime` - requested: absolute::LockTime, - /// Required `LockTime` - required: absolute::LockTime, - }, - /// Cannot enable RBF with a `Sequence` >= 0xFFFFFFFE - RbfSequence, - /// Cannot enable RBF with `Sequence` given a required OP_CSV - RbfSequenceCsv { - /// Given RBF `Sequence` - rbf: Sequence, - /// Required OP_CSV `Sequence` - csv: Sequence, - }, - /// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee - FeeTooLow { - /// Required fee absolute value (satoshi) - required: u64, - }, - /// When bumping a tx the fee rate requested is lower than required - FeeRateTooLow { - /// Required fee rate - required: bitcoin::FeeRate, - }, - /// `manually_selected_only` option is selected but no utxo has been passed - NoUtxosSelected, - /// Output created is under the dust limit, 546 satoshis - OutputBelowDustLimit(usize), - /// The `change_policy` was set but the wallet does not have a change_descriptor - ChangePolicyDescriptor, - /// There was an error with coin selection - CoinSelection(coin_selection::Error), - /// Wallet's UTXO set is not enough to cover recipient's requested plus fee - InsufficientFunds { - /// Sats needed for some transaction - needed: u64, - /// Sats available for spending - available: u64, - }, - /// Cannot build a tx without recipients - NoRecipients, - /// Partially signed bitcoin transaction error - Psbt(psbt::Error), - /// In order to use the [`TxBuilder::add_global_xpubs`] option every extended - /// key in the descriptor must either be a master key itself (having depth = 0) or have an - /// explicit origin provided - /// - /// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs - MissingKeyOrigin(String), - /// Happens when trying to spend an UTXO that is not in the internal database - UnknownUtxo, - /// Missing non_witness_utxo on foreign utxo for given `OutPoint` - MissingNonWitnessUtxo(OutPoint), - /// Miniscript PSBT error - MiniscriptPsbt(MiniscriptPsbtError), -} - -impl fmt::Display for CreateTxError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Descriptor(e) => e.fmt(f), - Self::Persist(e) => { - write!( - f, - "failed to load wallet data from or write wallet data to persistence backend: {}", - e - ) - } - Self::Policy(e) => e.fmt(f), - CreateTxError::SpendingPolicyRequired(keychain_kind) => { - write!(f, "Spending policy required: {:?}", keychain_kind) - } - CreateTxError::Version0 => { - write!(f, "Invalid version `0`") - } - CreateTxError::Version1Csv => { - write!( - f, - "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV" - ) - } - CreateTxError::LockTime { - requested, - required, - } => { - write!(f, "TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", required, requested) - } - CreateTxError::RbfSequence => { - write!(f, "Cannot enable RBF with a nSequence >= 0xFFFFFFFE") - } - CreateTxError::RbfSequenceCsv { rbf, csv } => { - write!( - f, - "Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`", - rbf, csv - ) - } - CreateTxError::FeeTooLow { required } => { - write!(f, "Fee to low: required {} sat", required) - } - CreateTxError::FeeRateTooLow { required } => { - write!( - f, - // Note: alternate fmt as sat/vb (ceil) available in bitcoin-0.31 - //"Fee rate too low: required {required:#}" - "Fee rate too low: required {} sat/vb", - crate::floating_rate!(required) - ) - } - CreateTxError::NoUtxosSelected => { - write!(f, "No UTXO selected") - } - CreateTxError::OutputBelowDustLimit(limit) => { - write!(f, "Output below the dust limit: {}", limit) - } - CreateTxError::ChangePolicyDescriptor => { - write!( - f, - "The `change_policy` can be set only if the wallet has a change_descriptor" - ) - } - CreateTxError::CoinSelection(e) => e.fmt(f), - CreateTxError::InsufficientFunds { needed, available } => { - write!( - f, - "Insufficient funds: {} sat available of {} sat needed", - available, needed - ) - } - CreateTxError::NoRecipients => { - write!(f, "Cannot build tx without recipients") - } - CreateTxError::Psbt(e) => e.fmt(f), - CreateTxError::MissingKeyOrigin(err) => { - write!(f, "Missing key origin: {}", err) - } - CreateTxError::UnknownUtxo => { - write!(f, "UTXO not found in the internal database") - } - CreateTxError::MissingNonWitnessUtxo(outpoint) => { - write!(f, "Missing non_witness_utxo on foreign utxo {}", outpoint) - } - CreateTxError::MiniscriptPsbt(err) => { - write!(f, "Miniscript PSBT error: {}", err) - } - } - } -} - -impl From for CreateTxError { - fn from(err: descriptor::error::Error) -> Self { - CreateTxError::Descriptor(err) - } -} - -impl From for CreateTxError { - fn from(err: PolicyError) -> Self { - CreateTxError::Policy(err) - } -} - -impl From for CreateTxError { - fn from(err: MiniscriptPsbtError) -> Self { - CreateTxError::MiniscriptPsbt(err) - } -} - -impl From for CreateTxError { - fn from(err: psbt::Error) -> Self { - CreateTxError::Psbt(err) - } -} - -impl From for CreateTxError { - fn from(err: coin_selection::Error) -> Self { - CreateTxError::CoinSelection(err) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for CreateTxError {} - -#[derive(Debug)] -/// Error returned from [`Wallet::build_fee_bump`] -/// -/// [`Wallet::build_fee_bump`]: super::Wallet::build_fee_bump -pub enum BuildFeeBumpError { - /// Happens when trying to spend an UTXO that is not in the internal database - UnknownUtxo(OutPoint), - /// Thrown when a tx is not found in the internal database - TransactionNotFound(Txid), - /// Happens when trying to bump a transaction that is already confirmed - TransactionConfirmed(Txid), - /// Trying to replace a tx that has a sequence >= `0xFFFFFFFE` - IrreplaceableTransaction(Txid), - /// Node doesn't have data to estimate a fee rate - FeeRateUnavailable, -} - -impl fmt::Display for BuildFeeBumpError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::UnknownUtxo(outpoint) => write!( - f, - "UTXO not found in the internal database with txid: {}, vout: {}", - outpoint.txid, outpoint.vout - ), - Self::TransactionNotFound(txid) => { - write!( - f, - "Transaction not found in the internal database with txid: {}", - txid - ) - } - Self::TransactionConfirmed(txid) => { - write!(f, "Transaction already confirmed with txid: {}", txid) - } - Self::IrreplaceableTransaction(txid) => { - write!(f, "Transaction can't be replaced with txid: {}", txid) - } - Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for BuildFeeBumpError {} diff --git a/crates/bdk/src/wallet/export.rs b/crates/bdk/src/wallet/export.rs deleted file mode 100644 index b6349309..00000000 --- a/crates/bdk/src/wallet/export.rs +++ /dev/null @@ -1,341 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Wallet export -//! -//! This modules implements the wallet export format used by [FullyNoded](https://github.com/Fonta1n3/FullyNoded/blob/10b7808c8b929b171cca537fb50522d015168ac9/Docs/Wallets/Wallet-Export-Spec.md). -//! -//! ## Examples -//! -//! ### Import from JSON -//! -//! ``` -//! # use std::str::FromStr; -//! # use bitcoin::*; -//! # use bdk::wallet::export::*; -//! # use bdk::*; -//! let import = r#"{ -//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)", -//! "blockheight":1782088, -//! "label":"testnet" -//! }"#; -//! -//! let import = FullyNodedExport::from_str(import)?; -//! let wallet = Wallet::new_no_persist( -//! &import.descriptor(), -//! import.change_descriptor().as_ref(), -//! Network::Testnet, -//! )?; -//! # Ok::<_, Box>(()) -//! ``` -//! -//! ### Export a `Wallet` -//! ``` -//! # use bitcoin::*; -//! # use bdk::wallet::export::*; -//! # use bdk::*; -//! let wallet = Wallet::new_no_persist( -//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)", -//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"), -//! Network::Testnet, -//! )?; -//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap(); -//! -//! println!("Exported: {}", export.to_string()); -//! # Ok::<_, Box>(()) -//! ``` - -use alloc::string::String; -use core::fmt; -use core::str::FromStr; -use serde::{Deserialize, Serialize}; - -use miniscript::descriptor::{ShInner, WshInner}; -use miniscript::{Descriptor, ScriptContext, Terminal}; - -use crate::types::KeychainKind; -use crate::wallet::Wallet; - -/// Alias for [`FullyNodedExport`] -#[deprecated(since = "0.18.0", note = "Please use [`FullyNodedExport`] instead")] -pub type WalletExport = FullyNodedExport; - -/// Structure that contains the export of a wallet -/// -/// For a usage example see [this module](crate::wallet::export)'s documentation. -#[derive(Debug, Serialize, Deserialize)] -pub struct FullyNodedExport { - descriptor: String, - /// Earliest block to rescan when looking for the wallet's transactions - pub blockheight: u32, - /// Arbitrary label for the wallet - pub label: String, -} - -impl fmt::Display for FullyNodedExport { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", serde_json::to_string(self).unwrap()) - } -} - -impl FromStr for FullyNodedExport { - type Err = serde_json::Error; - - fn from_str(s: &str) -> Result { - serde_json::from_str(s) - } -} - -fn remove_checksum(s: String) -> String { - s.split_once('#').map(|(a, _)| String::from(a)).unwrap() -} - -impl FullyNodedExport { - /// Export a wallet - /// - /// This function returns an error if it determines that the `wallet`'s descriptor(s) are not - /// supported by Bitcoin Core or don't follow the standard derivation paths defined by BIP44 - /// and others. - /// - /// If `include_blockheight` is `true`, this function will look into the `wallet`'s database - /// for the oldest transaction it knows and use that as the earliest block to rescan. - /// - /// If the database is empty or `include_blockheight` is false, the `blockheight` field - /// returned will be `0`. - pub fn export_wallet( - wallet: &Wallet, - label: &str, - include_blockheight: bool, - ) -> Result { - let descriptor = wallet - .get_descriptor_for_keychain(KeychainKind::External) - .to_string_with_secret( - &wallet - .get_signers(KeychainKind::External) - .as_key_map(wallet.secp_ctx()), - ); - let descriptor = remove_checksum(descriptor); - Self::is_compatible_with_core(&descriptor)?; - - let blockheight = if include_blockheight { - wallet.transactions().next().map_or(0, |canonical_tx| { - match canonical_tx.chain_position { - bdk_chain::ChainPosition::Confirmed(a) => a.confirmation_height, - bdk_chain::ChainPosition::Unconfirmed(_) => 0, - } - }) - } else { - 0 - }; - - let export = FullyNodedExport { - descriptor, - label: label.into(), - blockheight, - }; - - let change_descriptor = match wallet.public_descriptor(KeychainKind::Internal).is_some() { - false => None, - true => { - let descriptor = wallet - .get_descriptor_for_keychain(KeychainKind::Internal) - .to_string_with_secret( - &wallet - .get_signers(KeychainKind::Internal) - .as_key_map(wallet.secp_ctx()), - ); - Some(remove_checksum(descriptor)) - } - }; - if export.change_descriptor() != change_descriptor { - return Err("Incompatible change descriptor"); - } - - Ok(export) - } - - fn is_compatible_with_core(descriptor: &str) -> Result<(), &'static str> { - fn check_ms( - terminal: &Terminal, - ) -> Result<(), &'static str> { - if let Terminal::Multi(_, _) = terminal { - Ok(()) - } else { - Err("The descriptor contains operators not supported by Bitcoin Core") - } - } - - // pkh(), wpkh(), sh(wpkh()) are always fine, as well as multi() and sortedmulti() - match Descriptor::::from_str(descriptor).map_err(|_| "Invalid descriptor")? { - Descriptor::Pkh(_) | Descriptor::Wpkh(_) => Ok(()), - Descriptor::Sh(sh) => match sh.as_inner() { - ShInner::Wpkh(_) => Ok(()), - ShInner::SortedMulti(_) => Ok(()), - ShInner::Wsh(wsh) => match wsh.as_inner() { - WshInner::SortedMulti(_) => Ok(()), - WshInner::Ms(ms) => check_ms(&ms.node), - }, - ShInner::Ms(ms) => check_ms(&ms.node), - }, - Descriptor::Wsh(wsh) => match wsh.as_inner() { - WshInner::SortedMulti(_) => Ok(()), - WshInner::Ms(ms) => check_ms(&ms.node), - }, - _ => Err("The descriptor is not compatible with Bitcoin Core"), - } - } - - /// Return the external descriptor - pub fn descriptor(&self) -> String { - self.descriptor.clone() - } - - /// Return the internal descriptor, if present - pub fn change_descriptor(&self) -> Option { - let replaced = self.descriptor.replace("/0/*", "/1/*"); - - if replaced != self.descriptor { - Some(replaced) - } else { - None - } - } -} - -#[cfg(test)] -mod test { - use core::str::FromStr; - - use crate::std::string::ToString; - use bdk_chain::{BlockId, ConfirmationTime}; - use bitcoin::hashes::Hash; - use bitcoin::{transaction, BlockHash, Network, Transaction}; - - use super::*; - use crate::wallet::Wallet; - - fn get_test_wallet( - descriptor: &str, - change_descriptor: Option<&str>, - network: Network, - ) -> Wallet { - let mut wallet = Wallet::new_no_persist(descriptor, change_descriptor, network).unwrap(); - let transaction = Transaction { - input: vec![], - output: vec![], - version: transaction::Version::non_standard(0), - lock_time: bitcoin::absolute::LockTime::ZERO, - }; - wallet - .insert_checkpoint(BlockId { - height: 5001, - hash: BlockHash::all_zeros(), - }) - .unwrap(); - wallet - .insert_tx( - transaction, - ConfirmationTime::Confirmed { - height: 5000, - time: 0, - }, - ) - .unwrap(); - wallet - } - - #[test] - fn test_export_bip44() { - let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; - let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; - - let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin); - let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); - - assert_eq!(export.descriptor(), descriptor); - assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); - assert_eq!(export.blockheight, 5000); - assert_eq!(export.label, "Test Label"); - } - - #[test] - #[should_panic(expected = "Incompatible change descriptor")] - fn test_export_no_change() { - // This wallet explicitly doesn't have a change descriptor. It should be impossible to - // export, because exporting this kind of external descriptor normally implies the - // existence of an internal descriptor - - let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; - - let wallet = get_test_wallet(descriptor, None, Network::Bitcoin); - FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); - } - - #[test] - #[should_panic(expected = "Incompatible change descriptor")] - fn test_export_incompatible_change() { - // This wallet has a change descriptor, but the derivation path is not in the "standard" - // bip44/49/etc format - - let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; - let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)"; - - let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin); - FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); - } - - #[test] - fn test_export_multi() { - let descriptor = "wsh(multi(2,\ - [73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*,\ - [f9f62194/48'/0'/0'/2']tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/0/*,\ - [c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/0/*\ - ))"; - let change_descriptor = "wsh(multi(2,\ - [73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/1/*,\ - [f9f62194/48'/0'/0'/2']tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/1/*,\ - [c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\ - ))"; - - let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Testnet); - let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); - - assert_eq!(export.descriptor(), descriptor); - assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); - assert_eq!(export.blockheight, 5000); - assert_eq!(export.label, "Test Label"); - } - - #[test] - fn test_export_to_json() { - let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; - let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; - - let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin); - let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); - - assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}"); - } - - #[test] - fn test_export_from_json() { - let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; - let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; - - let import_str = "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}"; - let export = FullyNodedExport::from_str(import_str).unwrap(); - - assert_eq!(export.descriptor(), descriptor); - assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); - assert_eq!(export.blockheight, 5000); - assert_eq!(export.label, "Test Label"); - } -} diff --git a/crates/bdk/src/wallet/hardwaresigner.rs b/crates/bdk/src/wallet/hardwaresigner.rs deleted file mode 100644 index 5a210f64..00000000 --- a/crates/bdk/src/wallet/hardwaresigner.rs +++ /dev/null @@ -1,98 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! HWI Signer -//! -//! This module contains HWISigner, an implementation of a [TransactionSigner] to be -//! used with hardware wallets. -//! ```no_run -//! # use bdk::bitcoin::Network; -//! # use bdk::signer::SignerOrdering; -//! # use bdk::wallet::hardwaresigner::HWISigner; -//! # use bdk::wallet::AddressIndex::New; -//! # use bdk::{KeychainKind, SignOptions, Wallet}; -//! # use hwi::HWIClient; -//! # use std::sync::Arc; -//! # -//! # fn main() -> Result<(), Box> { -//! let mut devices = HWIClient::enumerate()?; -//! if devices.is_empty() { -//! panic!("No devices found!"); -//! } -//! let first_device = devices.remove(0)?; -//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?; -//! -//! # let mut wallet = Wallet::new_no_persist( -//! # "", -//! # None, -//! # Network::Testnet, -//! # )?; -//! # -//! // Adding the hardware signer to the BDK wallet -//! wallet.add_signer( -//! KeychainKind::External, -//! SignerOrdering(200), -//! Arc::new(custom_signer), -//! ); -//! -//! # Ok(()) -//! # } -//! ``` - -use bitcoin::bip32::Fingerprint; -use bitcoin::secp256k1::{All, Secp256k1}; -use bitcoin::Psbt; - -use hwi::error::Error; -use hwi::types::{HWIChain, HWIDevice}; -use hwi::HWIClient; - -use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner}; - -#[derive(Debug)] -/// Custom signer for Hardware Wallets -/// -/// This ignores `sign_options` and leaves the decisions up to the hardware wallet. -pub struct HWISigner { - fingerprint: Fingerprint, - client: HWIClient, -} - -impl HWISigner { - /// Create a instance from the specified device and chain - pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result { - let client = HWIClient::get_client(device, false, chain)?; - Ok(HWISigner { - fingerprint: device.fingerprint, - client, - }) - } -} - -impl SignerCommon for HWISigner { - fn id(&self, _secp: &Secp256k1) -> SignerId { - SignerId::Fingerprint(self.fingerprint) - } -} - -/// This implementation ignores `sign_options` -impl TransactionSigner for HWISigner { - fn sign_transaction( - &self, - psbt: &mut Psbt, - _sign_options: &crate::SignOptions, - _secp: &crate::wallet::utils::SecpCtx, - ) -> Result<(), SignerError> { - psbt.combine(self.client.sign_tx(psbt)?.psbt) - .expect("Failed to combine HW signed psbt with passed PSBT"); - Ok(()) - } -} diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs deleted file mode 100644 index 146d4677..00000000 --- a/crates/bdk/src/wallet/mod.rs +++ /dev/null @@ -1,2708 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Wallet -//! -//! This module defines the [`Wallet`]. -use crate::collections::{BTreeMap, HashMap}; -use alloc::{ - boxed::Box, - string::{String, ToString}, - sync::Arc, - vec::Vec, -}; -pub use bdk_chain::keychain::Balance; -use bdk_chain::{ - indexed_tx_graph, - keychain::{self, KeychainTxOutIndex}, - local_chain::{ - self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain, - }, - spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, - tx_graph::{CanonicalTx, TxGraph}, - Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut, - IndexedTxGraph, -}; -use bdk_persist::{Persist, PersistBackend}; -use bitcoin::secp256k1::{All, Secp256k1}; -use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; -use bitcoin::{ - absolute, psbt, Address, Block, FeeRate, Network, OutPoint, Script, ScriptBuf, Sequence, - Transaction, TxOut, Txid, Witness, -}; -use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt}; -use bitcoin::{constants::genesis_block, Amount}; -use core::fmt; -use core::ops::Deref; -use descriptor::error::Error as DescriptorError; -use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}; - -use bdk_chain::tx_graph::CalculateFeeError; - -pub mod coin_selection; -pub mod export; -pub mod signer; -pub mod tx_builder; -pub(crate) mod utils; - -pub mod error; - -pub use utils::IsDust; - -use coin_selection::DefaultCoinSelectionAlgorithm; -use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner}; -use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams}; -use utils::{check_nsequence_rbf, After, Older, SecpCtx}; - -use crate::descriptor::policy::BuildSatisfaction; -use crate::descriptor::{ - self, calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta, - ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils, -}; -use crate::psbt::PsbtUtils; -use crate::signer::SignerError; -use crate::types::*; -use crate::wallet::coin_selection::Excess::{Change, NoChange}; -use crate::wallet::error::{BuildFeeBumpError, CreateTxError, MiniscriptPsbtError}; - -const COINBASE_MATURITY: u32 = 100; - -/// A Bitcoin wallet -/// -/// The `Wallet` acts as a way of coherently interfacing with output descriptors and related transactions. -/// Its main components are: -/// -/// 1. output *descriptors* from which it can derive addresses. -/// 2. [`signer`]s that can contribute signatures to addresses instantiated from the descriptors. -/// -/// [`signer`]: crate::signer -#[derive(Debug)] -pub struct Wallet { - signers: Arc, - change_signers: Arc, - chain: LocalChain, - indexed_graph: IndexedTxGraph>, - persist: Persist, - network: Network, - secp: SecpCtx, -} - -/// An update to [`Wallet`]. -/// -/// It updates [`bdk_chain::keychain::KeychainTxOutIndex`], [`bdk_chain::TxGraph`] and [`local_chain::LocalChain`] atomically. -#[derive(Debug, Clone, Default)] -pub struct Update { - /// Contains the last active derivation indices per keychain (`K`), which is used to update the - /// [`KeychainTxOutIndex`]. - pub last_active_indices: BTreeMap, - - /// Update for the wallet's internal [`TxGraph`]. - pub graph: TxGraph, - - /// Update for the wallet's internal [`LocalChain`]. - /// - /// [`LocalChain`]: local_chain::LocalChain - pub chain: Option, -} - -impl From> for Update { - fn from(value: FullScanResult) -> Self { - Self { - last_active_indices: value.last_active_indices, - graph: value.graph_update, - chain: Some(value.chain_update), - } - } -} - -impl From for Update { - fn from(value: SyncResult) -> Self { - Self { - last_active_indices: BTreeMap::new(), - graph: value.graph_update, - chain: Some(value.chain_update), - } - } -} - -/// The changes made to a wallet by applying an [`Update`]. -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)] -pub struct ChangeSet { - /// Changes to the [`LocalChain`]. - /// - /// [`LocalChain`]: local_chain::LocalChain - pub chain: local_chain::ChangeSet, - - /// Changes to [`IndexedTxGraph`]. - /// - /// [`IndexedTxGraph`]: bdk_chain::indexed_tx_graph::IndexedTxGraph - pub indexed_tx_graph: indexed_tx_graph::ChangeSet< - ConfirmationTimeHeightAnchor, - keychain::ChangeSet, - >, - - /// Stores the network type of the wallet. - pub network: Option, -} - -impl Append for ChangeSet { - fn append(&mut self, other: Self) { - Append::append(&mut self.chain, other.chain); - Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph); - if other.network.is_some() { - debug_assert!( - self.network.is_none() || self.network == other.network, - "network type must be consistent" - ); - self.network = other.network; - } - } - - fn is_empty(&self) -> bool { - self.chain.is_empty() && self.indexed_tx_graph.is_empty() - } -} - -impl From for ChangeSet { - fn from(chain: local_chain::ChangeSet) -> Self { - Self { - chain, - ..Default::default() - } - } -} - -impl - From< - indexed_tx_graph::ChangeSet< - ConfirmationTimeHeightAnchor, - keychain::ChangeSet, - >, - > for ChangeSet -{ - fn from( - indexed_tx_graph: indexed_tx_graph::ChangeSet< - ConfirmationTimeHeightAnchor, - keychain::ChangeSet, - >, - ) -> Self { - Self { - indexed_tx_graph, - ..Default::default() - } - } -} - -/// A derived address and the index it was found at. -/// For convenience this automatically derefs to `Address` -#[derive(Debug, PartialEq, Eq)] -pub struct AddressInfo { - /// Child index of this address - pub index: u32, - /// Address - pub address: Address, - /// Type of keychain - pub keychain: KeychainKind, -} - -impl Deref for AddressInfo { - type Target = Address; - - fn deref(&self) -> &Self::Target { - &self.address - } -} - -impl fmt::Display for AddressInfo { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.address) - } -} - -impl Wallet { - /// Creates a wallet that does not persist data. - pub fn new_no_persist( - descriptor: E, - change_descriptor: Option, - network: Network, - ) -> Result { - Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e { - NewError::NonEmptyDatabase => unreachable!("mock-database cannot have data"), - NewError::Descriptor(e) => e, - NewError::Persist(_) => unreachable!("mock-write must always succeed"), - }) - } - - /// Creates a wallet that does not persist data, with a custom genesis hash. - pub fn new_no_persist_with_genesis_hash( - descriptor: E, - change_descriptor: Option, - network: Network, - genesis_hash: BlockHash, - ) -> Result { - Self::new_with_genesis_hash(descriptor, change_descriptor, (), network, genesis_hash) - .map_err(|e| match e { - NewError::NonEmptyDatabase => unreachable!("mock-database cannot have data"), - NewError::Descriptor(e) => e, - NewError::Persist(_) => unreachable!("mock-write must always succeed"), - }) - } -} - -/// The error type when constructing a fresh [`Wallet`]. -/// -/// Methods [`new`] and [`new_with_genesis_hash`] may return this error. -/// -/// [`new`]: Wallet::new -/// [`new_with_genesis_hash`]: Wallet::new_with_genesis_hash -#[derive(Debug)] -pub enum NewError { - /// Database already has data. - NonEmptyDatabase, - /// There was problem with the passed-in descriptor(s). - Descriptor(crate::descriptor::DescriptorError), - /// We were unable to write the wallet's data to the persistence backend. - Persist(anyhow::Error), -} - -impl fmt::Display for NewError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - NewError::NonEmptyDatabase => write!( - f, - "database already has data - use `load` or `new_or_load` methods instead" - ), - NewError::Descriptor(e) => e.fmt(f), - NewError::Persist(e) => e.fmt(f), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for NewError {} - -/// The error type when loading a [`Wallet`] from persistence. -/// -/// Method [`load`] may return this error. -/// -/// [`load`]: Wallet::load -#[derive(Debug)] -pub enum LoadError { - /// There was a problem with the passed-in descriptor(s). - Descriptor(crate::descriptor::DescriptorError), - /// Loading data from the persistence backend failed. - Persist(anyhow::Error), - /// Wallet not initialized, persistence backend is empty. - NotInitialized, - /// Data loaded from persistence is missing network type. - MissingNetwork, - /// Data loaded from persistence is missing genesis hash. - MissingGenesis, - /// Data loaded from persistence is missing descriptor. - MissingDescriptor, -} - -impl fmt::Display for LoadError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - LoadError::Descriptor(e) => e.fmt(f), - LoadError::Persist(e) => e.fmt(f), - LoadError::NotInitialized => { - write!(f, "wallet is not initialized, persistence backend is empty") - } - LoadError::MissingNetwork => write!(f, "loaded data is missing network type"), - LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"), - LoadError::MissingDescriptor => write!(f, "loaded data is missing descriptor"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for LoadError {} - -/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existent. -/// -/// Methods [`new_or_load`] and [`new_or_load_with_genesis_hash`] may return this error. -/// -/// [`new_or_load`]: Wallet::new_or_load -/// [`new_or_load_with_genesis_hash`]: Wallet::new_or_load_with_genesis_hash -#[derive(Debug)] -pub enum NewOrLoadError { - /// There is a problem with the passed-in descriptor. - Descriptor(crate::descriptor::DescriptorError), - /// Either writing to or loading from the persistence backend failed. - Persist(anyhow::Error), - /// Wallet is not initialized, persistence backend is empty. - NotInitialized, - /// The loaded genesis hash does not match what was provided. - LoadedGenesisDoesNotMatch { - /// The expected genesis block hash. - expected: BlockHash, - /// The block hash loaded from persistence. - got: Option, - }, - /// The loaded network type does not match what was provided. - LoadedNetworkDoesNotMatch { - /// The expected network type. - expected: Network, - /// The network type loaded from persistence. - got: Option, - }, - /// The loaded desccriptor does not match what was provided. - LoadedDescriptorDoesNotMatch { - /// The descriptor loaded from persistence. - got: Option, - /// The keychain of the descriptor not matching - keychain: KeychainKind, - }, -} - -impl fmt::Display for NewOrLoadError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - NewOrLoadError::Descriptor(e) => e.fmt(f), - NewOrLoadError::Persist(e) => write!( - f, - "failed to either write to or load from persistence, {}", - e - ), - NewOrLoadError::NotInitialized => { - write!(f, "wallet is not initialized, persistence backend is empty") - } - NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => { - write!(f, "loaded genesis hash is not {}, got {:?}", expected, got) - } - NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => { - write!(f, "loaded network type is not {}, got {:?}", expected, got) - } - NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => { - write!( - f, - "loaded descriptor is different from what was provided, got {:?} for keychain {:?}", - got, keychain - ) - } - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for NewOrLoadError {} - -/// An error that may occur when inserting a transaction into [`Wallet`]. -#[derive(Debug)] -pub enum InsertTxError { - /// The error variant that occurs when the caller attempts to insert a transaction with a - /// confirmation height that is greater than the internal chain tip. - ConfirmationHeightCannotBeGreaterThanTip { - /// The internal chain's tip height. - tip_height: u32, - /// The introduced transaction's confirmation height. - tx_height: u32, - }, -} - -impl fmt::Display for InsertTxError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - InsertTxError::ConfirmationHeightCannotBeGreaterThanTip { - tip_height, - tx_height, - } => { - write!(f, "cannot insert tx with confirmation height ({}) higher than internal tip height ({})", tx_height, tip_height) - } - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for InsertTxError {} - -/// An error that may occur when applying a block to [`Wallet`]. -#[derive(Debug)] -pub enum ApplyBlockError { - /// Occurs when the update chain cannot connect with original chain. - CannotConnect(CannotConnectError), - /// Occurs when the `connected_to` hash does not match the hash derived from `block`. - UnexpectedConnectedToHash { - /// Block hash of `connected_to`. - connected_to_hash: BlockHash, - /// Expected block hash of `connected_to`, as derived from `block`. - expected_hash: BlockHash, - }, -} - -impl fmt::Display for ApplyBlockError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ApplyBlockError::CannotConnect(err) => err.fmt(f), - ApplyBlockError::UnexpectedConnectedToHash { - expected_hash: block_hash, - connected_to_hash: checkpoint_hash, - } => write!( - f, - "`connected_to` hash {} differs from the expected hash {} (which is derived from `block`)", - checkpoint_hash, block_hash - ), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for ApplyBlockError {} - -impl Wallet { - /// Initialize an empty [`Wallet`]. - pub fn new( - descriptor: E, - change_descriptor: Option, - db: impl PersistBackend + Send + Sync + 'static, - network: Network, - ) -> Result { - let genesis_hash = genesis_block(network).block_hash(); - Self::new_with_genesis_hash(descriptor, change_descriptor, db, network, genesis_hash) - } - - /// Initialize an empty [`Wallet`] with a custom genesis hash. - /// - /// This is like [`Wallet::new`] with an additional `genesis_hash` parameter. This is useful - /// for syncing from alternative networks. - pub fn new_with_genesis_hash( - descriptor: E, - change_descriptor: Option, - mut db: impl PersistBackend + Send + Sync + 'static, - network: Network, - genesis_hash: BlockHash, - ) -> Result { - if let Ok(changeset) = db.load_from_persistence() { - if changeset.is_some() { - return Err(NewError::NonEmptyDatabase); - } - } - let secp = Secp256k1::new(); - let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); - let mut index = KeychainTxOutIndex::::default(); - - let (signers, change_signers) = - create_signers(&mut index, &secp, descriptor, change_descriptor, network) - .map_err(NewError::Descriptor)?; - - let indexed_graph = IndexedTxGraph::new(index); - - let mut persist = Persist::new(db); - persist.stage(ChangeSet { - chain: chain_changeset, - indexed_tx_graph: indexed_graph.initial_changeset(), - network: Some(network), - }); - persist.commit().map_err(NewError::Persist)?; - - Ok(Wallet { - signers, - change_signers, - network, - chain, - indexed_graph, - persist, - secp, - }) - } - - /// Load [`Wallet`] from the given persistence backend. - /// - /// Note that the descriptor secret keys are not persisted to the db; this means that after - /// calling this method the [`Wallet`] **won't** know the secret keys, and as such, won't be - /// able to sign transactions. - /// - /// If you wish to use the wallet to sign transactions, you need to add the secret keys - /// manually to the [`Wallet`]: - /// - /// ```rust,no_run - /// # use bdk::Wallet; - /// # use bdk::signer::{SignersContainer, SignerOrdering}; - /// # use bdk::descriptor::Descriptor; - /// # use bitcoin::key::Secp256k1; - /// # use bdk::KeychainKind; - /// # use bdk_file_store::Store; - /// # - /// # fn main() -> Result<(), anyhow::Error> { - /// # let temp_dir = tempfile::tempdir().expect("must create tempdir"); - /// # let file_path = temp_dir.path().join("store.db"); - /// # let db: Store = Store::create_new(&[], &file_path).expect("must create db"); - /// let secp = Secp256k1::new(); - /// - /// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap(); - /// let (internal_descriptor, internal_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)").unwrap(); - /// - /// let external_signer_container = SignersContainer::build(external_keymap, &external_descriptor, &secp); - /// let internal_signer_container = SignersContainer::build(internal_keymap, &internal_descriptor, &secp); - /// - /// let mut wallet = Wallet::load(db)?; - /// - /// external_signer_container.signers().into_iter() - /// .for_each(|s| wallet.add_signer(KeychainKind::External, SignerOrdering::default(), s.clone())); - /// internal_signer_container.signers().into_iter() - /// .for_each(|s| wallet.add_signer(KeychainKind::Internal, SignerOrdering::default(), s.clone())); - /// # Ok(()) - /// # } - /// ``` - /// - /// Alternatively, you can call [`Wallet::new_or_load`], which will add the private keys of the - /// passed-in descriptors to the [`Wallet`]. - pub fn load( - mut db: impl PersistBackend + Send + Sync + 'static, - ) -> Result { - let changeset = db - .load_from_persistence() - .map_err(LoadError::Persist)? - .ok_or(LoadError::NotInitialized)?; - Self::load_from_changeset(db, changeset) - } - - fn load_from_changeset( - db: impl PersistBackend + Send + Sync + 'static, - changeset: ChangeSet, - ) -> Result { - let secp = Secp256k1::new(); - let network = changeset.network.ok_or(LoadError::MissingNetwork)?; - let chain = - LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?; - let mut index = KeychainTxOutIndex::::default(); - let descriptor = changeset - .indexed_tx_graph - .indexer - .keychains_added - .get(&KeychainKind::External) - .ok_or(LoadError::MissingDescriptor)? - .clone(); - let change_descriptor = changeset - .indexed_tx_graph - .indexer - .keychains_added - .get(&KeychainKind::Internal) - .cloned(); - - let (signers, change_signers) = - create_signers(&mut index, &secp, descriptor, change_descriptor, network) - .expect("Can't fail: we passed in valid descriptors, recovered from the changeset"); - - let mut indexed_graph = IndexedTxGraph::new(index); - indexed_graph.apply_changeset(changeset.indexed_tx_graph); - - let persist = Persist::new(db); - - Ok(Wallet { - signers, - change_signers, - chain, - indexed_graph, - persist, - network, - secp, - }) - } - - /// Either loads [`Wallet`] from persistence, or initializes it if it does not exist. - /// - /// This method will fail if the loaded [`Wallet`] has different parameters to those provided. - pub fn new_or_load( - descriptor: E, - change_descriptor: Option, - db: impl PersistBackend + Send + Sync + 'static, - network: Network, - ) -> Result { - let genesis_hash = genesis_block(network).block_hash(); - Self::new_or_load_with_genesis_hash( - descriptor, - change_descriptor, - db, - network, - genesis_hash, - ) - } - - /// Either loads [`Wallet`] from persistence, or initializes it if it does not exist, using the - /// provided descriptor, change descriptor, network, and custom genesis hash. - /// - /// This method will fail if the loaded [`Wallet`] has different parameters to those provided. - /// This is like [`Wallet::new_or_load`] with an additional `genesis_hash` parameter. This is - /// useful for syncing from alternative networks. - pub fn new_or_load_with_genesis_hash( - descriptor: E, - change_descriptor: Option, - mut db: impl PersistBackend + Send + Sync + 'static, - network: Network, - genesis_hash: BlockHash, - ) -> Result { - let changeset = db - .load_from_persistence() - .map_err(NewOrLoadError::Persist)?; - match changeset { - Some(changeset) => { - let mut wallet = Self::load_from_changeset(db, changeset).map_err(|e| match e { - LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e), - LoadError::Persist(e) => NewOrLoadError::Persist(e), - LoadError::NotInitialized => NewOrLoadError::NotInitialized, - LoadError::MissingNetwork => NewOrLoadError::LoadedNetworkDoesNotMatch { - expected: network, - got: None, - }, - LoadError::MissingGenesis => NewOrLoadError::LoadedGenesisDoesNotMatch { - expected: genesis_hash, - got: None, - }, - LoadError::MissingDescriptor => NewOrLoadError::LoadedDescriptorDoesNotMatch { - got: None, - keychain: KeychainKind::External, - }, - })?; - if wallet.network != network { - return Err(NewOrLoadError::LoadedNetworkDoesNotMatch { - expected: network, - got: Some(wallet.network), - }); - } - if wallet.chain.genesis_hash() != genesis_hash { - return Err(NewOrLoadError::LoadedGenesisDoesNotMatch { - expected: genesis_hash, - got: Some(wallet.chain.genesis_hash()), - }); - } - - let (expected_descriptor, expected_descriptor_keymap) = descriptor - .into_wallet_descriptor(&wallet.secp, network) - .map_err(NewOrLoadError::Descriptor)?; - let wallet_descriptor = wallet.public_descriptor(KeychainKind::External).cloned(); - if wallet_descriptor != Some(expected_descriptor.clone()) { - return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch { - got: wallet_descriptor, - keychain: KeychainKind::External, - }); - } - // if expected descriptor has private keys add them as new signers - if !expected_descriptor_keymap.is_empty() { - let signer_container = SignersContainer::build( - expected_descriptor_keymap, - &expected_descriptor, - &wallet.secp, - ); - signer_container.signers().into_iter().for_each(|signer| { - wallet.add_signer( - KeychainKind::External, - SignerOrdering::default(), - signer.clone(), - ) - }); - } - - let expected_change_descriptor = if let Some(c) = change_descriptor { - Some( - c.into_wallet_descriptor(&wallet.secp, network) - .map_err(NewOrLoadError::Descriptor)?, - ) - } else { - None - }; - let wallet_change_descriptor = - wallet.public_descriptor(KeychainKind::Internal).cloned(); - - match (expected_change_descriptor, wallet_change_descriptor) { - (Some((expected_descriptor, expected_keymap)), Some(wallet_descriptor)) - if wallet_descriptor == expected_descriptor => - { - // if expected change descriptor has private keys add them as new signers - if !expected_keymap.is_empty() { - let signer_container = SignersContainer::build( - expected_keymap, - &expected_descriptor, - &wallet.secp, - ); - signer_container.signers().into_iter().for_each(|signer| { - wallet.add_signer( - KeychainKind::Internal, - SignerOrdering::default(), - signer.clone(), - ) - }); - } - } - (None, None) => (), - (_, wallet_descriptor) => { - return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch { - got: wallet_descriptor, - keychain: KeychainKind::Internal, - }); - } - } - - Ok(wallet) - } - None => Self::new_with_genesis_hash( - descriptor, - change_descriptor, - db, - network, - genesis_hash, - ) - .map_err(|e| match e { - NewError::NonEmptyDatabase => { - unreachable!("database is already checked to have no data") - } - NewError::Descriptor(e) => NewOrLoadError::Descriptor(e), - NewError::Persist(e) => NewOrLoadError::Persist(e), - }), - } - } - - /// Get the Bitcoin network the wallet is using. - pub fn network(&self) -> Network { - self.network - } - - /// Iterator over all keychains in this wallet - pub fn keychains(&self) -> impl Iterator { - self.indexed_graph.index.keychains() - } - - /// Peek an address of the given `keychain` at `index` without revealing it. - /// - /// For non-wildcard descriptors this returns the same address at every provided index. - /// - /// # Panics - /// - /// This panics when the caller requests for an address of derivation index greater than the - /// [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) max index. - pub fn peek_address(&self, keychain: KeychainKind, mut index: u32) -> AddressInfo { - let keychain = self.map_keychain(keychain); - let mut spk_iter = self - .indexed_graph - .index - .unbounded_spk_iter(&keychain) - .expect("Must exist (we called map_keychain)"); - if !spk_iter.descriptor().has_wildcard() { - index = 0; - } - let (index, spk) = spk_iter - .nth(index as usize) - .expect("derivation index is out of bounds"); - - AddressInfo { - index, - address: Address::from_script(&spk, self.network).expect("must have address form"), - keychain, - } - } - - /// Attempt to reveal the next address of the given `keychain`. - /// - /// This will increment the internal derivation index. If the keychain's descriptor doesn't - /// contain a wildcard or every address is already revealed up to the maximum derivation - /// index defined in [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki), - /// then returns the last revealed address. - /// - /// # Errors - /// - /// If writing to persistent storage fails. - pub fn reveal_next_address(&mut self, keychain: KeychainKind) -> anyhow::Result { - let keychain = self.map_keychain(keychain); - let ((index, spk), index_changeset) = self - .indexed_graph - .index - .reveal_next_spk(&keychain) - .expect("Must exist (we called map_keychain)"); - - self.persist - .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?; - - Ok(AddressInfo { - index, - address: Address::from_script(spk, self.network).expect("must have address form"), - keychain, - }) - } - - /// Reveal addresses up to and including the target `index` and return an iterator - /// of newly revealed addresses. - /// - /// If the target `index` is unreachable, we make a best effort to reveal up to the last - /// possible index. If all addresses up to the given `index` are already revealed, then - /// no new addresses are returned. - /// - /// # Errors - /// - /// If writing to persistent storage fails. - pub fn reveal_addresses_to( - &mut self, - keychain: KeychainKind, - index: u32, - ) -> anyhow::Result + '_> { - let keychain = self.map_keychain(keychain); - let (spk_iter, index_changeset) = self - .indexed_graph - .index - .reveal_to_target(&keychain, index) - .expect("must exist (we called map_keychain)"); - - self.persist - .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?; - - Ok(spk_iter.map(move |(index, spk)| AddressInfo { - index, - address: Address::from_script(&spk, self.network).expect("must have address form"), - keychain, - })) - } - - /// Get the next unused address for the given `keychain`, i.e. the address with the lowest - /// derivation index that hasn't been used. - /// - /// This will attempt to derive and reveal a new address if no newly revealed addresses - /// are available. See also [`reveal_next_address`](Self::reveal_next_address). - /// - /// # Errors - /// - /// If writing to persistent storage fails. - pub fn next_unused_address(&mut self, keychain: KeychainKind) -> anyhow::Result { - let keychain = self.map_keychain(keychain); - let ((index, spk), index_changeset) = self - .indexed_graph - .index - .next_unused_spk(&keychain) - .expect("must exist (we called map_keychain)"); - - self.persist - .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?; - - Ok(AddressInfo { - index, - address: Address::from_script(spk, self.network).expect("must have address form"), - keychain, - }) - } - - /// Marks an address used of the given `keychain` at `index`. - /// - /// Returns whether the given index was present and then removed from the unused set. - pub fn mark_used(&mut self, keychain: KeychainKind, index: u32) -> bool { - self.indexed_graph.index.mark_used(keychain, index) - } - - /// Undoes the effect of [`mark_used`] and returns whether the `index` was inserted - /// back into the unused set. - /// - /// Since this is only a superficial marker, it will have no effect if the address at the given - /// `index` was actually used, i.e. the wallet has previously indexed a tx output for the - /// derived spk. - /// - /// [`mark_used`]: Self::mark_used - pub fn unmark_used(&mut self, keychain: KeychainKind, index: u32) -> bool { - self.indexed_graph.index.unmark_used(keychain, index) - } - - /// List addresses that are revealed but unused. - /// - /// Note if the returned iterator is empty you can reveal more addresses - /// by using [`reveal_next_address`](Self::reveal_next_address) or - /// [`reveal_addresses_to`](Self::reveal_addresses_to). - pub fn list_unused_addresses( - &self, - keychain: KeychainKind, - ) -> impl DoubleEndedIterator + '_ { - let keychain = self.map_keychain(keychain); - self.indexed_graph - .index - .unused_keychain_spks(&keychain) - .map(move |(index, spk)| AddressInfo { - index, - address: Address::from_script(spk, self.network).expect("must have address form"), - keychain, - }) - } - - /// Return whether or not a `script` is part of this wallet (either internal or external) - pub fn is_mine(&self, script: &Script) -> bool { - self.indexed_graph.index.index_of_spk(script).is_some() - } - - /// Finds how the wallet derived the script pubkey `spk`. - /// - /// Will only return `Some(_)` if the wallet has given out the spk. - pub fn derivation_of_spk(&self, spk: &Script) -> Option<(KeychainKind, u32)> { - self.indexed_graph.index.index_of_spk(spk) - } - - /// Return the list of unspent outputs of this wallet - pub fn list_unspent(&self) -> impl Iterator + '_ { - self.indexed_graph - .graph() - .filter_chain_unspents( - &self.chain, - self.chain.tip().block_id(), - self.indexed_graph.index.outpoints(), - ) - .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) - } - - /// List all relevant outputs (includes both spent and unspent, confirmed and unconfirmed). - /// - /// To list only unspent outputs (UTXOs), use [`Wallet::list_unspent`] instead. - pub fn list_output(&self) -> impl Iterator + '_ { - self.indexed_graph - .graph() - .filter_chain_txouts( - &self.chain, - self.chain.tip().block_id(), - self.indexed_graph.index.outpoints(), - ) - .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) - } - - /// Get all the checkpoints the wallet is currently storing indexed by height. - pub fn checkpoints(&self) -> CheckPointIter { - self.chain.iter_checkpoints() - } - - /// Returns the latest checkpoint. - pub fn latest_checkpoint(&self) -> CheckPoint { - self.chain.tip() - } - - /// Get unbounded script pubkey iterators for both `Internal` and `External` keychains. - /// - /// This is intended to be used when doing a full scan of your addresses (e.g. after restoring - /// from seed words). You pass the `BTreeMap` of iterators to a blockchain data source (e.g. - /// electrum server) which will go through each address until it reaches a *stop gap*. - /// - /// Note carefully that iterators go over **all** script pubkeys on the keychains (not what - /// script pubkeys the wallet is storing internally). - pub fn all_unbounded_spk_iters( - &self, - ) -> BTreeMap + Clone> { - self.indexed_graph.index.all_unbounded_spk_iters() - } - - /// Get an unbounded script pubkey iterator for the given `keychain`. - /// - /// See [`all_unbounded_spk_iters`] for more documentation - /// - /// [`all_unbounded_spk_iters`]: Self::all_unbounded_spk_iters - pub fn unbounded_spk_iter( - &self, - keychain: KeychainKind, - ) -> impl Iterator + Clone { - let keychain = self.map_keychain(keychain); - self.indexed_graph - .index - .unbounded_spk_iter(&keychain) - .expect("Must exist (we called map_keychain)") - } - - /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the - /// wallet's database. - pub fn get_utxo(&self, op: OutPoint) -> Option { - let (keychain, index, _) = self.indexed_graph.index.txout(op)?; - self.indexed_graph - .graph() - .filter_chain_unspents( - &self.chain, - self.chain.tip().block_id(), - core::iter::once(((), op)), - ) - .map(|(_, full_txo)| new_local_utxo(keychain, index, full_txo)) - .next() - } - - /// Inserts a [`TxOut`] at [`OutPoint`] into the wallet's transaction graph. - /// - /// This is used for providing a previous output's value so that we can use [`calculate_fee`] - /// or [`calculate_fee_rate`] on a given transaction. Outputs inserted with this method will - /// not be returned in [`list_unspent`] or [`list_output`]. - /// - /// Any inserted `TxOut`s are not persisted until [`commit`] is called. - /// - /// **WARNING:** This should only be used to add `TxOut`s that the wallet does not own. Only - /// insert `TxOut`s that you trust the values for! - /// - /// [`calculate_fee`]: Self::calculate_fee - /// [`calculate_fee_rate`]: Self::calculate_fee_rate - /// [`list_unspent`]: Self::list_unspent - /// [`list_output`]: Self::list_output - /// [`commit`]: Self::commit - pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) { - let additions = self.indexed_graph.insert_txout(outpoint, txout); - self.persist.stage(ChangeSet::from(additions)); - } - - /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction. - /// - /// To calculate the fee for a [`Transaction`] with inputs not owned by this wallet you must - /// manually insert the TxOut(s) into the tx graph using the [`insert_txout`] function. - /// - /// Note `tx` does not have to be in the graph for this to work. - /// - /// # Examples - /// - /// ```rust, no_run - /// # use bitcoin::Txid; - /// # use bdk::Wallet; - /// # let mut wallet: Wallet = todo!(); - /// # let txid:Txid = todo!(); - /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; - /// let fee = wallet.calculate_fee(&tx).expect("fee"); - /// ``` - /// - /// ```rust, no_run - /// # use bitcoin::Psbt; - /// # use bdk::Wallet; - /// # let mut wallet: Wallet = todo!(); - /// # let mut psbt: Psbt = todo!(); - /// let tx = &psbt.clone().extract_tx().expect("tx"); - /// let fee = wallet.calculate_fee(tx).expect("fee"); - /// ``` - /// [`insert_txout`]: Self::insert_txout - pub fn calculate_fee(&self, tx: &Transaction) -> Result { - self.indexed_graph.graph().calculate_fee(tx) - } - - /// Calculate the [`FeeRate`] for a given transaction. - /// - /// To calculate the fee rate for a [`Transaction`] with inputs not owned by this wallet you must - /// manually insert the TxOut(s) into the tx graph using the [`insert_txout`] function. - /// - /// Note `tx` does not have to be in the graph for this to work. - /// - /// # Examples - /// - /// ```rust, no_run - /// # use bitcoin::Txid; - /// # use bdk::Wallet; - /// # let mut wallet: Wallet = todo!(); - /// # let txid:Txid = todo!(); - /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; - /// let fee_rate = wallet.calculate_fee_rate(&tx).expect("fee rate"); - /// ``` - /// - /// ```rust, no_run - /// # use bitcoin::Psbt; - /// # use bdk::Wallet; - /// # let mut wallet: Wallet = todo!(); - /// # let mut psbt: Psbt = todo!(); - /// let tx = &psbt.clone().extract_tx().expect("tx"); - /// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate"); - /// ``` - /// [`insert_txout`]: Self::insert_txout - pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result { - self.calculate_fee(tx) - .map(|fee| Amount::from_sat(fee) / tx.weight()) - } - - /// Compute the `tx`'s sent and received [`Amount`]s. - /// - /// This method returns a tuple `(sent, received)`. Sent is the sum of the txin amounts - /// that spend from previous txouts tracked by this wallet. Received is the summation - /// of this tx's outputs that send to script pubkeys tracked by this wallet. - /// - /// # Examples - /// - /// ```rust, no_run - /// # use bitcoin::Txid; - /// # use bdk::Wallet; - /// # let mut wallet: Wallet = todo!(); - /// # let txid:Txid = todo!(); - /// let tx = wallet.get_tx(txid).expect("tx exists").tx_node.tx; - /// let (sent, received) = wallet.sent_and_received(&tx); - /// ``` - /// - /// ```rust, no_run - /// # use bitcoin::Psbt; - /// # use bdk::Wallet; - /// # let mut wallet: Wallet = todo!(); - /// # let mut psbt: Psbt = todo!(); - /// let tx = &psbt.clone().extract_tx().expect("tx"); - /// let (sent, received) = wallet.sent_and_received(tx); - /// ``` - pub fn sent_and_received(&self, tx: &Transaction) -> (Amount, Amount) { - self.indexed_graph.index.sent_and_received(tx, ..) - } - - /// Get a single transaction from the wallet as a [`CanonicalTx`] (if the transaction exists). - /// - /// `CanonicalTx` contains the full transaction alongside meta-data such as: - /// * Blocks that the transaction is [`Anchor`]ed in. These may or may not be blocks that exist - /// in the best chain. - /// * The [`ChainPosition`] of the transaction in the best chain - whether the transaction is - /// confirmed or unconfirmed. If the transaction is confirmed, the anchor which proves the - /// confirmation is provided. If the transaction is unconfirmed, the unix timestamp of when - /// the transaction was last seen in the mempool is provided. - /// - /// ```rust, no_run - /// use bdk::{chain::ChainPosition, Wallet}; - /// use bdk_chain::Anchor; - /// # let wallet: Wallet = todo!(); - /// # let my_txid: bitcoin::Txid = todo!(); - /// - /// let canonical_tx = wallet.get_tx(my_txid).expect("panic if tx does not exist"); - /// - /// // get reference to full transaction - /// println!("my tx: {:#?}", canonical_tx.tx_node.tx); - /// - /// // list all transaction anchors - /// for anchor in canonical_tx.tx_node.anchors { - /// println!( - /// "tx is anchored by block of hash {}", - /// anchor.anchor_block().hash - /// ); - /// } - /// - /// // get confirmation status of transaction - /// match canonical_tx.chain_position { - /// ChainPosition::Confirmed(anchor) => println!( - /// "tx is confirmed at height {}, we know this since {}:{} is in the best chain", - /// anchor.confirmation_height, anchor.anchor_block.height, anchor.anchor_block.hash, - /// ), - /// ChainPosition::Unconfirmed(last_seen) => println!( - /// "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain", - /// last_seen, - /// ), - /// } - /// ``` - /// - /// [`Anchor`]: bdk_chain::Anchor - pub fn get_tx( - &self, - txid: Txid, - ) -> Option, ConfirmationTimeHeightAnchor>> { - let graph = self.indexed_graph.graph(); - - Some(CanonicalTx { - chain_position: graph.get_chain_position( - &self.chain, - self.chain.tip().block_id(), - txid, - )?, - tx_node: graph.get_tx_node(txid)?, - }) - } - - /// Add a new checkpoint to the wallet's internal view of the chain. - /// This stages but does not [`commit`] the change. - /// - /// Returns whether anything changed with the insertion (e.g. `false` if checkpoint was already - /// there). - /// - /// [`commit`]: Self::commit - pub fn insert_checkpoint( - &mut self, - block_id: BlockId, - ) -> Result { - let changeset = self.chain.insert_block(block_id)?; - let changed = !changeset.is_empty(); - self.persist.stage(changeset.into()); - Ok(changed) - } - - /// Add a transaction to the wallet's internal view of the chain. This stages but does not - /// [`commit`] the change. - /// - /// Returns whether anything changed with the transaction insertion (e.g. `false` if the - /// transaction was already inserted at the same position). - /// - /// A `tx` can be rejected if `position` has a height greater than the [`latest_checkpoint`]. - /// Therefore you should use [`insert_checkpoint`] to insert new checkpoints before manually - /// inserting new transactions. - /// - /// **WARNING:** If `position` is confirmed, we anchor the `tx` to a the lowest checkpoint that - /// is >= the `position`'s height. The caller is responsible for ensuring the `tx` exists in our - /// local view of the best chain's history. - /// - /// [`commit`]: Self::commit - /// [`latest_checkpoint`]: Self::latest_checkpoint - /// [`insert_checkpoint`]: Self::insert_checkpoint - pub fn insert_tx( - &mut self, - tx: Transaction, - position: ConfirmationTime, - ) -> Result { - let (anchor, last_seen) = match position { - ConfirmationTime::Confirmed { height, time } => { - // anchor tx to checkpoint with lowest height that is >= position's height - let anchor = self - .chain - .range(height..) - .last() - .ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip { - tip_height: self.chain.tip().height(), - tx_height: height, - }) - .map(|anchor_cp| ConfirmationTimeHeightAnchor { - anchor_block: anchor_cp.block_id(), - confirmation_height: height, - confirmation_time: time, - })?; - - (Some(anchor), None) - } - ConfirmationTime::Unconfirmed { last_seen } => (None, Some(last_seen)), - }; - - let mut changeset = ChangeSet::default(); - let txid = tx.txid(); - changeset.append(self.indexed_graph.insert_tx(tx).into()); - if let Some(anchor) = anchor { - changeset.append(self.indexed_graph.insert_anchor(txid, anchor).into()); - } - if let Some(last_seen) = last_seen { - changeset.append(self.indexed_graph.insert_seen_at(txid, last_seen).into()); - } - - let changed = !changeset.is_empty(); - self.persist.stage(changeset); - Ok(changed) - } - - /// Iterate over the transactions in the wallet. - pub fn transactions( - &self, - ) -> impl Iterator, ConfirmationTimeHeightAnchor>> + '_ - { - self.indexed_graph - .graph() - .list_chain_txs(&self.chain, self.chain.tip().block_id()) - } - - /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature - /// values. - pub fn get_balance(&self) -> Balance { - self.indexed_graph.graph().balance( - &self.chain, - self.chain.tip().block_id(), - self.indexed_graph.index.outpoints(), - |&(k, _), _| k == KeychainKind::Internal, - ) - } - - /// Add an external signer - /// - /// See [the `signer` module](signer) for an example. - pub fn add_signer( - &mut self, - keychain: KeychainKind, - ordering: SignerOrdering, - signer: Arc, - ) { - let signers = match keychain { - KeychainKind::External => Arc::make_mut(&mut self.signers), - KeychainKind::Internal => Arc::make_mut(&mut self.change_signers), - }; - - signers.add_external(signer.id(&self.secp), ordering, signer); - } - - /// Get the signers - /// - /// ## Example - /// - /// ``` - /// # use bdk::{Wallet, KeychainKind}; - /// # use bdk::bitcoin::Network; - /// let wallet = Wallet::new_no_persist("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet)?; - /// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) { - /// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/* - /// println!("secret_key: {}", secret_key); - /// } - /// - /// Ok::<(), Box>(()) - /// ``` - pub fn get_signers(&self, keychain: KeychainKind) -> Arc { - match keychain { - KeychainKind::External => Arc::clone(&self.signers), - KeychainKind::Internal => Arc::clone(&self.change_signers), - } - } - - /// Start building a transaction. - /// - /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction. - /// - /// ## Example - /// - /// ``` - /// # use std::str::FromStr; - /// # use bitcoin::*; - /// # use bdk::*; - /// # use bdk::wallet::ChangeSet; - /// # use bdk::wallet::error::CreateTxError; - /// # use bdk_persist::PersistBackend; - /// # use anyhow::Error; - /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; - /// # let mut wallet = doctest_wallet!(); - /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); - /// let psbt = { - /// let mut builder = wallet.build_tx(); - /// builder - /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); - /// builder.finish()? - /// }; - /// - /// // sign and broadcast ... - /// # Ok::<(), anyhow::Error>(()) - /// ``` - /// - /// [`TxBuilder`]: crate::TxBuilder - pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm, CreateTx> { - TxBuilder { - wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), - params: TxParams::default(), - coin_selection: DefaultCoinSelectionAlgorithm::default(), - phantom: core::marker::PhantomData, - } - } - - pub(crate) fn create_tx( - &mut self, - coin_selection: Cs, - params: TxParams, - ) -> Result { - let keychains: BTreeMap<_, _> = self.indexed_graph.index.keychains().collect(); - let external_descriptor = keychains.get(&KeychainKind::External).expect("must exist"); - let internal_descriptor = keychains.get(&KeychainKind::Internal); - - let external_policy = external_descriptor - .extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)? - .unwrap(); - let internal_policy = internal_descriptor - .as_ref() - .map(|desc| { - Ok::<_, CreateTxError>( - desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? - .unwrap(), - ) - }) - .transpose()?; - - // The policy allows spending external outputs, but it requires a policy path that hasn't been - // provided - if params.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange - && external_policy.requires_path() - && params.external_policy_path.is_none() - { - return Err(CreateTxError::SpendingPolicyRequired( - KeychainKind::External, - )); - }; - // Same for the internal_policy path, if present - if let Some(internal_policy) = &internal_policy { - if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden - && internal_policy.requires_path() - && params.internal_policy_path.is_none() - { - return Err(CreateTxError::SpendingPolicyRequired( - KeychainKind::Internal, - )); - }; - } - - let external_requirements = external_policy.get_condition( - params - .external_policy_path - .as_ref() - .unwrap_or(&BTreeMap::new()), - )?; - let internal_requirements = internal_policy - .map(|policy| { - Ok::<_, CreateTxError>( - policy.get_condition( - params - .internal_policy_path - .as_ref() - .unwrap_or(&BTreeMap::new()), - )?, - ) - }) - .transpose()?; - - let requirements = - external_requirements.merge(&internal_requirements.unwrap_or_default())?; - - let version = match params.version { - Some(tx_builder::Version(0)) => return Err(CreateTxError::Version0), - Some(tx_builder::Version(1)) if requirements.csv.is_some() => { - return Err(CreateTxError::Version1Csv) - } - Some(tx_builder::Version(x)) => x, - None if requirements.csv.is_some() => 2, - None => 1, - }; - - // We use a match here instead of a unwrap_or_else as it's way more readable :) - let current_height = match params.current_height { - // If they didn't tell us the current height, we assume it's the latest sync height. - None => { - let tip_height = self.chain.tip().height(); - absolute::LockTime::from_height(tip_height).expect("invalid height") - } - Some(h) => h, - }; - - let lock_time = match params.locktime { - // When no nLockTime is specified, we try to prevent fee sniping, if possible - None => { - // Fee sniping can be partially prevented by setting the timelock - // to current_height. If we don't know the current_height, - // we default to 0. - let fee_sniping_height = current_height; - - // We choose the biggest between the required nlocktime and the fee sniping - // height - match requirements.timelock { - // No requirement, just use the fee_sniping_height - None => fee_sniping_height, - // There's a block-based requirement, but the value is lower than the fee_sniping_height - Some(value @ absolute::LockTime::Blocks(_)) if value < fee_sniping_height => { - fee_sniping_height - } - // There's a time-based requirement or a block-based requirement greater - // than the fee_sniping_height use that value - Some(value) => value, - } - } - // Specific nLockTime required and we have no constraints, so just set to that value - Some(x) if requirements.timelock.is_none() => x, - // Specific nLockTime required and it's compatible with the constraints - Some(x) - if requirements.timelock.unwrap().is_same_unit(x) - && x >= requirements.timelock.unwrap() => - { - x - } - // Invalid nLockTime required - Some(x) => { - return Err(CreateTxError::LockTime { - requested: x, - required: requirements.timelock.unwrap(), - }) - } - }; - - // The nSequence to be by default for inputs unless an explicit sequence is specified. - let n_sequence = match (params.rbf, requirements.csv) { - // No RBF or CSV but there's an nLockTime, so the nSequence cannot be final - (None, None) if lock_time != absolute::LockTime::ZERO => { - Sequence::ENABLE_LOCKTIME_NO_RBF - } - // No RBF, CSV or nLockTime, make the transaction final - (None, None) => Sequence::MAX, - - // No RBF requested, use the value from CSV. Note that this value is by definition - // non-final, so even if a timelock is enabled this nSequence is fine, hence why we - // don't bother checking for it here. The same is true for all the other branches below - (None, Some(csv)) => csv, - - // RBF with a specific value but that value is too high - (Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => { - return Err(CreateTxError::RbfSequence) - } - // RBF with a specific value requested, but the value is incompatible with CSV - (Some(tx_builder::RbfValue::Value(rbf)), Some(csv)) - if !check_nsequence_rbf(rbf, csv) => - { - return Err(CreateTxError::RbfSequenceCsv { rbf, csv }) - } - - // RBF enabled with the default value with CSV also enabled. CSV takes precedence - (Some(tx_builder::RbfValue::Default), Some(csv)) => csv, - // Valid RBF, either default or with a specific value. We ignore the `CSV` value - // because we've already checked it before - (Some(rbf), _) => rbf.get_value(), - }; - - let (fee_rate, mut fee_amount) = match params.fee_policy.unwrap_or_default() { - //FIXME: see https://github.com/bitcoindevkit/bdk/issues/256 - FeePolicy::FeeAmount(fee) => { - if let Some(previous_fee) = params.bumping_fee { - if fee < previous_fee.absolute { - return Err(CreateTxError::FeeTooLow { - required: previous_fee.absolute, - }); - } - } - (FeeRate::ZERO, fee) - } - FeePolicy::FeeRate(rate) => { - if let Some(previous_fee) = params.bumping_fee { - let required_feerate = FeeRate::from_sat_per_kwu( - previous_fee.rate.to_sat_per_kwu() - + FeeRate::BROADCAST_MIN.to_sat_per_kwu(), // +1 sat/vb - ); - if rate < required_feerate { - return Err(CreateTxError::FeeRateTooLow { - required: required_feerate, - }); - } - } - (rate, 0) - } - }; - - let mut tx = Transaction { - version: transaction::Version::non_standard(version), - lock_time, - input: vec![], - output: vec![], - }; - - if params.manually_selected_only && params.utxos.is_empty() { - return Err(CreateTxError::NoUtxosSelected); - } - - // we keep it as a float while we accumulate it, and only round it at the end - let mut outgoing: u64 = 0; - let mut received: u64 = 0; - - let recipients = params.recipients.iter().map(|(r, v)| (r, *v)); - - for (index, (script_pubkey, value)) in recipients.enumerate() { - if !params.allow_dust - && value.is_dust(script_pubkey) - && !script_pubkey.is_provably_unspendable() - { - return Err(CreateTxError::OutputBelowDustLimit(index)); - } - - if self.is_mine(script_pubkey) { - received += value; - } - - let new_out = TxOut { - script_pubkey: script_pubkey.clone(), - value: Amount::from_sat(value), - }; - - tx.output.push(new_out); - - outgoing += value; - } - - fee_amount += (fee_rate * tx.weight()).to_sat(); - - if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed - && internal_descriptor.is_none() - { - return Err(CreateTxError::ChangePolicyDescriptor); - } - - let (required_utxos, optional_utxos) = - self.preselect_utxos(¶ms, Some(current_height.to_consensus_u32())); - - // get drain script - let drain_script = match params.drain_to { - Some(ref drain_recipient) => drain_recipient.clone(), - None => { - let change_keychain = self.map_keychain(KeychainKind::Internal); - let ((index, spk), index_changeset) = self - .indexed_graph - .index - .next_unused_spk(&change_keychain) - .expect("Keychain exists (we called map_keychain)"); - let spk = spk.into(); - self.indexed_graph.index.mark_used(change_keychain, index); - self.persist - .stage(ChangeSet::from(indexed_tx_graph::ChangeSet::from( - index_changeset, - ))); - self.persist.commit().map_err(CreateTxError::Persist)?; - spk - } - }; - - let (required_utxos, optional_utxos) = - coin_selection::filter_duplicates(required_utxos, optional_utxos); - - let coin_selection = coin_selection.coin_select( - required_utxos, - optional_utxos, - fee_rate, - outgoing + fee_amount, - &drain_script, - )?; - fee_amount += coin_selection.fee_amount; - let excess = &coin_selection.excess; - - tx.input = coin_selection - .selected - .iter() - .map(|u| bitcoin::TxIn { - previous_output: u.outpoint(), - script_sig: ScriptBuf::default(), - sequence: u.sequence().unwrap_or(n_sequence), - witness: Witness::new(), - }) - .collect(); - - if tx.output.is_empty() { - // Uh oh, our transaction has no outputs. - // We allow this when: - // - We have a drain_to address and the utxos we must spend (this happens, - // for example, when we RBF) - // - We have a drain_to address and drain_wallet set - // Otherwise, we don't know who we should send the funds to, and how much - // we should send! - if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) { - if let NoChange { - dust_threshold, - remaining_amount, - change_fee, - } = excess - { - return Err(CreateTxError::InsufficientFunds { - needed: *dust_threshold, - available: remaining_amount.saturating_sub(*change_fee), - }); - } - } else { - return Err(CreateTxError::NoRecipients); - } - } - - match excess { - NoChange { - remaining_amount, .. - } => fee_amount += remaining_amount, - Change { amount, fee } => { - if self.is_mine(&drain_script) { - received += amount; - } - fee_amount += fee; - - // create drain output - let drain_output = TxOut { - value: Amount::from_sat(*amount), - script_pubkey: drain_script, - }; - - // TODO: We should pay attention when adding a new output: this might increase - // the length of the "number of vouts" parameter by 2 bytes, potentially making - // our feerate too low - tx.output.push(drain_output); - } - }; - - // sort input/outputs according to the chosen algorithm - params.ordering.sort_tx(&mut tx); - - let psbt = self.complete_transaction(tx, coin_selection.selected, params)?; - Ok(psbt) - } - - /// Bump the fee of a transaction previously created with this wallet. - /// - /// Returns an error if the transaction is already confirmed or doesn't explicitly signal - /// *replace by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`] - /// pre-populated with the inputs and outputs of the original transaction. - /// - /// ## Example - /// - /// ```no_run - /// # // TODO: remove norun -- bumping fee seems to need the tx in the wallet database first. - /// # use std::str::FromStr; - /// # use bitcoin::*; - /// # use bdk::*; - /// # use bdk::wallet::ChangeSet; - /// # use bdk::wallet::error::CreateTxError; - /// # use bdk_persist::PersistBackend; - /// # use anyhow::Error; - /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; - /// # let mut wallet = doctest_wallet!(); - /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); - /// let mut psbt = { - /// let mut builder = wallet.build_tx(); - /// builder - /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)) - /// .enable_rbf(); - /// builder.finish()? - /// }; - /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; - /// let tx = psbt.clone().extract_tx().expect("tx"); - /// // broadcast tx but it's taking too long to confirm so we want to bump the fee - /// let mut psbt = { - /// let mut builder = wallet.build_fee_bump(tx.txid())?; - /// builder - /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")); - /// builder.finish()? - /// }; - /// - /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; - /// let fee_bumped_tx = psbt.extract_tx(); - /// // broadcast fee_bumped_tx to replace original - /// # Ok::<(), anyhow::Error>(()) - /// ``` - // TODO: support for merging multiple transactions while bumping the fees - pub fn build_fee_bump( - &mut self, - txid: Txid, - ) -> Result, BuildFeeBumpError> { - let graph = self.indexed_graph.graph(); - let txout_index = &self.indexed_graph.index; - let chain_tip = self.chain.tip().block_id(); - - let mut tx = graph - .get_tx(txid) - .ok_or(BuildFeeBumpError::TransactionNotFound(txid))? - .as_ref() - .clone(); - - let pos = graph - .get_chain_position(&self.chain, chain_tip, txid) - .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?; - if let ChainPosition::Confirmed(_) = pos { - return Err(BuildFeeBumpError::TransactionConfirmed(txid)); - } - - if !tx - .input - .iter() - .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD) - { - return Err(BuildFeeBumpError::IrreplaceableTransaction(tx.txid())); - } - - let fee = self - .calculate_fee(&tx) - .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?; - let fee_rate = self - .calculate_fee_rate(&tx) - .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?; - - // remove the inputs from the tx and process them - let original_txin = tx.input.drain(..).collect::>(); - let original_utxos = original_txin - .iter() - .map(|txin| -> Result<_, BuildFeeBumpError> { - let prev_tx = graph - .get_tx(txin.previous_output.txid) - .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?; - let txout = &prev_tx.output[txin.previous_output.vout as usize]; - - let confirmation_time: ConfirmationTime = graph - .get_chain_position(&self.chain, chain_tip, txin.previous_output.txid) - .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))? - .cloned() - .into(); - - let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) { - Some((keychain, derivation_index)) => { - let satisfaction_weight = self - .get_descriptor_for_keychain(keychain) - .max_weight_to_satisfy() - .unwrap(); - WeightedUtxo { - utxo: Utxo::Local(LocalOutput { - outpoint: txin.previous_output, - txout: txout.clone(), - keychain, - is_spent: true, - derivation_index, - confirmation_time, - }), - satisfaction_weight, - } - } - None => { - let satisfaction_weight = - serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(); - WeightedUtxo { - utxo: Utxo::Foreign { - outpoint: txin.previous_output, - sequence: Some(txin.sequence), - psbt_input: Box::new(psbt::Input { - witness_utxo: Some(txout.clone()), - non_witness_utxo: Some(prev_tx.as_ref().clone()), - ..Default::default() - }), - }, - satisfaction_weight, - } - } - }; - - Ok(weighted_utxo) - }) - .collect::, _>>()?; - - if tx.output.len() > 1 { - let mut change_index = None; - for (index, txout) in tx.output.iter().enumerate() { - let change_type = self.map_keychain(KeychainKind::Internal); - match txout_index.index_of_spk(&txout.script_pubkey) { - Some((keychain, _)) if keychain == change_type => change_index = Some(index), - _ => {} - } - } - - if let Some(change_index) = change_index { - tx.output.remove(change_index); - } - } - - let params = TxParams { - // TODO: figure out what rbf option should be? - version: Some(tx_builder::Version(tx.version.0)), - recipients: tx - .output - .into_iter() - .map(|txout| (txout.script_pubkey, txout.value.to_sat())) - .collect(), - utxos: original_utxos, - bumping_fee: Some(tx_builder::PreviousFee { - absolute: fee, - rate: fee_rate, - }), - ..Default::default() - }; - - Ok(TxBuilder { - wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), - params, - coin_selection: DefaultCoinSelectionAlgorithm::default(), - phantom: core::marker::PhantomData, - }) - } - - /// Sign a transaction with all the wallet's signers, in the order specified by every signer's - /// [`SignerOrdering`]. This function returns the `Result` type with an encapsulated `bool` that has the value true if the PSBT was finalized, or false otherwise. - /// - /// 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 - /// - /// ``` - /// # use std::str::FromStr; - /// # use bitcoin::*; - /// # use bdk::*; - /// # use bdk::wallet::ChangeSet; - /// # use bdk::wallet::error::CreateTxError; - /// # use bdk_persist::PersistBackend; - /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; - /// # let mut wallet = doctest_wallet!(); - /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); - /// let mut psbt = { - /// let mut builder = wallet.build_tx(); - /// builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); - /// builder.finish()? - /// }; - /// let finalized = wallet.sign(&mut psbt, SignOptions::default())?; - /// assert!(finalized, "we should have signed all the inputs"); - /// # Ok::<(),anyhow::Error>(()) - pub fn sign(&self, psbt: &mut Psbt, sign_options: SignOptions) -> Result { - // This adds all the PSBT metadata for the inputs, which will help us later figure out how - // to derive our keys - self.update_psbt_with_descriptor(psbt) - .map_err(SignerError::MiniscriptPsbt)?; - - // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones) - // 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()) - .filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none()) - .any(|i| i.non_witness_utxo.is_none()) - { - return Err(SignerError::MissingNonWitnessUtxo); - } - - // If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input - // is using `SIGHASH_ALL` or `SIGHASH_DEFAULT` for taproot - if !sign_options.allow_all_sighashes - && !psbt.inputs.iter().all(|i| { - i.sighash_type.is_none() - || i.sighash_type == Some(EcdsaSighashType::All.into()) - || i.sighash_type == Some(TapSighashType::All.into()) - || i.sighash_type == Some(TapSighashType::Default.into()) - }) - { - return Err(SignerError::NonStandardSighash); - } - - for signer in self - .signers - .signers() - .iter() - .chain(self.change_signers.signers().iter()) - { - signer.sign_transaction(psbt, &sign_options, &self.secp)?; - } - - // attempt to finalize - if sign_options.try_finalize { - self.finalize_psbt(psbt, sign_options) - } else { - Ok(false) - } - } - - /// Return the spending policies for the wallet's descriptor - pub fn policies(&self, keychain: KeychainKind) -> Result, DescriptorError> { - let signers = match keychain { - KeychainKind::External => &self.signers, - KeychainKind::Internal => &self.change_signers, - }; - - match self.public_descriptor(keychain) { - Some(desc) => Ok(desc.extract_policy(signers, BuildSatisfaction::None, &self.secp)?), - None => Ok(None), - } - } - - /// Return the "public" version of the wallet's descriptor, meaning a new descriptor that has - /// the same structure but with every secret key removed - /// - /// This can be used to build a watch-only version of a wallet - pub fn public_descriptor(&self, keychain: KeychainKind) -> Option<&ExtendedDescriptor> { - self.indexed_graph - .index - .keychains() - .find(|(k, _)| *k == &keychain) - .map(|(_, d)| d) - } - - /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass - /// validation and construct the respective `scriptSig` or `scriptWitness`. Please refer to - /// [BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Input_Finalizer) - /// for further information. - /// - /// Returns `true` if the PSBT could be finalized, and `false` otherwise. - /// - /// The [`SignOptions`] can be used to tweak the behavior of the finalizer. - pub fn finalize_psbt( - &self, - psbt: &mut Psbt, - sign_options: SignOptions, - ) -> Result { - let chain_tip = self.chain.tip().block_id(); - - let tx = &psbt.unsigned_tx; - let mut finished = true; - - for (n, input) in tx.input.iter().enumerate() { - let psbt_input = &psbt - .inputs - .get(n) - .ok_or(SignerError::InputIndexOutOfRange)?; - if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() { - continue; - } - let confirmation_height = self - .indexed_graph - .graph() - .get_chain_position(&self.chain, chain_tip, input.previous_output.txid) - .map(|chain_position| match chain_position { - ChainPosition::Confirmed(a) => a.confirmation_height, - ChainPosition::Unconfirmed(_) => u32::MAX, - }); - let current_height = sign_options - .assume_height - .unwrap_or_else(|| self.chain.tip().height()); - - // - Try to derive the descriptor by looking at the txout. If it's in our database, we - // know exactly which `keychain` to use, and which derivation index it is - // - If that fails, try to derive it by looking at the psbt input: the complete logic - // is in `src/descriptor/mod.rs`, but it will basically look at `bip32_derivation`, - // `redeem_script` and `witness_script` to determine the right derivation - // - If that also fails, it will try it on the internal descriptor, if present - let desc = psbt - .get_utxo_for(n) - .and_then(|txout| self.get_descriptor_for_txout(&txout)) - .or_else(|| { - self.indexed_graph.index.keychains().find_map(|(_, desc)| { - desc.derive_from_psbt_input(psbt_input, psbt.get_utxo_for(n), &self.secp) - }) - }); - - match desc { - Some(desc) => { - let mut tmp_input = bitcoin::TxIn::default(); - match desc.satisfy( - &mut tmp_input, - ( - PsbtInputSatisfier::new(psbt, n), - After::new(Some(current_height), false), - Older::new(Some(current_height), confirmation_height, false), - ), - ) { - Ok(_) => { - let psbt_input = &mut psbt.inputs[n]; - psbt_input.final_script_sig = Some(tmp_input.script_sig); - psbt_input.final_script_witness = Some(tmp_input.witness); - if sign_options.remove_partial_sigs { - psbt_input.partial_sigs.clear(); - } - if sign_options.remove_taproot_extras { - // We just constructed the final witness, clear these fields. - psbt_input.tap_key_sig = None; - psbt_input.tap_script_sigs.clear(); - psbt_input.tap_scripts.clear(); - psbt_input.tap_key_origins.clear(); - psbt_input.tap_internal_key = None; - psbt_input.tap_merkle_root = None; - } - } - Err(_) => finished = false, - } - } - None => finished = false, - } - } - - if finished && sign_options.remove_taproot_extras { - for output in &mut psbt.outputs { - output.tap_key_origins.clear(); - } - } - - Ok(finished) - } - - /// Return the secp256k1 context used for all signing operations - pub fn secp_ctx(&self) -> &SecpCtx { - &self.secp - } - - /// Returns the descriptor used to create addresses for a particular `keychain`. - pub fn get_descriptor_for_keychain(&self, keychain: KeychainKind) -> &ExtendedDescriptor { - self.public_descriptor(self.map_keychain(keychain)) - .expect("we mapped it to external if it doesn't exist") - } - - /// The derivation index of this wallet. It will return `None` if it has not derived any addresses. - /// Otherwise, it will return the index of the highest address it has derived. - pub fn derivation_index(&self, keychain: KeychainKind) -> Option { - self.indexed_graph.index.last_revealed_index(&keychain) - } - - /// The index of the next address that you would get if you were to ask the wallet for a new address - pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 { - let keychain = self.map_keychain(keychain); - self.indexed_graph - .index - .next_index(&keychain) - .expect("Keychain must exist (we called map_keychain)") - .0 - } - - /// Informs the wallet that you no longer intend to broadcast a tx that was built from it. - /// - /// This frees up the change address used when creating the tx for use in future transactions. - // TODO: Make this free up reserved utxos when that's implemented - pub fn cancel_tx(&mut self, tx: &Transaction) { - let txout_index = &mut self.indexed_graph.index; - for txout in &tx.output { - if let Some((keychain, index)) = txout_index.index_of_spk(&txout.script_pubkey) { - // NOTE: unmark_used will **not** make something unused if it has actually been used - // by a tx in the tracker. It only removes the superficial marking. - txout_index.unmark_used(keychain, index); - } - } - } - - fn map_keychain(&self, keychain: KeychainKind) -> KeychainKind { - if keychain == KeychainKind::Internal - && self.public_descriptor(KeychainKind::Internal).is_none() - { - KeychainKind::External - } else { - keychain - } - } - - fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { - let (keychain, child) = self - .indexed_graph - .index - .index_of_spk(&txout.script_pubkey)?; - let descriptor = self.get_descriptor_for_keychain(keychain); - descriptor.at_derivation_index(child).ok() - } - - fn get_available_utxos(&self) -> Vec<(LocalOutput, usize)> { - self.list_unspent() - .map(|utxo| { - let keychain = utxo.keychain; - (utxo, { - self.get_descriptor_for_keychain(keychain) - .max_weight_to_satisfy() - .unwrap() - }) - }) - .collect() - } - - /// Given the options returns the list of utxos that must be used to form the - /// transaction and any further that may be used if needed. - fn preselect_utxos( - &self, - params: &TxParams, - current_height: Option, - ) -> (Vec, Vec) { - let TxParams { - change_policy, - unspendable, - utxos, - drain_wallet, - manually_selected_only, - bumping_fee, - .. - } = params; - - let manually_selected = utxos.clone(); - // we mandate confirmed transactions if we're bumping the fee - let must_only_use_confirmed_tx = bumping_fee.is_some(); - let must_use_all_available = *drain_wallet; - - let chain_tip = self.chain.tip().block_id(); - // must_spend <- manually selected utxos - // may_spend <- all other available utxos - let mut may_spend = self.get_available_utxos(); - - may_spend.retain(|may_spend| { - !manually_selected - .iter() - .any(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint) - }); - let mut must_spend = manually_selected; - - // NOTE: we are intentionally ignoring `unspendable` here. i.e manual - // selection overrides unspendable. - if *manually_selected_only { - return (must_spend, vec![]); - } - - let satisfies_confirmed = may_spend - .iter() - .map(|u| -> bool { - let txid = u.0.outpoint.txid; - let tx = match self.indexed_graph.graph().get_tx(txid) { - Some(tx) => tx, - None => return false, - }; - let confirmation_time: ConfirmationTime = match self - .indexed_graph - .graph() - .get_chain_position(&self.chain, chain_tip, txid) - { - Some(chain_position) => chain_position.cloned().into(), - None => return false, - }; - - // Whether the UTXO is mature and, if needed, confirmed - let mut spendable = true; - if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() { - return false; - } - if tx.is_coinbase() { - debug_assert!( - confirmation_time.is_confirmed(), - "coinbase must always be confirmed" - ); - if let Some(current_height) = current_height { - match confirmation_time { - ConfirmationTime::Confirmed { height, .. } => { - // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375 - spendable &= - (current_height.saturating_sub(height)) >= COINBASE_MATURITY; - } - ConfirmationTime::Unconfirmed { .. } => spendable = false, - } - } - } - spendable - }) - .collect::>(); - - let mut i = 0; - may_spend.retain(|u| { - let retain = change_policy.is_satisfied_by(&u.0) - && !unspendable.contains(&u.0.outpoint) - && satisfies_confirmed[i]; - i += 1; - retain - }); - - let mut may_spend = may_spend - .into_iter() - .map(|(local_utxo, satisfaction_weight)| WeightedUtxo { - satisfaction_weight, - utxo: Utxo::Local(local_utxo), - }) - .collect(); - - if must_use_all_available { - must_spend.append(&mut may_spend); - } - - (must_spend, may_spend) - } - - fn complete_transaction( - &self, - tx: Transaction, - selected: Vec, - params: TxParams, - ) -> Result { - let mut psbt = Psbt::from_unsigned_tx(tx)?; - - if params.add_global_xpubs { - let all_xpubs = self - .keychains() - .flat_map(|(_, desc)| desc.get_extended_keys()) - .collect::>(); - - for xpub in all_xpubs { - let origin = match xpub.origin { - Some(origin) => origin, - None if xpub.xkey.depth == 0 => { - (xpub.root_fingerprint(&self.secp), vec![].into()) - } - _ => return Err(CreateTxError::MissingKeyOrigin(xpub.xkey.to_string())), - }; - - psbt.xpub.insert(xpub.xkey, origin); - } - } - - let mut lookup_output = selected - .into_iter() - .map(|utxo| (utxo.outpoint(), utxo)) - .collect::>(); - - // add metadata for the inputs - for (psbt_input, input) in psbt.inputs.iter_mut().zip(psbt.unsigned_tx.input.iter()) { - let utxo = match lookup_output.remove(&input.previous_output) { - Some(utxo) => utxo, - None => continue, - }; - - match utxo { - Utxo::Local(utxo) => { - *psbt_input = - match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) { - Ok(psbt_input) => psbt_input, - Err(e) => match e { - CreateTxError::UnknownUtxo => psbt::Input { - sighash_type: params.sighash, - ..psbt::Input::default() - }, - _ => return Err(e), - }, - } - } - Utxo::Foreign { - outpoint, - psbt_input: foreign_psbt_input, - .. - } => { - let is_taproot = foreign_psbt_input - .witness_utxo - .as_ref() - .map(|txout| txout.script_pubkey.is_p2tr()) - .unwrap_or(false); - if !is_taproot - && !params.only_witness_utxo - && foreign_psbt_input.non_witness_utxo.is_none() - { - return Err(CreateTxError::MissingNonWitnessUtxo(outpoint)); - } - *psbt_input = *foreign_psbt_input; - } - } - } - - self.update_psbt_with_descriptor(&mut psbt)?; - - Ok(psbt) - } - - /// get the corresponding PSBT Input for a LocalUtxo - pub fn get_psbt_input( - &self, - utxo: LocalOutput, - sighash_type: Option, - only_witness_utxo: bool, - ) -> Result { - // Try to find the prev_script in our db to figure out if this is internal or external, - // and the derivation index - let (keychain, child) = self - .indexed_graph - .index - .index_of_spk(&utxo.txout.script_pubkey) - .ok_or(CreateTxError::UnknownUtxo)?; - - let mut psbt_input = psbt::Input { - sighash_type, - ..psbt::Input::default() - }; - - let desc = self.get_descriptor_for_keychain(keychain); - let derived_descriptor = desc - .at_derivation_index(child) - .expect("child can't be hardened"); - - psbt_input - .update_with_descriptor_unchecked(&derived_descriptor) - .map_err(MiniscriptPsbtError::Conversion)?; - - let prev_output = utxo.outpoint; - if let Some(prev_tx) = self.indexed_graph.graph().get_tx(prev_output.txid) { - if desc.is_witness() || desc.is_taproot() { - psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); - } - if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) { - psbt_input.non_witness_utxo = Some(prev_tx.as_ref().clone()); - } - } - Ok(psbt_input) - } - - fn update_psbt_with_descriptor(&self, psbt: &mut Psbt) -> Result<(), MiniscriptPsbtError> { - // We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all - // the input utxos and outputs - let utxos = (0..psbt.inputs.len()) - .filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo))) - .chain( - psbt.unsigned_tx - .output - .iter() - .enumerate() - .map(|(i, out)| (false, i, out.clone())), - ) - .collect::>(); - - // Try to figure out the keychain and derivation for every input and output - for (is_input, index, out) in utxos.into_iter() { - if let Some((keychain, child)) = - self.indexed_graph.index.index_of_spk(&out.script_pubkey) - { - let desc = self.get_descriptor_for_keychain(keychain); - let desc = desc - .at_derivation_index(child) - .expect("child can't be hardened"); - - if is_input { - psbt.update_input_with_descriptor(index, &desc) - .map_err(MiniscriptPsbtError::UtxoUpdate)?; - } else { - psbt.update_output_with_descriptor(index, &desc) - .map_err(MiniscriptPsbtError::OutputUpdate)?; - } - } - } - - Ok(()) - } - - /// Return the checksum of the public descriptor associated to `keychain` - /// - /// Internally calls [`Self::get_descriptor_for_keychain`] to fetch the right descriptor - pub fn descriptor_checksum(&self, keychain: KeychainKind) -> String { - self.get_descriptor_for_keychain(keychain) - .to_string() - .split_once('#') - .unwrap() - .1 - .to_string() - } - - /// Applies an update to the wallet and stages the changes (but does not [`commit`] them). - /// - /// Usually you create an `update` by interacting with some blockchain data source and inserting - /// transactions related to your wallet into it. - /// - /// [`commit`]: Self::commit - pub fn apply_update(&mut self, update: impl Into) -> Result<(), CannotConnectError> { - let update = update.into(); - let mut changeset = match update.chain { - Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?), - None => ChangeSet::default(), - }; - - let (_, index_changeset) = self - .indexed_graph - .index - .reveal_to_target_multi(&update.last_active_indices); - changeset.append(ChangeSet::from(indexed_tx_graph::ChangeSet::from( - index_changeset, - ))); - changeset.append(ChangeSet::from( - self.indexed_graph.apply_update(update.graph), - )); - self.persist.stage(changeset); - Ok(()) - } - - /// Commits all currently [`staged`] changed to the persistence backend returning and error when - /// this fails. - /// - /// This returns whether the `update` resulted in any changes. - /// - /// [`staged`]: Self::staged - pub fn commit(&mut self) -> anyhow::Result { - self.persist.commit().map(|c| c.is_some()) - } - - /// Returns the changes that will be committed with the next call to [`commit`]. - /// - /// [`commit`]: Self::commit - pub fn staged(&self) -> &ChangeSet { - self.persist.staged() - } - - /// Get a reference to the inner [`TxGraph`]. - pub fn tx_graph(&self) -> &TxGraph { - self.indexed_graph.graph() - } - - /// Get a reference to the inner [`KeychainTxOutIndex`]. - pub fn spk_index(&self) -> &KeychainTxOutIndex { - &self.indexed_graph.index - } - - /// Get a reference to the inner [`LocalChain`]. - pub fn local_chain(&self) -> &LocalChain { - &self.chain - } - - /// Introduces a `block` of `height` to the wallet, and tries to connect it to the - /// `prev_blockhash` of the block's header. - /// - /// This is a convenience method that is equivalent to calling [`apply_block_connected_to`] - /// with `prev_blockhash` and `height-1` as the `connected_to` parameter. - /// - /// [`apply_block_connected_to`]: Self::apply_block_connected_to - pub fn apply_block(&mut self, block: &Block, height: u32) -> Result<(), CannotConnectError> { - let connected_to = match height.checked_sub(1) { - Some(prev_height) => BlockId { - height: prev_height, - hash: block.header.prev_blockhash, - }, - None => BlockId { - height, - hash: block.block_hash(), - }, - }; - self.apply_block_connected_to(block, height, connected_to) - .map_err(|err| match err { - ApplyHeaderError::InconsistentBlocks => { - unreachable!("connected_to is derived from the block so must be consistent") - } - ApplyHeaderError::CannotConnect(err) => err, - }) - } - - /// Applies relevant transactions from `block` of `height` to the wallet, and connects the - /// block to the internal chain. - /// - /// The `connected_to` parameter informs the wallet how this block connects to the internal - /// [`LocalChain`]. Relevant transactions are filtered from the `block` and inserted into the - /// internal [`TxGraph`]. - pub fn apply_block_connected_to( - &mut self, - block: &Block, - height: u32, - connected_to: BlockId, - ) -> Result<(), ApplyHeaderError> { - let mut changeset = ChangeSet::default(); - changeset.append( - self.chain - .apply_header_connected_to(&block.header, height, connected_to)? - .into(), - ); - changeset.append( - self.indexed_graph - .apply_block_relevant(block, height) - .into(), - ); - self.persist.stage(changeset); - Ok(()) - } - - /// Apply relevant unconfirmed transactions to the wallet. - /// - /// Transactions that are not relevant are filtered out. - /// - /// This method takes in an iterator of `(tx, last_seen)` where `last_seen` is the timestamp of - /// when the transaction was last seen in the mempool. This is used for conflict resolution - /// when there is conflicting unconfirmed transactions. The transaction with the later - /// `last_seen` is prioritized. - pub fn apply_unconfirmed_txs<'t>( - &mut self, - unconfirmed_txs: impl IntoIterator, - ) { - let indexed_graph_changeset = self - .indexed_graph - .batch_insert_relevant_unconfirmed(unconfirmed_txs); - self.persist.stage(ChangeSet::from(indexed_graph_changeset)); - } -} - -/// Methods to construct sync/full-scan requests for spk-based chain sources. -impl Wallet { - /// Create a partial [`SyncRequest`] for this wallet for all revealed spks. - /// - /// This is the first step when performing a spk-based wallet partial sync, the returned - /// [`SyncRequest`] collects all revealed script pubkeys from the wallet keychain needed to - /// start a blockchain sync with a spk based blockchain client. - pub fn start_sync_with_revealed_spks(&self) -> SyncRequest { - SyncRequest::from_chain_tip(self.chain.tip()) - .cache_graph_txs(self.tx_graph()) - .populate_with_revealed_spks(&self.indexed_graph.index, ..) - } - - /// Create a [`FullScanRequest] for this wallet. - /// - /// This is the first step when performing a spk-based wallet full scan, the returned - /// [`FullScanRequest] collects iterators for the wallet's keychain script pub keys needed to - /// start a blockchain full scan with a spk based blockchain client. - /// - /// This operation is generally only used when importing or restoring a previously used wallet - /// in which the list of used scripts is not known. - pub fn start_full_scan(&self) -> FullScanRequest { - FullScanRequest::from_keychain_txout_index(self.chain.tip(), &self.indexed_graph.index) - .cache_graph_txs(self.tx_graph()) - } -} - -impl AsRef> for Wallet { - fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { - self.indexed_graph.graph() - } -} - -/// Deterministically generate a unique name given the descriptors defining the wallet -/// -/// Compatible with [`wallet_name_from_descriptor`] -pub fn wallet_name_from_descriptor( - descriptor: T, - change_descriptor: Option, - network: Network, - secp: &SecpCtx, -) -> Result -where - T: IntoWalletDescriptor, -{ - //TODO check descriptors contains only public keys - let descriptor = descriptor - .into_wallet_descriptor(secp, network)? - .0 - .to_string(); - let mut wallet_name = calc_checksum(&descriptor[..descriptor.find('#').unwrap()])?; - if let Some(change_descriptor) = change_descriptor { - let change_descriptor = change_descriptor - .into_wallet_descriptor(secp, network)? - .0 - .to_string(); - wallet_name.push_str( - calc_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(), - ); - } - - Ok(wallet_name) -} - -fn new_local_utxo( - keychain: KeychainKind, - derivation_index: u32, - full_txo: FullTxOut, -) -> LocalOutput { - LocalOutput { - outpoint: full_txo.outpoint, - txout: full_txo.txout, - is_spent: full_txo.spent_by.is_some(), - confirmation_time: full_txo.chain_position.into(), - keychain, - derivation_index, - } -} - -fn create_signers( - index: &mut KeychainTxOutIndex, - secp: &Secp256k1, - descriptor: E, - change_descriptor: Option, - network: Network, -) -> Result<(Arc, Arc), crate::descriptor::error::Error> { - let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?; - let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp)); - let _ = index.insert_descriptor(KeychainKind::External, descriptor); - - let change_signers = match change_descriptor { - Some(descriptor) => { - let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?; - let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp)); - let _ = index.insert_descriptor(KeychainKind::Internal, descriptor); - signers - } - None => Arc::new(SignersContainer::new()), - }; - - Ok((signers, change_signers)) -} - -/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb. -#[macro_export] -#[doc(hidden)] -macro_rules! floating_rate { - ($rate:expr) => {{ - use $crate::bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; - // sat_kwu / 250.0 -> sat_vb - $rate.to_sat_per_kwu() as f64 / ((1000 / WITNESS_SCALE_FACTOR) as f64) - }}; -} - -#[macro_export] -#[doc(hidden)] -/// Macro for getting a wallet for use in a doctest -macro_rules! doctest_wallet { - () => {{ - use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash}; - use $crate::chain::{ConfirmationTime, BlockId}; - use $crate::{KeychainKind, wallet::Wallet}; - let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; - let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)"; - - let mut wallet = Wallet::new_no_persist( - descriptor, - Some(change_descriptor), - Network::Regtest, - ) - .unwrap(); - let address = wallet.peek_address(KeychainKind::External, 0).address; - let tx = Transaction { - version: transaction::Version::ONE, - lock_time: absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { - value: Amount::from_sat(500_000), - script_pubkey: address.script_pubkey(), - }], - }; - let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() }); - let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed { - height: 500, - time: 50_000 - }); - - wallet - }} -} diff --git a/crates/bdk/src/wallet/signer.rs b/crates/bdk/src/wallet/signer.rs deleted file mode 100644 index 4610657e..00000000 --- a/crates/bdk/src/wallet/signer.rs +++ /dev/null @@ -1,1193 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Generalized signers -//! -//! This module provides the ability to add customized signers to a [`Wallet`](super::Wallet) -//! through the [`Wallet::add_signer`](super::Wallet::add_signer) function. -//! -//! ``` -//! # use alloc::sync::Arc; -//! # use core::str::FromStr; -//! # use bitcoin::secp256k1::{Secp256k1, All}; -//! # use bitcoin::*; -//! # use bdk::signer::*; -//! # use bdk::*; -//! # #[derive(Debug)] -//! # struct CustomHSM; -//! # impl CustomHSM { -//! # fn hsm_sign_input(&self, _psbt: &mut Psbt, _input: usize) -> Result<(), SignerError> { -//! # Ok(()) -//! # } -//! # fn connect() -> Self { -//! # CustomHSM -//! # } -//! # fn get_id(&self) -> SignerId { -//! # SignerId::Dummy(0) -//! # } -//! # } -//! #[derive(Debug)] -//! struct CustomSigner { -//! device: CustomHSM, -//! } -//! -//! impl CustomSigner { -//! fn connect() -> Self { -//! CustomSigner { device: CustomHSM::connect() } -//! } -//! } -//! -//! impl SignerCommon for CustomSigner { -//! fn id(&self, _secp: &Secp256k1) -> SignerId { -//! self.device.get_id() -//! } -//! } -//! -//! impl InputSigner for CustomSigner { -//! fn sign_input( -//! &self, -//! psbt: &mut Psbt, -//! input_index: usize, -//! _sign_options: &SignOptions, -//! _secp: &Secp256k1, -//! ) -> Result<(), SignerError> { -//! self.device.hsm_sign_input(psbt, input_index)?; -//! -//! Ok(()) -//! } -//! } -//! -//! let custom_signer = CustomSigner::connect(); -//! -//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; -//! let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet)?; -//! wallet.add_signer( -//! KeychainKind::External, -//! SignerOrdering(200), -//! Arc::new(custom_signer) -//! ); -//! -//! # Ok::<_, anyhow::Error>(()) -//! ``` - -use crate::collections::BTreeMap; -use alloc::string::String; -use alloc::sync::Arc; -use alloc::vec::Vec; -use core::cmp::Ordering; -use core::fmt; -use core::ops::{Bound::Included, Deref}; - -use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, Xpriv}; -use bitcoin::hashes::hash160; -use bitcoin::secp256k1::Message; -use bitcoin::sighash::{EcdsaSighashType, TapSighash, TapSighashType}; -use bitcoin::{ecdsa, psbt, sighash, taproot}; -use bitcoin::{key::TapTweak, key::XOnlyPublicKey, secp256k1}; -use bitcoin::{PrivateKey, Psbt, PublicKey}; - -use miniscript::descriptor::{ - Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, - InnerXKey, KeyMap, SinglePriv, SinglePubKey, -}; -use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey}; - -use super::utils::SecpCtx; -use crate::descriptor::{DescriptorMeta, XKeyUtils}; -use crate::psbt::PsbtUtils; -use crate::wallet::error::MiniscriptPsbtError; - -/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among -/// multiple of them -#[derive(Debug, Clone, Ord, PartialOrd, PartialEq, Eq, Hash)] -pub enum SignerId { - /// Bitcoin HASH160 (RIPEMD160 after SHA256) hash of an ECDSA public key - PkHash(hash160::Hash), - /// The fingerprint of a BIP32 extended key - Fingerprint(Fingerprint), - /// Dummy identifier - Dummy(u64), -} - -impl From for SignerId { - fn from(hash: hash160::Hash) -> SignerId { - SignerId::PkHash(hash) - } -} - -impl From for SignerId { - fn from(fing: Fingerprint) -> SignerId { - SignerId::Fingerprint(fing) - } -} - -/// Signing error -#[derive(Debug)] -pub enum SignerError { - /// The private key is missing for the required public key - MissingKey, - /// The private key in use has the right fingerprint but derives differently than expected - InvalidKey, - /// The user canceled the operation - UserCanceled, - /// Input index is out of range - InputIndexOutOfRange, - /// The `non_witness_utxo` field of the transaction is required to sign this input - MissingNonWitnessUtxo, - /// The `non_witness_utxo` specified is invalid - InvalidNonWitnessUtxo, - /// The `witness_utxo` field of the transaction is required to sign this input - MissingWitnessUtxo, - /// The `witness_script` field of the transaction is required to sign this input - MissingWitnessScript, - /// The fingerprint and derivation path are missing from the psbt input - MissingHdKeypath, - /// The psbt contains a non-`SIGHASH_ALL` sighash in one of its input and the user hasn't - /// explicitly allowed them - /// - /// To enable signing transactions with non-standard sighashes set - /// [`SignOptions::allow_all_sighashes`] to `true`. - NonStandardSighash, - /// Invalid SIGHASH for the signing context in use - InvalidSighash, - /// Error while computing the hash to sign - SighashError(sighash::Error), - /// Miniscript PSBT error - MiniscriptPsbt(MiniscriptPsbtError), - /// To be used only by external libraries implementing [`InputSigner`] or - /// [`TransactionSigner`], so that they can return their own custom errors, without having to - /// modify [`SignerError`] in BDK. - External(String), -} - -impl From for SignerError { - fn from(e: sighash::Error) -> Self { - SignerError::SighashError(e) - } -} - -impl fmt::Display for SignerError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::MissingKey => write!(f, "Missing private key"), - Self::InvalidKey => write!(f, "The private key in use has the right fingerprint but derives differently than expected"), - Self::UserCanceled => write!(f, "The user canceled the operation"), - Self::InputIndexOutOfRange => write!(f, "Input index out of range"), - Self::MissingNonWitnessUtxo => write!(f, "Missing non-witness UTXO"), - Self::InvalidNonWitnessUtxo => write!(f, "Invalid non-witness UTXO"), - Self::MissingWitnessUtxo => write!(f, "Missing witness UTXO"), - Self::MissingWitnessScript => write!(f, "Missing witness script"), - Self::MissingHdKeypath => write!(f, "Missing fingerprint and derivation path"), - Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"), - Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"), - Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err), - Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err), - Self::External(err) => write!(f, "{}", err), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for SignerError {} - -/// Signing context -/// -/// Used by our software signers to determine the type of signatures to make -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SignerContext { - /// Legacy context - Legacy, - /// Segwit v0 context (BIP 143) - Segwitv0, - /// Taproot context (BIP 340) - Tap { - /// Whether the signer can sign for the internal key or not - is_internal_key: bool, - }, -} - -/// Wrapper to pair a signer with its context -#[derive(Debug, Clone)] -pub struct SignerWrapper { - signer: S, - ctx: SignerContext, -} - -impl SignerWrapper { - /// Create a wrapped signer from a signer and a context - pub fn new(signer: S, ctx: SignerContext) -> Self { - SignerWrapper { signer, ctx } - } -} - -impl Deref for SignerWrapper { - type Target = S; - - fn deref(&self) -> &Self::Target { - &self.signer - } -} - -/// Common signer methods -pub trait SignerCommon: fmt::Debug + Send + Sync { - /// Return the [`SignerId`] for this signer - /// - /// The [`SignerId`] can be used to lookup a signer in the [`Wallet`](crate::Wallet)'s signers map or to - /// compare two signers. - fn id(&self, secp: &SecpCtx) -> SignerId; - - /// Return the secret key for the signer - /// - /// This is used internally to reconstruct the original descriptor that may contain secrets. - /// External signers that are meant to keep key isolated should just return `None` here (which - /// is the default for this method, if not overridden). - fn descriptor_secret_key(&self) -> Option { - None - } -} - -/// PSBT Input signer -/// -/// This trait can be implemented to provide custom signers to the wallet. If the signer supports signing -/// individual inputs, this trait should be implemented and BDK will provide automatically an implementation -/// for [`TransactionSigner`]. -pub trait InputSigner: SignerCommon { - /// Sign a single psbt input - fn sign_input( - &self, - psbt: &mut Psbt, - input_index: usize, - sign_options: &SignOptions, - secp: &SecpCtx, - ) -> Result<(), SignerError>; -} - -/// PSBT signer -/// -/// This trait can be implemented when the signer can't sign inputs individually, but signs the whole transaction -/// at once. -pub trait TransactionSigner: SignerCommon { - /// Sign all the inputs of the psbt - fn sign_transaction( - &self, - psbt: &mut Psbt, - sign_options: &SignOptions, - secp: &SecpCtx, - ) -> Result<(), SignerError>; -} - -impl TransactionSigner for T { - fn sign_transaction( - &self, - psbt: &mut Psbt, - sign_options: &SignOptions, - secp: &SecpCtx, - ) -> Result<(), SignerError> { - for input_index in 0..psbt.inputs.len() { - self.sign_input(psbt, input_index, sign_options, secp)?; - } - - Ok(()) - } -} - -impl SignerCommon for SignerWrapper> { - fn id(&self, secp: &SecpCtx) -> SignerId { - SignerId::from(self.root_fingerprint(secp)) - } - - fn descriptor_secret_key(&self) -> Option { - Some(DescriptorSecretKey::XPrv(self.signer.clone())) - } -} - -impl InputSigner for SignerWrapper> { - fn sign_input( - &self, - psbt: &mut Psbt, - input_index: usize, - sign_options: &SignOptions, - secp: &SecpCtx, - ) -> Result<(), SignerError> { - if input_index >= psbt.inputs.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 tap_key_origins = psbt.inputs[input_index] - .tap_key_origins - .iter() - .map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource)); - let (public_key, full_path) = match psbt.inputs[input_index] - .bip32_derivation - .iter() - .map(|(pk, keysource)| (SinglePubKey::FullKey(PublicKey::new(*pk)), keysource)) - .chain(tap_key_origins) - .find_map(|(pk, keysource)| { - if self.matches(keysource, secp).is_some() { - Some((pk, keysource.1.clone())) - } else { - None - } - }) { - Some((pk, full_path)) => (pk, full_path), - None => return Ok(()), - }; - - let derived_key = match self.origin.clone() { - Some((_fingerprint, origin_path)) => { - let deriv_path = DerivationPath::from( - &full_path.into_iter().cloned().collect::>() - [origin_path.len()..], - ); - self.xkey.derive_priv(secp, &deriv_path).unwrap() - } - None => self.xkey.derive_priv(secp, &full_path).unwrap(), - }; - - let computed_pk = secp256k1::PublicKey::from_secret_key(secp, &derived_key.private_key); - let valid_key = match public_key { - SinglePubKey::FullKey(pk) if pk.inner == computed_pk => true, - SinglePubKey::XOnly(x_only) if XOnlyPublicKey::from(computed_pk) == x_only => true, - _ => false, - }; - if !valid_key { - Err(SignerError::InvalidKey) - } else { - // HD wallets imply compressed keys - let priv_key = PrivateKey { - compressed: true, - network: self.xkey.network, - inner: derived_key.private_key, - }; - - SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, sign_options, secp) - } - } -} - -fn multikey_to_xkeys( - multikey: DescriptorMultiXKey, -) -> Vec> { - multikey - .derivation_paths - .into_paths() - .into_iter() - .map(|derivation_path| DescriptorXKey { - origin: multikey.origin.clone(), - xkey: multikey.xkey.clone(), - derivation_path, - wildcard: multikey.wildcard, - }) - .collect() -} - -impl SignerCommon for SignerWrapper> { - fn id(&self, secp: &SecpCtx) -> SignerId { - SignerId::from(self.root_fingerprint(secp)) - } - - fn descriptor_secret_key(&self) -> Option { - Some(DescriptorSecretKey::MultiXPrv(self.signer.clone())) - } -} - -impl InputSigner for SignerWrapper> { - fn sign_input( - &self, - psbt: &mut Psbt, - input_index: usize, - sign_options: &SignOptions, - secp: &SecpCtx, - ) -> Result<(), SignerError> { - let xkeys = multikey_to_xkeys(self.signer.clone()); - for xkey in xkeys { - SignerWrapper::new(xkey, self.ctx).sign_input(psbt, input_index, sign_options, secp)? - } - Ok(()) - } -} - -impl SignerCommon for SignerWrapper { - fn id(&self, secp: &SecpCtx) -> SignerId { - SignerId::from(self.public_key(secp).to_pubkeyhash(SigType::Ecdsa)) - } - - fn descriptor_secret_key(&self) -> Option { - Some(DescriptorSecretKey::Single(SinglePriv { - key: self.signer, - origin: None, - })) - } -} - -impl InputSigner for SignerWrapper { - fn sign_input( - &self, - psbt: &mut Psbt, - input_index: usize, - sign_options: &SignOptions, - secp: &SecpCtx, - ) -> Result<(), SignerError> { - if input_index >= psbt.inputs.len() || input_index >= psbt.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 = PublicKey::from_private_key(secp, self); - let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner); - - if let SignerContext::Tap { is_internal_key } = self.ctx { - if let Some(psbt_internal_key) = psbt.inputs[input_index].tap_internal_key { - if is_internal_key - && psbt.inputs[input_index].tap_key_sig.is_none() - && sign_options.sign_with_tap_internal_key - && x_only_pubkey == psbt_internal_key - { - let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?; - sign_psbt_schnorr( - &self.inner, - x_only_pubkey, - None, - &mut psbt.inputs[input_index], - hash, - hash_ty, - secp, - ); - } - } - - if let Some((leaf_hashes, _)) = - psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey) - { - let leaf_hashes = leaf_hashes - .iter() - .filter(|lh| { - // Removing the leaves we shouldn't sign for - let should_sign = match &sign_options.tap_leaves_options { - TapLeavesOptions::All => true, - TapLeavesOptions::Include(v) => v.contains(lh), - TapLeavesOptions::Exclude(v) => !v.contains(lh), - TapLeavesOptions::None => false, - }; - // Filtering out the leaves without our key - should_sign - && !psbt.inputs[input_index] - .tap_script_sigs - .contains_key(&(x_only_pubkey, **lh)) - }) - .cloned() - .collect::>(); - for lh in leaf_hashes { - let (hash, hash_ty) = Tap::sighash(psbt, input_index, Some(lh))?; - sign_psbt_schnorr( - &self.inner, - x_only_pubkey, - Some(lh), - &mut psbt.inputs[input_index], - hash, - hash_ty, - secp, - ); - } - } - - return Ok(()); - } - - if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) { - return Ok(()); - } - - let (hash, hash_ty) = match self.ctx { - SignerContext::Segwitv0 => { - let (h, t) = Segwitv0::sighash(psbt, input_index, ())?; - let h = h.to_raw_hash(); - (h, t) - } - SignerContext::Legacy => { - let (h, t) = Legacy::sighash(psbt, input_index, ())?; - let h = h.to_raw_hash(); - (h, t) - } - _ => return Ok(()), // handled above - }; - sign_psbt_ecdsa( - &self.inner, - pubkey, - &mut psbt.inputs[input_index], - hash, - hash_ty, - secp, - sign_options.allow_grinding, - ); - - Ok(()) - } -} - -fn sign_psbt_ecdsa( - secret_key: &secp256k1::SecretKey, - pubkey: PublicKey, - psbt_input: &mut psbt::Input, - hash: impl bitcoin::hashes::Hash + bitcoin::secp256k1::ThirtyTwoByteHash, - hash_ty: EcdsaSighashType, - secp: &SecpCtx, - allow_grinding: bool, -) { - let msg = &Message::from(hash); - let sig = if allow_grinding { - secp.sign_ecdsa_low_r(msg, secret_key) - } else { - secp.sign_ecdsa(msg, secret_key) - }; - secp.verify_ecdsa(msg, &sig, &pubkey.inner) - .expect("invalid or corrupted ecdsa signature"); - - let final_signature = ecdsa::Signature { sig, hash_ty }; - psbt_input.partial_sigs.insert(pubkey, final_signature); -} - -// Calling this with `leaf_hash` = `None` will sign for key-spend -fn sign_psbt_schnorr( - secret_key: &secp256k1::SecretKey, - pubkey: XOnlyPublicKey, - leaf_hash: Option, - psbt_input: &mut psbt::Input, - hash: TapSighash, - hash_ty: TapSighashType, - secp: &SecpCtx, -) { - let keypair = secp256k1::Keypair::from_seckey_slice(secp, secret_key.as_ref()).unwrap(); - let keypair = match leaf_hash { - None => keypair - .tap_tweak(secp, psbt_input.tap_merkle_root) - .to_inner(), - Some(_) => keypair, // no tweak for script spend - }; - - let msg = &Message::from(hash); - let sig = secp.sign_schnorr(msg, &keypair); - secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair).0) - .expect("invalid or corrupted schnorr signature"); - - let final_signature = taproot::Signature { sig, hash_ty }; - - if let Some(lh) = leaf_hash { - psbt_input - .tap_script_sigs - .insert((pubkey, lh), final_signature); - } else { - psbt_input.tap_key_sig = Some(final_signature); - } -} - -/// Defines the order in which signers are called -/// -/// The default value is `100`. Signers with an ordering above that will be called later, -/// and they will thus see the partial signatures added to the transaction once they get to sign -/// themselves. -#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq)] -pub struct SignerOrdering(pub usize); - -impl Default for SignerOrdering { - fn default() -> Self { - SignerOrdering(100) - } -} - -#[derive(Debug, Clone)] -struct SignersContainerKey { - id: SignerId, - ordering: SignerOrdering, -} - -impl From<(SignerId, SignerOrdering)> for SignersContainerKey { - fn from(tuple: (SignerId, SignerOrdering)) -> Self { - SignersContainerKey { - id: tuple.0, - ordering: tuple.1, - } - } -} - -/// Container for multiple signers -#[derive(Debug, Default, Clone)] -pub struct SignersContainer(BTreeMap>); - -impl SignersContainer { - /// Create a map of public keys to secret keys - pub fn as_key_map(&self, secp: &SecpCtx) -> KeyMap { - self.0 - .values() - .filter_map(|signer| signer.descriptor_secret_key()) - .filter_map(|secret| secret.to_public(secp).ok().map(|public| (public, secret))) - .collect() - } - - /// Build a new signer container from a [`KeyMap`] - /// - /// Also looks at the corresponding descriptor to determine the [`SignerContext`] to attach to - /// the signers - pub fn build( - keymap: KeyMap, - descriptor: &Descriptor, - secp: &SecpCtx, - ) -> SignersContainer { - let mut container = SignersContainer::new(); - - for (pubkey, secret) in keymap { - let ctx = match descriptor { - Descriptor::Tr(tr) => SignerContext::Tap { - is_internal_key: tr.internal_key() == &pubkey, - }, - _ if descriptor.is_witness() => SignerContext::Segwitv0, - _ => SignerContext::Legacy, - }; - - match secret { - DescriptorSecretKey::Single(private_key) => container.add_external( - SignerId::from( - private_key - .key - .public_key(secp) - .to_pubkeyhash(SigType::Ecdsa), - ), - SignerOrdering::default(), - Arc::new(SignerWrapper::new(private_key.key, ctx)), - ), - DescriptorSecretKey::XPrv(xprv) => container.add_external( - SignerId::from(xprv.root_fingerprint(secp)), - SignerOrdering::default(), - Arc::new(SignerWrapper::new(xprv, ctx)), - ), - DescriptorSecretKey::MultiXPrv(xprv) => container.add_external( - SignerId::from(xprv.root_fingerprint(secp)), - SignerOrdering::default(), - Arc::new(SignerWrapper::new(xprv, ctx)), - ), - }; - } - - container - } -} - -impl SignersContainer { - /// Default constructor - pub fn new() -> Self { - SignersContainer(Default::default()) - } - - /// Adds an external signer to the container for the specified id. Optionally returns the - /// signer that was previously in the container, if any - pub fn add_external( - &mut self, - id: SignerId, - ordering: SignerOrdering, - signer: Arc, - ) -> Option> { - self.0.insert((id, ordering).into(), signer) - } - - /// Removes a signer from the container and returns it - pub fn remove( - &mut self, - id: SignerId, - ordering: SignerOrdering, - ) -> Option> { - self.0.remove(&(id, ordering).into()) - } - - /// Returns the list of identifiers of all the signers in the container - pub fn ids(&self) -> Vec<&SignerId> { - self.0 - .keys() - .map(|SignersContainerKey { id, .. }| id) - .collect() - } - - /// Returns the list of signers in the container, sorted by lowest to highest `ordering` - pub fn signers(&self) -> Vec<&Arc> { - self.0.values().collect() - } - - /// Finds the signer with lowest ordering for a given id in the container. - pub fn find(&self, id: SignerId) -> Option<&Arc> { - self.0 - .range(( - Included(&(id.clone(), SignerOrdering(0)).into()), - Included(&(id.clone(), SignerOrdering(usize::MAX)).into()), - )) - .filter(|(k, _)| k.id == id) - .map(|(_, v)| v) - .next() - } -} - -/// 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 should 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: - 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, - - /// Whether the signer should use the `sighash_type` set in the PSBT when signing, no matter - /// what its value is - /// - /// Defaults to `false` which will only allow signing using `SIGHASH_ALL`. - pub allow_all_sighashes: bool, - - /// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT. - /// - /// Defaults to `true` which will remove partial signatures during finalization. - pub remove_partial_sigs: bool, - - /// Whether to remove taproot specific fields from the PSBT on finalization. - /// - /// For inputs this includes the taproot internal key, merkle root, and individual - /// scripts and signatures. For both inputs and outputs it includes key origin info. - /// - /// Defaults to `true` which will remove all of the above mentioned fields when finalizing. - /// - /// See [`BIP371`](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) for details. - pub remove_taproot_extras: bool, - - /// Whether to try finalizing the PSBT after the inputs are signed. - /// - /// Defaults to `true` which will try finalizing PSBT after inputs are signed. - pub try_finalize: bool, - - /// Specifies which Taproot script-spend leaves we should sign for. This option is - /// ignored if we're signing a non-taproot PSBT. - /// - /// Defaults to All, i.e., the wallet will sign all the leaves it has a key for. - pub tap_leaves_options: TapLeavesOptions, - - /// Whether we should try to sign a taproot transaction with the taproot internal key - /// or not. This option is ignored if we're signing a non-taproot PSBT. - /// - /// Defaults to `true`, i.e., we always try to sign with the taproot internal key. - pub sign_with_tap_internal_key: bool, - - /// Whether we should grind ECDSA signature to ensure signing with low r - /// or not. - /// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r. - pub allow_grinding: bool, -} - -/// Customize which taproot script-path leaves the signer should sign. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub enum TapLeavesOptions { - /// The signer will sign all the leaves it has a key for. - #[default] - All, - /// The signer won't sign leaves other than the ones specified. Note that it could still ignore - /// some of the specified leaves, if it doesn't have the right key to sign them. - Include(Vec), - /// The signer won't sign the specified leaves. - Exclude(Vec), - /// The signer won't sign any leaf. - None, -} - -impl Default for SignOptions { - fn default() -> Self { - SignOptions { - trust_witness_utxo: false, - assume_height: None, - allow_all_sighashes: false, - remove_partial_sigs: true, - remove_taproot_extras: true, - try_finalize: true, - tap_leaves_options: TapLeavesOptions::default(), - sign_with_tap_internal_key: true, - allow_grinding: true, - } - } -} - -pub(crate) trait ComputeSighash { - type Extra; - type Sighash; - type SighashType; - - fn sighash( - psbt: &Psbt, - input_index: usize, - extra: Self::Extra, - ) -> Result<(Self::Sighash, Self::SighashType), SignerError>; -} - -impl ComputeSighash for Legacy { - type Extra = (); - type Sighash = sighash::LegacySighash; - type SighashType = EcdsaSighashType; - - fn sighash( - psbt: &Psbt, - input_index: usize, - _extra: (), - ) -> Result<(Self::Sighash, Self::SighashType), SignerError> { - if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { - return Err(SignerError::InputIndexOutOfRange); - } - - let psbt_input = &psbt.inputs[input_index]; - let tx_input = &psbt.unsigned_tx.input[input_index]; - - let sighash = psbt_input - .sighash_type - .unwrap_or_else(|| EcdsaSighashType::All.into()) - .ecdsa_hash_ty() - .map_err(|_| SignerError::InvalidSighash)?; - let script = match psbt_input.redeem_script { - Some(ref redeem_script) => redeem_script.clone(), - None => { - let non_witness_utxo = psbt_input - .non_witness_utxo - .as_ref() - .ok_or(SignerError::MissingNonWitnessUtxo)?; - let prev_out = non_witness_utxo - .output - .get(tx_input.previous_output.vout as usize) - .ok_or(SignerError::InvalidNonWitnessUtxo)?; - - prev_out.script_pubkey.clone() - } - }; - - Ok(( - sighash::SighashCache::new(&psbt.unsigned_tx).legacy_signature_hash( - input_index, - &script, - sighash.to_u32(), - )?, - sighash, - )) - } -} - -impl ComputeSighash for Segwitv0 { - type Extra = (); - type Sighash = sighash::SegwitV0Sighash; - type SighashType = EcdsaSighashType; - - fn sighash( - psbt: &Psbt, - input_index: usize, - _extra: (), - ) -> Result<(Self::Sighash, Self::SighashType), SignerError> { - if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { - return Err(SignerError::InputIndexOutOfRange); - } - - let psbt_input = &psbt.inputs[input_index]; - let tx_input = &psbt.unsigned_tx.input[input_index]; - - let sighash_type = psbt_input - .sighash_type - .unwrap_or_else(|| EcdsaSighashType::All.into()) - .ecdsa_hash_ty() - .map_err(|_| SignerError::InvalidSighash)?; - - // 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 mut sighasher = sighash::SighashCache::new(&psbt.unsigned_tx); - - let sighash = match psbt_input.witness_script { - Some(ref witness_script) => { - sighasher.p2wsh_signature_hash(input_index, witness_script, value, sighash_type)? - } - None => { - if utxo.script_pubkey.is_p2wpkh() { - sighasher.p2wpkh_signature_hash( - input_index, - &utxo.script_pubkey, - value, - sighash_type, - )? - } else if psbt_input - .redeem_script - .as_ref() - .map(|s| s.is_p2wpkh()) - .unwrap_or(false) - { - let script_pubkey = psbt_input.redeem_script.as_ref().unwrap(); - sighasher.p2wpkh_signature_hash( - input_index, - script_pubkey, - value, - sighash_type, - )? - } else { - return Err(SignerError::MissingWitnessScript); - } - } - }; - Ok((sighash, sighash_type)) - } -} - -impl ComputeSighash for Tap { - type Extra = Option; - type Sighash = TapSighash; - type SighashType = TapSighashType; - - fn sighash( - psbt: &Psbt, - input_index: usize, - extra: Self::Extra, - ) -> Result<(Self::Sighash, TapSighashType), SignerError> { - if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { - return Err(SignerError::InputIndexOutOfRange); - } - - let psbt_input = &psbt.inputs[input_index]; - - let sighash_type = psbt_input - .sighash_type - .unwrap_or_else(|| TapSighashType::Default.into()) - .taproot_hash_ty() - .map_err(|_| SignerError::InvalidSighash)?; - let witness_utxos = (0..psbt.inputs.len()) - .map(|i| psbt.get_utxo_for(i)) - .collect::>(); - let mut all_witness_utxos = vec![]; - - let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx); - let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0; - let prevouts = if is_anyone_can_pay { - sighash::Prevouts::One( - input_index, - witness_utxos[input_index] - .as_ref() - .ok_or(SignerError::MissingWitnessUtxo)?, - ) - } else if witness_utxos.iter().all(Option::is_some) { - all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref())); - sighash::Prevouts::All(&all_witness_utxos) - } else { - return Err(SignerError::MissingWitnessUtxo); - }; - - // Assume no OP_CODESEPARATOR - let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF)); - - Ok(( - cache.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)?, - sighash_type, - )) - } -} - -impl PartialOrd for SignersContainerKey { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for SignersContainerKey { - fn cmp(&self, other: &Self) -> Ordering { - self.ordering - .cmp(&other.ordering) - .then(self.id.cmp(&other.id)) - } -} - -impl PartialEq for SignersContainerKey { - fn eq(&self, other: &Self) -> bool { - self.id == other.id && self.ordering == other.ordering - } -} - -impl Eq for SignersContainerKey {} - -#[cfg(test)] -mod signers_container_tests { - use super::*; - use crate::descriptor; - use crate::descriptor::IntoWalletDescriptor; - use crate::keys::{DescriptorKey, IntoDescriptorKey}; - use assert_matches::assert_matches; - use bitcoin::bip32; - use bitcoin::secp256k1::{All, Secp256k1}; - use bitcoin::Network; - use core::str::FromStr; - use miniscript::ScriptContext; - - fn is_equal(this: &Arc, that: &Arc) -> bool { - let secp = Secp256k1::new(); - this.id(&secp) == that.id(&secp) - } - - // Signers added with the same ordering (like `Ordering::default`) created from `KeyMap` - // should be preserved and not overwritten. - // This happens usually when a set of signers is created from a descriptor with private keys. - #[test] - fn signers_with_same_ordering() { - let secp = Secp256k1::new(); - - let (prvkey1, _, _) = setup_keys(TPRV0_STR); - let (prvkey2, _, _) = setup_keys(TPRV1_STR); - let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap(); - let (wallet_desc, keymap) = desc - .into_wallet_descriptor(&secp, Network::Testnet) - .unwrap(); - - let signers = SignersContainer::build(keymap, &wallet_desc, &secp); - assert_eq!(signers.ids().len(), 2); - - let signers = signers.signers(); - assert_eq!(signers.len(), 2); - } - - #[test] - fn signers_sorted_by_ordering() { - let mut signers = SignersContainer::new(); - let signer1 = Arc::new(DummySigner { number: 1 }); - let signer2 = Arc::new(DummySigner { number: 2 }); - let signer3 = Arc::new(DummySigner { number: 3 }); - - // Mixed order insertions verifies we are not inserting at head or tail. - signers.add_external(SignerId::Dummy(2), SignerOrdering(2), signer2.clone()); - signers.add_external(SignerId::Dummy(1), SignerOrdering(1), signer1.clone()); - signers.add_external(SignerId::Dummy(3), SignerOrdering(3), signer3.clone()); - - // Check that signers are sorted from lowest to highest ordering - let signers = signers.signers(); - - assert!(is_equal(signers[0], &signer1)); - assert!(is_equal(signers[1], &signer2)); - assert!(is_equal(signers[2], &signer3)); - } - - #[test] - fn find_signer_by_id() { - let mut signers = SignersContainer::new(); - let signer1 = Arc::new(DummySigner { number: 1 }); - let signer2 = Arc::new(DummySigner { number: 2 }); - let signer3 = Arc::new(DummySigner { number: 3 }); - let signer4 = Arc::new(DummySigner { number: 3 }); // Same ID as `signer3` but will use lower ordering. - - let id1 = SignerId::Dummy(1); - let id2 = SignerId::Dummy(2); - let id3 = SignerId::Dummy(3); - let id_nonexistent = SignerId::Dummy(999); - - signers.add_external(id1.clone(), SignerOrdering(1), signer1.clone()); - signers.add_external(id2.clone(), SignerOrdering(2), signer2.clone()); - signers.add_external(id3.clone(), SignerOrdering(3), signer3.clone()); - - assert_matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1)); - assert_matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2)); - assert_matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3)); - - // The `signer4` has the same ID as `signer3` but lower ordering. - // It should be found by `id3` instead of `signer3`. - signers.add_external(id3.clone(), SignerOrdering(2), signer4.clone()); - assert_matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4)); - - // Can't find anything with ID that doesn't exist - assert_matches!(signers.find(id_nonexistent), None); - } - - #[derive(Debug, Clone, Copy)] - struct DummySigner { - number: u64, - } - - impl SignerCommon for DummySigner { - fn id(&self, _secp: &SecpCtx) -> SignerId { - SignerId::Dummy(self.number) - } - } - - impl TransactionSigner for DummySigner { - fn sign_transaction( - &self, - _psbt: &mut Psbt, - _sign_options: &SignOptions, - _secp: &SecpCtx, - ) -> Result<(), SignerError> { - Ok(()) - } - } - - const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf"; - const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N"; - - const PATH: &str = "m/44'/1'/0'/0"; - - fn setup_keys( - tprv: &str, - ) -> (DescriptorKey, DescriptorKey, Fingerprint) { - let secp: Secp256k1 = Secp256k1::new(); - let path = bip32::DerivationPath::from_str(PATH).unwrap(); - let tprv = bip32::Xpriv::from_str(tprv).unwrap(); - let tpub = bip32::Xpub::from_priv(&secp, &tprv); - let fingerprint = tprv.fingerprint(&secp); - let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap(); - let pubkey = (tpub, path).into_descriptor_key().unwrap(); - - (prvkey, pubkey, fingerprint) - } -} diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs deleted file mode 100644 index 5c3e70d5..00000000 --- a/crates/bdk/src/wallet/tx_builder.rs +++ /dev/null @@ -1,1083 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! Transaction builder -//! -//! ## Example -//! -//! ``` -//! # use std::str::FromStr; -//! # use bitcoin::*; -//! # use bdk::*; -//! # use bdk::wallet::ChangeSet; -//! # use bdk::wallet::error::CreateTxError; -//! # use bdk::wallet::tx_builder::CreateTx; -//! # use bdk_persist::PersistBackend; -//! # use anyhow::Error; -//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); -//! # let mut wallet = doctest_wallet!(); -//! // create a TxBuilder from a wallet -//! let mut tx_builder = wallet.build_tx(); -//! -//! tx_builder -//! // Create a transaction with one output to `to_address` of 50_000 satoshi -//! .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)) -//! // With a custom fee rate of 5.0 satoshi/vbyte -//! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")) -//! // Only spend non-change outputs -//! .do_not_spend_change() -//! // Turn on RBF signaling -//! .enable_rbf(); -//! let psbt = tx_builder.finish()?; -//! # Ok::<(), anyhow::Error>(()) -//! ``` - -use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; -use core::cell::RefCell; -use core::fmt; -use core::marker::PhantomData; - -use bitcoin::psbt::{self, Psbt}; -use bitcoin::script::PushBytes; -use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid}; - -use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; -use super::{CreateTxError, Wallet}; -use crate::collections::{BTreeMap, HashSet}; -use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; - -/// Context in which the [`TxBuilder`] is valid -pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {} - -/// Marker type to indicate the [`TxBuilder`] is being used to create a new transaction (as opposed -/// to bumping the fee of an existing one). -#[derive(Debug, Default, Clone)] -pub struct CreateTx; -impl TxBuilderContext for CreateTx {} - -/// Marker type to indicate the [`TxBuilder`] is being used to bump the fee of an existing transaction. -#[derive(Debug, Default, Clone)] -pub struct BumpFee; -impl TxBuilderContext for BumpFee {} - -/// A transaction builder -/// -/// A `TxBuilder` is created by calling [`build_tx`] or [`build_fee_bump`] on a wallet. After -/// assigning it, you set options on it until finally calling [`finish`] to consume the builder and -/// generate the transaction. -/// -/// Each option setting method on `TxBuilder` takes and returns `&mut self` so you can chain calls -/// as in the following example: -/// -/// ``` -/// # use bdk::*; -/// # use bdk::wallet::tx_builder::*; -/// # use bitcoin::*; -/// # use core::str::FromStr; -/// # use bdk::wallet::ChangeSet; -/// # use bdk::wallet::error::CreateTxError; -/// # use bdk_persist::PersistBackend; -/// # use anyhow::Error; -/// # let mut wallet = doctest_wallet!(); -/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); -/// # let addr2 = addr1.clone(); -/// // chaining -/// let psbt1 = { -/// let mut builder = wallet.build_tx(); -/// builder -/// .ordering(TxOrdering::Untouched) -/// .add_recipient(addr1.script_pubkey(), Amount::from_sat(50_000)) -/// .add_recipient(addr2.script_pubkey(), Amount::from_sat(50_000)); -/// builder.finish()? -/// }; -/// -/// // non-chaining -/// let psbt2 = { -/// let mut builder = wallet.build_tx(); -/// builder.ordering(TxOrdering::Untouched); -/// for addr in &[addr1, addr2] { -/// builder.add_recipient(addr.script_pubkey(), Amount::from_sat(50_000)); -/// } -/// builder.finish()? -/// }; -/// -/// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]); -/// # Ok::<(), anyhow::Error>(()) -/// ``` -/// -/// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`. -/// This means it is usually best to call [`coin_selection`] on the return value of `build_tx` before assigning it. -/// -/// For further examples see [this module](super::tx_builder)'s documentation; -/// -/// [`build_tx`]: Wallet::build_tx -/// [`build_fee_bump`]: Wallet::build_fee_bump -/// [`finish`]: Self::finish -/// [`coin_selection`]: Self::coin_selection -#[derive(Debug)] -pub struct TxBuilder<'a, Cs, Ctx> { - pub(crate) wallet: Rc>, - pub(crate) params: TxParams, - pub(crate) coin_selection: Cs, - pub(crate) phantom: PhantomData, -} - -/// The parameters for transaction creation sans coin selection algorithm. -//TODO: TxParams should eventually be exposed publicly. -#[derive(Default, Debug, Clone)] -pub(crate) struct TxParams { - pub(crate) recipients: Vec<(ScriptBuf, u64)>, - pub(crate) drain_wallet: bool, - pub(crate) drain_to: Option, - pub(crate) fee_policy: Option, - pub(crate) internal_policy_path: Option>>, - pub(crate) external_policy_path: Option>>, - pub(crate) utxos: Vec, - pub(crate) unspendable: HashSet, - pub(crate) manually_selected_only: bool, - pub(crate) sighash: Option, - pub(crate) ordering: TxOrdering, - pub(crate) locktime: Option, - pub(crate) rbf: Option, - pub(crate) version: Option, - pub(crate) change_policy: ChangeSpendPolicy, - 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, - pub(crate) current_height: Option, - pub(crate) allow_dust: bool, -} - -#[derive(Clone, Copy, Debug)] -pub(crate) struct PreviousFee { - pub absolute: u64, - pub rate: FeeRate, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) enum FeePolicy { - FeeRate(FeeRate), - FeeAmount(u64), -} - -impl Default for FeePolicy { - fn default() -> Self { - FeePolicy::FeeRate(FeeRate::BROADCAST_MIN) - } -} - -impl<'a, Cs: Clone, Ctx> Clone for TxBuilder<'a, Cs, Ctx> { - fn clone(&self) -> Self { - TxBuilder { - wallet: self.wallet.clone(), - params: self.params.clone(), - coin_selection: self.coin_selection.clone(), - phantom: PhantomData, - } - } -} - -// methods supported by both contexts, for any CoinSelectionAlgorithm -impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> { - /// Set a custom fee rate. - /// - /// This method sets the mining fee paid by the transaction as a rate on its size. - /// This means that the total fee paid is equal to `fee_rate` times the size - /// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default - /// relay policy. - /// - /// Note that this is really a minimum feerate -- it's possible to - /// overshoot it slightly since adding a change output to drain the remaining - /// excess might not be viable. - pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self { - self.params.fee_policy = Some(FeePolicy::FeeRate(fee_rate)); - self - } - - /// Set an absolute fee - /// The fee_absolute method refers to the absolute transaction fee in satoshis (sats). - /// If anyone sets both the fee_absolute method and the fee_rate method, - /// the FeePolicy enum will be set by whichever method was called last, - /// as the FeeRate and FeeAmount are mutually exclusive. - /// - /// Note that this is really a minimum absolute fee -- it's possible to - /// overshoot it slightly since adding a change output to drain the remaining - /// excess might not be viable. - pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self { - self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount)); - self - } - - /// Set the policy path to use while creating the transaction for a given keychain. - /// - /// This method accepts a map where the key is the policy node id (see - /// [`Policy::id`](crate::descriptor::Policy::id)) and the value is the list of the indexes of - /// the items that are intended to be satisfied from the policy node (see - /// [`SatisfiableItem::Thresh::items`](crate::descriptor::policy::SatisfiableItem::Thresh::items)). - /// - /// ## Example - /// - /// An example of when the policy path is needed is the following descriptor: - /// `wsh(thresh(2,pk(A),sj:and_v(v:pk(B),n:older(6)),snj:and_v(v:pk(C),after(630000))))`, - /// derived from the miniscript policy `thresh(2,pk(A),and(pk(B),older(6)),and(pk(C),after(630000)))`. - /// It declares three descriptor fragments, and at the top level it uses `thresh()` to - /// ensure that at least two of them are satisfied. The individual fragments are: - /// - /// 1. `pk(A)` - /// 2. `and(pk(B),older(6))` - /// 3. `and(pk(C),after(630000))` - /// - /// When those conditions are combined in pairs, it's clear that the transaction needs to be created - /// differently depending on how the user intends to satisfy the policy afterwards: - /// - /// * If fragments `1` and `2` are used, the transaction will need to use a specific - /// `n_sequence` in order to spend an `OP_CSV` branch. - /// * If fragments `1` and `3` are used, the transaction will need to use a specific `locktime` - /// in order to spend an `OP_CLTV` branch. - /// * If fragments `2` and `3` are used, the transaction will need both. - /// - /// When the spending policy is represented as a tree (see - /// [`Wallet::policies`](super::Wallet::policies)), every node - /// is assigned a unique identifier that can be used in the policy path to specify which of - /// the node's children the user intends to satisfy: for instance, assuming the `thresh()` - /// root node of this example has an id of `aabbccdd`, the policy path map would look like: - /// - /// `{ "aabbccdd" => [0, 1] }` - /// - /// where the key is the node's id, and the value is a list of the children that should be - /// used, in no particular order. - /// - /// If a particularly complex descriptor has multiple ambiguous thresholds in its structure, - /// multiple entries can be added to the map, one for each node that requires an explicit path. - /// - /// ``` - /// # use std::str::FromStr; - /// # use std::collections::BTreeMap; - /// # use bitcoin::*; - /// # use bdk::*; - /// # let to_address = - /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") - /// .unwrap() - /// .assume_checked(); - /// # let mut wallet = doctest_wallet!(); - /// let mut path = BTreeMap::new(); - /// path.insert("aabbccdd".to_string(), vec![0, 1]); - /// - /// let builder = wallet - /// .build_tx() - /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)) - /// .policy_path(path, KeychainKind::External); - /// - /// # Ok::<(), anyhow::Error>(()) - /// ``` - pub fn policy_path( - &mut self, - policy_path: BTreeMap>, - keychain: KeychainKind, - ) -> &mut Self { - let to_update = match keychain { - KeychainKind::Internal => &mut self.params.internal_policy_path, - KeychainKind::External => &mut self.params.external_policy_path, - }; - - *to_update = Some(policy_path); - self - } - - /// Add the list of outpoints to the internal list of UTXOs that **must** be spent. - /// - /// If an error occurs while adding any of the UTXOs then none of them are added and the error is returned. - /// - /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in - /// the "utxos" and the "unspendable" list, it will be spent. - pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, AddUtxoError> { - { - let wallet = self.wallet.borrow(); - let utxos = outpoints - .iter() - .map(|outpoint| { - wallet - .get_utxo(*outpoint) - .ok_or(AddUtxoError::UnknownUtxo(*outpoint)) - }) - .collect::, _>>()?; - - for utxo in utxos { - let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain); - let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap(); - self.params.utxos.push(WeightedUtxo { - satisfaction_weight, - utxo: Utxo::Local(utxo), - }); - } - } - - Ok(self) - } - - /// Add a utxo to the internal list of utxos that **must** be spent - /// - /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in - /// the "utxos" and the "unspendable" list, it will be spent. - pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, AddUtxoError> { - self.add_utxos(&[outpoint]) - } - - /// Add a foreign UTXO i.e. a UTXO not owned by this wallet. - /// - /// At a minimum to add a foreign UTXO we need: - /// - /// 1. `outpoint`: To add it to the raw transaction. - /// 2. `psbt_input`: To know the value. - /// 3. `satisfaction_weight`: To know how much weight/vbytes the input will add to the transaction for fee calculation. - /// - /// There are several security concerns about adding foreign UTXOs that application - /// developers should consider. First, how do you know the value of the input is correct? If a - /// `non_witness_utxo` is provided in the `psbt_input` then this method implicitly verifies the - /// value by checking it against the transaction. If only a `witness_utxo` is provided then this - /// method doesn't verify the value but just takes it as a given -- it is up to you to check - /// that whoever sent you the `input_psbt` was not lying! - /// - /// Secondly, you must somehow provide `satisfaction_weight` of the input. Depending on your - /// application it may be important that this be known precisely. If not, a malicious - /// counterparty may fool you into putting in a value that is too low, giving the transaction a - /// lower than expected feerate. They could also fool you into putting a value that is too high - /// causing you to pay a fee that is too high. The party who is broadcasting the transaction can - /// of course check the real input weight matches the expected weight prior to broadcasting. - /// - /// To guarantee the `max_weight_to_satisfy` is correct, you can require the party providing the - /// `psbt_input` provide a miniscript descriptor for the input so you can check it against the - /// `script_pubkey` and then ask it for the [`max_weight_to_satisfy`]. - /// - /// This is an **EXPERIMENTAL** feature, API and other major changes are expected. - /// - /// In order to use [`Wallet::calculate_fee`] or [`Wallet::calculate_fee_rate`] for a transaction - /// created with foreign UTXO(s) you must manually insert the corresponding TxOut(s) into the tx - /// graph using the [`Wallet::insert_txout`] function. - /// - /// # Errors - /// - /// This method returns errors in the following circumstances: - /// - /// 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 unless you set [`only_witness_utxo`] any non-taproot `psbt_input` you pass to this - /// method must have `non_witness_utxo` set otherwise you will get an error when [`finish`] - /// is called. - /// - /// [`only_witness_utxo`]: Self::only_witness_utxo - /// [`finish`]: Self::finish - /// [`max_weight_to_satisfy`]: miniscript::Descriptor::max_weight_to_satisfy - pub fn add_foreign_utxo( - &mut self, - outpoint: OutPoint, - psbt_input: psbt::Input, - satisfaction_weight: usize, - ) -> Result<&mut Self, AddForeignUtxoError> { - self.add_foreign_utxo_with_sequence( - outpoint, - psbt_input, - satisfaction_weight, - Sequence::MAX, - ) - } - - /// Same as [add_foreign_utxo](TxBuilder::add_foreign_utxo) but allows to set the nSequence value. - pub fn add_foreign_utxo_with_sequence( - &mut self, - outpoint: OutPoint, - psbt_input: psbt::Input, - satisfaction_weight: usize, - sequence: Sequence, - ) -> Result<&mut Self, AddForeignUtxoError> { - if psbt_input.witness_utxo.is_none() { - match psbt_input.non_witness_utxo.as_ref() { - Some(tx) => { - if tx.txid() != outpoint.txid { - return Err(AddForeignUtxoError::InvalidTxid { - input_txid: tx.txid(), - foreign_utxo: outpoint, - }); - } - if tx.output.len() <= outpoint.vout as usize { - return Err(AddForeignUtxoError::InvalidOutpoint(outpoint)); - } - } - None => { - return Err(AddForeignUtxoError::MissingUtxo); - } - } - } - - self.params.utxos.push(WeightedUtxo { - satisfaction_weight, - utxo: Utxo::Foreign { - outpoint, - sequence: Some(sequence), - psbt_input: Box::new(psbt_input), - }, - }); - - Ok(self) - } - - /// Only spend utxos added by [`add_utxo`]. - /// - /// The wallet will **not** add additional utxos to the transaction even if they are needed to - /// make the transaction valid. - /// - /// [`add_utxo`]: Self::add_utxo - pub fn manually_selected_only(&mut self) -> &mut Self { - self.params.manually_selected_only = true; - self - } - - /// Replace the internal list of unspendable utxos with a new list - /// - /// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::add_utxo`] - /// have priority over these. See the docs of the two linked methods for more details. - pub fn unspendable(&mut self, unspendable: Vec) -> &mut Self { - self.params.unspendable = unspendable.into_iter().collect(); - self - } - - /// Add a utxo to the internal list of unspendable utxos - /// - /// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::add_utxo`] - /// have priority over this. See the docs of the two linked methods for more details. - pub fn add_unspendable(&mut self, unspendable: OutPoint) -> &mut Self { - self.params.unspendable.insert(unspendable); - self - } - - /// Sign with a specific sig hash - /// - /// **Use this option very carefully** - pub fn sighash(&mut self, sighash: psbt::PsbtSighashType) -> &mut Self { - self.params.sighash = Some(sighash); - self - } - - /// Choose the ordering for inputs and outputs of the transaction - pub fn ordering(&mut self, ordering: TxOrdering) -> &mut Self { - self.params.ordering = ordering; - self - } - - /// Use a specific nLockTime while creating the transaction - /// - /// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator. - pub fn nlocktime(&mut self, locktime: absolute::LockTime) -> &mut Self { - self.params.locktime = Some(locktime); - self - } - - /// Build a transaction with a specific version - /// - /// The `version` should always be greater than `0` and greater than `1` if the wallet's - /// descriptors contain an "older" (OP_CSV) operator. - pub fn version(&mut self, version: i32) -> &mut Self { - self.params.version = Some(Version(version)); - self - } - - /// Do not spend change outputs - /// - /// This effectively adds all the change outputs to the "unspendable" list. See - /// [`TxBuilder::unspendable`]. - pub fn do_not_spend_change(&mut self) -> &mut Self { - self.params.change_policy = ChangeSpendPolicy::ChangeForbidden; - self - } - - /// Only spend change outputs - /// - /// This effectively adds all the non-change outputs to the "unspendable" list. See - /// [`TxBuilder::unspendable`]. - pub fn only_spend_change(&mut self) -> &mut Self { - self.params.change_policy = ChangeSpendPolicy::OnlyChange; - self - } - - /// Set a specific [`ChangeSpendPolicy`]. See [`TxBuilder::do_not_spend_change`] and - /// [`TxBuilder::only_spend_change`] for some shortcuts. - pub fn change_policy(&mut self, change_policy: ChangeSpendPolicy) -> &mut Self { - self.params.change_policy = change_policy; - self - } - - /// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::psbt::Input::witness_utxo) field when spending from - /// SegWit descriptors. - /// - /// 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 - } - - /// Fill-in the [`psbt::Output::redeem_script`](bitcoin::psbt::Output::redeem_script) and - /// [`psbt::Output::witness_script`](bitcoin::psbt::Output::witness_script) fields. - /// - /// This is useful for signers which always require it, like ColdCard hardware wallets. - pub fn include_output_redeem_witness_script(&mut self) -> &mut Self { - self.params.include_output_redeem_witness_script = true; - self - } - - /// Fill-in the `PSBT_GLOBAL_XPUB` field with the extended keys contained in both the external - /// and internal descriptors - /// - /// This is useful for offline signers that take part to a multisig. Some hardware wallets like - /// BitBox and ColdCard are known to require this. - pub fn add_global_xpubs(&mut self) -> &mut Self { - self.params.add_global_xpubs = true; - self - } - - /// Spend all the available inputs. This respects filters like [`TxBuilder::unspendable`] and the change policy. - pub fn drain_wallet(&mut self) -> &mut Self { - self.params.drain_wallet = true; - self - } - - /// Choose the coin selection algorithm - /// - /// Overrides the [`DefaultCoinSelectionAlgorithm`]. - /// - /// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder. - pub fn coin_selection( - self, - coin_selection: P, - ) -> TxBuilder<'a, P, Ctx> { - TxBuilder { - wallet: self.wallet, - params: self.params, - coin_selection, - phantom: PhantomData, - } - } - - /// Enable signaling RBF - /// - /// This will use the default nSequence value of `0xFFFFFFFD`. - pub fn enable_rbf(&mut self) -> &mut Self { - self.params.rbf = Some(RbfValue::Default); - self - } - - /// Enable signaling RBF with a specific nSequence value - /// - /// This can cause conflicts if the wallet's descriptors contain an "older" (OP_CSV) operator - /// and the given `nsequence` is lower than the CSV value. - /// - /// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not - /// be a valid nSequence to signal RBF. - pub fn enable_rbf_with_sequence(&mut self, nsequence: Sequence) -> &mut Self { - self.params.rbf = Some(RbfValue::Value(nsequence)); - self - } - - /// Set the current blockchain height. - /// - /// This will be used to: - /// 1. Set the nLockTime for preventing fee sniping. - /// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`]. - /// 2. Decide whether coinbase outputs are mature or not. If the coinbase outputs are not - /// mature at `current_height`, we ignore them in the coin selection. - /// If you want to create a transaction that spends immature coinbase inputs, manually - /// add them using [`TxBuilder::add_utxos`]. - /// - /// In both cases, if you don't provide a current height, we use the last sync height. - pub fn current_height(&mut self, height: u32) -> &mut Self { - self.params.current_height = - Some(absolute::LockTime::from_height(height).expect("Invalid height")); - self - } - - /// Set whether or not the dust limit is checked. - /// - /// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard. - pub fn allow_dust(&mut self, allow_dust: bool) -> &mut Self { - self.params.allow_dust = allow_dust; - self - } -} - -impl<'a, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, Cs, Ctx> { - /// Finish building the transaction. - /// - /// Returns a new [`Psbt`] per [`BIP174`]. - /// - /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki - pub fn finish(self) -> Result { - self.wallet - .borrow_mut() - .create_tx(self.coin_selection, self.params) - } -} - -#[derive(Debug)] -/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`] -pub enum AddUtxoError { - /// Happens when trying to spend an UTXO that is not in the internal database - UnknownUtxo(OutPoint), -} - -impl fmt::Display for AddUtxoError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::UnknownUtxo(outpoint) => write!( - f, - "UTXO not found in the internal database for txid: {} with vout: {}", - outpoint.txid, outpoint.vout - ), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for AddUtxoError {} - -#[derive(Debug)] -/// Error returned from [`TxBuilder::add_foreign_utxo`]. -pub enum AddForeignUtxoError { - /// Foreign utxo outpoint txid does not match PSBT input txid - InvalidTxid { - /// PSBT input txid - input_txid: Txid, - /// Foreign UTXO outpoint - foreign_utxo: OutPoint, - }, - /// Requested outpoint doesn't exist in the tx (vout greater than available outputs) - InvalidOutpoint(OutPoint), - /// Foreign utxo missing witness_utxo or non_witness_utxo - MissingUtxo, -} - -impl fmt::Display for AddForeignUtxoError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InvalidTxid { - input_txid, - foreign_utxo, - } => write!( - f, - "Foreign UTXO outpoint txid: {} does not match PSBT input txid: {}", - foreign_utxo.txid, input_txid, - ), - Self::InvalidOutpoint(outpoint) => write!( - f, - "Requested outpoint doesn't exist for txid: {} with vout: {}", - outpoint.txid, outpoint.vout, - ), - Self::MissingUtxo => write!(f, "Foreign utxo missing witness_utxo or non_witness_utxo"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for AddForeignUtxoError {} - -#[derive(Debug)] -/// Error returned from [`TxBuilder::allow_shrinking`] -pub enum AllowShrinkingError { - /// Script/PubKey was not in the original transaction - MissingScriptPubKey(ScriptBuf), -} - -impl fmt::Display for AllowShrinkingError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::MissingScriptPubKey(script_buf) => write!( - f, - "Script/PubKey was not in the original transaction: {}", - script_buf, - ), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for AllowShrinkingError {} - -impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs, CreateTx> { - /// Replace the recipients already added with a new list - pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, Amount)>) -> &mut Self { - self.params.recipients = recipients - .into_iter() - .map(|(script, amount)| (script, amount.to_sat())) - .collect(); - self - } - - /// Add a recipient to the internal list - pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: Amount) -> &mut Self { - self.params - .recipients - .push((script_pubkey, amount.to_sat())); - self - } - - /// Add data as an output, using OP_RETURN - pub fn add_data>(&mut self, data: &T) -> &mut Self { - let script = ScriptBuf::new_op_return(data); - self.add_recipient(script, Amount::ZERO); - self - } - - /// Sets the address to *drain* excess coins to. - /// - /// Usually, when there are excess coins they are sent to a change address generated by the - /// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of - /// your choosing. Just as with a change output, if the drain output is not needed (the excess - /// coins are too small) it will not be included in the resulting transaction. The only - /// difference is that it is valid to use `drain_to` without setting any ordinary recipients - /// with [`add_recipient`] (but it is perfectly fine to add recipients as well). - /// - /// If you choose not to set any recipients, you should either provide the utxos that the - /// transaction should spend via [`add_utxos`], or set [`drain_wallet`] to spend all of them. - /// - /// When bumping the fees of a transaction made with this option, you probably want to - /// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees. - /// - /// # Example - /// - /// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a - /// single address. - /// - /// ``` - /// # use std::str::FromStr; - /// # use bitcoin::*; - /// # use bdk::*; - /// # use bdk::wallet::ChangeSet; - /// # use bdk::wallet::error::CreateTxError; - /// # use bdk::wallet::tx_builder::CreateTx; - /// # use bdk_persist::PersistBackend; - /// # use anyhow::Error; - /// # let to_address = - /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") - /// .unwrap() - /// .assume_checked(); - /// # let mut wallet = doctest_wallet!(); - /// let mut tx_builder = wallet.build_tx(); - /// - /// tx_builder - /// // Spend all outputs in this wallet. - /// .drain_wallet() - /// // Send the excess (which is all the coins minus the fee) to this address. - /// .drain_to(to_address.script_pubkey()) - /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")) - /// .enable_rbf(); - /// let psbt = tx_builder.finish()?; - /// # Ok::<(), anyhow::Error>(()) - /// ``` - /// - /// [`allow_shrinking`]: Self::allow_shrinking - /// [`add_recipient`]: Self::add_recipient - /// [`add_utxos`]: Self::add_utxos - /// [`drain_wallet`]: Self::drain_wallet - pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self { - self.params.drain_to = Some(script_pubkey); - self - } -} - -// methods supported only by bump_fee -impl<'a> TxBuilder<'a, DefaultCoinSelectionAlgorithm, BumpFee> { - /// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this - /// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet - /// will attempt to find a change output to shrink instead. - /// - /// **Note** that the output may shrink to below the dust limit and therefore be removed. If it is - /// preserved then it is currently not guaranteed to be in the same position as it was - /// originally. - /// - /// Returns an `Err` if `script_pubkey` can't be found among the recipients of the - /// transaction we are bumping. - pub fn allow_shrinking( - &mut self, - script_pubkey: ScriptBuf, - ) -> Result<&mut Self, AllowShrinkingError> { - match self - .params - .recipients - .iter() - .position(|(recipient_script, _)| *recipient_script == script_pubkey) - { - Some(position) => { - self.params.recipients.remove(position); - self.params.drain_to = Some(script_pubkey); - Ok(self) - } - None => Err(AllowShrinkingError::MissingScriptPubKey(script_pubkey)), - } - } -} - -/// Ordering of the transaction's inputs and outputs -#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] -pub enum TxOrdering { - /// Randomized (default) - #[default] - Shuffle, - /// Unchanged - Untouched, - /// BIP69 / Lexicographic - Bip69Lexicographic, -} - -impl TxOrdering { - /// Sort transaction inputs and outputs by [`TxOrdering`] variant - pub fn sort_tx(&self, tx: &mut Transaction) { - match self { - TxOrdering::Untouched => {} - TxOrdering::Shuffle => { - use rand::seq::SliceRandom; - let mut rng = rand::thread_rng(); - tx.input.shuffle(&mut rng); - tx.output.shuffle(&mut rng); - } - TxOrdering::Bip69Lexicographic => { - tx.input.sort_unstable_by_key(|txin| { - (txin.previous_output.txid, txin.previous_output.vout) - }); - tx.output - .sort_unstable_by_key(|txout| (txout.value, txout.script_pubkey.clone())); - } - } - } -} - -/// Transaction version -/// -/// Has a default value of `1` -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] -pub(crate) struct Version(pub(crate) i32); - -impl Default for Version { - fn default() -> Self { - Version(1) - } -} - -/// RBF nSequence value -/// -/// Has a default value of `0xFFFFFFFD` -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] -pub(crate) enum RbfValue { - Default, - Value(Sequence), -} - -impl RbfValue { - pub(crate) fn get_value(&self) -> Sequence { - match self { - RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME, - RbfValue::Value(v) => *v, - } - } -} - -/// Policy regarding the use of change outputs when creating a transaction -#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] -pub enum ChangeSpendPolicy { - /// Use both change and non-change outputs (default) - #[default] - ChangeAllowed, - /// Only use change outputs (see [`TxBuilder::only_spend_change`]) - OnlyChange, - /// Only use non-change outputs (see [`TxBuilder::do_not_spend_change`]) - ChangeForbidden, -} - -impl ChangeSpendPolicy { - pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool { - match self { - ChangeSpendPolicy::ChangeAllowed => true, - ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal, - ChangeSpendPolicy::ChangeForbidden => utxo.keychain == KeychainKind::External, - } - } -} - -#[cfg(test)] -mod test { - const ORDERING_TEST_TX: &str = "0200000003c26f3eb7932f7acddc5ddd26602b77e7516079b03090a16e2c2f54\ - 85d1fd600f0100000000ffffffffc26f3eb7932f7acddc5ddd26602b77e75160\ - 79b03090a16e2c2f5485d1fd600f0000000000ffffffff571fb3e02278217852\ - dd5d299947e2b7354a639adc32ec1fa7b82cfb5dec530e0500000000ffffffff\ - 03e80300000000000002aaeee80300000000000001aa200300000000000001ff\ - 00000000"; - macro_rules! ordering_test_tx { - () => { - deserialize::(&Vec::::from_hex(ORDERING_TEST_TX).unwrap()) - .unwrap() - }; - } - - use bdk_chain::ConfirmationTime; - use bitcoin::consensus::deserialize; - use bitcoin::hex::FromHex; - use bitcoin::TxOut; - - use super::*; - - #[test] - fn test_output_ordering_default_shuffle() { - assert_eq!(TxOrdering::default(), TxOrdering::Shuffle); - } - - #[test] - fn test_output_ordering_untouched() { - let original_tx = ordering_test_tx!(); - let mut tx = original_tx.clone(); - - TxOrdering::Untouched.sort_tx(&mut tx); - - assert_eq!(original_tx, tx); - } - - #[test] - fn test_output_ordering_shuffle() { - let original_tx = ordering_test_tx!(); - let mut tx = original_tx.clone(); - - (0..40) - .find(|_| { - TxOrdering::Shuffle.sort_tx(&mut tx); - original_tx.input != tx.input - }) - .expect("it should have moved the inputs at least once"); - - let mut tx = original_tx.clone(); - (0..40) - .find(|_| { - TxOrdering::Shuffle.sort_tx(&mut tx); - original_tx.output != tx.output - }) - .expect("it should have moved the outputs at least once"); - } - - #[test] - fn test_output_ordering_bip69() { - use core::str::FromStr; - - let original_tx = ordering_test_tx!(); - let mut tx = original_tx; - - TxOrdering::Bip69Lexicographic.sort_tx(&mut tx); - - assert_eq!( - tx.input[0].previous_output, - bitcoin::OutPoint::from_str( - "0e53ec5dfb2cb8a71fec32dc9a634a35b7e24799295ddd5278217822e0b31f57:5" - ) - .unwrap() - ); - assert_eq!( - tx.input[1].previous_output, - bitcoin::OutPoint::from_str( - "0f60fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2:0" - ) - .unwrap() - ); - assert_eq!( - tx.input[2].previous_output, - bitcoin::OutPoint::from_str( - "0f60fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2:1" - ) - .unwrap() - ); - - assert_eq!(tx.output[0].value.to_sat(), 800); - assert_eq!(tx.output[1].script_pubkey, ScriptBuf::from(vec![0xAA])); - assert_eq!( - tx.output[2].script_pubkey, - ScriptBuf::from(vec![0xAA, 0xEE]) - ); - } - - fn get_test_utxos() -> Vec { - use bitcoin::hashes::Hash; - - vec![ - LocalOutput { - outpoint: OutPoint { - txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(), - vout: 0, - }, - txout: TxOut::NULL, - keychain: KeychainKind::External, - is_spent: false, - confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 }, - derivation_index: 0, - }, - LocalOutput { - outpoint: OutPoint { - txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(), - vout: 1, - }, - txout: TxOut::NULL, - keychain: KeychainKind::Internal, - is_spent: false, - confirmation_time: ConfirmationTime::Confirmed { - height: 32, - time: 42, - }, - derivation_index: 1, - }, - ] - } - - #[test] - fn test_change_spend_policy_default() { - let change_spend_policy = ChangeSpendPolicy::default(); - let filtered = get_test_utxos() - .into_iter() - .filter(|u| change_spend_policy.is_satisfied_by(u)) - .count(); - - assert_eq!(filtered, 2); - } - - #[test] - fn test_change_spend_policy_no_internal() { - let change_spend_policy = ChangeSpendPolicy::ChangeForbidden; - let filtered = get_test_utxos() - .into_iter() - .filter(|u| change_spend_policy.is_satisfied_by(u)) - .collect::>(); - - assert_eq!(filtered.len(), 1); - assert_eq!(filtered[0].keychain, KeychainKind::External); - } - - #[test] - fn test_change_spend_policy_only_internal() { - let change_spend_policy = ChangeSpendPolicy::OnlyChange; - let filtered = get_test_utxos() - .into_iter() - .filter(|u| change_spend_policy.is_satisfied_by(u)) - .collect::>(); - - assert_eq!(filtered.len(), 1); - assert_eq!(filtered[0].keychain, KeychainKind::Internal); - } - - #[test] - fn test_default_tx_version_1() { - let version = Version::default(); - assert_eq!(version.0, 1); - } -} diff --git a/crates/bdk/src/wallet/utils.rs b/crates/bdk/src/wallet/utils.rs deleted file mode 100644 index 208a88df..00000000 --- a/crates/bdk/src/wallet/utils.rs +++ /dev/null @@ -1,185 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -use bitcoin::secp256k1::{All, Secp256k1}; -use bitcoin::{absolute, Script, Sequence}; - -use miniscript::{MiniscriptKey, Satisfier, ToPublicKey}; - -/// Trait to check if a value is below the dust limit. -/// We are performing dust value calculation for a given script public key using rust-bitcoin to -/// keep it compatible with network dust rate -// we implement this trait to make sure we don't mess up the comparison with off-by-one like a < -// instead of a <= etc. -pub trait IsDust { - /// Check whether or not a value is below dust limit - fn is_dust(&self, script: &Script) -> bool; -} - -impl IsDust for u64 { - fn is_dust(&self, script: &Script) -> bool { - *self < script.dust_value().to_sat() - } -} - -pub struct After { - pub current_height: Option, - pub assume_height_reached: bool, -} - -impl After { - pub(crate) fn new(current_height: Option, assume_height_reached: bool) -> After { - After { - current_height, - assume_height_reached, - } - } -} - -pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool { - // The RBF value must enable relative timelocks - if !rbf.is_relative_lock_time() { - return false; - } - - // Both values should be represented in the same unit (either time-based or - // block-height based) - if rbf.is_time_locked() != csv.is_time_locked() { - return false; - } - - // The value should be at least `csv` - if rbf < csv { - return false; - } - - true -} - -impl Satisfier for After { - fn check_after(&self, n: absolute::LockTime) -> bool { - if let Some(current_height) = self.current_height { - current_height >= n.to_consensus_u32() - } else { - self.assume_height_reached - } - } -} - -pub struct Older { - pub current_height: Option, - pub create_height: Option, - pub assume_height_reached: bool, -} - -impl Older { - pub(crate) fn new( - current_height: Option, - create_height: Option, - assume_height_reached: bool, - ) -> Older { - Older { - current_height, - create_height, - assume_height_reached, - } - } -} - -impl Satisfier for Older { - fn check_older(&self, n: Sequence) -> bool { - if let Some(current_height) = self.current_height { - // TODO: test >= / > - current_height - >= self - .create_height - .unwrap_or(0) - .checked_add(n.to_consensus_u32()) - .expect("Overflowing addition") - } else { - self.assume_height_reached - } - } -} - -pub(crate) type SecpCtx = Secp256k1; - -#[cfg(test)] -mod test { - // When nSequence is lower than this flag the timelock is interpreted as block-height-based, - // otherwise it's time-based - pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22; - - use super::{check_nsequence_rbf, IsDust}; - use crate::bitcoin::{Address, Network, Sequence}; - use core::str::FromStr; - - #[test] - fn test_is_dust() { - let script_p2pkh = Address::from_str("1GNgwA8JfG7Kc8akJ8opdNWJUihqUztfPe") - .unwrap() - .require_network(Network::Bitcoin) - .unwrap() - .script_pubkey(); - assert!(script_p2pkh.is_p2pkh()); - assert!(545.is_dust(&script_p2pkh)); - assert!(!546.is_dust(&script_p2pkh)); - - let script_p2wpkh = Address::from_str("bc1qxlh2mnc0yqwas76gqq665qkggee5m98t8yskd8") - .unwrap() - .require_network(Network::Bitcoin) - .unwrap() - .script_pubkey(); - assert!(script_p2wpkh.is_p2wpkh()); - assert!(293.is_dust(&script_p2wpkh)); - assert!(!294.is_dust(&script_p2wpkh)); - } - - #[test] - fn test_check_nsequence_rbf_msb_set() { - let result = check_nsequence_rbf(Sequence(0x80000000), Sequence(5000)); - assert!(!result); - } - - #[test] - fn test_check_nsequence_rbf_lt_csv() { - let result = check_nsequence_rbf(Sequence(4000), Sequence(5000)); - assert!(!result); - } - - #[test] - fn test_check_nsequence_rbf_different_unit() { - let result = - check_nsequence_rbf(Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000), Sequence(5000)); - assert!(!result); - } - - #[test] - fn test_check_nsequence_rbf_mask() { - let result = check_nsequence_rbf(Sequence(0x3f + 10_000), Sequence(5000)); - assert!(result); - } - - #[test] - fn test_check_nsequence_rbf_same_unit_blocks() { - let result = check_nsequence_rbf(Sequence(10_000), Sequence(5000)); - assert!(result); - } - - #[test] - fn test_check_nsequence_rbf_same_unit_time() { - let result = check_nsequence_rbf( - Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000), - Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000), - ); - assert!(result); - } -} diff --git a/crates/bdk/tests/common.rs b/crates/bdk/tests/common.rs deleted file mode 100644 index ec421551..00000000 --- a/crates/bdk/tests/common.rs +++ /dev/null @@ -1,172 +0,0 @@ -#![allow(unused)] - -use bdk::{KeychainKind, LocalOutput, Wallet}; -use bdk_chain::indexed_tx_graph::Indexer; -use bdk_chain::{BlockId, ConfirmationTime}; -use bitcoin::hashes::Hash; -use bitcoin::{ - transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut, - Txid, -}; -use std::str::FromStr; - -// Return a fake wallet that appears to be funded for testing. -// -// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000 -// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 -// sats are the transaction fee. -pub fn get_funded_wallet_with_change( - descriptor: &str, - change: Option<&str>, -) -> (Wallet, bitcoin::Txid) { - let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest).unwrap(); - let change_address = wallet.peek_address(KeychainKind::External, 0).address; - let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5") - .expect("address") - .require_network(Network::Regtest) - .unwrap(); - - let tx0 = Transaction { - version: transaction::Version::ONE, - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint { - txid: Txid::all_zeros(), - vout: 0, - }, - script_sig: Default::default(), - sequence: Default::default(), - witness: Default::default(), - }], - output: vec![TxOut { - value: Amount::from_sat(76_000), - script_pubkey: change_address.script_pubkey(), - }], - }; - - let tx1 = Transaction { - version: transaction::Version::ONE, - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint { - txid: tx0.txid(), - vout: 0, - }, - script_sig: Default::default(), - sequence: Default::default(), - witness: Default::default(), - }], - output: vec![ - TxOut { - value: Amount::from_sat(50_000), - script_pubkey: change_address.script_pubkey(), - }, - TxOut { - value: Amount::from_sat(25_000), - script_pubkey: sendto_address.script_pubkey(), - }, - ], - }; - - wallet - .insert_checkpoint(BlockId { - height: 1_000, - hash: BlockHash::all_zeros(), - }) - .unwrap(); - wallet - .insert_checkpoint(BlockId { - height: 2_000, - hash: BlockHash::all_zeros(), - }) - .unwrap(); - wallet - .insert_tx( - tx0, - ConfirmationTime::Confirmed { - height: 1_000, - time: 100, - }, - ) - .unwrap(); - wallet - .insert_tx( - tx1.clone(), - ConfirmationTime::Confirmed { - height: 2_000, - time: 200, - }, - ) - .unwrap(); - - (wallet, tx1.txid()) -} - -// Return a fake wallet that appears to be funded for testing. -// -// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000 -// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 -// sats are the transaction fee. -pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) { - get_funded_wallet_with_change(descriptor, None) -} - -pub fn get_test_wpkh() -> &'static str { - "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)" -} - -pub fn get_test_single_sig_csv() -> &'static str { - // and(pk(Alice),older(6)) - "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))" -} - -pub fn get_test_a_or_b_plus_csv() -> &'static str { - // or(pk(Alice),and(pk(Bob),older(144))) - "wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))" -} - -pub fn get_test_single_sig_cltv() -> &'static str { - // and(pk(Alice),after(100000)) - "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))" -} - -pub fn get_test_tr_single_sig() -> &'static str { - "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)" -} - -pub fn get_test_tr_with_taptree() -> &'static str { - "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" -} - -pub fn get_test_tr_with_taptree_both_priv() -> &'static str { - "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(cNaQCDwmmh4dS9LzCgVtyy1e1xjCJ21GUDHe9K98nzb689JvinGV)})" -} - -pub fn get_test_tr_repeated_key() -> &'static str { - "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})" -} - -pub fn get_test_tr_single_sig_xprv() -> &'static str { - "tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)" -} - -pub fn get_test_tr_with_taptree_xprv() -> &'static str { - "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" -} - -pub fn get_test_tr_dup_keys() -> &'static str { - "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" -} - -/// Construct a new [`FeeRate`] from the given raw `sat_vb` feerate. This is -/// useful in cases where we want to create a feerate from a `f64`, as the -/// traditional [`FeeRate::from_sat_per_vb`] method will only accept an integer. -/// -/// **Note** this 'quick and dirty' conversion should only be used when the input -/// parameter has units of `satoshis/vbyte` **AND** is not expected to overflow, -/// or else the resulting value will be inaccurate. -pub fn feerate_unchecked(sat_vb: f64) -> FeeRate { - // 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu - let sat_kwu = (sat_vb * 250.0).ceil() as u64; - FeeRate::from_sat_per_kwu(sat_kwu) -} diff --git a/crates/bdk/tests/psbt.rs b/crates/bdk/tests/psbt.rs deleted file mode 100644 index 820d2d1d..00000000 --- a/crates/bdk/tests/psbt.rs +++ /dev/null @@ -1,221 +0,0 @@ -use bdk::bitcoin::{Amount, FeeRate, Psbt, TxIn}; -use bdk::{psbt, KeychainKind, SignOptions}; -use core::str::FromStr; -mod common; -use common::*; - -// 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::from_str(PSBT_STR).unwrap(); - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let send_to = wallet.peek_address(KeychainKind::External, 0); - let mut builder = wallet.build_tx(); - builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000)); - let mut psbt = builder.finish().unwrap(); - psbt.inputs.push(psbt_bip.inputs[0].clone()); - let options = SignOptions { - trust_witness_utxo: true, - ..Default::default() - }; - let _ = wallet.sign(&mut psbt, options).unwrap(); -} - -#[test] -#[should_panic(expected = "InputIndexOutOfRange")] -fn test_psbt_malformed_psbt_input_segwit() { - let psbt_bip = Psbt::from_str(PSBT_STR).unwrap(); - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let send_to = wallet.peek_address(KeychainKind::External, 0); - let mut builder = wallet.build_tx(); - builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000)); - let mut psbt = builder.finish().unwrap(); - psbt.inputs.push(psbt_bip.inputs[1].clone()); - let options = SignOptions { - trust_witness_utxo: true, - ..Default::default() - }; - let _ = wallet.sign(&mut psbt, options).unwrap(); -} - -#[test] -#[should_panic(expected = "InputIndexOutOfRange")] -fn test_psbt_malformed_tx_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let send_to = wallet.peek_address(KeychainKind::External, 0); - let mut builder = wallet.build_tx(); - builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000)); - let mut psbt = builder.finish().unwrap(); - psbt.unsigned_tx.input.push(TxIn::default()); - let options = SignOptions { - trust_witness_utxo: true, - ..Default::default() - }; - let _ = wallet.sign(&mut psbt, options).unwrap(); -} - -#[test] -fn test_psbt_sign_with_finalized() { - let psbt_bip = Psbt::from_str(PSBT_STR).unwrap(); - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let send_to = wallet.peek_address(KeychainKind::External, 0); - let mut builder = wallet.build_tx(); - builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000)); - let mut psbt = builder.finish().unwrap(); - - // add a finalized input - psbt.inputs.push(psbt_bip.inputs[0].clone()); - psbt.unsigned_tx - .input - .push(psbt_bip.unsigned_tx.input[0].clone()); - - let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); -} - -#[test] -fn test_psbt_fee_rate_with_witness_utxo() { - use psbt::PsbtUtils; - - let expected_fee_rate = FeeRate::from_sat_per_kwu(310); - - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.peek_address(KeychainKind::External, 0); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(expected_fee_rate); - let mut psbt = builder.finish().unwrap(); - let fee_amount = psbt.fee_amount(); - assert!(fee_amount.is_some()); - - let unfinalized_fee_rate = psbt.fee_rate().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let finalized_fee_rate = psbt.fee_rate().unwrap(); - assert!(finalized_fee_rate >= expected_fee_rate); - assert!(finalized_fee_rate < unfinalized_fee_rate); -} - -#[test] -fn test_psbt_fee_rate_with_nonwitness_utxo() { - use psbt::PsbtUtils; - - let expected_fee_rate = FeeRate::from_sat_per_kwu(310); - - let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.peek_address(KeychainKind::External, 0); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(expected_fee_rate); - let mut psbt = builder.finish().unwrap(); - let fee_amount = psbt.fee_amount(); - assert!(fee_amount.is_some()); - let unfinalized_fee_rate = psbt.fee_rate().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let finalized_fee_rate = psbt.fee_rate().unwrap(); - assert!(finalized_fee_rate >= expected_fee_rate); - assert!(finalized_fee_rate < unfinalized_fee_rate); -} - -#[test] -fn test_psbt_fee_rate_with_missing_txout() { - use psbt::PsbtUtils; - - let expected_fee_rate = FeeRate::from_sat_per_kwu(310); - - let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wpkh_wallet.peek_address(KeychainKind::External, 0); - let mut builder = wpkh_wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(expected_fee_rate); - let mut wpkh_psbt = builder.finish().unwrap(); - - wpkh_psbt.inputs[0].witness_utxo = None; - wpkh_psbt.inputs[0].non_witness_utxo = None; - assert!(wpkh_psbt.fee_amount().is_none()); - assert!(wpkh_psbt.fee_rate().is_none()); - - let (mut pkh_wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = pkh_wallet.peek_address(KeychainKind::External, 0); - let mut builder = pkh_wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - builder.fee_rate(expected_fee_rate); - let mut pkh_psbt = builder.finish().unwrap(); - - pkh_psbt.inputs[0].non_witness_utxo = None; - assert!(pkh_psbt.fee_amount().is_none()); - assert!(pkh_psbt.fee_rate().is_none()); -} - -#[test] -fn test_psbt_multiple_internalkey_signers() { - use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper}; - use bdk::KeychainKind; - use bitcoin::key::TapTweak; - use bitcoin::secp256k1::{schnorr, Keypair, Message, Secp256k1, XOnlyPublicKey}; - use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType}; - use bitcoin::{PrivateKey, TxOut}; - use std::sync::Arc; - - let secp = Secp256k1::new(); - let wif = "cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG"; - let desc = format!("tr({})", wif); - let prv = PrivateKey::from_wif(wif).unwrap(); - let keypair = Keypair::from_secret_key(&secp, &prv.inner); - - let (mut wallet, _) = get_funded_wallet(&desc); - let to_spend = wallet.get_balance().total(); - let send_to = wallet.peek_address(KeychainKind::External, 0); - let mut builder = wallet.build_tx(); - builder.drain_to(send_to.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - let unsigned_tx = psbt.unsigned_tx.clone(); - - // Adds a signer for the wrong internal key, bdk should not use this key to sign - wallet.add_signer( - KeychainKind::External, - // A signerordering lower than 100, bdk will use this signer first - SignerOrdering(0), - Arc::new(SignerWrapper::new( - PrivateKey::from_wif("5J5PZqvCe1uThJ3FZeUUFLCh2FuK9pZhtEK4MzhNmugqTmxCdwE").unwrap(), - SignerContext::Tap { - is_internal_key: true, - }, - )), - ); - let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); - assert!(finalized); - - // To verify, we need the signature, message, and pubkey - let witness = psbt.inputs[0].final_script_witness.as_ref().unwrap(); - assert!(!witness.is_empty()); - let signature = schnorr::Signature::from_slice(witness.iter().next().unwrap()).unwrap(); - - // the prevout we're spending - let prevouts = &[TxOut { - script_pubkey: send_to.script_pubkey(), - value: to_spend, - }]; - let prevouts = Prevouts::All(prevouts); - let input_index = 0; - let mut sighash_cache = SighashCache::new(unsigned_tx); - let sighash = sighash_cache - .taproot_key_spend_signature_hash(input_index, &prevouts, TapSighashType::Default) - .unwrap(); - let message = Message::from(sighash); - - // add tweak. this was taken from `signer::sign_psbt_schnorr` - let keypair = keypair.tap_tweak(&secp, None).to_inner(); - let (xonlykey, _parity) = XOnlyPublicKey::from_keypair(&keypair); - - // Must verify if we used the correct key to sign - let verify_res = secp.verify_schnorr(&signature, &message, &xonlykey); - assert!(verify_res.is_ok(), "The wrong internal key was used"); -} diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs deleted file mode 100644 index 9da65d54..00000000 --- a/crates/bdk/tests/wallet.rs +++ /dev/null @@ -1,3947 +0,0 @@ -use std::str::FromStr; - -use assert_matches::assert_matches; -use bdk::descriptor::{calc_checksum, IntoWalletDescriptor}; -use bdk::psbt::PsbtUtils; -use bdk::signer::{SignOptions, SignerError}; -use bdk::wallet::coin_selection::{self, LargestFirstCoinSelection}; -use bdk::wallet::error::CreateTxError; -use bdk::wallet::tx_builder::AddForeignUtxoError; -use bdk::wallet::NewError; -use bdk::wallet::{AddressInfo, Balance, Wallet}; -use bdk::KeychainKind; -use bdk_chain::collections::BTreeMap; -use bdk_chain::COINBASE_MATURITY; -use bdk_chain::{BlockId, ConfirmationTime}; -use bitcoin::hashes::Hash; -use bitcoin::key::Secp256k1; -use bitcoin::psbt; -use bitcoin::script::PushBytesBuf; -use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; -use bitcoin::taproot::TapNodeHash; -use bitcoin::{ - absolute, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, - Sequence, Transaction, TxIn, TxOut, Txid, Weight, -}; - -mod common; -use common::*; - -fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) -> OutPoint { - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let tx = Transaction { - version: transaction::Version::ONE, - lock_time: absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { - script_pubkey: addr.script_pubkey(), - value: Amount::from_sat(value), - }], - }; - - wallet.insert_tx(tx.clone(), height).unwrap(); - - OutPoint { - txid: tx.txid(), - vout: 0, - } -} - -fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { - let latest_cp = wallet.latest_checkpoint(); - let height = latest_cp.height(); - let anchor = if height == 0 { - ConfirmationTime::Unconfirmed { last_seen: 0 } - } else { - ConfirmationTime::Confirmed { height, time: 0 } - }; - receive_output(wallet, value, anchor) -} - -// The satisfaction size of a P2WPKH is 112 WU = -// 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len) -// On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for -// a total of 105 WU. -// Here, we push just once for simplicity, so we have to add an extra byte for the missing -// OP_PUSH. -const P2WPKH_FAKE_WITNESS_SIZE: usize = 106; - -const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48]; - -#[test] -fn load_recovers_wallet() { - let temp_dir = tempfile::tempdir().expect("must create tempdir"); - let file_path = temp_dir.path().join("store.db"); - - // create new wallet - let wallet_spk_index = { - let db = bdk_file_store::Store::create_new(DB_MAGIC, &file_path).expect("must create db"); - let mut wallet = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet) - .expect("must init wallet"); - - wallet.reveal_next_address(KeychainKind::External).unwrap(); - wallet.spk_index().clone() - }; - - // recover wallet - { - let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db"); - let wallet = Wallet::load(db).expect("must recover wallet"); - assert_eq!(wallet.network(), Network::Testnet); - assert_eq!( - wallet.spk_index().keychains().collect::>(), - wallet_spk_index.keychains().collect::>() - ); - assert_eq!( - wallet.spk_index().last_revealed_indices(), - wallet_spk_index.last_revealed_indices() - ); - let secp = Secp256k1::new(); - assert_eq!( - *wallet.get_descriptor_for_keychain(KeychainKind::External), - get_test_tr_single_sig_xprv() - .into_wallet_descriptor(&secp, wallet.network()) - .unwrap() - .0 - ); - } - - // `new` can only be called on empty db - { - let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db"); - let result = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet); - assert!(matches!(result, Err(NewError::NonEmptyDatabase))); - } -} - -#[test] -fn new_or_load() { - let temp_dir = tempfile::tempdir().expect("must create tempdir"); - let file_path = temp_dir.path().join("store.db"); - - // init wallet when non-existent - let wallet_keychains: BTreeMap<_, _> = { - let db = bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path) - .expect("must create db"); - let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet) - .expect("must init wallet"); - wallet.keychains().map(|(k, v)| (*k, v.clone())).collect() - }; - - // wrong network - { - let db = - bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db"); - let err = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Bitcoin) - .expect_err("wrong network"); - assert!( - matches!( - err, - bdk::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch { - got: Some(Network::Testnet), - expected: Network::Bitcoin - } - ), - "err: {}", - err, - ); - } - - // wrong genesis hash - { - let exp_blockhash = BlockHash::all_zeros(); - let got_blockhash = - bitcoin::blockdata::constants::genesis_block(Network::Testnet).block_hash(); - - let db = - bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db"); - let err = Wallet::new_or_load_with_genesis_hash( - get_test_wpkh(), - None, - db, - Network::Testnet, - exp_blockhash, - ) - .expect_err("wrong genesis hash"); - assert!( - matches!( - err, - bdk::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected } - if got == Some(got_blockhash) && expected == exp_blockhash - ), - "err: {}", - err, - ); - } - - // wrong external descriptor - { - let exp_descriptor = get_test_tr_single_sig(); - let got_descriptor = get_test_wpkh() - .into_wallet_descriptor(&Secp256k1::new(), Network::Testnet) - .unwrap() - .0; - - let db = - bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db"); - let err = Wallet::new_or_load(exp_descriptor, None, db, Network::Testnet) - .expect_err("wrong external descriptor"); - assert!( - matches!( - err, - bdk::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain } - if got == &Some(got_descriptor) && keychain == KeychainKind::External - ), - "err: {}", - err, - ); - } - - // wrong internal descriptor - { - let exp_descriptor = Some(get_test_tr_single_sig()); - let got_descriptor = None; - - let db = - bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db"); - let err = Wallet::new_or_load(get_test_wpkh(), exp_descriptor, db, Network::Testnet) - .expect_err("wrong internal descriptor"); - assert!( - matches!( - err, - bdk::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain } - if got == &got_descriptor && keychain == KeychainKind::Internal - ), - "err: {}", - err, - ); - } - - // all parameters match - { - let db = - bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db"); - let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet) - .expect("must recover wallet"); - assert_eq!(wallet.network(), Network::Testnet); - assert!(wallet - .keychains() - .map(|(k, v)| (*k, v.clone())) - .eq(wallet_keychains)); - } -} - -#[test] -fn test_descriptor_checksum() { - let (wallet, _) = get_funded_wallet(get_test_wpkh()); - let checksum = wallet.descriptor_checksum(KeychainKind::External); - assert_eq!(checksum.len(), 8); - - let raw_descriptor = wallet - .keychains() - .next() - .unwrap() - .1 - .to_string() - .split_once('#') - .unwrap() - .0 - .to_string(); - assert_eq!(calc_checksum(&raw_descriptor).unwrap(), checksum); -} - -#[test] -fn test_get_funded_wallet_balance() { - let (wallet, _) = get_funded_wallet(get_test_wpkh()); - - // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 - // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 - // sats are the transaction fee. - assert_eq!(wallet.get_balance().confirmed, Amount::from_sat(50_000)); -} - -#[test] -fn test_get_funded_wallet_sent_and_received() { - let (wallet, txid) = get_funded_wallet(get_test_wpkh()); - - let mut tx_amounts: Vec<(Txid, (Amount, Amount))> = wallet - .transactions() - .map(|ct| (ct.tx_node.txid, wallet.sent_and_received(&ct.tx_node))) - .collect(); - tx_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0)); - - let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; - let (sent, received) = wallet.sent_and_received(&tx); - - // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 - // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 - // sats are the transaction fee. - assert_eq!(sent.to_sat(), 76_000); - assert_eq!(received.to_sat(), 50_000); -} - -#[test] -fn test_get_funded_wallet_tx_fees() { - let (wallet, txid) = get_funded_wallet(get_test_wpkh()); - - let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; - let tx_fee = wallet.calculate_fee(&tx).expect("transaction fee"); - - // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 - // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 - // sats are the transaction fee. - assert_eq!(tx_fee, 1000) -} - -#[test] -fn test_get_funded_wallet_tx_fee_rate() { - let (wallet, txid) = get_funded_wallet(get_test_wpkh()); - - let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; - let tx_fee_rate = wallet - .calculate_fee_rate(&tx) - .expect("transaction fee rate"); - - // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 - // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 - // sats are the transaction fee. - - // tx weight = 452 wu, as vbytes = (452 + 3) / 4 = 113 - // fee_rate (sats per kwu) = fee / weight = 1000sat / 0.452kwu = 2212 - // fee_rate (sats per vbyte ceil) = fee / vsize = 1000sat / 113vb = 9 - assert_eq!(tx_fee_rate.to_sat_per_kwu(), 2212); - assert_eq!(tx_fee_rate.to_sat_per_vb_ceil(), 9); -} - -#[test] -fn test_list_output() { - let (wallet, txid) = get_funded_wallet(get_test_wpkh()); - let txos = wallet - .list_output() - .map(|op| (op.outpoint, op)) - .collect::>(); - assert_eq!(txos.len(), 2); - for (op, txo) in txos { - if op.txid == txid { - assert_eq!(txo.txout.value.to_sat(), 50_000); - assert!(!txo.is_spent); - } else { - assert_eq!(txo.txout.value.to_sat(), 76_000); - assert!(txo.is_spent); - } - } -} - -macro_rules! assert_fee_rate { - ($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({ - let psbt = $psbt.clone(); - #[allow(unused_mut)] - let mut tx = $psbt.clone().extract_tx().expect("failed to extract tx"); - $( - $( $add_signature )* - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - )* - - #[allow(unused_mut)] - #[allow(unused_assignments)] - let mut dust_change = false; - $( - $( $dust_change )* - dust_change = true; - )* - - let fee_amount = psbt - .inputs - .iter() - .fold(0, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value.to_sat()) - - psbt - .unsigned_tx - .output - .iter() - .fold(0, |acc, o| acc + o.value.to_sat()); - - assert_eq!(fee_amount, $fees); - - let tx_fee_rate = (Amount::from_sat(fee_amount) / tx.weight()) - .to_sat_per_kwu(); - let fee_rate = $fee_rate.to_sat_per_kwu(); - let half_default = FeeRate::BROADCAST_MIN.checked_div(2) - .unwrap() - .to_sat_per_kwu(); - - if !dust_change { - assert!(tx_fee_rate >= fee_rate && tx_fee_rate - fee_rate < half_default, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate); - } else { - assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate); - } - }); -} - -macro_rules! from_str { - ($e:expr, $t:ty) => {{ - use core::str::FromStr; - <$t>::from_str($e).unwrap() - }}; - - ($e:expr) => { - from_str!($e, _) - }; -} - -#[test] -#[should_panic(expected = "NoRecipients")] -fn test_create_tx_empty_recipients() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - wallet.build_tx().finish().unwrap(); -} - -#[test] -#[should_panic(expected = "NoUtxosSelected")] -fn test_create_tx_manually_selected_empty_utxos() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .manually_selected_only(); - builder.finish().unwrap(); -} - -#[test] -fn test_create_tx_version_0() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .version(0); - assert!(matches!(builder.finish(), Err(CreateTxError::Version0))); -} - -#[test] -fn test_create_tx_version_1_csv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .version(1); - assert!(matches!(builder.finish(), Err(CreateTxError::Version1Csv))); -} - -#[test] -fn test_create_tx_custom_version() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .version(42); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.version.0, 42); -} - -#[test] -fn test_create_tx_default_locktime_is_last_sync_height() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); - - // Since we never synced the wallet we don't have a last_sync_height - // we could use to try to prevent fee sniping. We default to 0. - assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 2_000); -} - -#[test] -fn test_create_tx_fee_sniping_locktime_last_sync() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - - let psbt = builder.finish().unwrap(); - - // If there's no current_height we're left with using the last sync height - assert_eq!( - psbt.unsigned_tx.lock_time.to_consensus_u32(), - wallet.latest_checkpoint().height() - ); -} - -#[test] -fn test_create_tx_default_locktime_cltv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 100_000); -} - -#[test] -fn test_create_tx_custom_locktime() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .current_height(630_001) - .nlocktime(absolute::LockTime::from_height(630_000).unwrap()); - let psbt = builder.finish().unwrap(); - - // When we explicitly specify a nlocktime - // we don't try any fee sniping prevention trick - // (we ignore the current_height) - assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 630_000); -} - -#[test] -fn test_create_tx_custom_locktime_compatible_with_cltv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .nlocktime(absolute::LockTime::from_height(630_000).unwrap()); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 630_000); -} - -#[test] -fn test_create_tx_custom_locktime_incompatible_with_cltv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .nlocktime(absolute::LockTime::from_height(50000).unwrap()); - assert!(matches!(builder.finish(), - Err(CreateTxError::LockTime { requested, required }) - if requested.to_consensus_u32() == 50_000 && required.to_consensus_u32() == 100_000)); -} - -#[test] -fn test_create_tx_no_rbf_csv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); -} - -#[test] -fn test_create_tx_with_default_rbf_csv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - // When CSV is enabled it takes precedence over the rbf value (unless forced by the user). - // It will be set to the OP_CSV value, in this case 6 - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); -} - -#[test] -fn test_create_tx_with_custom_rbf_csv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf_with_sequence(Sequence(3)); - assert!(matches!(builder.finish(), - Err(CreateTxError::RbfSequenceCsv { rbf, csv }) - if rbf.to_consensus_u32() == 3 && csv.to_consensus_u32() == 6)); -} - -#[test] -fn test_create_tx_no_rbf_cltv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); -} - -#[test] -fn test_create_tx_invalid_rbf_sequence() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf_with_sequence(Sequence(0xFFFFFFFE)); - assert!(matches!(builder.finish(), Err(CreateTxError::RbfSequence))); -} - -#[test] -fn test_create_tx_custom_rbf_sequence() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf_with_sequence(Sequence(0xDEADBEEF)); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF)); -} - -#[test] -fn test_create_tx_default_sequence() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); -} - -#[test] -fn test_create_tx_change_policy_no_internal() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .do_not_spend_change(); - assert!(matches!( - builder.finish(), - Err(CreateTxError::ChangePolicyDescriptor) - )); -} - -macro_rules! check_fee { - ($wallet:expr, $psbt: expr) => {{ - let tx = $psbt.clone().extract_tx().expect("failed to extract tx"); - let tx_fee = $wallet.calculate_fee(&tx).ok(); - assert_eq!(tx_fee, $psbt.fee_amount()); - tx_fee - }}; -} - -#[test] -fn test_create_tx_drain_wallet_and_drain_to() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!( - psbt.unsigned_tx.output[0].value.to_sat(), - 50_000 - fee.unwrap_or(0) - ); -} - -#[test] -fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") - .unwrap() - .assume_checked(); - let drain_addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(20_000)) - .drain_to(drain_addr.script_pubkey()) - .drain_wallet(); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - let outputs = psbt.unsigned_tx.output; - - assert_eq!(outputs.len(), 2); - let main_output = outputs - .iter() - .find(|x| x.script_pubkey == addr.script_pubkey()) - .unwrap(); - let drain_output = outputs - .iter() - .find(|x| x.script_pubkey == drain_addr.script_pubkey()) - .unwrap(); - assert_eq!(main_output.value.to_sat(), 20_000,); - assert_eq!(drain_output.value.to_sat(), 30_000 - fee.unwrap_or(0)); -} - -#[test] -fn test_create_tx_drain_to_and_utxos() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let utxos: Vec<_> = wallet.list_unspent().map(|u| u.outpoint).collect(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .add_utxos(&utxos) - .unwrap(); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!( - psbt.unsigned_tx.output[0].value.to_sat(), - 50_000 - fee.unwrap_or(0) - ); -} - -#[test] -#[should_panic(expected = "NoRecipients")] -fn test_create_tx_drain_to_no_drain_wallet_no_utxos() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let drain_addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(drain_addr.script_pubkey()); - builder.finish().unwrap(); -} - -#[test] -fn test_create_tx_default_fee_rate() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::BROADCAST_MIN, @add_signature); -} - -#[test] -fn test_create_tx_custom_fee_rate() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature); -} - -#[test] -fn test_create_tx_absolute_fee() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_absolute(100); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - assert_eq!(fee.unwrap_or(0), 100); - assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!( - psbt.unsigned_tx.output[0].value.to_sat(), - 50_000 - fee.unwrap_or(0) - ); -} - -#[test] -fn test_create_tx_absolute_zero_fee() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_absolute(0); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - assert_eq!(fee.unwrap_or(0), 0); - assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!( - psbt.unsigned_tx.output[0].value.to_sat(), - 50_000 - fee.unwrap_or(0) - ); -} - -#[test] -#[should_panic(expected = "InsufficientFunds")] -fn test_create_tx_absolute_high_fee() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_absolute(60_000); - let _ = builder.finish().unwrap(); -} - -#[test] -fn test_create_tx_add_change() { - use bdk::wallet::tx_builder::TxOrdering; - - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .ordering(TxOrdering::Untouched); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - assert_eq!(psbt.unsigned_tx.output.len(), 2); - assert_eq!(psbt.unsigned_tx.output[0].value.to_sat(), 25_000); - assert_eq!( - psbt.unsigned_tx.output[1].value.to_sat(), - 25_000 - fee.unwrap_or(0) - ); -} - -#[test] -fn test_create_tx_skip_change_dust() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(49_800)); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!(psbt.unsigned_tx.output[0].value.to_sat(), 49_800); - assert_eq!(fee.unwrap_or(0), 200); -} - -#[test] -#[should_panic(expected = "InsufficientFunds")] -fn test_create_tx_drain_to_dust_amount() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - // very high fee rate, so that the only output would be below dust - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_rate(FeeRate::from_sat_per_vb_unchecked(454)); - builder.finish().unwrap(); -} - -#[test] -fn test_create_tx_ordering_respected() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) - .add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)) - .ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - assert_eq!(psbt.unsigned_tx.output.len(), 3); - assert_eq!( - psbt.unsigned_tx.output[0].value.to_sat(), - 10_000 - fee.unwrap_or(0) - ); - assert_eq!(psbt.unsigned_tx.output[1].value.to_sat(), 10_000); - assert_eq!(psbt.unsigned_tx.output[2].value.to_sat(), 30_000); -} - -#[test] -fn test_create_tx_default_sighash() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.inputs[0].sighash_type, None); -} - -#[test] -fn test_create_tx_custom_sighash() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) - .sighash(EcdsaSighashType::Single.into()); - let psbt = builder.finish().unwrap(); - - assert_eq!( - psbt.inputs[0].sighash_type, - Some(EcdsaSighashType::Single.into()) - ); -} - -#[test] -fn test_create_tx_input_hd_keypaths() { - use bitcoin::bip32::{DerivationPath, Fingerprint}; - use core::str::FromStr; - - let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1); - assert_eq!( - psbt.inputs[0].bip32_derivation.values().next().unwrap(), - &( - Fingerprint::from_str("d34db33f").unwrap(), - DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap() - ) - ); -} - -#[test] -fn test_create_tx_output_hd_keypaths() { - use bitcoin::bip32::{DerivationPath, Fingerprint}; - use core::str::FromStr; - - let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); - - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1); - let expected_derivation_path = format!("m/44'/0'/0'/0/{}", addr.index); - assert_eq!( - psbt.outputs[0].bip32_derivation.values().next().unwrap(), - &( - Fingerprint::from_str("d34db33f").unwrap(), - DerivationPath::from_str(&expected_derivation_path).unwrap() - ) - ); -} - -#[test] -fn test_create_tx_set_redeem_script_p2sh() { - use bitcoin::hex::FromHex; - - let (mut wallet, _) = - get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - - assert_eq!( - psbt.inputs[0].redeem_script, - Some(ScriptBuf::from( - Vec::::from_hex( - "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" - ) - .unwrap() - )) - ); - assert_eq!(psbt.inputs[0].witness_script, None); -} - -#[test] -fn test_create_tx_set_witness_script_p2wsh() { - use bitcoin::hex::FromHex; - - let (mut wallet, _) = - get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.inputs[0].redeem_script, None); - assert_eq!( - psbt.inputs[0].witness_script, - Some(ScriptBuf::from( - Vec::::from_hex( - "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" - ) - .unwrap() - )) - ); -} - -#[test] -fn test_create_tx_set_redeem_witness_script_p2wsh_p2sh() { - let (mut wallet, _) = - get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - - let script = ScriptBuf::from_hex( - "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac", - ) - .unwrap(); - - assert_eq!(psbt.inputs[0].redeem_script, Some(script.to_p2wsh())); - assert_eq!(psbt.inputs[0].witness_script, Some(script)); -} - -#[test] -fn test_create_tx_non_witness_utxo() { - let (mut wallet, _) = - get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - - assert!(psbt.inputs[0].non_witness_utxo.is_some()); - assert!(psbt.inputs[0].witness_utxo.is_none()); -} - -#[test] -fn test_create_tx_only_witness_utxo() { - let (mut wallet, _) = - get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .only_witness_utxo() - .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_shwpkh_has_witness_utxo() { - let (mut wallet, _) = - get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - - assert!(psbt.inputs[0].witness_utxo.is_some()); -} - -#[test] -fn test_create_tx_both_non_witness_utxo_and_witness_utxo_default() { - let (mut wallet, _) = - get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - - assert!(psbt.inputs[0].non_witness_utxo.is_some()); - assert!(psbt.inputs[0].witness_utxo.is_some()); -} - -#[test] -fn test_create_tx_add_utxo() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let small_output_tx = Transaction { - input: vec![], - output: vec![TxOut { - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .unwrap() - .script_pubkey(), - value: Amount::from_sat(25_000), - }], - version: transaction::Version::non_standard(0), - lock_time: absolute::LockTime::ZERO, - }; - wallet - .insert_tx( - small_output_tx.clone(), - ConfirmationTime::Unconfirmed { last_seen: 0 }, - ) - .unwrap(); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) - .add_utxo(OutPoint { - txid: small_output_tx.txid(), - vout: 0, - }) - .unwrap(); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - - assert_eq!( - psbt.unsigned_tx.input.len(), - 2, - "should add an additional input since 25_000 < 30_000" - ); - assert_eq!( - sent_received.0, - Amount::from_sat(75_000), - "total should be sum of both inputs" - ); -} - -#[test] -#[should_panic(expected = "InsufficientFunds")] -fn test_create_tx_manually_selected_insufficient() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let small_output_tx = Transaction { - input: vec![], - output: vec![TxOut { - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .unwrap() - .script_pubkey(), - value: Amount::from_sat(25_000), - }], - version: transaction::Version::non_standard(0), - lock_time: absolute::LockTime::ZERO, - }; - - wallet - .insert_tx( - small_output_tx.clone(), - ConfirmationTime::Unconfirmed { last_seen: 0 }, - ) - .unwrap(); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) - .add_utxo(OutPoint { - txid: small_output_tx.txid(), - vout: 0, - }) - .unwrap() - .manually_selected_only(); - builder.finish().unwrap(); -} - -#[test] -#[should_panic(expected = "SpendingPolicyRequired(External)")] -fn test_create_tx_policy_path_required() { - let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)); - builder.finish().unwrap(); -} - -#[test] -fn test_create_tx_policy_path_no_csv() { - let descriptors = get_test_wpkh(); - let mut wallet = Wallet::new_no_persist(descriptors, None, Network::Regtest).unwrap(); - - let tx = Transaction { - version: transaction::Version::non_standard(0), - lock_time: absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .unwrap() - .script_pubkey(), - value: Amount::from_sat(50_000), - }], - }; - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); - let root_id = external_policy.id; - // child #0 is just the key "A" - let path = vec![(root_id, vec![0])].into_iter().collect(); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) - .policy_path(path, KeychainKind::External); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF)); -} - -#[test] -fn test_create_tx_policy_path_use_csv() { - let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); - - let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); - let root_id = external_policy.id; - // child #1 is or(pk(B),older(144)) - let path = vec![(root_id, vec![1])].into_iter().collect(); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) - .policy_path(path, KeychainKind::External); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144)); -} - -#[test] -fn test_create_tx_policy_path_ignored_subtree_with_csv() { - let (mut wallet, _) = get_funded_wallet("wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),or_i(and_v(v:pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(30)),and_v(v:pkh(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(90)))))"); - - let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); - let root_id = external_policy.id; - // child #0 is pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu) - let path = vec![(root_id, vec![0])].into_iter().collect(); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) - .policy_path(path, KeychainKind::External); - let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); -} - -#[test] -fn test_create_tx_global_xpubs_with_origin() { - use bitcoin::bip32; - use bitcoin::hex::FromHex; - - let (mut wallet, _) = get_funded_wallet("wpkh([73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .add_global_xpubs(); - let psbt = builder.finish().unwrap(); - - let key = bip32::Xpub::from_str("tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3").unwrap(); - let fingerprint = bip32::Fingerprint::from_hex("73756c7f").unwrap(); - let path = bip32::DerivationPath::from_str("m/48'/0'/0'/2'").unwrap(); - - assert_eq!(psbt.xpub.len(), 1); - assert_eq!(psbt.xpub.get(&key), Some(&(fingerprint, path))); -} - -#[test] -fn test_add_foreign_utxo() { - let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); - let (wallet2, _) = - get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let utxo = wallet2.list_unspent().next().expect("must take!"); - let foreign_utxo_satisfaction = wallet2 - .get_descriptor_for_keychain(KeychainKind::External) - .max_weight_to_satisfy() - .unwrap(); - - let psbt_input = psbt::Input { - witness_utxo: Some(utxo.txout.clone()), - ..Default::default() - }; - - let mut builder = wallet1.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)) - .only_witness_utxo() - .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) - .unwrap(); - let mut psbt = builder.finish().unwrap(); - wallet1.insert_txout(utxo.outpoint, utxo.txout); - let fee = check_fee!(wallet1, psbt); - let sent_received = - wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - - assert_eq!( - (sent_received.0 - sent_received.1).to_sat(), - 10_000 + fee.unwrap_or(0), - "we should have only net spent ~10_000" - ); - - assert!( - psbt.unsigned_tx - .input - .iter() - .any(|input| input.previous_output == utxo.outpoint), - "foreign_utxo should be in there" - ); - - 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( - &mut psbt, - SignOptions { - trust_witness_utxo: true, - ..Default::default() - }, - ) - .unwrap(); - assert!(finished, "all the inputs should have been signed now"); -} - -#[test] -#[should_panic( - expected = "MissingTxOut([OutPoint { txid: 0x21d7fb1bceda00ab4069fc52d06baa13470803e9050edd16f5736e5d8c4925fd, vout: 0 }])" -)] -fn test_calculate_fee_with_missing_foreign_utxo() { - let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); - let (wallet2, _) = - get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let utxo = wallet2.list_unspent().next().expect("must take!"); - let foreign_utxo_satisfaction = wallet2 - .get_descriptor_for_keychain(KeychainKind::External) - .max_weight_to_satisfy() - .unwrap(); - - let psbt_input = psbt::Input { - witness_utxo: Some(utxo.txout.clone()), - ..Default::default() - }; - - let mut builder = wallet1.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)) - .only_witness_utxo() - .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) - .unwrap(); - let psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - wallet1.calculate_fee(&tx).unwrap(); -} - -#[test] -fn test_add_foreign_utxo_invalid_psbt_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let outpoint = wallet.list_unspent().next().expect("must exist").outpoint; - let foreign_utxo_satisfaction = wallet - .get_descriptor_for_keychain(KeychainKind::External) - .max_weight_to_satisfy() - .unwrap(); - - let mut builder = wallet.build_tx(); - let result = - builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction); - assert!(matches!(result, Err(AddForeignUtxoError::MissingUtxo))); -} - -#[test] -fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { - let (mut wallet1, txid1) = get_funded_wallet(get_test_wpkh()); - let (wallet2, txid2) = - get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - - let utxo2 = wallet2.list_unspent().next().unwrap(); - let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone(); - let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx.clone(); - - let satisfaction_weight = wallet2 - .get_descriptor_for_keychain(KeychainKind::External) - .max_weight_to_satisfy() - .unwrap(); - - let mut builder = wallet1.build_tx(); - assert!( - builder - .add_foreign_utxo( - utxo2.outpoint, - psbt::Input { - non_witness_utxo: Some(tx1.as_ref().clone()), - ..Default::default() - }, - satisfaction_weight - ) - .is_err(), - "should fail when outpoint doesn't match psbt_input" - ); - assert!( - builder - .add_foreign_utxo( - utxo2.outpoint, - psbt::Input { - non_witness_utxo: Some(tx2.as_ref().clone()), - ..Default::default() - }, - satisfaction_weight - ) - .is_ok(), - "should be ok when outpoint does match psbt_input" - ); -} - -#[test] -fn test_add_foreign_utxo_only_witness_utxo() { - let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); - let (wallet2, txid2) = - get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let utxo2 = wallet2.list_unspent().next().unwrap(); - - let satisfaction_weight = wallet2 - .get_descriptor_for_keychain(KeychainKind::External) - .max_weight_to_satisfy() - .unwrap(); - - let mut builder = wallet1.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)); - - { - let mut builder = builder.clone(); - let psbt_input = psbt::Input { - witness_utxo: Some(utxo2.txout.clone()), - ..Default::default() - }; - builder - .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) - .unwrap(); - assert!( - builder.finish().is_err(), - "psbt_input with witness_utxo should fail with only witness_utxo" - ); - } - - { - 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.get_tx(txid2).unwrap().tx_node.tx; - let psbt_input = psbt::Input { - non_witness_utxo: Some(tx2.as_ref().clone()), - ..Default::default() - }; - builder - .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) - .unwrap(); - assert!( - builder.finish().is_ok(), - "psbt_input with non_witness_utxo should succeed by default" - ); - } -} - -#[test] -fn test_get_psbt_input() { - // this should grab a known good utxo and set the input - let (wallet, _) = get_funded_wallet(get_test_wpkh()); - for utxo in wallet.list_unspent() { - let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap(); - assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some()); - } -} - -#[test] -#[should_panic( - expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")" -)] -fn test_create_tx_global_xpubs_origin_missing() { - let (mut wallet, _) = get_funded_wallet("wpkh(tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .add_global_xpubs(); - builder.finish().unwrap(); -} - -#[test] -fn test_create_tx_global_xpubs_master_without_origin() { - use bitcoin::bip32; - use bitcoin::hex::FromHex; - - let (mut wallet, _) = get_funded_wallet("wpkh(tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL/0/*)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .add_global_xpubs(); - let psbt = builder.finish().unwrap(); - - let key = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL").unwrap(); - let fingerprint = bip32::Fingerprint::from_hex("997a323b").unwrap(); - - assert_eq!(psbt.xpub.len(), 1); - assert_eq!( - psbt.xpub.get(&key), - Some(&(fingerprint, bip32::DerivationPath::default())) - ); -} - -#[test] -#[should_panic(expected = "IrreplaceableTransaction")] -fn test_bump_fee_irreplaceable_tx() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); - - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.txid(); - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - wallet.build_fee_bump(txid).unwrap().finish().unwrap(); -} - -#[test] -#[should_panic(expected = "TransactionConfirmed")] -fn test_bump_fee_confirmed_tx() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); - - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.txid(); - - wallet - .insert_tx( - tx, - ConfirmationTime::Confirmed { - height: 42, - time: 42_000, - }, - ) - .unwrap(); - - wallet.build_fee_bump(txid).unwrap().finish().unwrap(); -} - -#[test] -fn test_bump_fee_low_fee_rate() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let feerate = psbt.fee_rate().unwrap(); - - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.txid(); - - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::BROADCAST_MIN); - let res = builder.finish(); - assert_matches!( - res, - Err(CreateTxError::FeeRateTooLow { .. }), - "expected FeeRateTooLow error" - ); - - let required = feerate.to_sat_per_kwu() + 250; // +1 sat/vb - let sat_vb = required as f64 / 250.0; - let expect = format!("Fee rate too low: required {} sat/vb", sat_vb); - assert_eq!(res.unwrap_err().to_string(), expect); -} - -#[test] -#[should_panic(expected = "FeeTooLow")] -fn test_bump_fee_low_abs() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.txid(); - - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(10); - builder.finish().unwrap(); -} - -#[test] -#[should_panic(expected = "FeeTooLow")] -fn test_bump_fee_zero_abs() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.txid(); - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(0); - builder.finish().unwrap(); -} - -#[test] -fn test_bump_fee_reduce_change() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let original_sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let original_fee = check_fee!(wallet, psbt); - - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.txid(); - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(feerate).enable_rbf(); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!(sent_received.0, original_sent_received.0); - assert_eq!( - sent_received.1 + Amount::from_sat(fee.unwrap_or(0)), - original_sent_received.1 + Amount::from_sat(original_fee.unwrap_or(0)) - ); - assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0)); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - Amount::from_sat(25_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(200); - builder.enable_rbf(); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!(sent_received.0, original_sent_received.0); - assert_eq!( - sent_received.1 + Amount::from_sat(fee.unwrap_or(0)), - original_sent_received.1 + Amount::from_sat(original_fee.unwrap_or(0)) - ); - assert!( - fee.unwrap_or(0) > original_fee.unwrap_or(0), - "{} > {}", - fee.unwrap_or(0), - original_fee.unwrap_or(0) - ); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - Amount::from_sat(25_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_eq!(fee.unwrap_or(0), 200); -} - -#[test] -fn test_bump_fee_reduce_single_recipient() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let tx = psbt.clone().extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let original_fee = check_fee!(wallet, psbt); - let txid = tx.txid(); - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .fee_rate(feerate) - .allow_shrinking(addr.script_pubkey()) - .unwrap(); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!(sent_received.0, original_sent_received.0); - assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0)); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.output.len(), 1); - assert_eq!( - tx.output[0].value + Amount::from_sat(fee.unwrap_or(0)), - sent_received.0 - ); - - assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature); -} - -#[test] -fn test_bump_fee_absolute_reduce_single_recipient() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let original_fee = check_fee!(wallet, psbt); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let txid = tx.txid(); - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .allow_shrinking(addr.script_pubkey()) - .unwrap() - .fee_absolute(300); - let psbt = builder.finish().unwrap(); - let tx = &psbt.unsigned_tx; - let sent_received = wallet.sent_and_received(tx); - let fee = check_fee!(wallet, psbt); - - assert_eq!(sent_received.0, original_sent_received.0); - assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0)); - - assert_eq!(tx.output.len(), 1); - assert_eq!( - tx.output[0].value + Amount::from_sat(fee.unwrap_or(0)), - sent_received.0 - ); - - assert_eq!(fee.unwrap_or(0), 300); -} - -#[test] -fn test_bump_fee_drain_wallet() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - // receive an extra tx so that our wallet has two utxos. - let tx = Transaction { - version: transaction::Version::ONE, - lock_time: absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .unwrap() - .script_pubkey(), - value: Amount::from_sat(25_000), - }], - }; - wallet - .insert_tx( - tx.clone(), - ConfirmationTime::Confirmed { - height: wallet.latest_checkpoint().height(), - time: 42_000, - }, - ) - .unwrap(); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .add_utxo(OutPoint { - txid: tx.txid(), - vout: 0, - }) - .unwrap() - .manually_selected_only() - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - - let txid = tx.txid(); - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - assert_eq!(original_sent_received.0, Amount::from_sat(25_000)); - - // for the new feerate, it should be enough to reduce the output, but since we specify - // `drain_wallet` we expect to spend everything - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .drain_wallet() - .allow_shrinking(addr.script_pubkey()) - .unwrap() - .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); - let psbt = builder.finish().unwrap(); - let sent_received = wallet.sent_and_received(&psbt.extract_tx().expect("failed to extract tx")); - - assert_eq!(sent_received.0, Amount::from_sat(75_000)); -} - -#[test] -#[should_panic(expected = "InsufficientFunds")] -fn test_bump_fee_remove_output_manually_selected_only() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - // receive an extra tx so that our wallet has two utxos. then we manually pick only one of - // them, and make sure that `bump_fee` doesn't try to add more. This fails because we've - // told the wallet it's not allowed to add more inputs AND it can't reduce the value of the - // existing output. In other words, bump_fee + manually_selected_only is always an error - // unless you've also set "allow_shrinking" OR there is a change output. - let init_tx = Transaction { - version: transaction::Version::ONE, - lock_time: absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .unwrap() - .script_pubkey(), - value: Amount::from_sat(25_000), - }], - }; - wallet - .insert_tx( - init_tx.clone(), - wallet - .transactions() - .last() - .unwrap() - .chain_position - .cloned() - .into(), - ) - .unwrap(); - let outpoint = OutPoint { - txid: init_tx.txid(), - vout: 0, - }; - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .add_utxo(outpoint) - .unwrap() - .manually_selected_only() - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let txid = tx.txid(); - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - assert_eq!(original_sent_received.0, Amount::from_sat(25_000)); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .manually_selected_only() - .fee_rate(FeeRate::from_sat_per_vb_unchecked(255)); - builder.finish().unwrap(); -} - -#[test] -fn test_bump_fee_add_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let init_tx = Transaction { - version: transaction::Version::ONE, - lock_time: absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .unwrap() - .script_pubkey(), - value: Amount::from_sat(25_000), - }], - }; - let pos = wallet - .transactions() - .last() - .unwrap() - .chain_position - .cloned() - .into(); - wallet.insert_tx(init_tx, pos).unwrap(); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let original_details = wallet.sent_and_received(&tx); - let txid = tx.txid(); - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50)); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - assert_eq!( - sent_received.0, - original_details.0 + Amount::from_sat(25_000) - ); - assert_eq!( - Amount::from_sat(fee.unwrap_or(0)) + sent_received.1, - Amount::from_sat(30_000) - ); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - Amount::from_sat(45_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); -} - -#[test] -fn test_bump_fee_absolute_add_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - receive_output_in_latest_block(&mut wallet, 25_000); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let txid = tx.txid(); - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_absolute(6_000); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!( - sent_received.0, - original_sent_received.0 + Amount::from_sat(25_000) - ); - assert_eq!( - Amount::from_sat(fee.unwrap_or(0)) + sent_received.1, - Amount::from_sat(30_000) - ); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - Amount::from_sat(45_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_eq!(fee.unwrap_or(0), 6_000); -} - -#[test] -fn test_bump_fee_no_change_add_input_and_change() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let op = receive_output_in_latest_block(&mut wallet, 25_000); - - // initially make a tx without change by using `drain_to` - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .add_utxo(op) - .unwrap() - .manually_selected_only() - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let original_sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let original_fee = check_fee!(wallet, psbt); - - let tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.txid(); - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - // now bump the fees without using `allow_shrinking`. the wallet should add an - // extra input and a change output, and leave the original output untouched - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50)); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - let original_send_all_amount = - original_sent_received.0 - Amount::from_sat(original_fee.unwrap_or(0)); - assert_eq!( - sent_received.0, - original_sent_received.0 + Amount::from_sat(50_000) - ); - assert_eq!( - sent_received.1, - Amount::from_sat(75_000) - original_send_all_amount - Amount::from_sat(fee.unwrap_or(0)) - ); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - original_send_all_amount - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - Amount::from_sat(75_000) - original_send_all_amount - Amount::from_sat(fee.unwrap_or(0)) - ); - - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); -} - -#[test] -fn test_bump_fee_add_input_change_dust() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - receive_output_in_latest_block(&mut wallet, 25_000); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let original_sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let original_fee = check_fee!(wallet, psbt); - - let mut tx = psbt.extract_tx().expect("failed to extract tx"); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realistic weight - } - let original_tx_weight = tx.weight(); - assert_eq!(tx.input.len(), 1); - assert_eq!(tx.output.len(), 2); - let txid = tx.txid(); - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - // We set a fee high enough that during rbf we are forced to add - // a new input and also that we have to remove the change - // that we had previously - - // We calculate the new weight as: - // original weight - // + extra input weight: 160 WU = (32 (prevout) + 4 (vout) + 4 (nsequence)) * 4 - // + input satisfaction weight: 112 WU = 106 (witness) + 2 (witness len) + (1 (script len)) * 4 - // - change output weight: 124 WU = (8 (value) + 1 (script len) + 22 (script)) * 4 - let new_tx_weight = - original_tx_weight + Weight::from_wu(160) + Weight::from_wu(112) - Weight::from_wu(124); - // two inputs (50k, 25k) and one output (45k) - epsilon - // We use epsilon here to avoid asking for a slightly too high feerate - let fee_abs = 50_000 + 25_000 - 45_000 - 10; - builder.fee_rate(Amount::from_sat(fee_abs) / new_tx_weight); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!( - original_sent_received.1, - Amount::from_sat(5_000 - original_fee.unwrap_or(0)) - ); - - assert_eq!( - sent_received.0, - original_sent_received.0 + Amount::from_sat(25_000) - ); - assert_eq!(fee.unwrap_or(0), 30_000); - assert_eq!(sent_received.1, Amount::ZERO); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 1); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - Amount::from_sat(45_000) - ); - - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(140), @dust_change, @add_signature); -} - -#[test] -fn test_bump_fee_force_add_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let mut tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let txid = tx.txid(); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - wallet - .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - // the new fee_rate is low enough that just reducing the change would be fine, but we force - // the addition of an extra input with `add_utxo()` - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .add_utxo(incoming_op) - .unwrap() - .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!( - sent_received.0, - original_sent_received.0 + Amount::from_sat(25_000) - ); - assert_eq!( - Amount::from_sat(fee.unwrap_or(0)) + sent_received.1, - Amount::from_sat(30_000) - ); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - Amount::from_sat(45_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature); -} - -#[test] -fn test_bump_fee_absolute_force_add_input() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let mut tx = psbt.extract_tx().expect("failed to extract tx"); - let original_sent_received = wallet.sent_and_received(&tx); - let txid = tx.txid(); - // skip saving the new utxos, we know they can't be used anyways - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - wallet - .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - // the new fee_rate is low enough that just reducing the change would be fine, but we force - // the addition of an extra input with `add_utxo()` - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.add_utxo(incoming_op).unwrap().fee_absolute(250); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - let fee = check_fee!(wallet, psbt); - - assert_eq!( - sent_received.0, - original_sent_received.0 + Amount::from_sat(25_000) - ); - assert_eq!( - Amount::from_sat(fee.unwrap_or(0)) + sent_received.1, - Amount::from_sat(30_000) - ); - - let tx = &psbt.unsigned_tx; - assert_eq!(tx.input.len(), 2); - assert_eq!(tx.output.len(), 2); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey == addr.script_pubkey()) - .unwrap() - .value, - Amount::from_sat(45_000) - ); - assert_eq!( - tx.output - .iter() - .find(|txout| txout.script_pubkey != addr.script_pubkey()) - .unwrap() - .value, - sent_received.1 - ); - - assert_eq!(fee.unwrap_or(0), 250); -} - -#[test] -#[should_panic(expected = "InsufficientFunds")] -fn test_bump_fee_unconfirmed_inputs_only() { - // We try to bump the fee, but: - // - We can't reduce the change, as we have no change - // - All our UTXOs are unconfirmed - // So, we fail with "InsufficientFunds", as per RBF rule 2: - // The replacement transaction may only include an unconfirmed input - // if that input was included in one of the original transactions. - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .drain_wallet() - .drain_to(addr.script_pubkey()) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - // Now we receive one transaction with 0 confirmations. We won't be able to use that for - // fee bumping, as it's still unconfirmed! - receive_output( - &mut wallet, - 25_000, - ConfirmationTime::Unconfirmed { last_seen: 0 }, - ); - let mut tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.txid(); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25)); - builder.finish().unwrap(); -} - -#[test] -fn test_bump_fee_unconfirmed_input() { - // We create a tx draining the wallet and spending one confirmed - // and one unconfirmed UTXO. We check that we can fee bump normally - // (BIP125 rule 2 only apply to newly added unconfirmed input, you can - // always fee bump with an unconfirmed input if it was included in the - // original transaction) - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - // We receive a tx with 0 confirmations, which will be used as an input - // in the drain tx. - receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0)); - let mut builder = wallet.build_tx(); - builder - .drain_wallet() - .drain_to(addr.script_pubkey()) - .enable_rbf(); - let psbt = builder.finish().unwrap(); - let mut tx = psbt.extract_tx().expect("failed to extract tx"); - let txid = tx.txid(); - for txin in &mut tx.input { - txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature - } - wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) - .unwrap(); - - let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder - .fee_rate(FeeRate::from_sat_per_vb_unchecked(15)) - .allow_shrinking(addr.script_pubkey()) - .unwrap(); - builder.finish().unwrap(); -} - -#[test] -fn test_fee_amount_negative_drain_val() { - // While building the transaction, bdk would calculate the drain_value - // as - // current_delta - fee_amount - drain_fee - // using saturating_sub, meaning that if the result would end up negative, - // it'll remain to zero instead. - // This caused a bug in master where we would calculate the wrong fee - // for a transaction. - // See https://github.com/bitcoindevkit/bdk/issues/660 - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt") - .unwrap() - .assume_checked(); - let fee_rate = FeeRate::from_sat_per_kwu(500); - let incoming_op = receive_output_in_latest_block(&mut wallet, 8859); - - let mut builder = wallet.build_tx(); - builder - .add_recipient(send_to.script_pubkey(), Amount::from_sat(8630)) - .add_utxo(incoming_op) - .unwrap() - .enable_rbf() - .fee_rate(fee_rate); - let psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - assert_eq!(psbt.inputs.len(), 1); - assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate, @add_signature); -} - -#[test] -fn test_sign_single_xprv() { - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx().expect("failed to extract tx"); - assert_eq!(extracted.input[0].witness.len(), 2); -} - -#[test] -fn test_sign_single_xprv_with_master_fingerprint_and_path() { - let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx().expect("failed to extract tx"); - assert_eq!(extracted.input[0].witness.len(), 2); -} - -#[test] -fn test_sign_single_xprv_bip44_path() { - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx().expect("failed to extract tx"); - assert_eq!(extracted.input[0].witness.len(), 2); -} - -#[test] -fn test_sign_single_xprv_sh_wpkh() { - let (mut wallet, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx().expect("failed to extract tx"); - assert_eq!(extracted.input[0].witness.len(), 2); -} - -#[test] -fn test_sign_single_wif() { - let (mut wallet, _) = - get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx().expect("failed to extract tx"); - assert_eq!(extracted.input[0].witness.len(), 2); -} - -#[test] -fn test_sign_single_xprv_no_hd_keypaths() { - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - psbt.inputs[0].bip32_derivation.clear(); - assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0); - - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); - assert!(finalized); - - let extracted = psbt.extract_tx().expect("failed to extract tx"); - assert_eq!(extracted.input[0].witness.len(), 2); -} - -#[test] -fn test_include_output_redeem_witness_script() { - let (mut wallet, _) = get_funded_wallet("sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))"); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .include_output_redeem_witness_script(); - let psbt = builder.finish().unwrap(); - - // p2sh-p2wsh transaction should contain both witness and redeem scripts - assert!(psbt - .outputs - .iter() - .any(|output| output.redeem_script.is_some() && output.witness_script.is_some())); -} - -#[test] -fn test_signing_only_one_of_multiple_inputs() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .include_output_redeem_witness_script(); - let mut psbt = builder.finish().unwrap(); - - // add another input to the psbt that is at least passable. - let dud_input = bitcoin::psbt::Input { - witness_utxo: Some(TxOut { - value: Amount::from_sat(100_000), - script_pubkey: miniscript::Descriptor::::from_str( - "wpkh(025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357)", - ) - .unwrap() - .script_pubkey(), - }), - ..Default::default() - }; - - psbt.inputs.push(dud_input); - psbt.unsigned_tx.input.push(bitcoin::TxIn::default()); - 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" - ); - assert!( - psbt.inputs[0].final_script_witness.is_some(), - "should finalized input it signed" - ) -} - -#[test] -fn test_remove_partial_sigs_after_finalize_sign_option() { - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - - for remove_partial_sigs in &[true, false] { - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - assert!(wallet - .sign( - &mut psbt, - SignOptions { - remove_partial_sigs: *remove_partial_sigs, - ..Default::default() - }, - ) - .unwrap()); - - psbt.inputs.iter().for_each(|input| { - if *remove_partial_sigs { - assert!(input.partial_sigs.is_empty()) - } else { - assert!(!input.partial_sigs.is_empty()) - } - }); - } -} - -#[test] -fn test_try_finalize_sign_option() { - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - - for try_finalize in &[true, false] { - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - let finalized = wallet - .sign( - &mut psbt, - SignOptions { - try_finalize: *try_finalize, - ..Default::default() - }, - ) - .unwrap(); - - psbt.inputs.iter().for_each(|input| { - if *try_finalize { - assert!(finalized); - assert!(input.final_script_sig.is_some()); - assert!(input.final_script_witness.is_some()); - } else { - assert!(!finalized); - assert!(input.final_script_sig.is_none()); - assert!(input.final_script_witness.is_none()); - } - }); - } -} - -#[test] -fn test_sign_nonstandard_sighash() { - let sighash = EcdsaSighashType::NonePlusAnyoneCanPay; - - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .sighash(sighash.into()) - .drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - let result = wallet.sign(&mut psbt, Default::default()); - assert!( - result.is_err(), - "Signing should have failed because the TX uses non-standard sighashes" - ); - assert_matches!( - result, - Err(SignerError::NonStandardSighash), - "Signing failed with the wrong error type" - ); - - // try again after opting-in - let result = wallet.sign( - &mut psbt, - SignOptions { - allow_all_sighashes: true, - ..Default::default() - }, - ); - assert!(result.is_ok(), "Signing should have worked"); - assert!( - result.unwrap(), - "Should finalize the input since we can produce signatures" - ); - - let extracted = psbt.extract_tx().expect("failed to extract tx"); - assert_eq!( - *extracted.input[0].witness.to_vec()[0].last().unwrap(), - sighash.to_u32() as u8, - "The signature should have been made with the right sighash" - ); -} - -#[test] -fn test_unused_address() { - let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", - None, Network::Testnet).unwrap(); - - // `list_unused_addresses` should be empty if we haven't revealed any - assert!(wallet - .list_unused_addresses(KeychainKind::External) - .next() - .is_none()); - - assert_eq!( - wallet - .next_unused_address(KeychainKind::External) - .unwrap() - .to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); - assert_eq!( - wallet - .list_unused_addresses(KeychainKind::External) - .next() - .unwrap() - .to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); -} - -#[test] -fn test_next_unused_address() { - let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; - let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet).unwrap(); - assert_eq!(wallet.derivation_index(KeychainKind::External), None); - - assert_eq!( - wallet - .next_unused_address(KeychainKind::External) - .unwrap() - .to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); - assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); - // calling next_unused again gives same address - assert_eq!( - wallet - .next_unused_address(KeychainKind::External) - .unwrap() - .to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); - assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); - - // test mark used / unused - assert!(wallet.mark_used(KeychainKind::External, 0)); - let next_unused_addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - assert_eq!(next_unused_addr.index, 1); - - assert!(wallet.unmark_used(KeychainKind::External, 0)); - let next_unused_addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - assert_eq!(next_unused_addr.index, 0); - - // use the above address - receive_output_in_latest_block(&mut wallet, 25_000); - - assert_eq!( - wallet - .next_unused_address(KeychainKind::External) - .unwrap() - .to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); - assert_eq!(wallet.derivation_index(KeychainKind::External), Some(1)); - - // trying to mark index 0 unused should return false - assert!(!wallet.unmark_used(KeychainKind::External, 0)); -} - -#[test] -fn test_peek_address_at_index() { - let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", - None, Network::Testnet).unwrap(); - - assert_eq!( - wallet.peek_address(KeychainKind::External, 1).to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); - - assert_eq!( - wallet.peek_address(KeychainKind::External, 0).to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); - - assert_eq!( - wallet.peek_address(KeychainKind::External, 2).to_string(), - "tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2" - ); - - // current new address is not affected - assert_eq!( - wallet - .reveal_next_address(KeychainKind::External) - .unwrap() - .to_string(), - "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" - ); - - assert_eq!( - wallet - .reveal_next_address(KeychainKind::External) - .unwrap() - .to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); -} - -#[test] -fn test_peek_address_at_index_not_derivable() { - let wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)", - None, Network::Testnet).unwrap(); - - assert_eq!( - wallet.peek_address(KeychainKind::External, 1).to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); - - assert_eq!( - wallet.peek_address(KeychainKind::External, 0).to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); - - assert_eq!( - wallet.peek_address(KeychainKind::External, 2).to_string(), - "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" - ); -} - -#[test] -fn test_returns_index_and_address() { - let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", - None, Network::Testnet).unwrap(); - - // new index 0 - assert_eq!( - wallet.reveal_next_address(KeychainKind::External).unwrap(), - AddressInfo { - index: 0, - address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a") - .unwrap() - .assume_checked(), - keychain: KeychainKind::External, - } - ); - - // new index 1 - assert_eq!( - wallet.reveal_next_address(KeychainKind::External).unwrap(), - AddressInfo { - index: 1, - address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7") - .unwrap() - .assume_checked(), - keychain: KeychainKind::External, - } - ); - - // peek index 25 - assert_eq!( - wallet.peek_address(KeychainKind::External, 25), - AddressInfo { - index: 25, - address: Address::from_str("tb1qsp7qu0knx3sl6536dzs0703u2w2ag6ppl9d0c2") - .unwrap() - .assume_checked(), - keychain: KeychainKind::External, - } - ); - - // new index 2 - assert_eq!( - wallet.reveal_next_address(KeychainKind::External).unwrap(), - AddressInfo { - index: 2, - address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2") - .unwrap() - .assume_checked(), - keychain: KeychainKind::External, - } - ); -} - -#[test] -fn test_sending_to_bip350_bech32m_address() { - let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let addr = Address::from_str("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); - builder.finish().unwrap(); -} - -#[test] -fn test_get_address() { - use bdk::descriptor::template::Bip84; - let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let wallet = Wallet::new_no_persist( - Bip84(key, KeychainKind::External), - Some(Bip84(key, KeychainKind::Internal)), - Network::Regtest, - ) - .unwrap(); - - assert_eq!( - wallet.peek_address(KeychainKind::External, 0), - AddressInfo { - index: 0, - address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w") - .unwrap() - .assume_checked(), - keychain: KeychainKind::External, - } - ); - - assert_eq!( - wallet.peek_address(KeychainKind::Internal, 0), - AddressInfo { - index: 0, - address: Address::from_str("bcrt1q0ue3s5y935tw7v3gmnh36c5zzsaw4n9c9smq79") - .unwrap() - .assume_checked(), - keychain: KeychainKind::Internal, - } - ); - - let wallet = - Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); - - assert_eq!( - wallet.peek_address(KeychainKind::Internal, 0), - AddressInfo { - index: 0, - address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w") - .unwrap() - .assume_checked(), - keychain: KeychainKind::External, - }, - "when there's no internal descriptor it should just use external" - ); -} - -#[test] -fn test_reveal_addresses() { - let desc = get_test_tr_single_sig_xprv(); - let mut wallet = Wallet::new_no_persist(desc, None, Network::Signet).unwrap(); - let keychain = KeychainKind::External; - - let last_revealed_addr = wallet - .reveal_addresses_to(keychain, 9) - .unwrap() - .last() - .unwrap(); - assert_eq!(wallet.derivation_index(keychain), Some(9)); - - let unused_addrs = wallet.list_unused_addresses(keychain).collect::>(); - assert_eq!(unused_addrs.len(), 10); - assert_eq!(unused_addrs.last().unwrap(), &last_revealed_addr); - - // revealing to an already revealed index returns nothing - let mut already_revealed = wallet.reveal_addresses_to(keychain, 9).unwrap(); - assert!(already_revealed.next().is_none()); -} - -#[test] -fn test_get_address_no_reuse_single_descriptor() { - use bdk::descriptor::template::Bip84; - use std::collections::HashSet; - - let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let mut wallet = - Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); - - let mut used_set = HashSet::new(); - - (0..3).for_each(|_| { - let external_addr = wallet - .reveal_next_address(KeychainKind::External) - .unwrap() - .address; - assert!(used_set.insert(external_addr)); - - let internal_addr = wallet - .reveal_next_address(KeychainKind::Internal) - .unwrap() - .address; - assert!(used_set.insert(internal_addr)); - }); -} - -#[test] -fn test_taproot_remove_tapfields_after_finalize_sign_option() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); - - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); - assert!(finalized); - - // removes tap_* from inputs - for input in &psbt.inputs { - assert!(input.tap_key_sig.is_none()); - assert!(input.tap_script_sigs.is_empty()); - assert!(input.tap_scripts.is_empty()); - assert!(input.tap_key_origins.is_empty()); - assert!(input.tap_internal_key.is_none()); - assert!(input.tap_merkle_root.is_none()); - } - // removes key origins from outputs - for output in &psbt.outputs { - assert!(output.tap_key_origins.is_empty()); - } -} - -#[test] -fn test_taproot_psbt_populate_tap_key_origins() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); - let addr = wallet.reveal_next_address(KeychainKind::External).unwrap(); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let psbt = builder.finish().unwrap(); - - assert_eq!( - psbt.inputs[0] - .tap_key_origins - .clone() - .into_iter() - .collect::>(), - vec![( - from_str!("b96d3a3dc76a4fc74e976511b23aecb78e0754c23c0ed7a6513e18cbbc7178e9"), - (vec![], (from_str!("f6a5cb8b"), from_str!("m/0"))) - )], - "Wrong input tap_key_origins" - ); - assert_eq!( - psbt.outputs[0] - .tap_key_origins - .clone() - .into_iter() - .collect::>(), - vec![( - from_str!("e9b03068cf4a2621d4f81e68f6c4216e6bd260fe6edf6acc55c8d8ae5aeff0a8"), - (vec![], (from_str!("f6a5cb8b"), from_str!("m/1"))) - )], - "Wrong output tap_key_origins" - ); -} - -#[test] -fn test_taproot_psbt_populate_tap_key_origins_repeated_key() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_repeated_key()); - let addr = wallet.reveal_next_address(KeychainKind::External).unwrap(); - - let path = vec![("rn4nre9c".to_string(), vec![0])] - .into_iter() - .collect(); - - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .policy_path(path, KeychainKind::External); - let psbt = builder.finish().unwrap(); - - let mut input_key_origins = psbt.inputs[0] - .tap_key_origins - .clone() - .into_iter() - .collect::>(); - input_key_origins.sort(); - - assert_eq!( - input_key_origins, - vec![ - ( - from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"), - ( - vec![ - from_str!( - "858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e" - ), - from_str!( - "f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903" - ), - ], - (FromStr::from_str("ece52657").unwrap(), vec![].into()) - ) - ), - ( - from_str!("b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"), - ( - vec![], - (FromStr::from_str("871fd295").unwrap(), vec![].into()) - ) - ) - ], - "Wrong input tap_key_origins" - ); - - let mut output_key_origins = psbt.outputs[0] - .tap_key_origins - .clone() - .into_iter() - .collect::>(); - output_key_origins.sort(); - - assert_eq!( - input_key_origins, output_key_origins, - "Wrong output tap_key_origins" - ); -} - -#[test] -fn test_taproot_psbt_input_tap_tree() { - use bitcoin::hex::FromHex; - use bitcoin::taproot; - - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let psbt = builder.finish().unwrap(); - - assert_eq!( - psbt.inputs[0].tap_merkle_root, - Some( - TapNodeHash::from_str( - "61f81509635053e52d9d1217545916167394490da2287aca4693606e43851986" - ) - .unwrap() - ), - ); - assert_eq!( - psbt.inputs[0].tap_scripts.clone().into_iter().collect::>(), - vec![ - (taproot::ControlBlock::decode(&Vec::::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b7ef769a745e625ed4b9a4982a4dc08274c59187e73e6f07171108f455081cb2").unwrap()).unwrap(), (ScriptBuf::from_hex("208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac").unwrap(), taproot::LeafVersion::TapScript)), - (taproot::ControlBlock::decode(&Vec::::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b9a515f7be31a70186e3c5937ee4a70cc4b4e1efe876c1d38e408222ffc64834").unwrap()).unwrap(), (ScriptBuf::from_hex("2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac").unwrap(), taproot::LeafVersion::TapScript)), - ], - ); - assert_eq!( - psbt.inputs[0].tap_internal_key, - Some(from_str!( - "b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55" - )) - ); - - // Since we are creating an output to the same address as the input, assert that the - // internal_key is the same - assert_eq!( - psbt.inputs[0].tap_internal_key, - psbt.outputs[0].tap_internal_key - ); - - let tap_tree: bitcoin::taproot::TapTree = serde_json::from_str(r#"[1,{"Script":["2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac",192]},1,{"Script":["208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac",192]}]"#).unwrap(); - assert_eq!(psbt.outputs[0].tap_tree, Some(tap_tree)); -} - -#[test] -fn test_taproot_sign_missing_witness_utxo() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - let witness_utxo = psbt.inputs[0].witness_utxo.take(); - - let result = wallet.sign( - &mut psbt, - SignOptions { - allow_all_sighashes: true, - ..Default::default() - }, - ); - assert_matches!( - result, - Err(SignerError::MissingWitnessUtxo), - "Signing should have failed with the correct error because the witness_utxo is missing" - ); - - // restore the witness_utxo - psbt.inputs[0].witness_utxo = witness_utxo; - - let result = wallet.sign( - &mut psbt, - SignOptions { - allow_all_sighashes: true, - ..Default::default() - }, - ); - - assert_matches!( - result, - Ok(true), - "Should finalize the input since we can produce signatures" - ); -} - -#[test] -fn test_taproot_sign_using_non_witness_utxo() { - let (mut wallet, prev_txid) = get_funded_wallet(get_test_tr_single_sig()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - psbt.inputs[0].witness_utxo = None; - psbt.inputs[0].non_witness_utxo = - Some(wallet.get_tx(prev_txid).unwrap().tx_node.as_ref().clone()); - assert!( - psbt.inputs[0].non_witness_utxo.is_some(), - "Previous tx should be present in the database" - ); - - let result = wallet.sign(&mut psbt, Default::default()); - assert!(result.is_ok(), "Signing should have worked"); - assert!( - result.unwrap(), - "Should finalize the input since we can produce signatures" - ); -} - -#[test] -fn test_taproot_foreign_utxo() { - let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); - let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig()); - - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let utxo = wallet2.list_unspent().next().unwrap(); - let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); - let foreign_utxo_satisfaction = wallet2 - .get_descriptor_for_keychain(KeychainKind::External) - .max_weight_to_satisfy() - .unwrap(); - - assert!( - psbt_input.non_witness_utxo.is_none(), - "`non_witness_utxo` should never be populated for taproot" - ); - - let mut builder = wallet1.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)) - .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) - .unwrap(); - let psbt = builder.finish().unwrap(); - let sent_received = - wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); - wallet1.insert_txout(utxo.outpoint, utxo.txout); - let fee = check_fee!(wallet1, psbt); - - assert_eq!( - sent_received.0 - sent_received.1, - Amount::from_sat(10_000 + fee.unwrap_or(0)), - "we should have only net spent ~10_000" - ); - - assert!( - psbt.unsigned_tx - .input - .iter() - .any(|input| input.previous_output == utxo.outpoint), - "foreign_utxo should be in there" - ); -} - -fn test_spend_from_wallet(mut wallet: Wallet) { - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let mut psbt = builder.finish().unwrap(); - - assert!( - wallet.sign(&mut psbt, Default::default()).unwrap(), - "Unable to finalize tx" - ); -} - -// #[test] -// fn test_taproot_key_spend() { -// let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); -// test_spend_from_wallet(wallet); - -// let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); -// test_spend_from_wallet(wallet); -// } - -#[test] -fn test_taproot_no_key_spend() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let mut psbt = builder.finish().unwrap(); - - assert!( - wallet - .sign( - &mut psbt, - SignOptions { - sign_with_tap_internal_key: false, - ..Default::default() - }, - ) - .unwrap(), - "Unable to finalize tx" - ); - - assert!(psbt.inputs.iter().all(|i| i.tap_key_sig.is_none())); -} - -#[test] -fn test_taproot_script_spend() { - let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); - test_spend_from_wallet(wallet); - - let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree_xprv()); - test_spend_from_wallet(wallet); -} - -#[test] -fn test_taproot_script_spend_sign_all_leaves() { - use bdk::signer::TapLeavesOptions; - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let mut psbt = builder.finish().unwrap(); - - assert!( - wallet - .sign( - &mut psbt, - SignOptions { - tap_leaves_options: TapLeavesOptions::All, - ..Default::default() - }, - ) - .unwrap(), - "Unable to finalize tx" - ); - - assert!(psbt - .inputs - .iter() - .all(|i| i.tap_script_sigs.len() == i.tap_scripts.len())); -} - -#[test] -fn test_taproot_script_spend_sign_include_some_leaves() { - use bdk::signer::TapLeavesOptions; - use bitcoin::taproot::TapLeafHash; - - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let mut psbt = builder.finish().unwrap(); - let mut script_leaves: Vec<_> = psbt.inputs[0] - .tap_scripts - .clone() - .values() - .map(|(script, version)| TapLeafHash::from_script(script, *version)) - .collect(); - let included_script_leaves = vec![script_leaves.pop().unwrap()]; - let excluded_script_leaves = script_leaves; - - assert!( - wallet - .sign( - &mut psbt, - SignOptions { - tap_leaves_options: TapLeavesOptions::Include(included_script_leaves.clone()), - ..Default::default() - }, - ) - .unwrap(), - "Unable to finalize tx" - ); - - assert!(psbt.inputs[0] - .tap_script_sigs - .iter() - .all(|s| included_script_leaves.contains(&s.0 .1) - && !excluded_script_leaves.contains(&s.0 .1))); -} - -#[test] -fn test_taproot_script_spend_sign_exclude_some_leaves() { - use bdk::signer::TapLeavesOptions; - use bitcoin::taproot::TapLeafHash; - - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let mut psbt = builder.finish().unwrap(); - let mut script_leaves: Vec<_> = psbt.inputs[0] - .tap_scripts - .clone() - .values() - .map(|(script, version)| TapLeafHash::from_script(script, *version)) - .collect(); - let included_script_leaves = [script_leaves.pop().unwrap()]; - let excluded_script_leaves = script_leaves; - - assert!( - wallet - .sign( - &mut psbt, - SignOptions { - tap_leaves_options: TapLeavesOptions::Exclude(excluded_script_leaves.clone()), - ..Default::default() - }, - ) - .unwrap(), - "Unable to finalize tx" - ); - - assert!(psbt.inputs[0] - .tap_script_sigs - .iter() - .all(|s| included_script_leaves.contains(&s.0 .1) - && !excluded_script_leaves.contains(&s.0 .1))); -} - -#[test] -fn test_taproot_script_spend_sign_no_leaves() { - use bdk::signer::TapLeavesOptions; - let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let mut psbt = builder.finish().unwrap(); - - wallet - .sign( - &mut psbt, - SignOptions { - tap_leaves_options: TapLeavesOptions::None, - ..Default::default() - }, - ) - .unwrap(); - - assert!(psbt.inputs.iter().all(|i| i.tap_script_sigs.is_empty())); -} - -#[test] -fn test_taproot_sign_derive_index_from_psbt() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); - - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - - let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); - let mut psbt = builder.finish().unwrap(); - - // re-create the wallet with an empty db - let wallet_empty = - Wallet::new_no_persist(get_test_tr_single_sig_xprv(), None, Network::Regtest).unwrap(); - - // signing with an empty db means that we will only look at the psbt to infer the - // derivation index - assert!( - wallet_empty.sign(&mut psbt, Default::default()).unwrap(), - "Unable to finalize tx" - ); -} - -#[test] -fn test_taproot_sign_explicit_sighash_all() { - let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .sighash(TapSighashType::All.into()) - .drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - let result = wallet.sign(&mut psbt, Default::default()); - assert!( - result.is_ok(), - "Signing should work because SIGHASH_ALL is safe" - ) -} - -#[test] -fn test_taproot_sign_non_default_sighash() { - let sighash = TapSighashType::NonePlusAnyoneCanPay; - - let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .sighash(sighash.into()) - .drain_wallet(); - let mut psbt = builder.finish().unwrap(); - - let witness_utxo = psbt.inputs[0].witness_utxo.take(); - - let result = wallet.sign(&mut psbt, Default::default()); - assert!( - result.is_err(), - "Signing should have failed because the TX uses non-standard sighashes" - ); - assert_matches!( - result, - Err(SignerError::NonStandardSighash), - "Signing failed with the wrong error type" - ); - - // try again after opting-in - let result = wallet.sign( - &mut psbt, - SignOptions { - allow_all_sighashes: true, - ..Default::default() - }, - ); - assert!( - result.is_err(), - "Signing should have failed because the witness_utxo is missing" - ); - assert_matches!( - result, - Err(SignerError::MissingWitnessUtxo), - "Signing failed with the wrong error type" - ); - - // restore the witness_utxo - psbt.inputs[0].witness_utxo = witness_utxo; - - let result = wallet.sign( - &mut psbt, - SignOptions { - allow_all_sighashes: true, - ..Default::default() - }, - ); - - assert!(result.is_ok(), "Signing should have worked"); - assert!( - result.unwrap(), - "Should finalize the input since we can produce signatures" - ); - - let extracted = psbt.extract_tx().expect("failed to extract tx"); - assert_eq!( - *extracted.input[0].witness.to_vec()[0].last().unwrap(), - sighash as u8, - "The signature should have been made with the right sighash" - ); -} - -#[test] -fn test_spend_coinbase() { - let descriptor = get_test_wpkh(); - let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Regtest).unwrap(); - - let confirmation_height = 5; - wallet - .insert_checkpoint(BlockId { - height: confirmation_height, - hash: BlockHash::all_zeros(), - }) - .unwrap(); - let coinbase_tx = Transaction { - version: transaction::Version::ONE, - lock_time: absolute::LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint::null(), - ..Default::default() - }], - output: vec![TxOut { - script_pubkey: wallet - .next_unused_address(KeychainKind::External) - .unwrap() - .script_pubkey(), - value: Amount::from_sat(25_000), - }], - }; - wallet - .insert_tx( - coinbase_tx, - ConfirmationTime::Confirmed { - height: confirmation_height, - time: 30_000, - }, - ) - .unwrap(); - - let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1; - let maturity_time = confirmation_height + COINBASE_MATURITY; - - let balance = wallet.get_balance(); - assert_eq!( - balance, - Balance { - immature: Amount::from_sat(25_000), - trusted_pending: Amount::ZERO, - untrusted_pending: Amount::ZERO, - confirmed: Amount::ZERO - } - ); - - // We try to create a transaction, only to notice that all - // our funds are unspendable - let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") - .unwrap() - .assume_checked(); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), balance.immature / 2) - .current_height(confirmation_height); - assert!(matches!( - builder.finish(), - Err(CreateTxError::CoinSelection( - coin_selection::Error::InsufficientFunds { - needed: _, - available: 0 - } - )) - )); - - // Still unspendable... - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), balance.immature / 2) - .current_height(not_yet_mature_time); - assert_matches!( - builder.finish(), - Err(CreateTxError::CoinSelection( - coin_selection::Error::InsufficientFunds { - needed: _, - available: 0 - } - )) - ); - - wallet - .insert_checkpoint(BlockId { - height: maturity_time, - hash: BlockHash::all_zeros(), - }) - .unwrap(); - let balance = wallet.get_balance(); - assert_eq!( - balance, - Balance { - immature: Amount::ZERO, - trusted_pending: Amount::ZERO, - untrusted_pending: Amount::ZERO, - confirmed: Amount::from_sat(25_000) - } - ); - let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), balance.confirmed / 2) - .current_height(maturity_time); - builder.finish().unwrap(); -} - -#[test] -fn test_allow_dust_limit() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); - - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - - let mut builder = wallet.build_tx(); - - builder.add_recipient(addr.script_pubkey(), Amount::ZERO); - - assert_matches!( - builder.finish(), - Err(CreateTxError::OutputBelowDustLimit(0)) - ); - - let mut builder = wallet.build_tx(); - - builder - .allow_dust(true) - .add_recipient(addr.script_pubkey(), Amount::ZERO); - - assert!(builder.finish().is_ok()); -} - -#[test] -fn test_fee_rate_sign_no_grinding_high_r() { - // Our goal is to obtain a transaction with a signature with high-R (71 bytes - // instead of 70). We then check that our fee rate and fee calculation is - // alright. - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); - let mut builder = wallet.build_tx(); - let mut data = PushBytesBuf::try_from(vec![0]).unwrap(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_rate(fee_rate) - .add_data(&data); - let mut psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - let (op_return_vout, _) = psbt - .unsigned_tx - .output - .iter() - .enumerate() - .find(|(_n, i)| i.script_pubkey.is_op_return()) - .unwrap(); - - let mut sig_len: usize = 0; - // We try to sign many different times until we find a longer signature (71 bytes) - while sig_len < 71 { - // Changing the OP_RETURN data will make the signature change (but not the fee, until - // data[0] is small enough) - data.as_mut_bytes()[0] += 1; - psbt.unsigned_tx.output[op_return_vout].script_pubkey = ScriptBuf::new_op_return(&data); - // Clearing the previous signature - psbt.inputs[0].partial_sigs.clear(); - // Signing - wallet - .sign( - &mut psbt, - SignOptions { - remove_partial_sigs: false, - try_finalize: false, - allow_grinding: false, - ..Default::default() - }, - ) - .unwrap(); - // We only have one key in the partial_sigs map, this is a trick to retrieve it - let key = psbt.inputs[0].partial_sigs.keys().next().unwrap(); - sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len(); - } - // Actually finalizing the transaction... - wallet - .sign( - &mut psbt, - SignOptions { - remove_partial_sigs: false, - allow_grinding: false, - ..Default::default() - }, - ) - .unwrap(); - // ...and checking that everything is fine - assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate); -} - -#[test] -fn test_fee_rate_sign_grinding_low_r() { - // Our goal is to obtain a transaction with a signature with low-R (70 bytes) - // by setting the `allow_grinding` signing option as true. - // We then check that our fee rate and fee calculation is alright and that our - // signature is 70 bytes. - let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); - let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); - let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); - let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .fee_rate(fee_rate); - let mut psbt = builder.finish().unwrap(); - let fee = check_fee!(wallet, psbt); - - wallet - .sign( - &mut psbt, - SignOptions { - remove_partial_sigs: false, - allow_grinding: true, - ..Default::default() - }, - ) - .unwrap(); - - let key = psbt.inputs[0].partial_sigs.keys().next().unwrap(); - let sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len(); - assert_eq!(sig_len, 70); - assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate); -} - -#[test] -fn test_taproot_load_descriptor_duplicated_keys() { - // Added after issue https://github.com/bitcoindevkit/bdk/issues/760 - // - // Having the same key in multiple taproot leaves is safe and should be accepted by BDK - - let (wallet, _) = get_funded_wallet(get_test_tr_dup_keys()); - let addr = wallet.peek_address(KeychainKind::External, 0); - - assert_eq!( - addr.to_string(), - "bcrt1pvysh4nmh85ysrkpwtrr8q8gdadhgdejpy6f9v424a8v9htjxjhyqw9c5s5" - ); -} - -#[test] -/// The wallet should re-use previously allocated change addresses when the tx using them is cancelled -fn test_tx_cancellation() { - macro_rules! new_tx { - ($wallet:expr) => {{ - let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") - .unwrap() - .assume_checked(); - let mut builder = $wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)); - - let psbt = builder.finish().unwrap(); - - psbt - }}; - } - - let (mut wallet, _) = - get_funded_wallet_with_change(get_test_wpkh(), Some(get_test_tr_single_sig_xprv())); - - let psbt1 = new_tx!(wallet); - let change_derivation_1 = psbt1 - .unsigned_tx - .output - .iter() - .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey)) - .unwrap(); - assert_eq!(change_derivation_1, (KeychainKind::Internal, 0)); - - let psbt2 = new_tx!(wallet); - - let change_derivation_2 = psbt2 - .unsigned_tx - .output - .iter() - .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey)) - .unwrap(); - assert_eq!(change_derivation_2, (KeychainKind::Internal, 1)); - - wallet.cancel_tx(&psbt1.extract_tx().expect("failed to extract tx")); - - let psbt3 = new_tx!(wallet); - let change_derivation_3 = psbt3 - .unsigned_tx - .output - .iter() - .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey)) - .unwrap(); - assert_eq!(change_derivation_3, (KeychainKind::Internal, 0)); - - let psbt3 = new_tx!(wallet); - let change_derivation_3 = psbt3 - .unsigned_tx - .output - .iter() - .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey)) - .unwrap(); - assert_eq!(change_derivation_3, (KeychainKind::Internal, 2)); - - wallet.cancel_tx(&psbt3.extract_tx().expect("failed to extract tx")); - - let psbt3 = new_tx!(wallet); - let change_derivation_4 = psbt3 - .unsigned_tx - .output - .iter() - .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey)) - .unwrap(); - assert_eq!(change_derivation_4, (KeychainKind::Internal, 2)); -} - -#[test] -fn test_thread_safety() { - fn thread_safe() {} - thread_safe::(); // compiles only if true -} diff --git a/crates/hwi/Cargo.toml b/crates/hwi/Cargo.toml index 711a2f94..972569c7 100644 --- a/crates/hwi/Cargo.toml +++ b/crates/hwi/Cargo.toml @@ -9,5 +9,5 @@ license = "MIT OR Apache-2.0" readme = "README.md" [dependencies] -bdk = { path = "../bdk" } +bdk_wallet = { path = "../wallet" } hwi = { version = "0.8.0", features = [ "miniscript"] } diff --git a/crates/hwi/src/lib.rs b/crates/hwi/src/lib.rs index ab87e8a8..129ceac2 100644 --- a/crates/hwi/src/lib.rs +++ b/crates/hwi/src/lib.rs @@ -3,10 +3,10 @@ //! This crate contains HWISigner, an implementation of a [`TransactionSigner`] to be //! used with hardware wallets. //! ```no_run -//! # use bdk::bitcoin::Network; -//! # use bdk::signer::SignerOrdering; +//! # use bdk_wallet::bitcoin::Network; +//! # use bdk_wallet::signer::SignerOrdering; //! # use bdk_hwi::HWISigner; -//! # use bdk::{KeychainKind, SignOptions, Wallet}; +//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet}; //! # use hwi::HWIClient; //! # use std::sync::Arc; //! # @@ -35,7 +35,7 @@ //! # } //! ``` //! -//! [`TransactionSigner`]: bdk::wallet::signer::TransactionSigner +//! [`TransactionSigner`]: bdk_wallet::wallet::signer::TransactionSigner mod signer; pub use signer::*; diff --git a/crates/hwi/src/signer.rs b/crates/hwi/src/signer.rs index a297291c..bbb62661 100644 --- a/crates/hwi/src/signer.rs +++ b/crates/hwi/src/signer.rs @@ -1,12 +1,12 @@ -use bdk::bitcoin::bip32::Fingerprint; -use bdk::bitcoin::secp256k1::{All, Secp256k1}; -use bdk::bitcoin::Psbt; +use bdk_wallet::bitcoin::bip32::Fingerprint; +use bdk_wallet::bitcoin::secp256k1::{All, Secp256k1}; +use bdk_wallet::bitcoin::Psbt; use hwi::error::Error; use hwi::types::{HWIChain, HWIDevice}; use hwi::HWIClient; -use bdk::signer::{SignerCommon, SignerError, SignerId, TransactionSigner}; +use bdk_wallet::signer::{SignerCommon, SignerError, SignerId, TransactionSigner}; #[derive(Debug)] /// Custom signer for Hardware Wallets @@ -38,7 +38,7 @@ impl TransactionSigner for HWISigner { fn sign_transaction( &self, psbt: &mut Psbt, - _sign_options: &bdk::SignOptions, + _sign_options: &bdk_wallet::SignOptions, _secp: &Secp256k1, ) -> Result<(), SignerError> { psbt.combine( @@ -61,9 +61,9 @@ impl TransactionSigner for HWISigner { // fn test_hardware_signer() { // use std::sync::Arc; // -// use bdk::tests::get_funded_wallet; -// use bdk::signer::SignerOrdering; -// use bdk::bitcoin::Network; +// use bdk_wallet::tests::get_funded_wallet; +// use bdk_wallet::signer::SignerOrdering; +// use bdk_wallet::bitcoin::Network; // use crate::HWISigner; // use hwi::HWIClient; // @@ -78,12 +78,12 @@ impl TransactionSigner for HWISigner { // // let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]); // wallet.add_signer( -// bdk::KeychainKind::External, +// bdk_wallet::KeychainKind::External, // SignerOrdering(200), // Arc::new(custom_signer), // ); // -// let addr = wallet.get_address(bdk::wallet::AddressIndex::LastUnused); +// let addr = wallet.get_address(bdk_wallet::wallet::AddressIndex::LastUnused); // let mut builder = wallet.build_tx(); // builder.drain_to(addr.script_pubkey()).drain_wallet(); // let (mut psbt, _) = builder.finish().unwrap(); diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml new file mode 100644 index 00000000..10e428c5 --- /dev/null +++ b/crates/wallet/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "bdk_wallet" +homepage = "https://bitcoindevkit.org" +version = "1.0.0-alpha.11" +repository = "https://github.com/bitcoindevkit/bdk" +documentation = "https://docs.rs/bdk" +description = "A modern, lightweight, descriptor-based wallet library" +keywords = ["bitcoin", "wallet", "descriptor", "psbt"] +readme = "README.md" +license = "MIT OR Apache-2.0" +authors = ["Bitcoin Dev Kit Developers"] +edition = "2021" +rust-version = "1.63" + +[dependencies] +anyhow = { version = "1", default-features = false } +rand = "^0.8" +miniscript = { version = "11.0.0", features = ["serde"], default-features = false } +bitcoin = { version = "0.31.0", features = ["serde", "base64", "rand-std"], default-features = false } +serde = { version = "^1.0", features = ["derive"] } +serde_json = { version = "^1.0" } +bdk_chain = { path = "../chain", version = "0.14.0", features = ["miniscript", "serde"], default-features = false } +bdk_persist = { path = "../persist", version = "0.2.0" } + +# Optional dependencies +bip39 = { version = "2.0", optional = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = "0.2" +js-sys = "0.3" + +[features] +default = ["std"] +std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"] +compiler = ["miniscript/compiler"] +all-keys = ["keys-bip39"] +keys-bip39 = ["bip39"] + +# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended +# for libraries to explicitly include the "getrandom/js" feature, so we only do it when +# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support +dev-getrandom-wasm = ["getrandom/js"] + +[dev-dependencies] +lazy_static = "1.4" +assert_matches = "1.5.0" +tempfile = "3" +bdk_file_store = { path = "../file_store" } +anyhow = "1" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[[example]] +name = "mnemonic_to_descriptors" +path = "examples/mnemonic_to_descriptors.rs" +required-features = ["all-keys"] + +[[example]] +name = "miniscriptc" +path = "examples/compiler.rs" +required-features = ["compiler"] diff --git a/crates/wallet/README.md b/crates/wallet/README.md new file mode 100644 index 00000000..37e16186 --- /dev/null +++ b/crates/wallet/README.md @@ -0,0 +1,228 @@ +
+

BDK

+ + + +

+ A modern, lightweight, descriptor-based wallet library written in Rust! +

+ +

+ Crate Info + MIT or Apache-2.0 Licensed + CI Status + + API Docs + Rustc Version 1.63.0+ + Chat on Discord +

+ +

+ Project Homepage + | + Documentation +

+
+ +# BDK Wallet + +The `bdk_wallet` crate provides the [`Wallet`] type which is a simple, high-level +interface built from the low-level components of [`bdk_chain`]. `Wallet` is a good starting point +for many simple applications as well as a good demonstration of how to use the other mechanisms to +construct a wallet. It has two keychains (external and internal) which are defined by +[miniscript descriptors][`rust-miniscript`] and uses them to generate addresses. When you give it +chain data it also uses the descriptors to find transaction outputs owned by them. From there, you +can create and sign transactions. + +For details about the API of `Wallet` see the [module-level documentation][`Wallet`]. + +## Blockchain data + +In order to get blockchain data for `Wallet` to consume, you should configure a client from +an available chain source. Typically you make a request to the chain source and get a response +that the `Wallet` can use to update its view of the chain. + +**Blockchain Data Sources** + +* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures. +* [`bdk_electrum`]: Grabs blockchain data from Electrum for updating BDK structures. +* [`bdk_bitcoind_rpc`]: Grabs blockchain data from Bitcoin Core for updating BDK structures. + +**Examples** + +* [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async) +* [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking) +* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum) +* [`example-crates/wallet_rpc`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_rpc) + +## Persistence + +To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`] implementation. + +**Implementations** + +* [`bdk_file_store`]: A simple flat-file implementation of [`PersistBackend`]. + +**Example** + + +```rust,compile_fail +use bdk_wallet::{bitcoin::Network, wallet::{ChangeSet, Wallet}}; + +fn main() { + // Create a new file `Store`. + let db = bdk_file_store::Store::::open_or_create_new(b"magic_bytes", "path/to/my_wallet.db").expect("create store"); + + let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)"; + let mut wallet = Wallet::new_or_load(descriptor, None, db, Network::Testnet).expect("create or load wallet"); + + // Insert a single `TxOut` at `OutPoint` into the wallet. + let _ = wallet.insert_txout(outpoint, txout); + wallet.commit().expect("must write to database"); +} +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## Testing + +### Unit testing + +```bash +cargo test +``` + +# License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](../../LICENSE-APACHE) or ) +* MIT license ([LICENSE-MIT](../../LICENSE-MIT) or ) + +at your option. + +# Contribution + +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. + +[`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html +[`PersistBackend`]: https://docs.rs/bdk_chain/latest/bdk_chain/trait.PersistBackend.html +[`bdk_chain`]: https://docs.rs/bdk_chain/latest +[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest +[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest +[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest +[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest +[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html diff --git a/crates/wallet/examples/compiler.rs b/crates/wallet/examples/compiler.rs new file mode 100644 index 00000000..116df473 --- /dev/null +++ b/crates/wallet/examples/compiler.rs @@ -0,0 +1,65 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +extern crate bdk_wallet; +extern crate bitcoin; +extern crate miniscript; +extern crate serde_json; + +use std::error::Error; +use std::str::FromStr; + +use bitcoin::Network; +use miniscript::policy::Concrete; +use miniscript::Descriptor; + +use bdk_wallet::{KeychainKind, Wallet}; + +/// Miniscript policy is a high level abstraction of spending conditions. Defined in the +/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html +/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy +/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet +/// can be derived from the policy. +/// +/// This example demonstrates the interaction between a bdk wallet and miniscript policy. + +fn main() -> Result<(), Box> { + // We start with a generic miniscript policy string + let policy_str = "or(10@thresh(4,pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)),1@and(older(4209713),thresh(2,pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068))))"; + println!("Compiling policy: \n{}", policy_str); + + // Parse the string as a [`Concrete`] type miniscript policy. + let policy = Concrete::::from_str(policy_str)?; + + // Create a `wsh` type descriptor from the policy. + // `policy.compile()` returns the resulting miniscript from the policy. + let descriptor = Descriptor::new_wsh(policy.compile()?)?; + + println!("Compiled into following Descriptor: \n{}", descriptor); + + // Create a new wallet from this descriptor + let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?; + + println!( + "First derived address from the descriptor: \n{}", + wallet.next_unused_address(KeychainKind::External)?, + ); + + // BDK also has it's own `Policy` structure to represent the spending condition in a more + // human readable json format. + let spending_policy = wallet.policies(KeychainKind::External)?; + println!( + "The BDK spending policy: \n{}", + serde_json::to_string_pretty(&spending_policy)? + ); + + Ok(()) +} diff --git a/crates/wallet/examples/mnemonic_to_descriptors.rs b/crates/wallet/examples/mnemonic_to_descriptors.rs new file mode 100644 index 00000000..76c53cf2 --- /dev/null +++ b/crates/wallet/examples/mnemonic_to_descriptors.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use anyhow::anyhow; +use bdk_wallet::bitcoin::bip32::DerivationPath; +use bdk_wallet::bitcoin::secp256k1::Secp256k1; +use bdk_wallet::bitcoin::Network; +use bdk_wallet::descriptor; +use bdk_wallet::descriptor::IntoWalletDescriptor; +use bdk_wallet::keys::bip39::{Language, Mnemonic, WordCount}; +use bdk_wallet::keys::{GeneratableKey, GeneratedKey}; +use bdk_wallet::miniscript::Tap; +use std::str::FromStr; + +/// This example demonstrates how to generate a mnemonic phrase +/// using BDK and use that to generate a descriptor string. +fn main() -> Result<(), anyhow::Error> { + let secp = Secp256k1::new(); + + // In this example we are generating a 12 words mnemonic phrase + // but it is also possible generate 15, 18, 21 and 24 words + // using their respective `WordCount` variant. + let mnemonic: GeneratedKey<_, Tap> = + Mnemonic::generate((WordCount::Words12, Language::English)) + .map_err(|_| anyhow!("Mnemonic generation error"))?; + + println!("Mnemonic phrase: {}", *mnemonic); + let mnemonic_with_passphrase = (mnemonic, None); + + // define external and internal derivation key path + let external_path = DerivationPath::from_str("m/86h/1h/0h/0").unwrap(); + let internal_path = DerivationPath::from_str("m/86h/1h/0h/1").unwrap(); + + // generate external and internal descriptor from mnemonic + let (external_descriptor, ext_keymap) = + descriptor!(tr((mnemonic_with_passphrase.clone(), external_path)))? + .into_wallet_descriptor(&secp, Network::Testnet)?; + let (internal_descriptor, int_keymap) = + descriptor!(tr((mnemonic_with_passphrase, internal_path)))? + .into_wallet_descriptor(&secp, Network::Testnet)?; + + println!("tpub external descriptor: {}", external_descriptor); + println!("tpub internal descriptor: {}", internal_descriptor); + println!( + "tprv external descriptor: {}", + external_descriptor.to_string_with_secret(&ext_keymap) + ); + println!( + "tprv internal descriptor: {}", + internal_descriptor.to_string_with_secret(&int_keymap) + ); + + Ok(()) +} diff --git a/crates/wallet/examples/policy.rs b/crates/wallet/examples/policy.rs new file mode 100644 index 00000000..e748c3fd --- /dev/null +++ b/crates/wallet/examples/policy.rs @@ -0,0 +1,60 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +extern crate bdk_wallet; +use std::error::Error; + +use bdk_wallet::bitcoin::Network; +use bdk_wallet::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor}; +use bdk_wallet::wallet::signer::SignersContainer; + +/// This example describes the use of the BDK's [`bdk_wallet::descriptor::policy`] module. +/// +/// Policy is higher abstraction representation of the wallet descriptor spending condition. +/// This is useful to express complex miniscript spending conditions into more human readable form. +/// The resulting `Policy` structure can be used to derive spending conditions the wallet is capable +/// to spend from. +/// +/// This example demos a Policy output for a 2of2 multisig between between 2 parties, where the wallet holds +/// one of the Extend Private key. + +fn main() -> Result<(), Box> { + let secp = bitcoin::secp256k1::Secp256k1::new(); + + // The descriptor used in the example + // The form is "wsh(multi(2, , ))" + let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))"; + + // Use the descriptor string to derive the full descriptor and a keymap. + // The wallet descriptor can be used to create a new bdk_wallet::wallet. + // While the `keymap` can be used to create a `SignerContainer`. + // + // The `SignerContainer` can sign for `PSBT`s. + // a bdk_wallet::wallet internally uses these to handle transaction signing. + // But they can be used as independent tools also. + let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?; + + println!("Example Descriptor for policy analysis : {}", wallet_desc); + + // Create the signer with the keymap and descriptor. + let signers_container = SignersContainer::build(keymap, &wallet_desc, &secp); + + // Extract the Policy from the given descriptor and signer. + // Note that Policy is a wallet specific structure. It depends on the the descriptor, and + // what the concerned wallet with a given signer can sign for. + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp)? + .expect("We expect a policy"); + + println!("Derived Policy for the descriptor {:#?}", policy); + + Ok(()) +} diff --git a/crates/wallet/src/descriptor/checksum.rs b/crates/wallet/src/descriptor/checksum.rs new file mode 100644 index 00000000..243376bc --- /dev/null +++ b/crates/wallet/src/descriptor/checksum.rs @@ -0,0 +1,147 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptor checksum +//! +//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the +//! checksum of a descriptor + +use crate::descriptor::DescriptorError; +use alloc::string::String; + +const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "; +const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + +fn poly_mod(mut c: u64, val: u64) -> u64 { + let c0 = c >> 35; + c = ((c & 0x7ffffffff) << 5) ^ val; + if c0 & 1 > 0 { + c ^= 0xf5dee51989 + }; + if c0 & 2 > 0 { + c ^= 0xa9fdca3312 + }; + if c0 & 4 > 0 { + c ^= 0x1bab10e32d + }; + if c0 & 8 > 0 { + c ^= 0x3706b1677a + }; + if c0 & 16 > 0 { + c ^= 0x644d626ffd + }; + + c +} + +/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation +pub fn calc_checksum_bytes(mut desc: &str) -> Result<[u8; 8], DescriptorError> { + let mut c = 1; + let mut cls = 0; + let mut clscount = 0; + + let mut original_checksum = None; + if let Some(split) = desc.split_once('#') { + desc = split.0; + original_checksum = Some(split.1); + } + + for ch in desc.as_bytes() { + let pos = INPUT_CHARSET + .iter() + .position(|b| b == ch) + .ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64; + c = poly_mod(c, pos & 31); + cls = cls * 3 + (pos >> 5); + clscount += 1; + if clscount == 3 { + c = poly_mod(c, cls); + cls = 0; + clscount = 0; + } + } + if clscount > 0 { + c = poly_mod(c, cls); + } + (0..8).for_each(|_| c = poly_mod(c, 0)); + c ^= 1; + + let mut checksum = [0_u8; 8]; + for j in 0..8 { + checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize]; + } + + // if input data already had a checksum, check calculated checksum against original checksum + if let Some(original_checksum) = original_checksum { + if original_checksum.as_bytes() != checksum { + return Err(DescriptorError::InvalidDescriptorChecksum); + } + } + + Ok(checksum) +} + +/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation +pub fn calc_checksum(desc: &str) -> Result { + // unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET` + calc_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) }) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::descriptor::calc_checksum; + use assert_matches::assert_matches; + + // test calc_checksum() function; it should return the same value as Bitcoin Core + #[test] + fn test_calc_checksum() { + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"; + assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62"); + + let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)"; + assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs"); + } + + // test calc_checksum() function; it should return the same value as Bitcoin Core even if the + // descriptor string includes a checksum hash + #[test] + fn test_calc_checksum_with_checksum_hash() { + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62"; + assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62"); + + let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmfs"; + assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs"); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26"; + assert_matches!( + calc_checksum(desc), + Err(DescriptorError::InvalidDescriptorChecksum) + ); + + let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf"; + assert_matches!( + calc_checksum(desc), + Err(DescriptorError::InvalidDescriptorChecksum) + ); + } + + #[test] + fn test_calc_checksum_invalid_character() { + let sparkle_heart = unsafe { core::str::from_utf8_unchecked(&[240, 159, 146, 150]) }; + let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart); + + assert_matches!( + calc_checksum(&invalid_desc), + Err(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0] + ); + } +} diff --git a/crates/wallet/src/descriptor/dsl.rs b/crates/wallet/src/descriptor/dsl.rs new file mode 100644 index 00000000..0d7e7c8e --- /dev/null +++ b/crates/wallet/src/descriptor/dsl.rs @@ -0,0 +1,1217 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptors DSL + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_top_level_sh { + // disallow `sortedmulti` in `bare()` + ( Bare, new, new, Legacy, sortedmulti $( $inner:tt )* ) => { + compile_error!("`bare()` descriptors can't contain any `sortedmulti()` operands"); + }; + ( Bare, new, new, Legacy, sortedmulti_vec $( $inner:tt )* ) => { + compile_error!("`bare()` descriptors can't contain any `sortedmulti_vec()` operands"); + }; + + ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti $( $inner:tt )* ) => {{ + use core::marker::PhantomData; + + use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey}; + use $crate::miniscript::$ctx; + + let build_desc = |k, pks| { + Ok((Descriptor::::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>)) + }; + + $crate::impl_sortedmulti!(build_desc, sortedmulti $( $inner )*) + }}; + ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti_vec $( $inner:tt )* ) => {{ + use core::marker::PhantomData; + + use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey}; + use $crate::miniscript::$ctx; + + let build_desc = |k, pks| { + Ok((Descriptor::::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>)) + }; + + $crate::impl_sortedmulti!(build_desc, sortedmulti_vec $( $inner )*) + }}; + + ( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, $( $minisc:tt )* ) => {{ + use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey}; + + $crate::fragment!($( $minisc )*) + .and_then(|(minisc, keymap, networks)| Ok(($inner_struct::$constructor(minisc)?, keymap, networks))) + .and_then(|(inner, key_map, valid_networks)| Ok((Descriptor::::$inner_struct(inner), key_map, valid_networks))) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_top_level_pk { + ( $inner_type:ident, $ctx:ty, $key:expr ) => {{ + use $crate::miniscript::descriptor::$inner_type; + + #[allow(unused_imports)] + use $crate::keys::{DescriptorKey, IntoDescriptorKey}; + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + $key.into_descriptor_key() + .and_then(|key: DescriptorKey<$ctx>| key.extract(&secp)) + .map_err($crate::descriptor::DescriptorError::Key) + .map(|(pk, key_map, valid_networks)| ($inner_type::new(pk), key_map, valid_networks)) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_top_level_tr { + ( $internal_key:expr, $tap_tree:expr ) => {{ + use $crate::miniscript::descriptor::{ + Descriptor, DescriptorPublicKey, KeyMap, TapTree, Tr, + }; + use $crate::miniscript::Tap; + + #[allow(unused_imports)] + use $crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks}; + + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + $internal_key + .into_descriptor_key() + .and_then(|key: DescriptorKey| key.extract(&secp)) + .map_err($crate::descriptor::DescriptorError::Key) + .and_then(|(pk, mut key_map, mut valid_networks)| { + let tap_tree = $tap_tree.map( + |(tap_tree, tree_keymap, tree_networks): ( + TapTree, + KeyMap, + ValidNetworks, + )| { + key_map.extend(tree_keymap.into_iter()); + valid_networks = + $crate::keys::merge_networks(&valid_networks, &tree_networks); + + tap_tree + }, + ); + + Ok(( + Descriptor::::Tr(Tr::new(pk, tap_tree)?), + key_map, + valid_networks, + )) + }) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_leaf_opcode { + ( $terminal_variant:ident ) => {{ + use $crate::descriptor::CheckMiniscript; + + $crate::miniscript::Miniscript::from_ast( + $crate::miniscript::miniscript::decode::Terminal::$terminal_variant, + ) + .map_err($crate::descriptor::DescriptorError::Miniscript) + .and_then(|minisc| { + minisc.check_miniscript()?; + Ok(minisc) + }) + .map(|minisc| { + ( + minisc, + $crate::miniscript::descriptor::KeyMap::default(), + $crate::keys::any_network(), + ) + }) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_leaf_opcode_value { + ( $terminal_variant:ident, $value:expr ) => {{ + use $crate::descriptor::CheckMiniscript; + + $crate::miniscript::Miniscript::from_ast( + $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($value), + ) + .map_err($crate::descriptor::DescriptorError::Miniscript) + .and_then(|minisc| { + minisc.check_miniscript()?; + Ok(minisc) + }) + .map(|minisc| { + ( + minisc, + $crate::miniscript::descriptor::KeyMap::default(), + $crate::keys::any_network(), + ) + }) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_leaf_opcode_value_two { + ( $terminal_variant:ident, $one:expr, $two:expr ) => {{ + use $crate::descriptor::CheckMiniscript; + + $crate::miniscript::Miniscript::from_ast( + $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($one, $two), + ) + .map_err($crate::descriptor::DescriptorError::Miniscript) + .and_then(|minisc| { + minisc.check_miniscript()?; + Ok(minisc) + }) + .map(|minisc| { + ( + minisc, + $crate::miniscript::descriptor::KeyMap::default(), + $crate::keys::any_network(), + ) + }) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_node_opcode_two { + ( $terminal_variant:ident, $( $inner:tt )* ) => ({ + use $crate::descriptor::CheckMiniscript; + + let inner = $crate::fragment_internal!( @t $( $inner )* ); + let (a, b) = $crate::descriptor::dsl::TupleTwo::from(inner).flattened(); + + a + .and_then(|a| Ok((a, b?))) + .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks))| { + // join key_maps + a_keymap.extend(b_keymap.into_iter()); + + let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant( + $crate::alloc::sync::Arc::new(a_minisc), + $crate::alloc::sync::Arc::new(b_minisc), + ))?; + + minisc.check_miniscript()?; + + Ok((minisc, a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks))) + }) + }); +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_node_opcode_three { + ( $terminal_variant:ident, $( $inner:tt )* ) => ({ + use $crate::descriptor::CheckMiniscript; + + let inner = $crate::fragment_internal!( @t $( $inner )* ); + let (a, b, c) = $crate::descriptor::dsl::TupleThree::from(inner).flattened(); + + a + .and_then(|a| Ok((a, b?, c?))) + .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks), (c_minisc, c_keymap, c_networks))| { + // join key_maps + a_keymap.extend(b_keymap.into_iter()); + a_keymap.extend(c_keymap.into_iter()); + + let networks = $crate::keys::merge_networks(&a_networks, &b_networks); + let networks = $crate::keys::merge_networks(&networks, &c_networks); + + let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant( + $crate::alloc::sync::Arc::new(a_minisc), + $crate::alloc::sync::Arc::new(b_minisc), + $crate::alloc::sync::Arc::new(c_minisc), + ))?; + + minisc.check_miniscript()?; + + Ok((minisc, a_keymap, networks)) + }) + }); +} + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_sortedmulti { + ( $build_desc:expr, sortedmulti_vec ( $thresh:expr, $keys:expr ) ) => ({ + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + $crate::keys::make_sortedmulti($thresh, $keys, $build_desc, &secp) + }); + ( $build_desc:expr, sortedmulti ( $thresh:expr $(, $key:expr )+ ) ) => ({ + use $crate::keys::IntoDescriptorKey; + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + let keys = vec![ + $( + $key.into_descriptor_key(), + )* + ]; + + keys.into_iter().collect::, _>>() + .map_err($crate::descriptor::DescriptorError::Key) + .and_then(|keys| $crate::keys::make_sortedmulti($thresh, keys, $build_desc, &secp)) + }); + +} + +#[doc(hidden)] +#[macro_export] +macro_rules! parse_tap_tree { + ( @merge $tree_a:expr, $tree_b:expr) => {{ + use $crate::miniscript::descriptor::TapTree; + + $tree_a + .and_then(|tree_a| Ok((tree_a, $tree_b?))) + .and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| { + a_keymap.extend(b_keymap.into_iter()); + Ok((TapTree::combine(a_tree, b_tree), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks))) + }) + + }}; + + // Two sub-trees + ( { { $( $tree_a:tt )* }, { $( $tree_b:tt )* } } ) => {{ + let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } ); + let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + + // One leaf and a sub-tree + ( { $op_a:ident ( $( $minisc_a:tt )* ), { $( $tree_b:tt )* } } ) => {{ + let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) ); + let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + ( { { $( $tree_a:tt )* }, $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{ + let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } ); + let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + + // Two leaves + ( { $op_a:ident ( $( $minisc_a:tt )* ), $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{ + let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) ); + let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + + // Single leaf + ( $op:ident ( $( $minisc:tt )* ) ) => {{ + use $crate::alloc::sync::Arc; + use $crate::miniscript::descriptor::TapTree; + + $crate::fragment!( $op ( $( $minisc )* ) ) + .map(|(a_minisc, a_keymap, a_networks)| (TapTree::Leaf(Arc::new(a_minisc)), a_keymap, a_networks)) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! apply_modifier { + ( $terminal_variant:ident, $inner:expr ) => {{ + use $crate::descriptor::CheckMiniscript; + + $inner + .map_err(|e| -> $crate::descriptor::DescriptorError { e.into() }) + .and_then(|(minisc, keymap, networks)| { + let minisc = $crate::miniscript::Miniscript::from_ast( + $crate::miniscript::miniscript::decode::Terminal::$terminal_variant( + $crate::alloc::sync::Arc::new(minisc), + ), + )?; + + minisc.check_miniscript()?; + + Ok((minisc, keymap, networks)) + }) + }}; + + ( a: $inner:expr ) => {{ + $crate::apply_modifier!(Alt, $inner) + }}; + ( s: $inner:expr ) => {{ + $crate::apply_modifier!(Swap, $inner) + }}; + ( c: $inner:expr ) => {{ + $crate::apply_modifier!(Check, $inner) + }}; + ( d: $inner:expr ) => {{ + $crate::apply_modifier!(DupIf, $inner) + }}; + ( v: $inner:expr ) => {{ + $crate::apply_modifier!(Verify, $inner) + }}; + ( j: $inner:expr ) => {{ + $crate::apply_modifier!(NonZero, $inner) + }}; + ( n: $inner:expr ) => {{ + $crate::apply_modifier!(ZeroNotEqual, $inner) + }}; + + // Modifiers expanded to other operators + ( t: $inner:expr ) => {{ + $inner.and_then(|(a_minisc, a_keymap, a_networks)| { + $crate::impl_leaf_opcode_value_two!( + AndV, + $crate::alloc::sync::Arc::new(a_minisc), + $crate::alloc::sync::Arc::new($crate::fragment!(true).unwrap().0) + ) + .map(|(minisc, _, _)| (minisc, a_keymap, a_networks)) + }) + }}; + ( l: $inner:expr ) => {{ + $inner.and_then(|(a_minisc, a_keymap, a_networks)| { + $crate::impl_leaf_opcode_value_two!( + OrI, + $crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0), + $crate::alloc::sync::Arc::new(a_minisc) + ) + .map(|(minisc, _, _)| (minisc, a_keymap, a_networks)) + }) + }}; + ( u: $inner:expr ) => {{ + $inner.and_then(|(a_minisc, a_keymap, a_networks)| { + $crate::impl_leaf_opcode_value_two!( + OrI, + $crate::alloc::sync::Arc::new(a_minisc), + $crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0) + ) + .map(|(minisc, _, _)| (minisc, a_keymap, a_networks)) + }) + }}; +} + +/// Macro to write full descriptors with code +/// +/// This macro expands to a `Result` of +/// [`DescriptorTemplateOut`](super::template::DescriptorTemplateOut) and [`DescriptorError`](crate::descriptor::DescriptorError) +/// +/// The syntax is very similar to the normal descriptor syntax, with the exception that modifiers +/// cannot be grouped together. For instance, a descriptor fragment like `sdv:older(144)` has to be +/// broken up to `s:d:v:older(144)`. +/// +/// The `pk()`, `pk_k()` and `pk_h()` operands can take as argument any type that implements +/// [`IntoDescriptorKey`]. This means that keys can also be written inline as strings, but in that +/// case they must be wrapped in quotes, which is another difference compared to the standard +/// descriptor syntax. +/// +/// [`IntoDescriptorKey`]: crate::keys::IntoDescriptorKey +/// +/// ## Example +/// +/// Signature plus timelock descriptor: +/// +/// ``` +/// # use std::str::FromStr; +/// let (my_descriptor, my_keys_map, networks) = bdk_wallet::descriptor!(sh(wsh(and_v(v:pk("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy"),older(50)))))?; +/// # Ok::<(), Box>(()) +/// ``` +/// +/// ------- +/// +/// 2-of-3 that becomes a 1-of-3 after a timelock has expired. Both `descriptor_a` and `descriptor_b` are equivalent: the first +/// syntax is more suitable for a fixed number of items known at compile time, while the other accepts a +/// [`Vec`] of items, which makes it more suitable for writing dynamic descriptors. +/// +/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sndv:older(...)))` +/// +/// ``` +/// # use std::str::FromStr; +/// let my_key_1 = bitcoin::PublicKey::from_str( +/// "02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c", +/// )?; +/// let my_key_2 = +/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; +/// let my_timelock = 50; +/// +/// let (descriptor_a, key_map_a, networks) = bdk_wallet::descriptor! { +/// wsh ( +/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n:d:v:older(my_timelock)) +/// ) +/// }?; +/// +/// #[rustfmt::skip] +/// let b_items = vec![ +/// bdk_wallet::fragment!(pk(my_key_1))?, +/// bdk_wallet::fragment!(s:pk(my_key_2))?, +/// bdk_wallet::fragment!(s:n:d:v:older(my_timelock))?, +/// ]; +/// let (descriptor_b, mut key_map_b, networks) = +/// bdk_wallet::descriptor!(wsh(thresh_vec(2, b_items)))?; +/// +/// assert_eq!(descriptor_a, descriptor_b); +/// assert_eq!(key_map_a.len(), key_map_b.len()); +/// # Ok::<(), Box>(()) +/// ``` +/// +/// ------ +/// +/// Simple 2-of-2 multi-signature, equivalent to: `wsh(multi(2, ...))` +/// +/// ``` +/// # use std::str::FromStr; +/// let my_key_1 = bitcoin::PublicKey::from_str( +/// "02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c", +/// )?; +/// let my_key_2 = +/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; +/// +/// let (descriptor, key_map, networks) = bdk_wallet::descriptor! { +/// wsh ( +/// multi(2, my_key_1, my_key_2) +/// ) +/// }?; +/// # Ok::<(), Box>(()) +/// ``` +/// +/// ------ +/// +/// Native-Segwit single-sig, equivalent to: `wpkh(...)` +/// +/// ``` +/// let my_key = +/// bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; +/// +/// let (descriptor, key_map, networks) = bdk_wallet::descriptor!(wpkh(my_key))?; +/// # Ok::<(), Box>(()) +/// ``` +/// +/// [`Vec`]: alloc::vec::Vec +#[macro_export] +macro_rules! descriptor { + ( bare ( $( $minisc:tt )* ) ) => ({ + $crate::impl_top_level_sh!(Bare, new, new, Legacy, $( $minisc )*) + }); + ( sh ( wsh ( $( $minisc:tt )* ) ) ) => ({ + $crate::descriptor!(shwsh ($( $minisc )*)) + }); + ( shwsh ( $( $minisc:tt )* ) ) => ({ + $crate::impl_top_level_sh!(Sh, new_wsh, new_wsh_sortedmulti, Segwitv0, $( $minisc )*) + }); + ( pk ( $key:expr ) ) => ({ + // `pk()` is actually implemented as `bare(pk())` + $crate::descriptor!( bare ( pk ( $key ) ) ) + }); + ( pkh ( $key:expr ) ) => ({ + use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey}; + + $crate::impl_top_level_pk!(Pkh, $crate::miniscript::Legacy, $key) + .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c))) + .map(|(a, b, c)| (Descriptor::::Pkh(a), b, c)) + }); + ( wpkh ( $key:expr ) ) => ({ + use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey}; + + $crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key) + .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c))) + .map(|(a, b, c)| (Descriptor::::Wpkh(a), b, c)) + }); + ( sh ( wpkh ( $key:expr ) ) ) => ({ + $crate::descriptor!(shwpkh ( $key )) + }); + ( shwpkh ( $key:expr ) ) => ({ + use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, Sh}; + + $crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key) + .and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c))) + .and_then(|(a, b, c)| Ok((Descriptor::::Sh(Sh::new_wpkh(a.into_inner())?), b, c))) + }); + ( sh ( $( $minisc:tt )* ) ) => ({ + $crate::impl_top_level_sh!(Sh, new, new_sortedmulti, Legacy, $( $minisc )*) + }); + ( wsh ( $( $minisc:tt )* ) ) => ({ + $crate::impl_top_level_sh!(Wsh, new, new_sortedmulti, Segwitv0, $( $minisc )*) + }); + + ( tr ( $internal_key:expr ) ) => ({ + $crate::impl_top_level_tr!($internal_key, None) + }); + ( tr ( $internal_key:expr, $( $taptree:tt )* ) ) => ({ + let tap_tree = $crate::parse_tap_tree!( $( $taptree )* ); + tap_tree + .and_then(|tap_tree| $crate::impl_top_level_tr!($internal_key, Some(tap_tree))) + }); +} + +#[doc(hidden)] +pub struct TupleTwo { + pub a: A, + pub b: B, +} + +impl TupleTwo { + pub fn flattened(self) -> (A, B) { + (self.a, self.b) + } +} + +impl From<(A, (B, ()))> for TupleTwo { + fn from((a, (b, _)): (A, (B, ()))) -> Self { + TupleTwo { a, b } + } +} + +#[doc(hidden)] +pub struct TupleThree { + pub a: A, + pub b: B, + pub c: C, +} + +impl TupleThree { + pub fn flattened(self) -> (A, B, C) { + (self.a, self.b, self.c) + } +} + +impl From<(A, (B, (C, ())))> for TupleThree { + fn from((a, (b, (c, _))): (A, (B, (C, ())))) -> Self { + TupleThree { a, b, c } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! group_multi_keys { + ( $( $key:expr ),+ ) => {{ + use $crate::keys::IntoDescriptorKey; + + let keys = vec![ + $( + $key.into_descriptor_key(), + )* + ]; + + keys.into_iter().collect::, _>>() + .map_err($crate::descriptor::DescriptorError::Key) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! fragment_internal { + // The @v prefix is used to parse a sequence of operands and return them in a vector. This is + // used by operands that take a variable number of arguments, like `thresh()` and `multi()`. + ( @v $op:ident ( $( $args:tt )* ) $( $tail:tt )* ) => ({ + let mut v = vec![$crate::fragment!( $op ( $( $args )* ) )]; + v.append(&mut $crate::fragment_internal!( @v $( $tail )* )); + + v + }); + // Match modifiers + ( @v $modif:tt : $( $tail:tt )* ) => ({ + let mut v = $crate::fragment_internal!( @v $( $tail )* ); + let first = v.drain(..1).next().unwrap(); + + let first = $crate::apply_modifier!($modif:first); + + let mut v_final = vec![first]; + v_final.append(&mut v); + + v_final + }); + // Remove commas between operands + ( @v , $( $tail:tt )* ) => ({ + $crate::fragment_internal!( @v $( $tail )* ) + }); + ( @v ) => ({ + vec![] + }); + + // The @t prefix is used to parse a sequence of operands and return them in a tuple. This + // allows checking at compile-time the number of arguments passed to an operand. For this + // reason it's used by `and_*()`, `or_*()`, etc. + // + // Unfortunately, due to the fact that concatenating tuples is pretty hard, the final result + // adds in the first spot the parsed operand and in the second spot the result of parsing + // all the following ones. For two operands the type then corresponds to: (X, (X, ())). For + // three operands it's (X, (X, (X, ()))), etc. + // + // To check that the right number of arguments has been passed we can "cast" those tuples to + // more convenient structures like `TupleTwo`. If the conversion succeeds, the right number of + // args was passed. Otherwise the compilation fails entirely. + ( @t $op:ident ( $( $args:tt )* ) $( $tail:tt )* ) => ({ + ($crate::fragment!( $op ( $( $args )* ) ), $crate::fragment_internal!( @t $( $tail )* )) + }); + // Match modifiers + ( @t $modif:tt : $( $tail:tt )* ) => ({ + let (first, tail) = $crate::fragment_internal!( @t $( $tail )* ); + ($crate::apply_modifier!($modif:first), tail) + }); + // Remove commas between operands + ( @t , $( $tail:tt )* ) => ({ + $crate::fragment_internal!( @t $( $tail )* ) + }); + ( @t ) => ({}); + + // Fallback to calling `fragment!()` + ( $( $tokens:tt )* ) => ({ + $crate::fragment!($( $tokens )*) + }); +} + +/// Macro to write descriptor fragments with code +/// +/// This macro will be expanded to an object of type `Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError>`. It allows writing +/// fragments of larger descriptors that can be pieced together using `fragment!(thresh_vec(m, ...))`. +/// +/// The syntax to write macro fragment is the same as documented for the [`descriptor`] macro. +#[macro_export] +macro_rules! fragment { + // Modifiers + ( $modif:tt : $( $tail:tt )* ) => ({ + let op = $crate::fragment!( $( $tail )* ); + $crate::apply_modifier!($modif:op) + }); + + // Miniscript + ( true ) => ({ + $crate::impl_leaf_opcode!(True) + }); + ( false ) => ({ + $crate::impl_leaf_opcode!(False) + }); + ( pk_k ( $key:expr ) ) => ({ + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + $crate::keys::make_pk($key, &secp) + }); + ( pk ( $key:expr ) ) => ({ + $crate::fragment!(c:pk_k ( $key )) + }); + ( pk_h ( $key:expr ) ) => ({ + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + $crate::keys::make_pkh($key, &secp) + }); + ( after ( $value:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(After, $crate::miniscript::AbsLockTime::from_consensus($value)) + }); + ( older ( $value:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!! + }); + ( sha256 ( $hash:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(Sha256, $hash) + }); + ( hash256 ( $hash:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(Hash256, $hash) + }); + ( ripemd160 ( $hash:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(Ripemd160, $hash) + }); + ( hash160 ( $hash:expr ) ) => ({ + $crate::impl_leaf_opcode_value!(Hash160, $hash) + }); + ( and_v ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(AndV, $( $inner )*) + }); + ( and_b ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(AndB, $( $inner )*) + }); + ( and_or ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_three!(AndOr, $( $inner )*) + }); + ( andor ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_three!(AndOr, $( $inner )*) + }); + ( or_b ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(OrB, $( $inner )*) + }); + ( or_d ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(OrD, $( $inner )*) + }); + ( or_c ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(OrC, $( $inner )*) + }); + ( or_i ( $( $inner:tt )* ) ) => ({ + $crate::impl_node_opcode_two!(OrI, $( $inner )*) + }); + ( thresh_vec ( $thresh:expr, $items:expr ) ) => ({ + use $crate::miniscript::descriptor::KeyMap; + + let (items, key_maps_networks): ($crate::alloc::vec::Vec<_>, $crate::alloc::vec::Vec<_>) = $items.into_iter().map(|(a, b, c)| (a, (b, c))).unzip(); + let items = items.into_iter().map($crate::alloc::sync::Arc::new).collect(); + + let (key_maps, valid_networks) = key_maps_networks.into_iter().fold((KeyMap::default(), $crate::keys::any_network()), |(mut keys_acc, net_acc), (key, net)| { + keys_acc.extend(key.into_iter()); + let net_acc = $crate::keys::merge_networks(&net_acc, &net); + + (keys_acc, net_acc) + }); + + $crate::impl_leaf_opcode_value_two!(Thresh, $thresh, items) + .map(|(minisc, _, _)| (minisc, key_maps, valid_networks)) + }); + ( thresh ( $thresh:expr, $( $inner:tt )* ) ) => ({ + let items = $crate::fragment_internal!( @v $( $inner )* ); + + items.into_iter().collect::, _>>() + .and_then(|items| $crate::fragment!(thresh_vec($thresh, items))) + }); + ( multi_vec ( $thresh:expr, $keys:expr ) ) => ({ + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp) + }); + ( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({ + $crate::group_multi_keys!( $( $key ),* ) + .and_then(|keys| $crate::fragment!( multi_vec ( $thresh, keys ) )) + }); + ( multi_a_vec ( $thresh:expr, $keys:expr ) ) => ({ + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::MultiA, $keys, &secp) + }); + ( multi_a ( $thresh:expr $(, $key:expr )+ ) ) => ({ + $crate::group_multi_keys!( $( $key ),* ) + .and_then(|keys| $crate::fragment!( multi_a_vec ( $thresh, keys ) )) + }); + + // `sortedmulti()` is handled separately + ( sortedmulti ( $( $inner:tt )* ) ) => ({ + compile_error!("`sortedmulti` can only be used as the root operand of a descriptor"); + }); + ( sortedmulti_vec ( $( $inner:tt )* ) ) => ({ + compile_error!("`sortedmulti_vec` can only be used as the root operand of a descriptor"); + }); +} + +#[cfg(test)] +mod test { + use alloc::string::ToString; + use bitcoin::secp256k1::Secp256k1; + use miniscript::descriptor::{DescriptorPublicKey, KeyMap}; + use miniscript::{Descriptor, Legacy, Segwitv0}; + + use core::str::FromStr; + + use crate::descriptor::{DescriptorError, DescriptorMeta}; + use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks}; + use bitcoin::bip32; + use bitcoin::Network::{Bitcoin, Regtest, Signet, Testnet}; + use bitcoin::PrivateKey; + + // test the descriptor!() macro + + // verify descriptor generates expected script(s) (if bare or pk) or address(es) + fn check( + desc: Result<(Descriptor, KeyMap, ValidNetworks), DescriptorError>, + is_witness: bool, + is_fixed: bool, + expected: &[&str], + ) { + let (desc, _key_map, _networks) = desc.unwrap(); + assert_eq!(desc.is_witness(), is_witness); + assert_eq!(!desc.has_wildcard(), is_fixed); + for i in 0..expected.len() { + let child_desc = desc + .at_derivation_index(i as u32) + .expect("i is not hardened"); + let address = child_desc.address(Regtest); + if let Ok(address) = address { + assert_eq!(address.to_string(), *expected.get(i).unwrap()); + } else { + let script = child_desc.script_pubkey(); + assert_eq!(script.to_hex_string(), *expected.get(i).unwrap()); + } + } + } + + // - at least one of each "type" of operator; i.e. one modifier, one leaf_opcode, one leaf_opcode_value, etc. + // - mixing up key types that implement IntoDescriptorKey in multi() or thresh() + + // expected script for pk and bare manually created + // expected addresses created with `bitcoin-cli getdescriptorinfo` (for hash) and `bitcoin-cli deriveaddresses` + + #[test] + fn test_fixed_legacy_descriptors() { + let pubkey1 = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + let pubkey2 = bitcoin::PublicKey::from_str( + "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", + ) + .unwrap(); + + check( + descriptor!(bare(multi(1,pubkey1,pubkey2))), + false, + true, + &["512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd21032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af52ae"], + ); + check( + descriptor!(pk(pubkey1)), + false, + true, + &["2103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac"], + ); + check( + descriptor!(pkh(pubkey1)), + false, + true, + &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"], + ); + check( + descriptor!(sh(multi(1, pubkey1, pubkey2))), + false, + true, + &["2MymURoV1bzuMnWMGiXzyomDkeuxXY7Suey"], + ); + } + + #[test] + fn test_fixed_segwitv0_descriptors() { + let pubkey1 = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + let pubkey2 = bitcoin::PublicKey::from_str( + "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", + ) + .unwrap(); + + check( + descriptor!(wpkh(pubkey1)), + true, + true, + &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"], + ); + check( + descriptor!(sh(wpkh(pubkey1))), + true, + true, + &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"], + ); + check( + descriptor!(wsh(multi(1, pubkey1, pubkey2))), + true, + true, + &["bcrt1qgw8jvv2hsrvjfa6q66rk6har7d32lrqm5unnf5cl63q9phxfvgps5fyfqe"], + ); + check( + descriptor!(sh(wsh(multi(1, pubkey1, pubkey2)))), + true, + true, + &["2NCidRJysy7apkmE6JF5mLLaJFkrN3Ub9iy"], + ); + } + + #[test] + fn test_fixed_threeop_descriptors() { + let redeem_key = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + let move_key = bitcoin::PublicKey::from_str( + "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", + ) + .unwrap(); + + check( + descriptor!(sh(wsh(and_or(pk(redeem_key), older(1000), pk(move_key))))), + true, + true, + &["2MypGwr5eQWAWWJtiJgUEToVxc4zuokjQRe"], + ); + } + + #[test] + fn test_bip32_legacy_descriptors() { + let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); + check( + descriptor!(pk(desc_key)), + false, + false, + &[ + "2102363ad03c10024e1b597a5b01b9982807fb638e00b06f3b2d4a89707de3b93c37ac", + "2102063a21fd780df370ed2fc8c4b86aa5ea642630609c203009df631feb7b480dd2ac", + "2102ba2685ad1fa5891cb100f1656b2ce3801822ccb9bac0336734a6f8c1b93ebbc0ac", + ], + ); + + let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); + check( + descriptor!(pkh(desc_key)), + false, + false, + &[ + "muvBdsVpJxpFuTHMKA47htJPdCvdt4F9DP", + "mxQSHK7DL2t1DN3xFxov1janCoXSSkrSPj", + "mfz43r15GiWo4nizmyzMNubsnkDpByFFAn", + ], + ); + + let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); + let desc_key1 = (xprv, path).into_descriptor_key().unwrap(); + let desc_key2 = (xprv, path2).into_descriptor_key().unwrap(); + + check( + descriptor!(sh(multi(1, desc_key1, desc_key2))), + false, + false, + &[ + "2MtMDXsfwefZkEEhVViEPidvcKRUtJamJJ8", + "2MwAUZ1NYyWjhVvGTethFL6n7nZhS8WE6At", + "2MuT6Bj66HLwZd7s4SoD8XbK4GwriKEA6Gr", + ], + ); + } + + #[test] + fn test_bip32_segwitv0_descriptors() { + let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); + check( + descriptor!(wpkh(desc_key)), + true, + false, + &[ + "bcrt1qnhm8w9fhc8cxzgqsmqdf9fyjccyvc0gltnymu0", + "bcrt1qhylfd55rn75w9fj06zspctad5w4hz33rf0ttad", + "bcrt1qq5sq3a6k9av9d8cne0k9wcldy4nqey5yt6889r", + ], + ); + + let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap(); + check( + descriptor!(sh(wpkh(desc_key))), + true, + false, + &[ + "2MxvjQCaLqZ5QxZ7XotZDQ63hZw3NPss763", + "2NDUoevN4QMzhvHDMGhKuiT2fN9HXbFRMwn", + "2NF4BEAY2jF1Fu8vqfN3NVKoFtom77pUxrx", + ], + ); + + let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); + let desc_key1 = (xprv, path.clone()).into_descriptor_key().unwrap(); + let desc_key2 = (xprv, path2.clone()).into_descriptor_key().unwrap(); + check( + descriptor!(wsh(multi(1, desc_key1, desc_key2))), + true, + false, + &[ + "bcrt1qfxv8mxmlv5sz8q2mnuyaqdfe9jr4vvmx0csjhn092p6f4qfygfkq2hng49", + "bcrt1qerj85g243e6jlcdxpmn9spk0gefcwvu7nw7ee059d5ydzpdhkm2qwfkf5k", + "bcrt1qxkl2qss3k58q9ktc8e89pwr4gnptfpw4hju4xstxcjc0hkcae3jstluty7", + ], + ); + + let desc_key1 = (xprv, path).into_descriptor_key().unwrap(); + let desc_key2 = (xprv, path2).into_descriptor_key().unwrap(); + check( + descriptor!(sh(wsh(multi(1, desc_key1, desc_key2)))), + true, + false, + &[ + "2NFCtXvx9q4ci2kvKub17iSTgvRXGctCGhz", + "2NB2PrFPv5NxWCpygas8tPrGJG2ZFgeuwJw", + "2N79ZAGo5cMi5Jt7Wo9L5YmF5GkEw7sjWdC", + ], + ); + } + + #[test] + fn test_dsl_sortedmulti() { + let key_1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let path_1 = bip32::DerivationPath::from_str("m/0").unwrap(); + + let key_2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap(); + let path_2 = bip32::DerivationPath::from_str("m/1").unwrap(); + + let desc_key1 = (key_1, path_1); + let desc_key2 = (key_2, path_2); + + check( + descriptor!(sh(sortedmulti(1, desc_key1.clone(), desc_key2.clone()))), + false, + false, + &[ + "2MsxzPEJDBzpGffJXPaDpfXZAUNnZhaMh2N", + "2My3x3DLPK3UbGWGpxrXr1RnbD8MNC4FpgS", + "2NByEuiQT7YLqHCTNxL5KwYjvtuCYcXNBSC", + "2N1TGbP81kj2VUKTSWgrwxoMfuWjvfUdyu7", + "2N3Bomq2fpAcLRNfZnD3bCWK9quan28CxCR", + "2N9nrZaEzEFDqEAU9RPvDnXGT6AVwBDKAQb", + ], + ); + + check( + descriptor!(sh(wsh(sortedmulti( + 1, + desc_key1.clone(), + desc_key2.clone() + )))), + true, + false, + &[ + "2NCogc5YyM4N6ruv1hUa7WLMW1BPeCK7N9B", + "2N6mkSAKi1V2oaBXby7XHdvBMKEDRQcFpNe", + "2NFmTSttm9v6bXeoWaBvpMcgfPQcZhNn3Eh", + "2Mvib87RBPUHXNEpX5S5Kv1qqrhBfgBGsJM", + "2MtMv5mcK2EjcLsH8Txpx2JxLLzHr4ttczL", + "2MsWCB56rb4T6yPv8QudZGHERTwNgesE4f6", + ], + ); + + check( + descriptor!(wsh(sortedmulti_vec(1, vec![desc_key1, desc_key2]))), + true, + false, + &[ + "bcrt1qcvq0lg8q7a47ytrd7zk5y7uls7mulrenjgvflwylpppgwf8029es4vhpnj", + "bcrt1q80yn8sdt6l7pjvkz25lglyaqctlmsq9ugk80rmxt8yu0npdsj97sc7l4de", + "bcrt1qrvf6024v9s50qhffe3t2fr2q9ckdhx2g6jz32chm2pp24ymgtr5qfrdmct", + "bcrt1q6srfmra0ynypym35c7jvsxt2u4yrugeajq95kg2ps7lk6h2gaunsq9lzxn", + "bcrt1qhl8rrzzcdpu7tcup3lcg7tge52sqvwy5fcv4k78v6kxtwmqf3v6qpvyjza", + "bcrt1ql2elz9mhm9ll27ddpewhxs732xyl2fk2kpkqz9gdyh33wgcun4vstrd49k", + ], + ); + } + + // - verify the valid_networks returned is correctly computed based on the keys present in the descriptor + #[test] + fn test_valid_networks() { + let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key = (xprv, path).into_descriptor_key().unwrap(); + + let (_desc, _key_map, valid_networks) = descriptor!(pkh(desc_key)).unwrap(); + assert_eq!( + valid_networks, + [Testnet, Regtest, Signet].iter().cloned().collect() + ); + + let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap(); + let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap(); + let desc_key = (xprv, path).into_descriptor_key().unwrap(); + + let (_desc, _key_map, valid_networks) = descriptor!(wpkh(desc_key)).unwrap(); + assert_eq!(valid_networks, [Bitcoin].iter().cloned().collect()); + } + + // - verify the key_maps are correctly merged together + #[test] + fn test_key_maps_merged() { + let secp = Secp256k1::new(); + + let xprv1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let path1 = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key1 = (xprv1, path1.clone()).into_descriptor_key().unwrap(); + + let xprv2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap(); + let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); + let desc_key2 = (xprv2, path2.clone()).into_descriptor_key().unwrap(); + + let xprv3 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap(); + let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap(); + let desc_key3 = (xprv3, path3.clone()).into_descriptor_key().unwrap(); + + let (_desc, key_map, _valid_networks) = + descriptor!(sh(wsh(multi(2, desc_key1, desc_key2, desc_key3)))).unwrap(); + assert_eq!(key_map.len(), 3); + + let desc_key1: DescriptorKey = (xprv1, path1).into_descriptor_key().unwrap(); + let desc_key2: DescriptorKey = (xprv2, path2).into_descriptor_key().unwrap(); + let desc_key3: DescriptorKey = (xprv3, path3).into_descriptor_key().unwrap(); + + let (key1, _key_map, _valid_networks) = desc_key1.extract(&secp).unwrap(); + let (key2, _key_map, _valid_networks) = desc_key2.extract(&secp).unwrap(); + let (key3, _key_map, _valid_networks) = desc_key3.extract(&secp).unwrap(); + assert_eq!(key_map.get(&key1).unwrap().to_string(), "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*"); + assert_eq!(key_map.get(&key2).unwrap().to_string(), "tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF/2147483647'/0/*"); + assert_eq!(key_map.get(&key3).unwrap().to_string(), "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf/10/20/30/40/*"); + } + + // - verify the ScriptContext is correctly validated (i.e. passing a type that only impl IntoDescriptorKey to a pkh() descriptor should throw a compilation error + #[test] + fn test_script_context_validation() { + // this compiles + let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key: DescriptorKey = (xprv, path).into_descriptor_key().unwrap(); + + let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap(); + assert_eq!(desc.to_string(), "pkh(tpubD6NzVbkrYhZ4WR7a4vY1VT3khMJMeAxVsfq9TBJyJWrNk247zCJtV7AWf6UJP7rAVsn8NNKdJi3gFyKPTmWZS9iukb91xbn2HbFSMQm2igY/0/*)#yrnz9pp2"); + + // as expected this does not compile due to invalid context + //let desc_key:DescriptorKey = (xprv, path.clone()).into_descriptor_key().unwrap(); + //let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap(); + } + + #[test] + fn test_dsl_modifiers() { + let private_key = + PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); + let (descriptor, _, _) = + descriptor!(wsh(thresh(2,n:d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap(); + + assert_eq!(descriptor.to_string(), "wsh(thresh(2,ndv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#zzk3ux8g") + } + + #[test] + #[should_panic(expected = "Miniscript(ContextError(UncompressedKeysNotAllowed))")] + fn test_dsl_miniscript_checks() { + let mut uncompressed_pk = + PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap(); + uncompressed_pk.compressed = false; + + descriptor!(wsh(v: pk(uncompressed_pk))).unwrap(); + } + + #[test] + fn test_dsl_tr_only_key() { + let private_key = + PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); + let (descriptor, _, _) = descriptor!(tr(private_key)).unwrap(); + + assert_eq!( + descriptor.to_string(), + "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)#heq9m95v" + ) + } + + #[test] + fn test_dsl_tr_simple_tree() { + let private_key = + PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); + let (descriptor, _, _) = + descriptor!(tr(private_key, { pk(private_key), pk(private_key) })).unwrap(); + + assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,{pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)})#xy5fjw6d") + } + + #[test] + fn test_dsl_tr_single_leaf() { + let private_key = + PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); + let (descriptor, _, _) = descriptor!(tr(private_key, pk(private_key))).unwrap(); + + assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c))#lzl2vmc7") + } +} diff --git a/crates/wallet/src/descriptor/error.rs b/crates/wallet/src/descriptor/error.rs new file mode 100644 index 00000000..b2809f21 --- /dev/null +++ b/crates/wallet/src/descriptor/error.rs @@ -0,0 +1,123 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptor errors +use core::fmt; + +/// Errors related to the parsing and usage of descriptors +#[derive(Debug)] +pub enum Error { + /// Invalid HD Key path, such as having a wildcard but a length != 1 + InvalidHdKeyPath, + /// The provided descriptor doesn't match its checksum + InvalidDescriptorChecksum, + /// The descriptor contains hardened derivation steps on public extended keys + HardenedDerivationXpub, + /// The descriptor contains multipath keys + MultiPath, + + /// Error thrown while working with [`keys`](crate::keys) + Key(crate::keys::KeyError), + /// Error while extracting and manipulating policies + Policy(crate::descriptor::policy::PolicyError), + + /// Invalid byte found in the descriptor checksum + InvalidDescriptorCharacter(u8), + + /// BIP32 error + Bip32(bitcoin::bip32::Error), + /// Error during base58 decoding + Base58(bitcoin::base58::Error), + /// Key-related error + Pk(bitcoin::key::Error), + /// Miniscript error + Miniscript(miniscript::Error), + /// Hex decoding error + Hex(bitcoin::hex::HexToBytesError), +} + +impl From for Error { + fn from(key_error: crate::keys::KeyError) -> Error { + match key_error { + crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner), + crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner), + e => Error::Key(e), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidHdKeyPath => write!(f, "Invalid HD key path"), + Self::InvalidDescriptorChecksum => { + write!(f, "The provided descriptor doesn't match its checksum") + } + Self::HardenedDerivationXpub => write!( + f, + "The descriptor contains hardened derivation steps on public extended keys" + ), + Self::MultiPath => write!( + f, + "The descriptor contains multipath keys, which are not supported yet" + ), + Self::Key(err) => write!(f, "Key error: {}", err), + Self::Policy(err) => write!(f, "Policy error: {}", err), + Self::InvalidDescriptorCharacter(char) => { + write!(f, "Invalid descriptor character: {}", char) + } + Self::Bip32(err) => write!(f, "BIP32 error: {}", err), + Self::Base58(err) => write!(f, "Base58 error: {}", err), + Self::Pk(err) => write!(f, "Key-related error: {}", err), + Self::Miniscript(err) => write!(f, "Miniscript error: {}", err), + Self::Hex(err) => write!(f, "Hex decoding error: {}", err), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + +impl From for Error { + fn from(err: bitcoin::bip32::Error) -> Self { + Error::Bip32(err) + } +} + +impl From for Error { + fn from(err: bitcoin::base58::Error) -> Self { + Error::Base58(err) + } +} + +impl From for Error { + fn from(err: bitcoin::key::Error) -> Self { + Error::Pk(err) + } +} + +impl From for Error { + fn from(err: miniscript::Error) -> Self { + Error::Miniscript(err) + } +} + +impl From for Error { + fn from(err: bitcoin::hex::HexToBytesError) -> Self { + Error::Hex(err) + } +} + +impl From for Error { + fn from(err: crate::descriptor::policy::PolicyError) -> Self { + Error::Policy(err) + } +} diff --git a/crates/wallet/src/descriptor/mod.rs b/crates/wallet/src/descriptor/mod.rs new file mode 100644 index 00000000..4b1135fe --- /dev/null +++ b/crates/wallet/src/descriptor/mod.rs @@ -0,0 +1,900 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptors +//! +//! This module contains generic utilities to work with descriptors, plus some re-exported types +//! from [`miniscript`]. + +use crate::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; + +use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, KeySource, Xpub}; +use bitcoin::{key::XOnlyPublicKey, secp256k1, PublicKey}; +use bitcoin::{psbt, taproot}; +use bitcoin::{Network, TxOut}; + +use miniscript::descriptor::{ + DefiniteDescriptorKey, DescriptorMultiXKey, DescriptorSecretKey, DescriptorType, + DescriptorXKey, InnerXKey, KeyMap, SinglePubKey, Wildcard, +}; +pub use miniscript::{ + Descriptor, DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0, +}; +use miniscript::{ForEachKey, MiniscriptKey, TranslatePk}; + +use crate::descriptor::policy::BuildSatisfaction; + +pub mod checksum; +#[doc(hidden)] +pub mod dsl; +pub mod error; +pub mod policy; +pub mod template; + +pub use self::checksum::calc_checksum; +use self::checksum::calc_checksum_bytes; +pub use self::error::Error as DescriptorError; +pub use self::policy::Policy; +use self::template::DescriptorTemplateOut; +use crate::keys::{IntoDescriptorKey, KeyError}; +use crate::wallet::signer::SignersContainer; +use crate::wallet::utils::SecpCtx; + +/// Alias for a [`Descriptor`] that can contain extended keys using [`DescriptorPublicKey`] +pub type ExtendedDescriptor = Descriptor; + +/// Alias for a [`Descriptor`] that contains extended **derived** keys +pub type DerivedDescriptor = Descriptor; + +/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or +/// [`psbt::Output`] +/// +/// [`psbt::Input`]: bitcoin::psbt::Input +/// [`psbt::Output`]: bitcoin::psbt::Output +pub type HdKeyPaths = BTreeMap; + +/// Alias for the type of maps that represent taproot key origins in a [`psbt::Input`] or +/// [`psbt::Output`] +/// +/// [`psbt::Input`]: bitcoin::psbt::Input +/// [`psbt::Output`]: bitcoin::psbt::Output +pub type TapKeyOrigins = BTreeMap, KeySource)>; + +/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`] +pub trait IntoWalletDescriptor { + /// Convert to wallet descriptor + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError>; +} + +impl IntoWalletDescriptor for &str { + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + let descriptor = match self.split_once('#') { + Some((desc, original_checksum)) => { + let checksum = calc_checksum_bytes(desc)?; + if original_checksum.as_bytes() != checksum { + return Err(DescriptorError::InvalidDescriptorChecksum); + } + desc + } + None => self, + }; + + ExtendedDescriptor::parse_descriptor(secp, descriptor)? + .into_wallet_descriptor(secp, network) + } +} + +impl IntoWalletDescriptor for &String { + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + self.as_str().into_wallet_descriptor(secp, network) + } +} + +impl IntoWalletDescriptor for ExtendedDescriptor { + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + (self, KeyMap::default()).into_wallet_descriptor(secp, network) + } +} + +impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) { + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + use crate::keys::DescriptorKey; + + struct Translator<'s, 'd> { + secp: &'s SecpCtx, + descriptor: &'d ExtendedDescriptor, + network: Network, + } + + impl<'s, 'd> miniscript::Translator + for Translator<'s, 'd> + { + fn pk(&mut self, pk: &DescriptorPublicKey) -> Result { + let secp = &self.secp; + + let (_, _, networks) = if self.descriptor.is_taproot() { + let descriptor_key: DescriptorKey = + pk.clone().into_descriptor_key()?; + descriptor_key.extract(secp)? + } else if self.descriptor.is_witness() { + let descriptor_key: DescriptorKey = + pk.clone().into_descriptor_key()?; + descriptor_key.extract(secp)? + } else { + let descriptor_key: DescriptorKey = + pk.clone().into_descriptor_key()?; + descriptor_key.extract(secp)? + }; + + if networks.contains(&self.network) { + Ok(Default::default()) + } else { + Err(DescriptorError::Key(KeyError::InvalidNetwork)) + } + } + fn sha256( + &mut self, + _sha256: &::Sha256, + ) -> Result { + Ok(Default::default()) + } + fn hash256( + &mut self, + _hash256: &::Hash256, + ) -> Result { + Ok(Default::default()) + } + fn ripemd160( + &mut self, + _ripemd160: &::Ripemd160, + ) -> Result { + Ok(Default::default()) + } + fn hash160( + &mut self, + _hash160: &::Hash160, + ) -> Result { + Ok(Default::default()) + } + } + + // check the network for the keys + use miniscript::TranslateErr; + match self.0.translate_pk(&mut Translator { + secp, + network, + descriptor: &self.0, + }) { + Ok(_) => {} + Err(TranslateErr::TranslatorErr(e)) => return Err(e), + Err(TranslateErr::OuterError(e)) => return Err(e.into()), + } + + Ok(self) + } +} + +impl IntoWalletDescriptor for DescriptorTemplateOut { + fn into_wallet_descriptor( + self, + _secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + struct Translator { + network: Network, + } + + impl miniscript::Translator + for Translator + { + fn pk( + &mut self, + pk: &DescriptorPublicKey, + ) -> Result { + // workaround for xpubs generated by other key types, like bip39: since when the + // conversion is made one network has to be chosen, what we generally choose + // "mainnet", but then override the set of valid networks to specify that all of + // them are valid. here we reset the network to make sure the wallet struct gets a + // descriptor with the right network everywhere. + let pk = match pk { + DescriptorPublicKey::XPub(ref xpub) => { + let mut xpub = xpub.clone(); + xpub.xkey.network = self.network; + + DescriptorPublicKey::XPub(xpub) + } + other => other.clone(), + }; + + Ok(pk) + } + miniscript::translate_hash_clone!( + DescriptorPublicKey, + DescriptorPublicKey, + DescriptorError + ); + } + + let (desc, keymap, networks) = self; + + if !networks.contains(&network) { + return Err(DescriptorError::Key(KeyError::InvalidNetwork)); + } + + // fixup the network for keys that need it in the descriptor + use miniscript::TranslateErr; + let translated = match desc.translate_pk(&mut Translator { network }) { + Ok(descriptor) => descriptor, + Err(TranslateErr::TranslatorErr(e)) => return Err(e), + Err(TranslateErr::OuterError(e)) => return Err(e.into()), + }; + // ...and in the key map + let fixed_keymap = keymap + .into_iter() + .map(|(mut k, mut v)| { + match (&mut k, &mut v) { + (DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => { + xpub.xkey.network = network; + xprv.xkey.network = network; + } + (_, DescriptorSecretKey::Single(key)) => { + key.key.network = network; + } + _ => {} + } + + (k, v) + }) + .collect(); + + Ok((translated, fixed_keymap)) + } +} + +/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the +/// descriptor +pub(crate) fn into_wallet_descriptor_checked( + inner: T, + secp: &SecpCtx, + network: Network, +) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?; + + // Ensure the keys don't contain any hardened derivation steps or hardened wildcards + let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| { + if let DescriptorPublicKey::XPub(DescriptorXKey { + derivation_path, + wildcard, + .. + }) = k + { + return *wildcard == Wildcard::Hardened + || derivation_path.into_iter().any(ChildNumber::is_hardened); + } + + false + }); + if descriptor_contains_hardened_steps { + return Err(DescriptorError::HardenedDerivationXpub); + } + + if descriptor.is_multipath() { + return Err(DescriptorError::MultiPath); + } + + // Run miniscript's sanity check, which will look for duplicated keys and other potential + // issues + descriptor.sanity_check()?; + + Ok((descriptor, keymap)) +} + +#[doc(hidden)] +/// Used internally mainly by the `descriptor!()` and `fragment!()` macros +pub trait CheckMiniscript { + fn check_miniscript(&self) -> Result<(), miniscript::Error>; +} + +impl CheckMiniscript + for miniscript::Miniscript +{ + fn check_miniscript(&self) -> Result<(), miniscript::Error> { + Ctx::check_global_validity(self)?; + + Ok(()) + } +} + +/// Trait implemented on [`Descriptor`]s to add a method to extract the spending [`policy`] +pub trait ExtractPolicy { + /// Extract the spending [`policy`] + fn extract_policy( + &self, + signers: &SignersContainer, + psbt: BuildSatisfaction, + secp: &SecpCtx, + ) -> Result, DescriptorError>; +} + +pub(crate) trait XKeyUtils { + fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint; +} + +impl XKeyUtils for DescriptorMultiXKey +where + T: InnerXKey, +{ + fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint { + match self.origin { + Some((fingerprint, _)) => fingerprint, + None => self.xkey.xkey_fingerprint(secp), + } + } +} + +impl XKeyUtils for DescriptorXKey +where + T: InnerXKey, +{ + fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint { + match self.origin { + Some((fingerprint, _)) => fingerprint, + None => self.xkey.xkey_fingerprint(secp), + } + } +} + +pub(crate) trait DescriptorMeta { + fn is_witness(&self) -> bool; + fn is_taproot(&self) -> bool; + fn get_extended_keys(&self) -> Vec>; + fn derive_from_hd_keypaths( + &self, + hd_keypaths: &HdKeyPaths, + secp: &SecpCtx, + ) -> Option; + fn derive_from_tap_key_origins( + &self, + tap_key_origins: &TapKeyOrigins, + secp: &SecpCtx, + ) -> Option; + fn derive_from_psbt_key_origins( + &self, + key_origins: BTreeMap, + secp: &SecpCtx, + ) -> Option; + fn derive_from_psbt_input( + &self, + psbt_input: &psbt::Input, + utxo: Option, + secp: &SecpCtx, + ) -> Option; +} + +impl DescriptorMeta for ExtendedDescriptor { + fn is_witness(&self) -> bool { + matches!( + self.desc_type(), + DescriptorType::Wpkh + | DescriptorType::ShWpkh + | DescriptorType::Wsh + | DescriptorType::ShWsh + | DescriptorType::ShWshSortedMulti + | DescriptorType::WshSortedMulti + ) + } + + fn is_taproot(&self) -> bool { + self.desc_type() == DescriptorType::Tr + } + + fn get_extended_keys(&self) -> Vec> { + let mut answer = Vec::new(); + + self.for_each_key(|pk| { + if let DescriptorPublicKey::XPub(xpub) = pk { + answer.push(xpub.clone()); + } + + true + }); + + answer + } + + fn derive_from_psbt_key_origins( + &self, + key_origins: BTreeMap, + secp: &SecpCtx, + ) -> Option { + // Ensure that deriving `xpub` with `path` yields `expected` + let verify_key = + |xpub: &DescriptorXKey, path: &DerivationPath, expected: &SinglePubKey| { + let derived = xpub + .xkey + .derive_pub(secp, path) + .expect("The path should never contain hardened derivation steps") + .public_key; + + match expected { + SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true, + SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true, + _ => false, + } + }; + + let mut path_found = None; + + // using `for_any_key` should make this stop as soon as we return `true` + self.for_any_key(|key| { + if let DescriptorPublicKey::XPub(xpub) = key { + // Check if the key matches one entry in our `key_origins`. If it does, `matches()` will + // return the "prefix" that matched, so we remove that prefix from the full path + // found in `key_origins` and save it in `derive_path`. We expect this to be a derivation + // path of length 1 if the key is `wildcard` and an empty path otherwise. + let root_fingerprint = xpub.root_fingerprint(secp); + let derive_path = key_origins + .get_key_value(&root_fingerprint) + .and_then(|(fingerprint, (path, expected))| { + xpub.matches(&(*fingerprint, (*path).clone()), secp) + .zip(Some((path, expected))) + }) + .and_then(|(prefix, (full_path, expected))| { + let derive_path = full_path + .into_iter() + .skip(prefix.into_iter().count()) + .cloned() + .collect::(); + + // `derive_path` only contains the replacement index for the wildcard, if present, or + // an empty path for fixed descriptors. To verify the key we also need the normal steps + // that come before the wildcard, so we take them directly from `xpub` and then append + // the final index + if verify_key( + xpub, + &xpub.derivation_path.extend(derive_path.clone()), + expected, + ) { + Some(derive_path) + } else { + None + } + }); + + match derive_path { + Some(path) if xpub.wildcard != Wildcard::None && path.len() == 1 => { + // Ignore hardened wildcards + if let ChildNumber::Normal { index } = path[0] { + path_found = Some(index); + return true; + } + } + Some(path) if xpub.wildcard == Wildcard::None && path.is_empty() => { + path_found = Some(0); + return true; + } + _ => {} + } + } + + false + }); + + path_found.map(|path| { + self.at_derivation_index(path) + .expect("We ignore hardened wildcards") + }) + } + + fn derive_from_hd_keypaths( + &self, + hd_keypaths: &HdKeyPaths, + secp: &SecpCtx, + ) -> Option { + // "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins` + let key_origins = hd_keypaths + .iter() + .map(|(pk, (fingerprint, path))| { + ( + *fingerprint, + (path, SinglePubKey::FullKey(PublicKey::new(*pk))), + ) + }) + .collect(); + self.derive_from_psbt_key_origins(key_origins, secp) + } + + fn derive_from_tap_key_origins( + &self, + tap_key_origins: &TapKeyOrigins, + secp: &SecpCtx, + ) -> Option { + // "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins` + let key_origins = tap_key_origins + .iter() + .map(|(pk, (_, (fingerprint, path)))| (*fingerprint, (path, SinglePubKey::XOnly(*pk)))) + .collect(); + self.derive_from_psbt_key_origins(key_origins, secp) + } + + fn derive_from_psbt_input( + &self, + psbt_input: &psbt::Input, + utxo: Option, + secp: &SecpCtx, + ) -> Option { + if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) { + return Some(derived); + } + if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) { + return Some(derived); + } + if self.has_wildcard() { + // We can't try to bruteforce the derivation index, exit here + return None; + } + + let descriptor = self.at_derivation_index(0).expect("0 is not hardened"); + match descriptor.desc_type() { + // TODO: add pk() here + DescriptorType::Pkh + | DescriptorType::Wpkh + | DescriptorType::ShWpkh + | DescriptorType::Tr + if utxo.is_some() + && descriptor.script_pubkey() == utxo.as_ref().unwrap().script_pubkey => + { + Some(descriptor) + } + DescriptorType::Bare | DescriptorType::Sh | DescriptorType::ShSortedMulti + if psbt_input.redeem_script.is_some() + && &descriptor.explicit_script().unwrap() + == psbt_input.redeem_script.as_ref().unwrap() => + { + Some(descriptor) + } + DescriptorType::Wsh + | DescriptorType::ShWsh + | DescriptorType::ShWshSortedMulti + | DescriptorType::WshSortedMulti + if psbt_input.witness_script.is_some() + && &descriptor.explicit_script().unwrap() + == psbt_input.witness_script.as_ref().unwrap() => + { + Some(descriptor) + } + _ => None, + } + } +} + +#[cfg(test)] +mod test { + use alloc::string::ToString; + use core::str::FromStr; + + use assert_matches::assert_matches; + use bitcoin::hex::FromHex; + use bitcoin::secp256k1::Secp256k1; + use bitcoin::ScriptBuf; + use bitcoin::{bip32, Psbt}; + + use super::*; + use crate::psbt::PsbtUtils; + + #[test] + fn test_derive_from_psbt_input_wpkh_wif() { + let descriptor = Descriptor::::from_str( + "wpkh(02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737)", + ) + .unwrap(); + let psbt = Psbt::deserialize( + &Vec::::from_hex( + "70736274ff010052010000000162307be8e431fbaff807cdf9cdc3fde44d7402\ + 11bc8342c31ffd6ec11fe35bcc0100000000ffffffff01328601000000000016\ + 001493ce48570b55c42c2af816aeaba06cfee1224fae000000000001011fa086\ + 01000000000016001493ce48570b55c42c2af816aeaba06cfee1224fae010304\ + 010000000000", + ) + .unwrap(), + ) + .unwrap(); + + assert!(descriptor + .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) + .is_some()); + } + + #[test] + fn test_derive_from_psbt_input_pkh_tpub() { + let descriptor = Descriptor::::from_str( + "pkh([0f056943/44h/0h/0h]tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/10/*)", + ) + .unwrap(); + let psbt = Psbt::deserialize( + &Vec::::from_hex( + "70736274ff010053010000000145843b86be54a3cd8c9e38444e1162676c00df\ + e7964122a70df491ea12fd67090100000000ffffffff01c19598000000000017\ + a91432bb94283282f72b2e034709e348c44d5a4db0ef8700000000000100f902\ + 0000000001010167e99c0eb67640f3a1b6805f2d8be8238c947f8aaf49eb0a9c\ + bee6a42c984200000000171600142b29a22019cca05b9c2b2d283a4c4489e1cf\ + 9f8ffeffffff02a01dced06100000017a914e2abf033cadbd74f0f4c74946201\ + decd20d5c43c8780969800000000001976a9148b0fce5fb1264e599a65387313\ + 3c95478b902eb288ac02473044022015d9211576163fa5b001e84dfa3d44efd9\ + 86b8f3a0d3d2174369288b2b750906022048dacc0e5d73ae42512fd2b97e2071\ + a8d0bce443b390b1fe0b8128fe70ec919e01210232dad1c5a67dcb0116d407e2\ + 52584228ab7ec00e8b9779d0c3ffe8114fc1a7d2c80600000103040100000022\ + 0603433b83583f8c4879b329dd08bbc7da935e4cc02f637ff746e05f0466ffb2\ + a6a2180f0569432c00008000000080000000800a000000000000000000", + ) + .unwrap(), + ) + .unwrap(); + + assert!(descriptor + .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) + .is_some()); + } + + #[test] + fn test_derive_from_psbt_input_wsh() { + let descriptor = Descriptor::::from_str( + "wsh(and_v(v:pk(03b6633fef2397a0a9de9d7b6f23aef8368a6e362b0581f0f0af70d5ecfd254b14),older(6)))", + ) + .unwrap(); + let psbt = Psbt::deserialize( + &Vec::::from_hex( + "70736274ff01005302000000011c8116eea34408ab6529223c9a176606742207\ + 67a1ff1d46a6e3c4a88243ea6e01000000000600000001109698000000000017\ + a914ad105f61102e0d01d7af40d06d6a5c3ae2f7fde387000000000001012b80\ + 969800000000002200203ca72f106a72234754890ca7640c43f65d2174e44d33\ + 336030f9059345091044010304010000000105252103b6633fef2397a0a9de9d\ + 7b6f23aef8368a6e362b0581f0f0af70d5ecfd254b14ad56b20000", + ) + .unwrap(), + ) + .unwrap(); + + assert!(descriptor + .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) + .is_some()); + } + + #[test] + fn test_derive_from_psbt_input_sh() { + let descriptor = Descriptor::::from_str( + "sh(and_v(v:pk(021403881a5587297818fcaf17d239cefca22fce84a45b3b1d23e836c4af671dbb),after(630000)))", + ) + .unwrap(); + let psbt = Psbt::deserialize( + &Vec::::from_hex( + "70736274ff0100530100000001bc8c13df445dfadcc42afa6dc841f85d22b01d\ + a6270ebf981740f4b7b1d800390000000000feffffff01ba9598000000000017\ + a91457b148ba4d3e5fa8608a8657875124e3d1c9390887f09c0900000100e002\ + 0000000001016ba1bbe05cc93574a0d611ec7d93ad0ab6685b28d0cd80e8a82d\ + debb326643c90100000000feffffff02809698000000000017a914d9a6e8c455\ + 8e16c8253afe53ce37ad61cf4c38c487403504cf6100000017a9144044fb6e0b\ + 757dfc1b34886b6a95aef4d3db137e870247304402202a9b72d939bcde8ba2a1\ + e0980597e47af4f5c152a78499143c3d0a78ac2286a602207a45b1df9e93b8c9\ + 6f09f5c025fe3e413ca4b905fe65ee55d32a3276439a9b8f012102dc1fcc2636\ + 4da1aa718f03d8d9bd6f2ff410ed2cf1245a168aa3bcc995ac18e0a806000001\ + 03040100000001042821021403881a5587297818fcaf17d239cefca22fce84a4\ + 5b3b1d23e836c4af671dbbad03f09c09b10000", + ) + .unwrap(), + ) + .unwrap(); + + assert!(descriptor + .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0), &Secp256k1::new()) + .is_some()); + } + + #[test] + fn test_to_wallet_descriptor_fixup_networks() { + use crate::keys::{any_network, IntoDescriptorKey}; + + let secp = Secp256k1::new(); + + let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap(); + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + + // here `to_descriptor_key` will set the valid networks for the key to only mainnet, since + // we are using an "xpub" + let key = (xprv, path.clone()).into_descriptor_key().unwrap(); + // override it with any. this happens in some key conversions, like bip39 + let key = key.override_valid_networks(any_network()); + + // make a descriptor out of it + let desc = crate::descriptor!(wpkh(key)).unwrap(); + // this should convert the key that supports "any_network" to the right network (testnet) + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let mut xprv_testnet = xprv; + xprv_testnet.network = Network::Testnet; + + let xpub_testnet = bip32::Xpub::from_priv(&secp, &xprv_testnet); + let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey { + xkey: xpub_testnet, + origin: None, + derivation_path: path, + wildcard: Wildcard::Unhardened, + }); + + assert_eq!(wallet_desc.to_string(), "wpkh(tpubD6NzVbkrYhZ4XtJzoDja5snUjBNQRP5B3f4Hyn1T1x6PVPxzzVjvw6nJx2D8RBCxog9GEVjZoyStfepTz7TtKoBVdkCtnc7VCJh9dD4RAU9/0/*)#a3svx0ha"); + assert_eq!( + keymap + .get(&desc_pubkey) + .map(|key| key.to_public(&secp).unwrap()), + Some(desc_pubkey) + ); + } + + // test IntoWalletDescriptor trait from &str with and without checksum appended + #[test] + fn test_descriptor_from_str_with_checksum() { + let secp = Secp256k1::new(); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw" + .into_wallet_descriptor(&secp, Network::Testnet); + assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum)); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw" + .into_wallet_descriptor(&secp, Network::Testnet); + assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum)); + } + + // test IntoWalletDescriptor trait from &str with keys from right and wrong network + #[test] + fn test_descriptor_from_str_with_keys_network() { + let secp = Secp256k1::new(); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" + .into_wallet_descriptor(&secp, Network::Regtest); + assert!(desc.is_ok()); + + let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" + .into_wallet_descriptor(&secp, Network::Regtest); + assert!(desc.is_ok()); + + let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))" + .into_wallet_descriptor(&secp, Network::Testnet); + assert!(desc.is_ok()); + + let desc = "sh(wpkh(02864bb4ad00cefa806098a69e192bbda937494e69eb452b87bb3f20f6283baedb))" + .into_wallet_descriptor(&secp, Network::Bitcoin); + assert!(desc.is_ok()); + + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)" + .into_wallet_descriptor(&secp, Network::Bitcoin); + assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork))); + + let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)" + .into_wallet_descriptor(&secp, Network::Bitcoin); + assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork))); + } + + // test IntoWalletDescriptor trait from the output of the descriptor!() macro + #[test] + fn test_descriptor_from_str_from_output_of_macro() { + let secp = Secp256k1::new(); + + let tpub = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap(); + let path = bip32::DerivationPath::from_str("m/1/2").unwrap(); + let key = (tpub, path).into_descriptor_key().unwrap(); + + // make a descriptor out of it + let desc = crate::descriptor!(wpkh(key)).unwrap(); + + let (wallet_desc, _) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let wallet_desc_str = wallet_desc.to_string(); + assert_eq!(wallet_desc_str, "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)#67ju93jw"); + + let (wallet_desc2, _) = wallet_desc_str + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + assert_eq!(wallet_desc, wallet_desc2) + } + + #[test] + fn test_into_wallet_descriptor_checked() { + let secp = Secp256k1::new(); + + let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)"; + let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet); + + assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub)); + + let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/<0;1>/*)"; + let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet); + + assert_matches!(result, Err(DescriptorError::MultiPath)); + + // repeated pubkeys + let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))"; + let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet); + + assert!(result.is_err()); + } + + #[test] + fn test_sh_wsh_sortedmulti_redeemscript() { + use miniscript::psbt::PsbtInputExt; + + let secp = Secp256k1::new(); + + let descriptor = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))"; + let (descriptor, _) = + into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap(); + + let descriptor = descriptor.at_derivation_index(0).unwrap(); + + let script = ScriptBuf::from_hex("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap(); + + let mut psbt_input = psbt::Input::default(); + psbt_input + .update_with_descriptor_unchecked(&descriptor) + .unwrap(); + + assert_eq!(psbt_input.redeem_script, Some(script.to_p2wsh())); + assert_eq!(psbt_input.witness_script, Some(script)); + } +} diff --git a/crates/wallet/src/descriptor/policy.rs b/crates/wallet/src/descriptor/policy.rs new file mode 100644 index 00000000..bf8a661e --- /dev/null +++ b/crates/wallet/src/descriptor/policy.rs @@ -0,0 +1,1905 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptor policy +//! +//! This module implements the logic to extract and represent the spending policies of a descriptor +//! in a more human-readable format. +//! +//! This is an **EXPERIMENTAL** feature, API and other major changes are expected. +//! +//! ## Example +//! +//! ``` +//! # use std::sync::Arc; +//! # use bdk_wallet::descriptor::*; +//! # use bdk_wallet::wallet::signer::*; +//! # use bdk_wallet::bitcoin::secp256k1::Secp256k1; +//! use bdk_wallet::descriptor::policy::BuildSatisfaction; +//! let secp = Secp256k1::new(); +//! let desc = "wsh(and_v(v:pk(cV3oCth6zxZ1UVsHLnGothsWNsaoxRhC6aeNi5VbSdFpwUkgkEci),or_d(pk(cVMTy7uebJgvFaSBwcgvwk8qn8xSLc97dKow4MBetjrrahZoimm2),older(12960))))"; +//! +//! let (extended_desc, key_map) = ExtendedDescriptor::parse_descriptor(&secp, desc)?; +//! println!("{:?}", extended_desc); +//! +//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp)); +//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?; +//! println!("policy: {}", serde_json::to_string(&policy).unwrap()); +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +use crate::collections::{BTreeMap, HashSet, VecDeque}; +use alloc::string::String; +use alloc::vec::Vec; +use core::cmp::max; + +use core::fmt; + +use serde::ser::SerializeMap; +use serde::{Serialize, Serializer}; + +use bitcoin::bip32::Fingerprint; +use bitcoin::hashes::{hash160, ripemd160, sha256}; +use bitcoin::{absolute, key::XOnlyPublicKey, PublicKey, Sequence}; + +use miniscript::descriptor::{ + DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner, +}; +use miniscript::hash256; +use miniscript::{ + Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey, +}; + +use crate::descriptor::ExtractPolicy; +use crate::keys::ExtScriptContext; +use crate::wallet::signer::{SignerId, SignersContainer}; +use crate::wallet::utils::{After, Older, SecpCtx}; + +use super::checksum::calc_checksum; +use super::error::Error; +use super::XKeyUtils; +use bitcoin::psbt::{self, Psbt}; +use miniscript::psbt::PsbtInputSatisfier; + +/// A unique identifier for a key +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum PkOrF { + /// A legacy public key + Pubkey(PublicKey), + /// A x-only public key + XOnlyPubkey(XOnlyPublicKey), + /// An extended key fingerprint + Fingerprint(Fingerprint), +} + +impl PkOrF { + fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self { + match k { + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::FullKey(pk), + .. + }) => PkOrF::Pubkey(*pk), + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::XOnly(pk), + .. + }) => PkOrF::XOnlyPubkey(*pk), + DescriptorPublicKey::XPub(xpub) => PkOrF::Fingerprint(xpub.root_fingerprint(secp)), + DescriptorPublicKey::MultiXPub(multi) => { + PkOrF::Fingerprint(multi.root_fingerprint(secp)) + } + } + } +} + +/// An item that needs to be satisfied +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(tag = "type", rename_all = "UPPERCASE")] +pub enum SatisfiableItem { + // Leaves + /// ECDSA Signature for a raw public key + EcdsaSignature(PkOrF), + /// Schnorr Signature for a raw public key + SchnorrSignature(PkOrF), + /// SHA256 preimage hash + Sha256Preimage { + /// The digest value + hash: sha256::Hash, + }, + /// Double SHA256 preimage hash + Hash256Preimage { + /// The digest value + hash: hash256::Hash, + }, + /// RIPEMD160 preimage hash + Ripemd160Preimage { + /// The digest value + hash: ripemd160::Hash, + }, + /// SHA256 then RIPEMD160 preimage hash + Hash160Preimage { + /// The digest value + hash: hash160::Hash, + }, + /// Absolute timeclock timestamp + AbsoluteTimelock { + /// The timelock value + value: absolute::LockTime, + }, + /// Relative timelock locktime + RelativeTimelock { + /// The timelock value + value: Sequence, + }, + /// Multi-signature public keys with threshold count + Multisig { + /// The raw public key or extended key fingerprint + keys: Vec, + /// The required threshold count + threshold: usize, + }, + + // Complex item + /// Threshold items with threshold count + Thresh { + /// The policy items + items: Vec, + /// The required threshold count + threshold: usize, + }, +} + +impl SatisfiableItem { + /// Returns whether the [`SatisfiableItem`] is a leaf item + pub fn is_leaf(&self) -> bool { + !matches!( + self, + SatisfiableItem::Thresh { + items: _, + threshold: _, + } + ) + } + + /// Returns a unique id for the [`SatisfiableItem`] + pub fn id(&self) -> String { + calc_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem")) + .expect("Failed to compute a SatisfiableItem id") + } +} + +fn combinations(vec: &[usize], size: usize) -> Vec> { + assert!(vec.len() >= size); + + let mut answer = Vec::new(); + + let mut queue = VecDeque::new(); + for (index, val) in vec.iter().enumerate() { + let mut new_vec = Vec::with_capacity(size); + new_vec.push(*val); + queue.push_back((index, new_vec)); + } + + while let Some((index, vals)) = queue.pop_front() { + if vals.len() >= size { + answer.push(vals); + } else { + for (new_index, val) in vec.iter().skip(index + 1).enumerate() { + let mut cloned = vals.clone(); + cloned.push(*val); + queue.push_front((new_index, cloned)); + } + } + } + + answer +} + +fn mix(vec: Vec>) -> Vec> { + if vec.is_empty() || vec.iter().any(Vec::is_empty) { + return vec![]; + } + + let mut answer = Vec::new(); + let size = vec.len(); + + let mut queue = VecDeque::new(); + for i in &vec[0] { + let mut new_vec = Vec::with_capacity(size); + new_vec.push(i.clone()); + queue.push_back(new_vec); + } + + while let Some(vals) = queue.pop_front() { + if vals.len() >= size { + answer.push(vals); + } else { + let level = vals.len(); + for i in &vec[level] { + let mut cloned = vals.clone(); + cloned.push(i.clone()); + queue.push_front(cloned); + } + } + } + + answer +} + +/// Type for a map of sets of [`Condition`] items keyed by each set's index +pub type ConditionMap = BTreeMap>; +/// Type for a map of folded sets of [`Condition`] items keyed by a vector of the combined set's indexes +pub type FoldedConditionMap = BTreeMap, HashSet>; + +fn serialize_folded_cond_map( + input_map: &FoldedConditionMap, + serializer: S, +) -> Result +where + S: Serializer, +{ + let mut map = serializer.serialize_map(Some(input_map.len()))?; + for (k, v) in input_map { + let k_string = format!("{:?}", k); + map.serialize_entry(&k_string, v)?; + } + map.end() +} + +/// Represent if and how much a policy item is satisfied by the wallet's descriptor +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(tag = "type", rename_all = "UPPERCASE")] +pub enum Satisfaction { + /// Only a partial satisfaction of some kind of threshold policy + Partial { + /// Total number of items + n: usize, + /// Threshold + m: usize, + /// The items that can be satisfied by the descriptor or are satisfied in the PSBT + items: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + /// Whether the items are sorted in lexicographic order (used by `sortedmulti`) + sorted: Option, + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + /// Extra conditions that also need to be satisfied + conditions: ConditionMap, + }, + /// Can reach the threshold of some kind of threshold policy + PartialComplete { + /// Total number of items + n: usize, + /// Threshold + m: usize, + /// The items that can be satisfied by the descriptor + items: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + /// Whether the items are sorted in lexicographic order (used by `sortedmulti`) + sorted: Option, + #[serde( + serialize_with = "serialize_folded_cond_map", + skip_serializing_if = "BTreeMap::is_empty" + )] + /// Extra conditions that also need to be satisfied + conditions: FoldedConditionMap, + }, + + /// Can satisfy the policy item + Complete { + /// Extra conditions that also need to be satisfied + condition: Condition, + }, + /// Cannot satisfy or contribute to the policy item + None, +} + +impl Satisfaction { + /// Returns whether the [`Satisfaction`] is a leaf item + pub fn is_leaf(&self) -> bool { + match self { + Satisfaction::None | Satisfaction::Complete { .. } => true, + Satisfaction::PartialComplete { .. } | Satisfaction::Partial { .. } => false, + } + } + + // add `inner` as one of self's partial items. this only makes sense on partials + fn add(&mut self, inner: &Satisfaction, inner_index: usize) -> Result<(), PolicyError> { + match self { + Satisfaction::None | Satisfaction::Complete { .. } => Err(PolicyError::AddOnLeaf), + Satisfaction::PartialComplete { .. } => Err(PolicyError::AddOnPartialComplete), + Satisfaction::Partial { + n, + ref mut conditions, + ref mut items, + .. + } => { + if inner_index >= *n || items.contains(&inner_index) { + return Err(PolicyError::IndexOutOfRange(inner_index)); + } + + match inner { + // not relevant if not completed yet + Satisfaction::None | Satisfaction::Partial { .. } => return Ok(()), + Satisfaction::Complete { condition } => { + items.push(inner_index); + conditions.insert(inner_index, vec![*condition].into_iter().collect()); + } + Satisfaction::PartialComplete { + conditions: other_conditions, + .. + } => { + items.push(inner_index); + let conditions_set = other_conditions + .values() + .fold(HashSet::new(), |set, i| set.union(i).cloned().collect()); + conditions.insert(inner_index, conditions_set); + } + } + + Ok(()) + } + } + } + + fn finalize(&mut self) { + // if partial try to bump it to a partialcomplete + if let Satisfaction::Partial { + n, + m, + items, + conditions, + sorted, + } = self + { + if items.len() >= *m { + let mut map = BTreeMap::new(); + let indexes = combinations(items, *m); + // `indexes` at this point is a Vec>, with the "n choose k" of items (m of n) + indexes + .into_iter() + // .inspect(|x| println!("--- orig --- {:?}", x)) + // we map each of the combinations of elements into a tuple of ([chosen items], [conditions]). unfortunately, those items have potentially more than one + // condition (think about ORs), so we also use `mix` to expand those, i.e. [[0], [1, 2]] becomes [[0, 1], [0, 2]]. This is necessary to make sure that we + // consider every possible options and check whether or not they are compatible. + // since this step can turn one item of the iterator into multiple ones, we use `flat_map()` to expand them out + .flat_map(|i_vec| { + mix(i_vec + .iter() + .map(|i| { + conditions + .get(i) + .map(|set| set.clone().into_iter().collect()) + .unwrap_or_default() + }) + .collect()) + .into_iter() + .map(|x| (i_vec.clone(), x)) + .collect::, Vec)>>() + }) + // .inspect(|x| println!("flat {:?}", x)) + // try to fold all the conditions for this specific combination of indexes/options. if they are not compatible, try_fold will be Err + .map(|(key, val)| { + ( + key, + val.into_iter() + .try_fold(Condition::default(), |acc, v| acc.merge(&v)), + ) + }) + // .inspect(|x| println!("try_fold {:?}", x)) + // filter out all the incompatible combinations + .filter(|(_, val)| val.is_ok()) + // .inspect(|x| println!("filter {:?}", x)) + // push them into the map + .for_each(|(key, val)| { + map.entry(key) + .or_insert_with(HashSet::new) + .insert(val.unwrap()); + }); + // TODO: if the map is empty, the conditions are not compatible, return an error? + *self = Satisfaction::PartialComplete { + n: *n, + m: *m, + items: items.clone(), + conditions: map, + sorted: *sorted, + }; + } + } + } +} + +impl From for Satisfaction { + fn from(other: bool) -> Self { + if other { + Satisfaction::Complete { + condition: Default::default(), + } + } else { + Satisfaction::None + } + } +} + +/// Descriptor spending policy +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Policy { + /// Identifier for this policy node + pub id: String, + + /// Type of this policy node + #[serde(flatten)] + pub item: SatisfiableItem, + /// How much a given PSBT already satisfies this policy node in terms of signatures + pub satisfaction: Satisfaction, + /// How the wallet's descriptor can satisfy this policy node + pub contribution: Satisfaction, +} + +/// An extra condition that must be satisfied but that is out of control of the user +/// TODO: use `bitcoin::LockTime` and `bitcoin::Sequence` +#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Default, Serialize)] +pub struct Condition { + /// Optional CheckSequenceVerify condition + #[serde(skip_serializing_if = "Option::is_none")] + pub csv: Option, + /// Optional timelock condition + #[serde(skip_serializing_if = "Option::is_none")] + pub timelock: Option, +} + +impl Condition { + fn merge_nlocktime( + a: absolute::LockTime, + b: absolute::LockTime, + ) -> Result { + if !a.is_same_unit(b) { + Err(PolicyError::MixedTimelockUnits) + } else if a > b { + Ok(a) + } else { + Ok(b) + } + } + + fn merge_nsequence(a: Sequence, b: Sequence) -> Result { + if a.is_time_locked() != b.is_time_locked() { + Err(PolicyError::MixedTimelockUnits) + } else { + Ok(max(a, b)) + } + } + + pub(crate) fn merge(mut self, other: &Condition) -> Result { + match (self.csv, other.csv) { + (Some(a), Some(b)) => self.csv = Some(Self::merge_nsequence(a, b)?), + (None, any) => self.csv = any, + _ => {} + } + + match (self.timelock, other.timelock) { + (Some(a), Some(b)) => self.timelock = Some(Self::merge_nlocktime(a, b)?), + (None, any) => self.timelock = any, + _ => {} + } + + Ok(self) + } + + /// Returns `true` if there are no extra conditions to verify + pub fn is_null(&self) -> bool { + self.csv.is_none() && self.timelock.is_none() + } +} + +/// Errors that can happen while extracting and manipulating policies +#[derive(Debug, PartialEq, Eq)] +pub enum PolicyError { + /// Not enough items are selected to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`] + NotEnoughItemsSelected(String), + /// Index out of range for an item to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`] + IndexOutOfRange(usize), + /// Can not add to an item that is [`Satisfaction::None`] or [`Satisfaction::Complete`] + AddOnLeaf, + /// Can not add to an item that is [`Satisfaction::PartialComplete`] + AddOnPartialComplete, + /// Can not merge CSV or timelock values unless both are less than or both are equal or greater than 500_000_000 + MixedTimelockUnits, + /// Incompatible conditions (not currently used) + IncompatibleConditions, +} + +impl fmt::Display for PolicyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NotEnoughItemsSelected(err) => write!(f, "Not enough items selected: {}", err), + Self::IndexOutOfRange(index) => write!(f, "Index out of range: {}", index), + Self::AddOnLeaf => write!(f, "Add on leaf"), + Self::AddOnPartialComplete => write!(f, "Add on partial complete"), + Self::MixedTimelockUnits => write!(f, "Mixed timelock units"), + Self::IncompatibleConditions => write!(f, "Incompatible conditions"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for PolicyError {} + +impl Policy { + fn new(item: SatisfiableItem) -> Self { + Policy { + id: item.id(), + item, + satisfaction: Satisfaction::None, + contribution: Satisfaction::None, + } + } + + fn make_and(a: Option, b: Option) -> Result, PolicyError> { + match (a, b) { + (None, None) => Ok(None), + (Some(x), None) | (None, Some(x)) => Ok(Some(x)), + (Some(a), Some(b)) => Self::make_thresh(vec![a, b], 2), + } + } + + fn make_or(a: Option, b: Option) -> Result, PolicyError> { + match (a, b) { + (None, None) => Ok(None), + (Some(x), None) | (None, Some(x)) => Ok(Some(x)), + (Some(a), Some(b)) => Self::make_thresh(vec![a, b], 1), + } + } + + fn make_thresh(items: Vec, threshold: usize) -> Result, PolicyError> { + if threshold == 0 { + return Ok(None); + } + + let mut contribution = Satisfaction::Partial { + n: items.len(), + m: threshold, + items: vec![], + 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)) + } + + fn make_multisig( + keys: &[DescriptorPublicKey], + signers: &SignersContainer, + build_sat: BuildSatisfaction, + threshold: usize, + sorted: bool, + secp: &SecpCtx, + ) -> Result, PolicyError> { + if threshold == 0 { + return Ok(None); + } + + let parsed_keys = keys.iter().map(|k| PkOrF::from_key(k, secp)).collect(); + + let mut contribution = Satisfaction::Partial { + n: keys.len(), + m: threshold, + items: vec![], + 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( + &Satisfaction::Complete { + condition: Default::default(), + }, + index, + )?; + } + + if let Some(psbt) = build_sat.psbt() { + if Ctx::find_signature(psbt, key, secp) { + satisfaction.add( + &Satisfaction::Complete { + condition: Default::default(), + }, + index, + )?; + } + } + } + satisfaction.finalize(); + contribution.finalize(); + + let mut policy: Policy = SatisfiableItem::Multisig { + keys: parsed_keys, + threshold, + } + .into(); + policy.contribution = contribution; + policy.satisfaction = satisfaction; + + Ok(Some(policy)) + } + + /// Return whether or not a specific path in the policy tree is required to unambiguously + /// create a transaction + /// + /// What this means is that for some spending policies the user should select which paths in + /// the tree it intends to satisfy while signing, because the transaction must be created differently based + /// on that. + pub fn requires_path(&self) -> bool { + self.get_condition(&BTreeMap::new()).is_err() + } + + /// Return the conditions that are set by the spending policy for a given path in the + /// policy tree + pub fn get_condition( + &self, + path: &BTreeMap>, + ) -> Result { + // if items.len() == threshold, selected can be omitted and we take all of them by default + let default = match &self.item { + SatisfiableItem::Thresh { items, threshold } if items.len() == *threshold => { + (0..*threshold).collect() + } + SatisfiableItem::Multisig { keys, .. } => (0..keys.len()).collect(), + _ => HashSet::new(), + }; + let selected: HashSet<_> = match path.get(&self.id) { + Some(arr) => arr.iter().copied().collect(), + _ => default, + }; + + match &self.item { + SatisfiableItem::Thresh { items, threshold } => { + let mapped_req = items + .iter() + .map(|i| i.get_condition(path)) + .collect::>(); + + // if all the requirements are null we don't care about `selected` because there + // are no requirements + if mapped_req + .iter() + .all(|cond| matches!(cond, Ok(c) if c.is_null())) + { + return Ok(Condition::default()); + } + + // make sure all the indexes in the `selected` list are within range + for index in &selected { + if *index >= items.len() { + return Err(PolicyError::IndexOutOfRange(*index)); + } + } + + // if we have something, make sure we have enough items. note that the user can set + // an empty value for this step in case of n-of-n, because `selected` is set to all + // the elements above + if selected.len() < *threshold { + return Err(PolicyError::NotEnoughItemsSelected(self.id.clone())); + } + + // check the selected items, see if there are conflicting requirements + mapped_req + .into_iter() + .enumerate() + .filter(|(index, _)| selected.contains(index)) + .try_fold(Condition::default(), |acc, (_, cond)| acc.merge(&cond?)) + } + SatisfiableItem::Multisig { keys, threshold } => { + if selected.len() < *threshold { + return Err(PolicyError::NotEnoughItemsSelected(self.id.clone())); + } + if let Some(item) = selected.into_iter().find(|&i| i >= keys.len()) { + return Err(PolicyError::IndexOutOfRange(item)); + } + + Ok(Condition::default()) + } + SatisfiableItem::AbsoluteTimelock { value } => Ok(Condition { + csv: None, + timelock: Some(*value), + }), + SatisfiableItem::RelativeTimelock { value } => Ok(Condition { + csv: Some(*value), + timelock: None, + }), + _ => Ok(Condition::default()), + } + } +} + +impl From for Policy { + fn from(other: SatisfiableItem) -> Self { + Self::new(other) + } +} + +fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId { + // For consistency we always compute the key hash in "ecdsa" form (with the leading sign + // prefix) even if we are in a taproot descriptor. We just want some kind of unique identifier + // for a key, so it doesn't really matter how the identifier is computed. + match key { + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::FullKey(pk), + .. + }) => pk.to_pubkeyhash(SigType::Ecdsa).into(), + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::XOnly(pk), + .. + }) => pk.to_pubkeyhash(SigType::Ecdsa).into(), + DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(), + DescriptorPublicKey::MultiXPub(xpub) => xpub.root_fingerprint(secp).into(), + } +} + +fn make_generic_signature SatisfiableItem, F: Fn(&Psbt) -> bool>( + key: &DescriptorPublicKey, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + make_policy: M, + find_sig: F, +) -> Policy { + let mut policy: Policy = make_policy().into(); + + policy.contribution = if signers.find(signer_id(key, secp)).is_some() { + Satisfaction::Complete { + condition: Default::default(), + } + } else { + Satisfaction::None + }; + + if let Some(psbt) = build_sat.psbt() { + policy.satisfaction = if find_sig(psbt) { + Satisfaction::Complete { + condition: Default::default(), + } + } else { + Satisfaction::None + }; + } + + policy +} + +fn generic_sig_in_psbt< + // C is for "check", it's a closure we use to *check* if a psbt input contains the signature + // for a specific key + C: Fn(&psbt::Input, &SinglePubKey) -> bool, + // E is for "extract", it extracts a key from the bip32 derivations found in the psbt input + E: Fn(&psbt::Input, Fingerprint) -> Option, +>( + psbt: &Psbt, + key: &DescriptorPublicKey, + secp: &SecpCtx, + check: C, + extract: E, +) -> bool { + //TODO check signature validity + psbt.inputs.iter().all(|input| match key { + DescriptorPublicKey::Single(SinglePub { key, .. }) => check(input, key), + DescriptorPublicKey::XPub(xpub) => { + //TODO check actual derivation matches + match extract(input, xpub.root_fingerprint(secp)) { + Some(pubkey) => check(input, &pubkey), + None => false, + } + } + DescriptorPublicKey::MultiXPub(xpub) => { + //TODO check actual derivation matches + match extract(input, xpub.root_fingerprint(secp)) { + Some(pubkey) => check(input, &pubkey), + None => false, + } + } + }) +} + +trait SigExt: ScriptContext { + fn make_signature( + key: &DescriptorPublicKey, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Policy; + + fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool; +} + +impl SigExt for T { + fn make_signature( + key: &DescriptorPublicKey, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Policy { + if T::as_enum().is_taproot() { + make_generic_signature( + key, + signers, + build_sat, + secp, + || SatisfiableItem::SchnorrSignature(PkOrF::from_key(key, secp)), + |psbt| Self::find_signature(psbt, key, secp), + ) + } else { + make_generic_signature( + key, + signers, + build_sat, + secp, + || SatisfiableItem::EcdsaSignature(PkOrF::from_key(key, secp)), + |psbt| Self::find_signature(psbt, key, secp), + ) + } + } + + fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool { + if T::as_enum().is_taproot() { + generic_sig_in_psbt( + psbt, + key, + secp, + |input, pk| { + let pk = match pk { + SinglePubKey::XOnly(pk) => pk, + _ => return false, + }; + + if input.tap_internal_key == Some(*pk) && input.tap_key_sig.is_some() { + true + } else { + input.tap_script_sigs.keys().any(|(sk, _)| sk == pk) + } + }, + |input, fing| { + input + .tap_key_origins + .iter() + .find(|(_, (_, (f, _)))| f == &fing) + .map(|(pk, _)| SinglePubKey::XOnly(*pk)) + }, + ) + } else { + generic_sig_in_psbt( + psbt, + key, + secp, + |input, pk| match pk { + SinglePubKey::FullKey(pk) => input.partial_sigs.contains_key(pk), + _ => false, + }, + |input, fing| { + input + .bip32_derivation + .iter() + .find(|(_, (f, _))| f == &fing) + .map(|(pk, _)| SinglePubKey::FullKey(PublicKey::new(*pk))) + }, + ) + } + } +} + +impl ExtractPolicy for Miniscript { + fn extract_policy( + &self, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Result, Error> { + Ok(match &self.node { + // Leaves + Terminal::True | Terminal::False => None, + Terminal::PkK(pubkey) => Some(Ctx::make_signature(pubkey, signers, build_sat, secp)), + Terminal::PkH(pubkey_hash) => { + Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp)) + } + Terminal::After(value) => { + let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { + value: (*value).into(), + } + .into(); + policy.contribution = Satisfaction::Complete { + condition: Condition { + timelock: Some((*value).into()), + csv: None, + }, + }; + if let BuildSatisfaction::PsbtTimelocks { + current_height, + psbt, + .. + } = build_sat + { + let after = After::new(Some(current_height), false); + let after_sat = + Satisfier::::check_after(&after, (*value).into()); + let inputs_sat = psbt_inputs_sat(psbt).all(|sat| { + Satisfier::::check_after(&sat, (*value).into()) + }); + if after_sat && inputs_sat { + policy.satisfaction = policy.contribution.clone(); + } + } + + Some(policy) + } + Terminal::Older(value) => { + let mut policy: Policy = SatisfiableItem::RelativeTimelock { value: *value }.into(); + policy.contribution = Satisfaction::Complete { + condition: Condition { + timelock: None, + 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::::check_older(&older, *value); + let inputs_sat = psbt_inputs_sat(psbt) + .all(|sat| Satisfier::::check_older(&sat, *value)); + if older_sat && inputs_sat { + policy.satisfaction = policy.contribution.clone(); + } + } + + Some(policy) + } + Terminal::Sha256(hash) => Some(SatisfiableItem::Sha256Preimage { hash: *hash }.into()), + Terminal::Hash256(hash) => { + Some(SatisfiableItem::Hash256Preimage { hash: *hash }.into()) + } + Terminal::Ripemd160(hash) => { + Some(SatisfiableItem::Ripemd160Preimage { hash: *hash }.into()) + } + Terminal::Hash160(hash) => { + Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into()) + } + Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => { + Policy::make_multisig::(pks, signers, build_sat, *k, false, secp)? + } + // Identities + Terminal::Alt(inner) + | Terminal::Swap(inner) + | Terminal::Check(inner) + | Terminal::DupIf(inner) + | Terminal::Verify(inner) + | Terminal::NonZero(inner) + | 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, 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, build_sat, secp)?, + y.extract_policy(signers, build_sat, 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, 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, build_sat, secp)) + .collect::, _>>()? + .into_iter() + .flatten() + .collect(); + + if mapped.len() < nodes.len() { + threshold = match threshold.checked_sub(nodes.len() - mapped.len()) { + None => return Ok(None), + Some(x) => x, + }; + } + + Policy::make_thresh(mapped, threshold)? + } + + // Unsupported + Terminal::RawPkH(_) => None, + }) + } +} + +fn psbt_inputs_sat(psbt: &Psbt) -> impl Iterator { + (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 { + fn extract_policy( + &self, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Result, Error> { + fn make_sortedmulti( + keys: &SortedMultiVec, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Result, Error> { + Ok(Policy::make_multisig::( + keys.pks.as_ref(), + signers, + build_sat, + keys.k, + true, + secp, + )?) + } + + match self { + Descriptor::Pkh(pk) => Ok(Some(miniscript::Legacy::make_signature( + pk.as_inner(), + signers, + build_sat, + secp, + ))), + Descriptor::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature( + pk.as_inner(), + signers, + build_sat, + secp, + ))), + Descriptor::Sh(sh) => match sh.as_inner() { + ShInner::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_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, 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, build_sat, secp)?), + WshInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp), + }, + Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?), + Descriptor::Tr(tr) => { + // If there's no tap tree, treat this as a single sig, otherwise build a `Thresh` + // node with threshold = 1 and the key spend signature plus all the tree leaves + let key_spend_sig = + miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp); + + if tr.tap_tree().is_none() { + Ok(Some(key_spend_sig)) + } else { + let mut items = vec![key_spend_sig]; + items.append( + &mut tr + .iter_scripts() + .filter_map(|(_, ms)| { + ms.extract_policy(signers, build_sat, secp).transpose() + }) + .collect::, _>>()?, + ); + + Ok(Policy::make_thresh(items, 1)?) + } + } + } + } +} + +#[cfg(test)] +mod test { + use crate::descriptor; + use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor}; + + use super::*; + use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh}; + use crate::keys::{DescriptorKey, IntoDescriptorKey}; + use crate::wallet::signer::SignersContainer; + use alloc::{string::ToString, sync::Arc}; + use assert_matches::assert_matches; + use bitcoin::bip32; + use bitcoin::secp256k1::Secp256k1; + use bitcoin::Network; + use core::str::FromStr; + + const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf"; + const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N"; + + const PATH: &str = "m/44'/1'/0'/0"; + + fn setup_keys( + tprv: &str, + path: &str, + secp: &SecpCtx, + ) -> (DescriptorKey, DescriptorKey, Fingerprint) { + let path = bip32::DerivationPath::from_str(path).unwrap(); + let tprv = bip32::Xpriv::from_str(tprv).unwrap(); + let tpub = bip32::Xpub::from_priv(secp, &tprv); + let fingerprint = tprv.fingerprint(secp); + let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap(); + let pubkey = (tpub, path).into_descriptor_key().unwrap(); + + (prvkey, pubkey, fingerprint) + } + + // test ExtractPolicy trait for simple descriptors; wpkh(), sh(multi()) + + #[test] + fn test_extract_policy_for_wpkh() { + let secp = Secp256k1::new(); + + let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR, PATH, &secp); + let desc = descriptor!(wpkh(pubkey)).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint); + assert_matches!(&policy.contribution, Satisfaction::None); + + let desc = descriptor!(wpkh(prvkey)).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint); + assert_matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none()); + } + + // 2 pub keys descriptor, required 2 prv keys + #[test] + fn test_extract_policy_for_sh_multi_partial_0of2() { + let secp = Secp256k1::new(); + let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let desc = descriptor!(sh(multi(2, pubkey0, pubkey1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1) + ); + // TODO should this be "Satisfaction::None" since we have no prv keys? + // TODO should items and conditions not be empty? + assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize + && m == &2usize + && items.is_empty() + && conditions.is_empty() + ); + } + + // 1 prv and 1 pub key descriptor, required 2 prv keys + #[test] + fn test_extract_policy_for_sh_multi_partial_1of2() { + let secp = Secp256k1::new(); + let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let desc = descriptor!(sh(multi(2, prvkey0, pubkey1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1) + ); + + assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize + && m == &2usize + && items.len() == 1 + && conditions.contains_key(&0) + ); + } + + // 1 prv and 1 pub key descriptor, required 1 prv keys + #[test] + #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225 + fn test_extract_policy_for_sh_multi_complete_1of2() { + let secp = Secp256k1::new(); + + let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let desc = descriptor!(sh(multi(1, pubkey0, prvkey1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &1 + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1) + ); + assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2 + && m == &1 + && items.len() == 2 + && conditions.contains_key(&vec![0]) + && conditions.contains_key(&vec![1]) + ); + } + + // 2 prv keys descriptor, required 2 prv keys + #[test] + fn test_extract_policy_for_sh_multi_complete_2of2() { + let secp = Secp256k1::new(); + + let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let desc = descriptor!(sh(multi(2, prvkey0, prvkey1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2 + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1) + ); + + assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2 + && m == &2 + && items.len() == 2 + && conditions.contains_key(&vec![0,1]) + ); + } + + // test ExtractPolicy trait with extended and single keys + + #[test] + fn test_extract_policy_for_single_wpkh() { + let secp = Secp256k1::new(); + + let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR, PATH, &secp); + let desc = descriptor!(wpkh(pubkey)).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint); + assert_matches!(&policy.contribution, Satisfaction::None); + + let desc = descriptor!(wpkh(prvkey)).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == fingerprint); + assert_matches!(policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none()); + } + + // single key, 1 prv and 1 pub key descriptor, required 1 prv keys + #[test] + #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225 + fn test_extract_policy_for_single_wsh_multi_complete_1of2() { + let secp = Secp256k1::new(); + + let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let desc = descriptor!(sh(multi(1, pubkey0, prvkey1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(policy.item, Multisig { keys, threshold } if threshold == 1 + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1) + ); + assert_matches!(policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == 2 + && m == 1 + && items.len() == 2 + && conditions.contains_key(&vec![0]) + && conditions.contains_key(&vec![1]) + ); + } + + // test ExtractPolicy trait with descriptors containing timelocks in a thresh() + + #[test] + #[ignore] // see https://github.com/bitcoindevkit/bdk/issues/225 + fn test_extract_policy_for_wsh_multi_timelock() { + let secp = Secp256k1::new(); + + let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let (_prvkey1, pubkey1, _fingerprint1) = setup_keys(TPRV1_STR, PATH, &secp); + let sequence = 50; + #[rustfmt::skip] + let desc = descriptor!(wsh(thresh( + 2, + pk(prvkey0), + s:pk(pubkey1), + s:d:v:older(sequence) + ))) + .unwrap(); + + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2); + + assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &3 + && m == &2 + && items.len() == 3 + && conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none() + && conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence)) + && conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence)) + ); + } + + // - mixed timelocks should fail + + #[test] + #[ignore] + fn test_extract_policy_for_wsh_mixed_timelocks() { + let secp = Secp256k1::new(); + let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let locktime_threshold = 500000000; // if less than this means block number, else block time in seconds + let locktime_blocks = 100; + let locktime_seconds = locktime_blocks + locktime_threshold; + let desc = descriptor!(sh(and_v( + v: pk(prvkey0), + and_v(v: after(locktime_seconds), after(locktime_blocks)) + ))) + .unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let _policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + // println!("desc policy = {:?}", policy); // TODO remove + // TODO how should this fail with mixed timelocks? + } + + // - multiple timelocks of the same type should be correctly merged together + #[test] + #[ignore] + fn test_extract_policy_for_multiple_same_timelocks() { + let secp = Secp256k1::new(); + let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR, PATH, &secp); + let locktime_blocks0 = 100; + let locktime_blocks1 = 200; + let desc = descriptor!(sh(and_v( + v: pk(prvkey0), + and_v(v: after(locktime_blocks0), after(locktime_blocks1)) + ))) + .unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let _policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + // println!("desc policy = {:?}", policy); // TODO remove + // TODO how should this merge timelocks? + let (prvkey1, _pubkey1, _fingerprint1) = setup_keys(TPRV0_STR, PATH, &secp); + let locktime_seconds0 = 500000100; + let locktime_seconds1 = 500000200; + let desc = descriptor!(sh(and_v( + v: pk(prvkey1), + and_v(v: after(locktime_seconds0), after(locktime_seconds1)) + ))) + .unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + let _policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + // println!("desc policy = {:?}", policy); // TODO remove + + // TODO how should this merge timelocks? + } + + #[test] + fn test_get_condition_multisig() { + let secp = Secp256k1::new(); + + let (_, pk0, _) = setup_keys(TPRV0_STR, PATH, &secp); + let (_, pk1, _) = setup_keys(TPRV1_STR, PATH, &secp); + + let desc = descriptor!(wsh(multi(1, pk0, pk1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + // no args, choose the default + let no_args = policy.get_condition(&vec![].into_iter().collect()); + assert_eq!(no_args, Ok(Condition::default())); + + // enough args + let eq_thresh = + policy.get_condition(&vec![(policy.id.clone(), vec![0])].into_iter().collect()); + assert_eq!(eq_thresh, Ok(Condition::default())); + + // more args, it doesn't really change anything + let gt_thresh = + policy.get_condition(&vec![(policy.id.clone(), vec![0, 1])].into_iter().collect()); + assert_eq!(gt_thresh, Ok(Condition::default())); + + // not enough args, error + let lt_thresh = + policy.get_condition(&vec![(policy.id.clone(), vec![])].into_iter().collect()); + assert_eq!( + lt_thresh, + Err(PolicyError::NotEnoughItemsSelected(policy.id.clone())) + ); + + // index out of range + let out_of_range = + policy.get_condition(&vec![(policy.id.clone(), vec![5])].into_iter().collect()); + assert_eq!(out_of_range, Err(PolicyError::IndexOutOfRange(5))); + } + + const ALICE_TPRV_STR:&str = "tprv8ZgxMBicQKsPf6T5X327efHnvJDr45Xnb8W4JifNWtEoqXu9MRYS4v1oYe6DFcMVETxy5w3bqpubYRqvcVTqovG1LifFcVUuJcbwJwrhYzP"; + const BOB_TPRV_STR:&str = "tprv8ZgxMBicQKsPeinZ155cJAn117KYhbaN6MV3WeG6sWhxWzcvX1eg1awd4C9GpUN1ncLEM2rzEvunAg3GizdZD4QPPCkisTz99tXXB4wZArp"; + const CAROL_TPRV_STR:&str = "tprv8ZgxMBicQKsPdC3CicFifuLCEyVVdXVUNYorxUWj3iGZ6nimnLAYAY9SYB7ib8rKzRxrCKFcEytCt6szwd2GHnGPRCBLAEAoSVDefSNk4Bt"; + const ALICE_BOB_PATH: &str = "m/0'"; + + #[test] + fn test_extract_satisfaction() { + 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"; + + 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(multi(2, prvkey_alice, prvkey_bob))).unwrap(); + + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let addr = wallet_desc + .at_derivation_index(0) + .unwrap() + .address(Network::Testnet) + .unwrap(); + assert_eq!( + "tb1qg3cwv3xt50gdg875qvjjpfgaps86gtk4rz0ejvp6ttc5ldnlxuvqlcn0xk", + addr.to_string() + ); + + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let psbt = Psbt::from_str(ALICE_SIGNED_PSBT).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()); + + assert_matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2 + && m == &2 + && items == &vec![0] + ); + + let psbt = Psbt::from_str(BOB_SIGNED_PSBT).unwrap(); + 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_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2 + && m == &2 + && items == &vec![1] + ); + + let psbt = Psbt::from_str(ALICE_BOB_SIGNED_PSBT).unwrap(); + let policy_alice_bob_psbt = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp) + .unwrap() + .unwrap(); + assert_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,n: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::build(keymap, &wallet_desc, &secp)); + + let addr = wallet_desc + .at_derivation_index(0) + .unwrap() + .address(Network::Testnet) + .unwrap(); + assert_eq!( + "tb1qsydsey4hexagwkvercqsmes6yet0ndkyt6uzcphtqnygjd8hmzmsfxrv58", + addr.to_string() + ); + + let psbt = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED).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::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED).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()); + } + + #[test] + fn test_extract_pkh() { + 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 (prvkey_carol, _, _) = setup_keys(CAROL_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(wsh(c: andor( + pk(prvkey_alice), + pk_k(prvkey_bob), + pk_h(prvkey_carol), + ))) + .unwrap(); + + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let policy = wallet_desc.extract_policy(&signers_container, BuildSatisfaction::None, &secp); + assert!(policy.is_ok()); + } + + #[test] + fn test_extract_tr_key_spend() { + let secp = Secp256k1::new(); + + let (prvkey, _, fingerprint) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(prvkey)).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap(); + assert_eq!( + policy, + Some(Policy { + id: "48u0tz0n".to_string(), + item: SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(fingerprint)), + satisfaction: Satisfaction::None, + contribution: Satisfaction::Complete { + condition: Condition::default() + } + }) + ); + } + + #[test] + fn test_extract_tr_script_spend() { + let secp = Secp256k1::new(); + + let (alice_prv, _, alice_fing) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + let (_, bob_pub, bob_fing) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(bob_pub, pk(alice_prv))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert_matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2); + assert_matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1]); + + let alice_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(alice_fing)); + let bob_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(bob_fing)); + + let thresh_items = match policy.item { + SatisfiableItem::Thresh { items, .. } => items, + _ => unreachable!(), + }; + + assert_eq!(thresh_items[0].item, bob_sig); + assert_eq!(thresh_items[1].item, alice_sig); + } + + #[test] + fn test_extract_tr_satisfaction_key_spend() { + const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSIRYnkGTDxwXMHP32fkDFoGJY28trxbkkVgR2z7jZa2pOJA0AyRF8LgAAAIADAAAAARcgJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQAAA=="; + const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSARNAIsRvARpRxuyQosVA7guRQT9vXr+S25W2tnP2xOGBsSgq7A4RL8yrbvwDmNlWw9R0Nc/6t+IsyCyy7dD/lbUGgyEWJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQNAMkRfC4AAACAAwAAAAEXICeQZMPHBcwc/fZ+QMWgYljby2vFuSRWBHbPuNlrak4kAAA="; + + let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap(); + let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap(); + + let secp = Secp256k1::new(); + + let (_, pubkey, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(pubkey)).unwrap(); + let (wallet_desc, _) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let policy_unsigned = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&unsigned_psbt), + &secp, + ) + .unwrap() + .unwrap(); + let policy_signed = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&signed_psbt), + &secp, + ) + .unwrap() + .unwrap(); + + assert_eq!(policy_unsigned.satisfaction, Satisfaction::None); + assert_eq!( + policy_signed.satisfaction, + Satisfaction::Complete { + condition: Default::default() + } + ); + } + + #[test] + fn test_extract_tr_satisfaction_script_spend() { + const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2IhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA=="; + const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2AQcAAQhCAUALcP9w/+Ddly9DWdhHTnQ9uCDWLPZjR6vKbKePswW2Ee6W5KNfrklus/8z98n7BQ1U4vADHk0FbadeeL8rrbHlARNAC3D/cP/g3ZcvQ1nYR050Pbgg1iz2Y0erymynj7MFthHuluSjX65JbrP/M/fJ+wUNVOLwAx5NBW2nXni/K62x5UEUeEbK57HG1FUp69HHhjBZH9bSvss8e3qhLoMuXPK5hBr2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHUAXNmWieJ80Fs+PMa2C186YOBPZbYG/ieEUkagMwzJ788SoCucNdp5wnxfpuJVygFhglDrXGzujFtC82PrMohwuIhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA=="; + + let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap(); + let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap(); + + let secp = Secp256k1::new(); + + let (_, alice_pub, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + let (_, bob_pub, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(bob_pub, pk(alice_pub))).unwrap(); + let (wallet_desc, _) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let policy_unsigned = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&unsigned_psbt), + &secp, + ) + .unwrap() + .unwrap(); + let policy_signed = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&signed_psbt), + &secp, + ) + .unwrap() + .unwrap(); + + assert_matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2); + assert_matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty()); + + assert_matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2); + assert_matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1]); + + let satisfied_items = match policy_signed.item { + SatisfiableItem::Thresh { items, .. } => items, + _ => unreachable!(), + }; + + assert_eq!( + satisfied_items[0].satisfaction, + Satisfaction::Complete { + condition: Default::default() + } + ); + assert_eq!( + satisfied_items[1].satisfaction, + Satisfaction::Complete { + condition: Default::default() + } + ); + } +} diff --git a/crates/wallet/src/descriptor/template.rs b/crates/wallet/src/descriptor/template.rs new file mode 100644 index 00000000..528be1f3 --- /dev/null +++ b/crates/wallet/src/descriptor/template.rs @@ -0,0 +1,985 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Descriptor templates +//! +//! This module contains the definition of various common script templates that are ready to be +//! used. See the documentation of each template for an example. + +use bitcoin::bip32; +use bitcoin::Network; + +use miniscript::{Legacy, Segwitv0, Tap}; + +use super::{ExtendedDescriptor, IntoWalletDescriptor, KeyMap}; +use crate::descriptor::DescriptorError; +use crate::keys::{DerivableKey, IntoDescriptorKey, ValidNetworks}; +use crate::wallet::utils::SecpCtx; +use crate::{descriptor, KeychainKind}; + +/// Type alias for the return type of [`DescriptorTemplate`], [`descriptor!`](crate::descriptor!) and others +pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks); + +/// Trait for descriptor templates that can be built into a full descriptor +/// +/// Since [`IntoWalletDescriptor`] is implemented for any [`DescriptorTemplate`], they can also be +/// passed directly to the [`Wallet`](crate::Wallet) constructor. +/// +/// ## Example +/// +/// ``` +/// use bdk_wallet::descriptor::error::Error as DescriptorError; +/// use bdk_wallet::keys::{IntoDescriptorKey, KeyError}; +/// use bdk_wallet::miniscript::Legacy; +/// use bdk_wallet::template::{DescriptorTemplate, DescriptorTemplateOut}; +/// use bitcoin::Network; +/// +/// struct MyP2PKH>(K); +/// +/// impl> DescriptorTemplate for MyP2PKH { +/// fn build(self, network: Network) -> Result { +/// Ok(bdk_wallet::descriptor!(pkh(self.0))?) +/// } +/// } +/// ``` +pub trait DescriptorTemplate { + /// Build the complete descriptor + fn build(self, network: Network) -> Result; +} + +/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its +/// [`build`](DescriptorTemplate::build) method +impl IntoWalletDescriptor for T { + fn into_wallet_descriptor( + self, + secp: &SecpCtx, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + self.build(network)?.into_wallet_descriptor(secp, network) + } +} + +/// P2PKH template. Expands to a descriptor `pkh(key)` +/// +/// ## Example +/// +/// ``` +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::Wallet; +/// # use bdk_wallet::KeychainKind; +/// use bdk_wallet::template::P2Pkh; +/// +/// let key = +/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; +/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet)?; +/// +/// assert_eq!( +/// wallet +/// .next_unused_address(KeychainKind::External)? +/// .to_string(), +/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT" +/// ); +/// # Ok::<_, Box>(()) +/// ``` +pub struct P2Pkh>(pub K); + +impl> DescriptorTemplate for P2Pkh { + fn build(self, _network: Network) -> Result { + descriptor!(pkh(self.0)) + } +} + +/// P2WPKH-P2SH template. Expands to a descriptor `sh(wpkh(key))` +/// +/// ## Example +/// +/// ``` +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::Wallet; +/// # use bdk_wallet::KeychainKind; +/// use bdk_wallet::template::P2Wpkh_P2Sh; +/// +/// let key = +/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; +/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet)?; +/// +/// assert_eq!( +/// wallet +/// .next_unused_address(KeychainKind::External)? +/// .to_string(), +/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5" +/// ); +/// # Ok::<_, Box>(()) +/// ``` +#[allow(non_camel_case_types)] +pub struct P2Wpkh_P2Sh>(pub K); + +impl> DescriptorTemplate for P2Wpkh_P2Sh { + fn build(self, _network: Network) -> Result { + descriptor!(sh(wpkh(self.0))) + } +} + +/// P2WPKH template. Expands to a descriptor `wpkh(key)` +/// +/// ## Example +/// +/// ``` +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::{Wallet}; +/// # use bdk_wallet::KeychainKind; +/// use bdk_wallet::template::P2Wpkh; +/// +/// let key = +/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; +/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet)?; +/// +/// assert_eq!( +/// wallet +/// .next_unused_address(KeychainKind::External)? +/// .to_string(), +/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd" +/// ); +/// # Ok::<_, Box>(()) +/// ``` +pub struct P2Wpkh>(pub K); + +impl> DescriptorTemplate for P2Wpkh { + fn build(self, _network: Network) -> Result { + descriptor!(wpkh(self.0)) + } +} + +/// P2TR template. Expands to a descriptor `tr(key)` +/// +/// ## Example +/// +/// ``` +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::Wallet; +/// # use bdk_wallet::KeychainKind; +/// use bdk_wallet::template::P2TR; +/// +/// let key = +/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; +/// let mut wallet = Wallet::new_no_persist(P2TR(key), None, Network::Testnet)?; +/// +/// assert_eq!( +/// wallet +/// .next_unused_address(KeychainKind::External)? +/// .to_string(), +/// "tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46" +/// ); +/// # Ok::<_, Box>(()) +/// ``` +pub struct P2TR>(pub K); + +impl> DescriptorTemplate for P2TR { + fn build(self, _network: Network) -> Result { + descriptor!(tr(self.0)) + } +} + +/// BIP44 template. Expands to `pkh(key/44'/{0,1}'/0'/{0,1}/*)` +/// +/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`). +/// +/// See [`Bip44Public`] for a template that can work with a `xpub`/`tpub`. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::{Wallet, KeychainKind}; +/// use bdk_wallet::template::Bip44; +/// +/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip44(key.clone(), KeychainKind::External), +/// Some(Bip44(key, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip44>(pub K, pub KeychainKind); + +impl> DescriptorTemplate for Bip44 { + fn build(self, network: Network) -> Result { + P2Pkh(legacy::make_bipxx_private(44, self.0, self.1, network)?).build(network) + } +} + +/// BIP44 public template. Expands to `pkh(key/{0,1}/*)` +/// +/// This assumes that the key used has already been derived with `m/44'/0'/0'` for Mainnet or `m/44'/1'/0'` for Testnet. +/// +/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs. +/// +/// See [`Bip44`] for a template that does the full derivation, but requires private data +/// for the key. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::{Wallet, KeychainKind}; +/// use bdk_wallet::template::Bip44Public; +/// +/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?; +/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip44Public(key.clone(), fingerprint, KeychainKind::External), +/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip44Public>(pub K, pub bip32::Fingerprint, pub KeychainKind); + +impl> DescriptorTemplate for Bip44Public { + fn build(self, network: Network) -> Result { + P2Pkh(legacy::make_bipxx_public( + 44, self.0, self.1, self.2, network, + )?) + .build(network) + } +} + +/// BIP49 template. Expands to `sh(wpkh(key/49'/{0,1}'/0'/{0,1}/*))` +/// +/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`). +/// +/// See [`Bip49Public`] for a template that can work with a `xpub`/`tpub`. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::{Wallet, KeychainKind}; +/// use bdk_wallet::template::Bip49; +/// +/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip49(key.clone(), KeychainKind::External), +/// Some(Bip49(key, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip49>(pub K, pub KeychainKind); + +impl> DescriptorTemplate for Bip49 { + fn build(self, network: Network) -> Result { + P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1, network)?).build(network) + } +} + +/// BIP49 public template. Expands to `sh(wpkh(key/{0,1}/*))` +/// +/// This assumes that the key used has already been derived with `m/49'/0'/0'` for Mainnet or `m/49'/1'/0'` for Testnet. +/// +/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs. +/// +/// See [`Bip49`] for a template that does the full derivation, but requires private data +/// for the key. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::{Wallet, KeychainKind}; +/// use bdk_wallet::template::Bip49Public; +/// +/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?; +/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip49Public(key.clone(), fingerprint, KeychainKind::External), +/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip49Public>(pub K, pub bip32::Fingerprint, pub KeychainKind); + +impl> DescriptorTemplate for Bip49Public { + fn build(self, network: Network) -> Result { + P2Wpkh_P2Sh(segwit_v0::make_bipxx_public( + 49, self.0, self.1, self.2, network, + )?) + .build(network) + } +} + +/// BIP84 template. Expands to `wpkh(key/84'/{0,1}'/0'/{0,1}/*)` +/// +/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`). +/// +/// See [`Bip84Public`] for a template that can work with a `xpub`/`tpub`. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::{Wallet, KeychainKind}; +/// use bdk_wallet::template::Bip84; +/// +/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip84(key.clone(), KeychainKind::External), +/// Some(Bip84(key, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip84>(pub K, pub KeychainKind); + +impl> DescriptorTemplate for Bip84 { + fn build(self, network: Network) -> Result { + P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1, network)?).build(network) + } +} + +/// BIP84 public template. Expands to `wpkh(key/{0,1}/*)` +/// +/// This assumes that the key used has already been derived with `m/84'/0'/0'` for Mainnet or `m/84'/1'/0'` for Testnet. +/// +/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs. +/// +/// See [`Bip84`] for a template that does the full derivation, but requires private data +/// for the key. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::{Wallet, KeychainKind}; +/// use bdk_wallet::template::Bip84Public; +/// +/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?; +/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip84Public(key.clone(), fingerprint, KeychainKind::External), +/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip84Public>(pub K, pub bip32::Fingerprint, pub KeychainKind); + +impl> DescriptorTemplate for Bip84Public { + fn build(self, network: Network) -> Result { + P2Wpkh(segwit_v0::make_bipxx_public( + 84, self.0, self.1, self.2, network, + )?) + .build(network) + } +} + +/// BIP86 template. Expands to `tr(key/86'/{0,1}'/0'/{0,1}/*)` +/// +/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`). +/// +/// See [`Bip86Public`] for a template that can work with a `xpub`/`tpub`. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::{Wallet, KeychainKind}; +/// use bdk_wallet::template::Bip86; +/// +/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip86(key.clone(), KeychainKind::External), +/// Some(Bip86(key, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip86>(pub K, pub KeychainKind); + +impl> DescriptorTemplate for Bip86 { + fn build(self, network: Network) -> Result { + P2TR(segwit_v1::make_bipxx_private(86, self.0, self.1, network)?).build(network) + } +} + +/// BIP86 public template. Expands to `tr(key/{0,1}/*)` +/// +/// This assumes that the key used has already been derived with `m/86'/0'/0'` for Mainnet or `m/86'/1'/0'` for Testnet. +/// +/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs. +/// +/// See [`Bip86`] for a template that does the full derivation, but requires private data +/// for the key. +/// +/// ## Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use bdk_wallet::bitcoin::{PrivateKey, Network}; +/// # use bdk_wallet::{Wallet, KeychainKind}; +/// use bdk_wallet::template::Bip86Public; +/// +/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?; +/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; +/// let mut wallet = Wallet::new_no_persist( +/// Bip86Public(key.clone(), fingerprint, KeychainKind::External), +/// Some(Bip86Public(key, fingerprint, KeychainKind::Internal)), +/// Network::Testnet, +/// )?; +/// +/// assert_eq!(wallet.next_unused_address(KeychainKind::External)?.to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37"); +/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku"); +/// # Ok::<_, Box>(()) +/// ``` +pub struct Bip86Public>(pub K, pub bip32::Fingerprint, pub KeychainKind); + +impl> DescriptorTemplate for Bip86Public { + fn build(self, network: Network) -> Result { + P2TR(segwit_v1::make_bipxx_public( + 86, self.0, self.1, self.2, network, + )?) + .build(network) + } +} + +macro_rules! expand_make_bipxx { + ( $mod_name:ident, $ctx:ty ) => { + mod $mod_name { + use super::*; + + pub(super) fn make_bipxx_private>( + bip: u32, + key: K, + keychain: KeychainKind, + network: Network, + ) -> Result, DescriptorError> { + let mut derivation_path = alloc::vec::Vec::with_capacity(4); + derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?); + + match network { + Network::Bitcoin => { + derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?); + } + _ => { + derivation_path.push(bip32::ChildNumber::from_hardened_idx(1)?); + } + } + derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?); + + match keychain { + KeychainKind::External => { + derivation_path.push(bip32::ChildNumber::from_normal_idx(0)?) + } + KeychainKind::Internal => { + derivation_path.push(bip32::ChildNumber::from_normal_idx(1)?) + } + }; + + let derivation_path: bip32::DerivationPath = derivation_path.into(); + + Ok((key, derivation_path)) + } + pub(super) fn make_bipxx_public>( + bip: u32, + key: K, + parent_fingerprint: bip32::Fingerprint, + keychain: KeychainKind, + network: Network, + ) -> Result, DescriptorError> { + let derivation_path: bip32::DerivationPath = match keychain { + KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(), + KeychainKind::Internal => vec![bip32::ChildNumber::from_normal_idx(1)?].into(), + }; + + let source_path = bip32::DerivationPath::from(vec![ + bip32::ChildNumber::from_hardened_idx(bip)?, + match network { + Network::Bitcoin => bip32::ChildNumber::from_hardened_idx(0)?, + _ => bip32::ChildNumber::from_hardened_idx(1)?, + }, + bip32::ChildNumber::from_hardened_idx(0)?, + ]); + + Ok((key, (parent_fingerprint, source_path), derivation_path)) + } + } + }; +} + +expand_make_bipxx!(legacy, Legacy); +expand_make_bipxx!(segwit_v0, Segwitv0); +expand_make_bipxx!(segwit_v1, Tap); + +#[cfg(test)] +mod test { + // test existing descriptor templates, make sure they are expanded to the right descriptors + + use alloc::{string::ToString, vec::Vec}; + use core::str::FromStr; + + use super::*; + use crate::descriptor::{DescriptorError, DescriptorMeta}; + use crate::keys::ValidNetworks; + use assert_matches::assert_matches; + use miniscript::descriptor::{DescriptorPublicKey, KeyMap}; + use miniscript::Descriptor; + + // BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)` + #[test] + fn test_bip44_template_cointype() { + use bitcoin::bip32::ChildNumber::{self, Hardened}; + + let xprvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap(); + assert_eq!(Network::Bitcoin, xprvkey.network); + let xdesc = Bip44(xprvkey, KeychainKind::Internal) + .build(Network::Bitcoin) + .unwrap(); + + if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 { + let path: Vec = pkh.into_inner().full_derivation_path().unwrap().into(); + let purpose = path.first().unwrap(); + assert_matches!(purpose, Hardened { index: 44 }); + let coin_type = path.get(1).unwrap(); + assert_matches!(coin_type, Hardened { index: 0 }); + } + + let tprvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + assert_eq!(Network::Testnet, tprvkey.network); + let tdesc = Bip44(tprvkey, KeychainKind::Internal) + .build(Network::Testnet) + .unwrap(); + + if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 { + let path: Vec = pkh.into_inner().full_derivation_path().unwrap().into(); + let purpose = path.first().unwrap(); + assert_matches!(purpose, Hardened { index: 44 }); + let coin_type = path.get(1).unwrap(); + assert_matches!(coin_type, Hardened { index: 1 }); + } + } + + // verify template descriptor generates expected address(es) + fn check( + desc: Result<(Descriptor, KeyMap, ValidNetworks), DescriptorError>, + is_witness: bool, + is_taproot: bool, + is_fixed: bool, + network: Network, + expected: &[&str], + ) { + let (desc, _key_map, _networks) = desc.unwrap(); + assert_eq!(desc.is_witness(), is_witness); + assert_eq!(desc.is_taproot(), is_taproot); + assert_eq!(!desc.has_wildcard(), is_fixed); + for i in 0..expected.len() { + let index = i as u32; + let child_desc = if !desc.has_wildcard() { + desc.at_derivation_index(0).unwrap() + } else { + desc.at_derivation_index(index).unwrap() + }; + let address = child_desc.address(network).unwrap(); + assert_eq!(address.to_string(), *expected.get(i).unwrap()); + } + } + + // P2PKH + #[test] + fn test_p2ph_template() { + let prvkey = + bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") + .unwrap(); + check( + P2Pkh(prvkey).build(Network::Bitcoin), + false, + false, + true, + Network::Regtest, + &["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"], + ); + + let pubkey = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + check( + P2Pkh(pubkey).build(Network::Bitcoin), + false, + false, + true, + Network::Regtest, + &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"], + ); + } + + // P2WPKH-P2SH `sh(wpkh(key))` + #[test] + fn test_p2wphp2sh_template() { + let prvkey = + bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") + .unwrap(); + check( + P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin), + true, + false, + true, + Network::Regtest, + &["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"], + ); + + let pubkey = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + check( + P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin), + true, + false, + true, + Network::Regtest, + &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"], + ); + } + + // P2WPKH `wpkh(key)` + #[test] + fn test_p2wph_template() { + let prvkey = + bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") + .unwrap(); + check( + P2Wpkh(prvkey).build(Network::Bitcoin), + true, + false, + true, + Network::Regtest, + &["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"], + ); + + let pubkey = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + check( + P2Wpkh(pubkey).build(Network::Bitcoin), + true, + false, + true, + Network::Regtest, + &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"], + ); + } + + // P2TR `tr(key)` + #[test] + fn test_p2tr_template() { + let prvkey = + bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") + .unwrap(); + check( + P2TR(prvkey).build(Network::Bitcoin), + false, + true, + true, + Network::Regtest, + &["bcrt1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xqnwtkqq"], + ); + + let pubkey = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + check( + P2TR(pubkey).build(Network::Bitcoin), + false, + true, + true, + Network::Regtest, + &["bcrt1pw74tdcrxlzn5r8z6ku2vztr86fgq0m245s72mjktf4afwzsf8ugs4evwdf"], + ); + } + + // BIP44 `pkh(key/44'/0'/0'/{0,1}/*)` + #[test] + fn test_bip44_template() { + let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + check( + Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin), + false, + false, + false, + Network::Regtest, + &[ + "n453VtnjDHPyDt2fDstKSu7A3YCJoHZ5g5", + "mvfrrumXgTtwFPWDNUecBBgzuMXhYM7KRP", + "mzYvhRAuQqbdSKMVVzXNYyqihgNdRadAUQ", + ], + ); + check( + Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin), + false, + false, + false, + Network::Regtest, + &[ + "muHF98X9KxEzdKrnFAX85KeHv96eXopaip", + "n4hpyLJE5ub6B5Bymv4eqFxS5KjrewSmYR", + "mgvkdv1ffmsXd2B1sRKQ5dByK3SzpG42rA", + ], + ); + } + + // BIP44 public `pkh(key/{0,1}/*)` + #[test] + fn test_bip44_public_template() { + let pubkey = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap(); + let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap(); + check( + Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin), + false, + false, + false, + Network::Regtest, + &[ + "miNG7dJTzJqNbFS19svRdTCisC65dsubtR", + "n2UqaDbCjWSFJvpC84m3FjUk5UaeibCzYg", + "muCPpS6Ue7nkzeJMWDViw7Lkwr92Yc4K8g", + ], + ); + check( + Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin), + false, + false, + false, + Network::Regtest, + &[ + "moDr3vJ8wpt5nNxSK55MPq797nXJb2Ru9H", + "ms7A1Yt4uTezT2XkefW12AvLoko8WfNJMG", + "mhYiyat2rtEnV77cFfQsW32y1m2ceCGHPo", + ], + ); + } + + // BIP49 `sh(wpkh(key/49'/0'/0'/{0,1}/*))` + #[test] + fn test_bip49_template() { + let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + check( + Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin), + true, + false, + false, + Network::Regtest, + &[ + "2N9bCAJXGm168MjVwpkBdNt6ucka3PKVoUV", + "2NDckYkqrYyDMtttEav5hB3Bfw9EGAW5HtS", + "2NAFTVtksF9T4a97M7nyCjwUBD24QevZ5Z4", + ], + ); + check( + Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin), + true, + false, + false, + Network::Regtest, + &[ + "2NB3pA8PnzJLGV8YEKNDFpbViZv3Bm1K6CG", + "2NBiX2Wzxngb5rPiWpUiJQ2uLVB4HBjFD4p", + "2NA8ek4CdQ6aMkveYF6AYuEYNrftB47QGTn", + ], + ); + } + + // BIP49 public `sh(wpkh(key/{0,1}/*))` + #[test] + fn test_bip49_public_template() { + let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap(); + let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap(); + check( + Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin), + true, + false, + false, + Network::Regtest, + &[ + "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt", + "2NCTQfJ1sZa3wQ3pPseYRHbaNEpC3AquEfX", + "2MveFxAuC8BYPzTybx7FxSzW8HSd8ATT4z7", + ], + ); + check( + Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin), + true, + false, + false, + Network::Regtest, + &[ + "2NF2vttKibwyxigxtx95Zw8K7JhDbo5zPVJ", + "2Mtmyd8taksxNVWCJ4wVvaiss7QPZGcAJuH", + "2NBs3CTVYPr1HCzjB4YFsnWCPCtNg8uMEfp", + ], + ); + } + + // BIP84 `wpkh(key/84'/0'/0'/{0,1}/*)` + #[test] + fn test_bip84_template() { + let prvkey = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + check( + Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin), + true, + false, + false, + Network::Regtest, + &[ + "bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s", + "bcrt1qx0v6zgfwe50m4kqc58cqzcyem7ay2sfl3gvqhp", + "bcrt1q4h7fq9zhxst6e69p3n882nfj649l7w9g3zccfp", + ], + ); + check( + Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin), + true, + false, + false, + Network::Regtest, + &[ + "bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa", + "bcrt1qqqasfhxpkkf7zrxqnkr2sfhn74dgsrc3e3ky45", + "bcrt1qpks7n0gq74hsgsz3phn5vuazjjq0f5eqhsgyce", + ], + ); + } + + // BIP84 public `wpkh(key/{0,1}/*)` + #[test] + fn test_bip84_public_template() { + let pubkey = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap(); + let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap(); + check( + Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin), + true, + false, + false, + Network::Regtest, + &[ + "bcrt1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2prcdvd0h", + "bcrt1q3lncdlwq3lgcaaeyruynjnlccr0ve0kakh6ana", + "bcrt1qt9800y6xl3922jy3uyl0z33jh5wfpycyhcylr9", + ], + ); + check( + Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin), + true, + false, + false, + Network::Regtest, + &[ + "bcrt1qm6wqukenh7guu792lj2njgw9n78cmwsy8xy3z2", + "bcrt1q694twxtjn4nnrvnyvra769j0a23rllj5c6cgwp", + "bcrt1qhlac3c5ranv5w5emlnqs7wxhkxt8maelylcarp", + ], + ); + } + + // BIP86 `tr(key/86'/0'/0'/{0,1}/*)` + // Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki + #[test] + fn test_bip86_template() { + let prvkey = bitcoin::bip32::Xpriv::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap(); + check( + Bip86(prvkey, KeychainKind::External).build(Network::Bitcoin), + false, + true, + false, + Network::Bitcoin, + &[ + "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", + "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh", + "bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8", + ], + ); + check( + Bip86(prvkey, KeychainKind::Internal).build(Network::Bitcoin), + false, + true, + false, + Network::Bitcoin, + &[ + "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", + "bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj", + "bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5", + ], + ); + } + + // BIP86 public `tr(key/{0,1}/*)` + // Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki + #[test] + fn test_bip86_public_template() { + let pubkey = bitcoin::bip32::Xpub::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap(); + let fingerprint = bitcoin::bip32::Fingerprint::from_str("73c5da0a").unwrap(); + check( + Bip86Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin), + false, + true, + false, + Network::Bitcoin, + &[ + "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", + "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh", + "bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8", + ], + ); + check( + Bip86Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin), + false, + true, + false, + Network::Bitcoin, + &[ + "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", + "bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj", + "bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5", + ], + ); + } +} diff --git a/crates/wallet/src/keys/bip39.rs b/crates/wallet/src/keys/bip39.rs new file mode 100644 index 00000000..7158505f --- /dev/null +++ b/crates/wallet/src/keys/bip39.rs @@ -0,0 +1,227 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! BIP-0039 + +// TODO: maybe write our own implementation of bip39? Seems stupid to have an extra dependency for +// something that should be fairly simple to re-implement. + +use alloc::string::String; +use bitcoin::bip32; +use bitcoin::Network; + +use miniscript::ScriptContext; + +pub use bip39::{Error, Language, Mnemonic}; + +type Seed = [u8; 64]; + +/// Type describing entropy length (aka word count) in the mnemonic +pub enum WordCount { + /// 12 words mnemonic (128 bits entropy) + Words12 = 128, + /// 15 words mnemonic (160 bits entropy) + Words15 = 160, + /// 18 words mnemonic (192 bits entropy) + Words18 = 192, + /// 21 words mnemonic (224 bits entropy) + Words21 = 224, + /// 24 words mnemonic (256 bits entropy) + Words24 = 256, +} + +use super::{ + any_network, DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey, KeyError, +}; + +fn set_valid_on_any_network( + descriptor_key: DescriptorKey, +) -> DescriptorKey { + // We have to pick one network to build the xprv, but since the bip39 standard doesn't + // encode the network, the xprv we create is actually valid everywhere. So we override the + // valid networks with `any_network()`. + descriptor_key.override_valid_networks(any_network()) +} + +/// Type for a BIP39 mnemonic with an optional passphrase +pub type MnemonicWithPassphrase = (Mnemonic, Option); + +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +impl DerivableKey for Seed { + fn into_extended_key(self) -> Result, KeyError> { + Ok(bip32::Xpriv::new_master(Network::Bitcoin, &self[..])?.into()) + } + + fn into_descriptor_key( + self, + source: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + let descriptor_key = self + .into_extended_key()? + .into_descriptor_key(source, derivation_path)?; + + Ok(set_valid_on_any_network(descriptor_key)) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +impl DerivableKey for MnemonicWithPassphrase { + fn into_extended_key(self) -> Result, KeyError> { + let (mnemonic, passphrase) = self; + let seed: Seed = mnemonic.to_seed(passphrase.as_deref().unwrap_or("")); + + seed.into_extended_key() + } + + fn into_descriptor_key( + self, + source: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + let descriptor_key = self + .into_extended_key()? + .into_descriptor_key(source, derivation_path)?; + + Ok(set_valid_on_any_network(descriptor_key)) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +impl DerivableKey for (GeneratedKey, Option) { + fn into_extended_key(self) -> Result, KeyError> { + let (mnemonic, passphrase) = self; + (mnemonic.into_key(), passphrase).into_extended_key() + } + + fn into_descriptor_key( + self, + source: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + let (mnemonic, passphrase) = self; + (mnemonic.into_key(), passphrase).into_descriptor_key(source, derivation_path) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +impl DerivableKey for Mnemonic { + fn into_extended_key(self) -> Result, KeyError> { + (self, None).into_extended_key() + } + + fn into_descriptor_key( + self, + source: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + let descriptor_key = self + .into_extended_key()? + .into_descriptor_key(source, derivation_path)?; + + Ok(set_valid_on_any_network(descriptor_key)) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +impl GeneratableKey for Mnemonic { + type Entropy = [u8; 32]; + + type Options = (WordCount, Language); + type Error = Option; + + fn generate_with_entropy( + (word_count, language): Self::Options, + entropy: Self::Entropy, + ) -> Result, Self::Error> { + let entropy = &entropy[..(word_count as usize / 8)]; + let mnemonic = Mnemonic::from_entropy_in(language, entropy)?; + + Ok(GeneratedKey::new(mnemonic, any_network())) + } +} + +#[cfg(test)] +mod test { + use alloc::string::ToString; + use core::str::FromStr; + + use bitcoin::bip32; + + use bip39::{Language, Mnemonic}; + + use crate::keys::{any_network, GeneratableKey, GeneratedKey}; + + use super::WordCount; + + #[test] + fn test_keys_bip39_mnemonic() { + let mnemonic = + "aim bunker wash balance finish force paper analyst cabin spoon stable organ"; + let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap(); + let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap(); + + let key = (mnemonic, path); + let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap(); + assert_eq!(desc.to_string(), "wpkh([be83839f/44'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)#0r8v4nkv"); + assert_eq!(keys.len(), 1); + assert_eq!(networks.len(), 4); + } + + #[test] + fn test_keys_bip39_mnemonic_passphrase() { + let mnemonic = + "aim bunker wash balance finish force paper analyst cabin spoon stable organ"; + let mnemonic = Mnemonic::parse_in(Language::English, mnemonic).unwrap(); + let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap(); + + let key = ((mnemonic, Some("passphrase".into())), path); + let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap(); + assert_eq!(desc.to_string(), "wpkh([8f6cb80c/44'/0'/0']xpub6DWYS8bbihFevy29M4cbw4ZR3P5E12jB8R88gBDWCTCNpYiDHhYWNywrCF9VZQYagzPmsZpxXpytzSoxynyeFr4ZyzheVjnpLKuse4fiwZw/0/*)#h0j0tg5m"); + assert_eq!(keys.len(), 1); + assert_eq!(networks.len(), 4); + } + + #[test] + fn test_keys_generate_bip39() { + let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = + Mnemonic::generate_with_entropy( + (WordCount::Words12, Language::English), + crate::keys::test::TEST_ENTROPY, + ) + .unwrap(); + assert_eq!(generated_mnemonic.valid_networks, any_network()); + assert_eq!( + generated_mnemonic.to_string(), + "primary fetch primary fetch primary fetch primary fetch primary fetch primary fever" + ); + + let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = + Mnemonic::generate_with_entropy( + (WordCount::Words24, Language::English), + crate::keys::test::TEST_ENTROPY, + ) + .unwrap(); + assert_eq!(generated_mnemonic.valid_networks, any_network()); + assert_eq!(generated_mnemonic.to_string(), "primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary fetch primary foster"); + } + + #[test] + fn test_keys_generate_bip39_random() { + let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = + Mnemonic::generate((WordCount::Words12, Language::English)).unwrap(); + assert_eq!(generated_mnemonic.valid_networks, any_network()); + + let generated_mnemonic: GeneratedKey<_, miniscript::Segwitv0> = + Mnemonic::generate((WordCount::Words24, Language::English)).unwrap(); + assert_eq!(generated_mnemonic.valid_networks, any_network()); + } +} diff --git a/crates/wallet/src/keys/mod.rs b/crates/wallet/src/keys/mod.rs new file mode 100644 index 00000000..5f6b54cd --- /dev/null +++ b/crates/wallet/src/keys/mod.rs @@ -0,0 +1,1008 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Key formats + +use crate::collections::HashSet; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::any::TypeId; +use core::fmt; +use core::marker::PhantomData; +use core::ops::Deref; +use core::str::FromStr; + +use bitcoin::secp256k1::{self, Secp256k1, Signing}; + +use bitcoin::bip32; +use bitcoin::{key::XOnlyPublicKey, Network, PrivateKey, PublicKey}; + +use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard}; +pub use miniscript::descriptor::{ + DescriptorPublicKey, DescriptorSecretKey, KeyMap, SinglePriv, SinglePub, SinglePubKey, + SortedMultiVec, +}; +pub use miniscript::ScriptContext; +use miniscript::{Miniscript, Terminal}; + +use crate::descriptor::{CheckMiniscript, DescriptorError}; +use crate::wallet::utils::SecpCtx; + +#[cfg(feature = "keys-bip39")] +#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] +pub mod bip39; + +/// Set of valid networks for a key +pub type ValidNetworks = HashSet; + +/// Create a set containing mainnet, testnet, signet, and regtest +pub fn any_network() -> ValidNetworks { + vec![ + Network::Bitcoin, + Network::Testnet, + Network::Regtest, + Network::Signet, + ] + .into_iter() + .collect() +} +/// Create a set only containing mainnet +pub fn mainnet_network() -> ValidNetworks { + vec![Network::Bitcoin].into_iter().collect() +} +/// Create a set containing testnet and regtest +pub fn test_networks() -> ValidNetworks { + vec![Network::Testnet, Network::Regtest, Network::Signet] + .into_iter() + .collect() +} +/// Compute the intersection of two sets +pub fn merge_networks(a: &ValidNetworks, b: &ValidNetworks) -> ValidNetworks { + a.intersection(b).cloned().collect() +} + +/// Container for public or secret keys +#[derive(Debug)] +pub enum DescriptorKey { + #[doc(hidden)] + Public(DescriptorPublicKey, ValidNetworks, PhantomData), + #[doc(hidden)] + Secret(DescriptorSecretKey, ValidNetworks, PhantomData), +} + +impl DescriptorKey { + /// Create an instance given a public key and a set of valid networks + pub fn from_public(public: DescriptorPublicKey, networks: ValidNetworks) -> Self { + DescriptorKey::Public(public, networks, PhantomData) + } + + /// Create an instance given a secret key and a set of valid networks + pub fn from_secret(secret: DescriptorSecretKey, networks: ValidNetworks) -> Self { + DescriptorKey::Secret(secret, networks, PhantomData) + } + + /// Override the computed set of valid networks + pub fn override_valid_networks(self, networks: ValidNetworks) -> Self { + match self { + DescriptorKey::Public(key, _, _) => DescriptorKey::Public(key, networks, PhantomData), + DescriptorKey::Secret(key, _, _) => DescriptorKey::Secret(key, networks, PhantomData), + } + } + + // This method is used internally by `bdk_wallet::fragment!` and `bdk_wallet::descriptor!`. It has to be + // public because it is effectively called by external crates once the macros are expanded, + // but since it is not meant to be part of the public api we hide it from the docs. + #[doc(hidden)] + pub fn extract( + self, + secp: &SecpCtx, + ) -> Result<(DescriptorPublicKey, KeyMap, ValidNetworks), KeyError> { + match self { + DescriptorKey::Public(public, valid_networks, _) => { + Ok((public, KeyMap::default(), valid_networks)) + } + DescriptorKey::Secret(secret, valid_networks, _) => { + let mut key_map = KeyMap::new(); + + let public = secret + .to_public(secp) + .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?; + key_map.insert(public.clone(), secret); + + Ok((public, key_map, valid_networks)) + } + } + } +} + +/// Enum representation of the known valid [`ScriptContext`]s +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum ScriptContextEnum { + /// Legacy scripts + Legacy, + /// Segwitv0 scripts + Segwitv0, + /// Taproot scripts + Tap, +} + +impl ScriptContextEnum { + /// Returns whether the script context is [`ScriptContextEnum::Legacy`] + pub fn is_legacy(&self) -> bool { + self == &ScriptContextEnum::Legacy + } + + /// Returns whether the script context is [`ScriptContextEnum::Segwitv0`] + pub fn is_segwit_v0(&self) -> bool { + self == &ScriptContextEnum::Segwitv0 + } + + /// Returns whether the script context is [`ScriptContextEnum::Tap`] + pub fn is_taproot(&self) -> bool { + self == &ScriptContextEnum::Tap + } +} + +/// Trait that adds extra useful methods to [`ScriptContext`]s +pub trait ExtScriptContext: ScriptContext { + /// Returns the [`ScriptContext`] as a [`ScriptContextEnum`] + fn as_enum() -> ScriptContextEnum; + + /// Returns whether the script context is [`Legacy`](miniscript::Legacy) + fn is_legacy() -> bool { + Self::as_enum().is_legacy() + } + + /// Returns whether the script context is [`Segwitv0`](miniscript::Segwitv0) + fn is_segwit_v0() -> bool { + Self::as_enum().is_segwit_v0() + } + + /// Returns whether the script context is [`Tap`](miniscript::Tap), aka Taproot or Segwit V1 + fn is_taproot() -> bool { + Self::as_enum().is_taproot() + } +} + +impl ExtScriptContext for Ctx { + fn as_enum() -> ScriptContextEnum { + match TypeId::of::() { + t if t == TypeId::of::() => ScriptContextEnum::Legacy, + t if t == TypeId::of::() => ScriptContextEnum::Segwitv0, + t if t == TypeId::of::() => ScriptContextEnum::Tap, + _ => unimplemented!("Unknown ScriptContext type"), + } + } +} + +/// Trait for objects that can be turned into a public or secret [`DescriptorKey`] +/// +/// The generic type `Ctx` is used to define the context in which the key is valid: some key +/// formats, like the mnemonics used by Electrum wallets, encode internally whether the wallet is +/// legacy or segwit. Thus, trying to turn a valid legacy mnemonic into a `DescriptorKey` +/// that would become part of a segwit descriptor should fail. +/// +/// For key types that do care about this, the [`ExtScriptContext`] trait provides some useful +/// methods that can be used to check at runtime which `Ctx` is being used. +/// +/// For key types that can do this check statically (because they can only work within a +/// single `Ctx`), the "specialized" trait can be implemented to make the compiler handle the type +/// checking. +/// +/// Keys also have control over the networks they support: constructing the return object with +/// [`DescriptorKey::from_public`] or [`DescriptorKey::from_secret`] allows to specify a set of +/// [`ValidNetworks`]. +/// +/// ## Examples +/// +/// Key type valid in any context: +/// +/// ``` +/// use bdk_wallet::bitcoin::PublicKey; +/// +/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError, ScriptContext}; +/// +/// pub struct MyKeyType { +/// pubkey: PublicKey, +/// } +/// +/// impl IntoDescriptorKey for MyKeyType { +/// fn into_descriptor_key(self) -> Result, KeyError> { +/// self.pubkey.into_descriptor_key() +/// } +/// } +/// ``` +/// +/// Key type that is only valid on mainnet: +/// +/// ``` +/// use bdk_wallet::bitcoin::PublicKey; +/// +/// use bdk_wallet::keys::{ +/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError, +/// ScriptContext, SinglePub, SinglePubKey, +/// }; +/// +/// pub struct MyKeyType { +/// pubkey: PublicKey, +/// } +/// +/// impl IntoDescriptorKey for MyKeyType { +/// fn into_descriptor_key(self) -> Result, KeyError> { +/// Ok(DescriptorKey::from_public( +/// DescriptorPublicKey::Single(SinglePub { +/// origin: None, +/// key: SinglePubKey::FullKey(self.pubkey), +/// }), +/// mainnet_network(), +/// )) +/// } +/// } +/// ``` +/// +/// Key type that internally encodes in which context it's valid. The context is checked at runtime: +/// +/// ``` +/// use bdk_wallet::bitcoin::PublicKey; +/// +/// use bdk_wallet::keys::{ +/// DescriptorKey, ExtScriptContext, IntoDescriptorKey, KeyError, ScriptContext, +/// }; +/// +/// pub struct MyKeyType { +/// is_legacy: bool, +/// pubkey: PublicKey, +/// } +/// +/// impl IntoDescriptorKey for MyKeyType { +/// fn into_descriptor_key(self) -> Result, KeyError> { +/// if Ctx::is_legacy() == self.is_legacy { +/// self.pubkey.into_descriptor_key() +/// } else { +/// Err(KeyError::InvalidScriptContext) +/// } +/// } +/// } +/// ``` +/// +/// Key type that can only work within [`miniscript::Segwitv0`] context. Only the specialized version +/// of the trait is implemented. +/// +/// This example deliberately fails to compile, to demonstrate how the compiler can catch when keys +/// are misused. In this case, the "segwit-only" key is used to build a `pkh()` descriptor, which +/// makes the compiler (correctly) fail. +/// +/// ```compile_fail +/// use bdk_wallet::bitcoin::PublicKey; +/// use core::str::FromStr; +/// +/// use bdk_wallet::keys::{DescriptorKey, IntoDescriptorKey, KeyError}; +/// +/// pub struct MySegwitOnlyKeyType { +/// pubkey: PublicKey, +/// } +/// +/// impl IntoDescriptorKey for MySegwitOnlyKeyType { +/// fn into_descriptor_key(self) -> Result, KeyError> { +/// self.pubkey.into_descriptor_key() +/// } +/// } +/// +/// let key = MySegwitOnlyKeyType { +/// pubkey: PublicKey::from_str("...")?, +/// }; +/// let (descriptor, _, _) = bdk_wallet::descriptor!(pkh(key))?; +/// // ^^^^^ changing this to `wpkh` would make it compile +/// +/// # Ok::<_, Box>(()) +/// ``` +pub trait IntoDescriptorKey: Sized { + /// Turn the key into a [`DescriptorKey`] within the requested [`ScriptContext`] + fn into_descriptor_key(self) -> Result, KeyError>; +} + +/// Enum for extended keys that can be either `xprv` or `xpub` +/// +/// An instance of [`ExtendedKey`] can be constructed from an [`Xpriv`](bip32::Xpriv) +/// or an [`Xpub`](bip32::Xpub) by using the `From` trait. +/// +/// Defaults to the [`Legacy`](miniscript::Legacy) context. +pub enum ExtendedKey { + /// A private extended key, aka an `xprv` + Private((bip32::Xpriv, PhantomData)), + /// A public extended key, aka an `xpub` + Public((bip32::Xpub, PhantomData)), +} + +impl ExtendedKey { + /// Return whether or not the key contains the private data + pub fn has_secret(&self) -> bool { + match self { + ExtendedKey::Private(_) => true, + ExtendedKey::Public(_) => false, + } + } + + /// Transform the [`ExtendedKey`] into an [`Xpriv`](bip32::Xpriv) for the + /// given [`Network`], if the key contains the private data + pub fn into_xprv(self, network: Network) -> Option { + match self { + ExtendedKey::Private((mut xprv, _)) => { + xprv.network = network; + Some(xprv) + } + ExtendedKey::Public(_) => None, + } + } + + /// Transform the [`ExtendedKey`] into an [`Xpub`](bip32::Xpub) for the + /// given [`Network`] + pub fn into_xpub( + self, + network: bitcoin::Network, + secp: &Secp256k1, + ) -> bip32::Xpub { + let mut xpub = match self { + ExtendedKey::Private((xprv, _)) => bip32::Xpub::from_priv(secp, &xprv), + ExtendedKey::Public((xpub, _)) => xpub, + }; + + xpub.network = network; + xpub + } +} + +impl From for ExtendedKey { + fn from(xpub: bip32::Xpub) -> Self { + ExtendedKey::Public((xpub, PhantomData)) + } +} + +impl From for ExtendedKey { + fn from(xprv: bip32::Xpriv) -> Self { + ExtendedKey::Private((xprv, PhantomData)) + } +} + +/// Trait for keys that can be derived. +/// +/// When extra metadata are provided, a [`DerivableKey`] can be transformed into a +/// [`DescriptorKey`]: the trait [`IntoDescriptorKey`] is automatically implemented +/// for `(DerivableKey, DerivationPath)` and +/// `(DerivableKey, KeySource, DerivationPath)` tuples. +/// +/// For key types that don't encode any indication about the path to use (like bip39), it's +/// generally recommended to implement this trait instead of [`IntoDescriptorKey`]. The same +/// rules regarding script context and valid networks apply. +/// +/// ## Examples +/// +/// Key types that can be directly converted into an [`Xpriv`] or +/// an [`Xpub`] can implement only the required `into_extended_key()` method. +/// +/// ``` +/// use bdk_wallet::bitcoin; +/// use bdk_wallet::bitcoin::bip32; +/// use bdk_wallet::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext}; +/// +/// struct MyCustomKeyType { +/// key_data: bitcoin::PrivateKey, +/// chain_code: [u8; 32], +/// network: bitcoin::Network, +/// } +/// +/// impl DerivableKey for MyCustomKeyType { +/// fn into_extended_key(self) -> Result, KeyError> { +/// let xprv = bip32::Xpriv { +/// network: self.network, +/// depth: 0, +/// parent_fingerprint: bip32::Fingerprint::default(), +/// private_key: self.key_data.inner, +/// chain_code: bip32::ChainCode::from(&self.chain_code), +/// child_number: bip32::ChildNumber::Normal { index: 0 }, +/// }; +/// +/// xprv.into_extended_key() +/// } +/// } +/// ``` +/// +/// Types that don't internally encode the [`Network`] in which they are valid need some extra +/// steps to override the set of valid networks, otherwise only the network specified in the +/// [`Xpriv`] or [`Xpub`] will be considered valid. +/// +/// ``` +/// use bdk_wallet::bitcoin; +/// use bdk_wallet::bitcoin::bip32; +/// use bdk_wallet::keys::{ +/// any_network, DerivableKey, DescriptorKey, ExtendedKey, KeyError, ScriptContext, +/// }; +/// +/// struct MyCustomKeyType { +/// key_data: bitcoin::PrivateKey, +/// chain_code: [u8; 32], +/// } +/// +/// impl DerivableKey for MyCustomKeyType { +/// fn into_extended_key(self) -> Result, KeyError> { +/// let xprv = bip32::Xpriv { +/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here +/// depth: 0, +/// parent_fingerprint: bip32::Fingerprint::default(), +/// private_key: self.key_data.inner, +/// chain_code: bip32::ChainCode::from(&self.chain_code), +/// child_number: bip32::ChildNumber::Normal { index: 0 }, +/// }; +/// +/// xprv.into_extended_key() +/// } +/// +/// fn into_descriptor_key( +/// self, +/// source: Option, +/// derivation_path: bip32::DerivationPath, +/// ) -> Result, KeyError> { +/// let descriptor_key = self +/// .into_extended_key()? +/// .into_descriptor_key(source, derivation_path)?; +/// +/// // Override the set of valid networks here +/// Ok(descriptor_key.override_valid_networks(any_network())) +/// } +/// } +/// ``` +/// +/// [`DerivationPath`]: (bip32::DerivationPath) +/// [`Xpriv`]: (bip32::Xpriv) +/// [`Xpub`]: (bip32::Xpub) +pub trait DerivableKey: Sized { + /// Consume `self` and turn it into an [`ExtendedKey`] + #[cfg_attr( + feature = "keys-bip39", + doc = r##" +This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait, +like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled. +```rust +use bdk_wallet::bitcoin::Network; +use bdk_wallet::keys::{DerivableKey, ExtendedKey}; +use bdk_wallet::keys::bip39::{Mnemonic, Language}; + +# fn main() -> Result<(), Box> { +let xkey: ExtendedKey = + Mnemonic::parse_in( + Language::English, + "jelly crash boy whisper mouse ecology tuna soccer memory million news short", + )? + .into_extended_key()?; +let xprv = xkey.into_xprv(Network::Bitcoin).unwrap(); +# Ok(()) } +``` +"## + )] + fn into_extended_key(self) -> Result, KeyError>; + + /// Consume `self` and turn it into a [`DescriptorKey`] by adding the extra metadata, such as + /// key origin and derivation path + fn into_descriptor_key( + self, + origin: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + match self.into_extended_key()? { + ExtendedKey::Private((xprv, _)) => DescriptorSecretKey::XPrv(DescriptorXKey { + origin, + xkey: xprv, + derivation_path, + wildcard: Wildcard::Unhardened, + }) + .into_descriptor_key(), + ExtendedKey::Public((xpub, _)) => DescriptorPublicKey::XPub(DescriptorXKey { + origin, + xkey: xpub, + derivation_path, + wildcard: Wildcard::Unhardened, + }) + .into_descriptor_key(), + } + } +} + +/// Identity conversion +impl DerivableKey for ExtendedKey { + fn into_extended_key(self) -> Result, KeyError> { + Ok(self) + } +} + +impl DerivableKey for bip32::Xpub { + fn into_extended_key(self) -> Result, KeyError> { + Ok(self.into()) + } +} + +impl DerivableKey for bip32::Xpriv { + fn into_extended_key(self) -> Result, KeyError> { + Ok(self.into()) + } +} + +/// Output of a [`GeneratableKey`] key generation +pub struct GeneratedKey { + key: K, + valid_networks: ValidNetworks, + phantom: PhantomData, +} + +impl GeneratedKey { + fn new(key: K, valid_networks: ValidNetworks) -> Self { + GeneratedKey { + key, + valid_networks, + phantom: PhantomData, + } + } + + /// Consumes `self` and returns the key + pub fn into_key(self) -> K { + self.key + } +} + +impl Deref for GeneratedKey { + type Target = K; + + fn deref(&self) -> &Self::Target { + &self.key + } +} + +impl Clone for GeneratedKey { + fn clone(&self) -> GeneratedKey { + GeneratedKey { + key: self.key.clone(), + valid_networks: self.valid_networks.clone(), + phantom: self.phantom, + } + } +} + +// Make generated "derivable" keys themselves "derivable". Also make sure they are assigned the +// right `valid_networks`. +impl DerivableKey for GeneratedKey +where + Ctx: ScriptContext, + K: DerivableKey, +{ + fn into_extended_key(self) -> Result, KeyError> { + self.key.into_extended_key() + } + + fn into_descriptor_key( + self, + origin: Option, + derivation_path: bip32::DerivationPath, + ) -> Result, KeyError> { + let descriptor_key = self.key.into_descriptor_key(origin, derivation_path)?; + Ok(descriptor_key.override_valid_networks(self.valid_networks)) + } +} + +// Make generated keys directly usable in descriptors, and make sure they get assigned the right +// `valid_networks`. +impl IntoDescriptorKey for GeneratedKey +where + Ctx: ScriptContext, + K: IntoDescriptorKey, +{ + fn into_descriptor_key(self) -> Result, KeyError> { + let desc_key = self.key.into_descriptor_key()?; + Ok(desc_key.override_valid_networks(self.valid_networks)) + } +} + +/// Trait for keys that can be generated +/// +/// The same rules about [`ScriptContext`] and [`ValidNetworks`] from [`IntoDescriptorKey`] apply. +/// +/// This trait is particularly useful when combined with [`DerivableKey`]: if `Self` +/// implements it, the returned [`GeneratedKey`] will also implement it. The same is true for +/// [`IntoDescriptorKey`]: the generated keys can be directly used in descriptors if `Self` is also +/// [`IntoDescriptorKey`]. +pub trait GeneratableKey: Sized { + /// Type specifying the amount of entropy required e.g. `[u8;32]` + type Entropy: AsMut<[u8]> + Default; + + /// Extra options required by the `generate_with_entropy` + type Options; + /// Returned error in case of failure + type Error: core::fmt::Debug; + + /// Generate a key given the extra options and the entropy + fn generate_with_entropy( + options: Self::Options, + entropy: Self::Entropy, + ) -> Result, Self::Error>; + + /// Generate a key given the options with a random entropy + fn generate(options: Self::Options) -> Result, Self::Error> { + use rand::{thread_rng, Rng}; + + let mut entropy = Self::Entropy::default(); + thread_rng().fill(entropy.as_mut()); + Self::generate_with_entropy(options, entropy) + } +} + +/// Trait that allows generating a key with the default options +/// +/// This trait is automatically implemented if the [`GeneratableKey::Options`] implements [`Default`]. +pub trait GeneratableDefaultOptions: GeneratableKey +where + Ctx: ScriptContext, + >::Options: Default, +{ + /// Generate a key with the default options and a given entropy + fn generate_with_entropy_default( + entropy: Self::Entropy, + ) -> Result, Self::Error> { + Self::generate_with_entropy(Default::default(), entropy) + } + + /// Generate a key with the default options and a random entropy + fn generate_default() -> Result, Self::Error> { + Self::generate(Default::default()) + } +} + +/// Automatic implementation of [`GeneratableDefaultOptions`] for [`GeneratableKey`]s where +/// `Options` implements `Default` +impl GeneratableDefaultOptions for K +where + Ctx: ScriptContext, + K: GeneratableKey, + >::Options: Default, +{ +} + +impl GeneratableKey for bip32::Xpriv { + type Entropy = [u8; 32]; + + type Options = (); + type Error = bip32::Error; + + fn generate_with_entropy( + _: Self::Options, + entropy: Self::Entropy, + ) -> Result, Self::Error> { + // pick a arbitrary network here, but say that we support all of them + let xprv = bip32::Xpriv::new_master(Network::Bitcoin, entropy.as_ref())?; + Ok(GeneratedKey::new(xprv, any_network())) + } +} + +/// Options for generating a [`PrivateKey`] +/// +/// Defaults to creating compressed keys, which save on-chain bytes and fees +#[derive(Debug, Copy, Clone)] +pub struct PrivateKeyGenerateOptions { + /// Whether the generated key should be "compressed" or not + pub compressed: bool, +} + +impl Default for PrivateKeyGenerateOptions { + fn default() -> Self { + PrivateKeyGenerateOptions { compressed: true } + } +} + +impl GeneratableKey for PrivateKey { + type Entropy = [u8; secp256k1::constants::SECRET_KEY_SIZE]; + + type Options = PrivateKeyGenerateOptions; + type Error = bip32::Error; + + fn generate_with_entropy( + options: Self::Options, + entropy: Self::Entropy, + ) -> Result, Self::Error> { + // pick a arbitrary network here, but say that we support all of them + let inner = secp256k1::SecretKey::from_slice(&entropy)?; + let private_key = PrivateKey { + compressed: options.compressed, + network: Network::Bitcoin, + inner, + }; + + Ok(GeneratedKey::new(private_key, any_network())) + } +} + +impl> IntoDescriptorKey + for (T, bip32::DerivationPath) +{ + fn into_descriptor_key(self) -> Result, KeyError> { + self.0.into_descriptor_key(None, self.1) + } +} + +impl> IntoDescriptorKey + for (T, bip32::KeySource, bip32::DerivationPath) +{ + fn into_descriptor_key(self) -> Result, KeyError> { + self.0.into_descriptor_key(Some(self.1), self.2) + } +} + +fn expand_multi_keys, Ctx: ScriptContext>( + pks: Vec, + secp: &SecpCtx, +) -> Result<(Vec, KeyMap, ValidNetworks), KeyError> { + let (pks, key_maps_networks): (Vec<_>, Vec<_>) = pks + .into_iter() + .map(|key| key.into_descriptor_key()?.extract(secp)) + .collect::, _>>()? + .into_iter() + .map(|(a, b, c)| (a, (b, c))) + .unzip(); + + let (key_map, valid_networks) = key_maps_networks.into_iter().fold( + (KeyMap::default(), any_network()), + |(mut keys_acc, net_acc), (key, net)| { + keys_acc.extend(key); + let net_acc = merge_networks(&net_acc, &net); + + (keys_acc, net_acc) + }, + ); + + Ok((pks, key_map, valid_networks)) +} + +// Used internally by `bdk_wallet::fragment!` to build `pk_k()` fragments +#[doc(hidden)] +pub fn make_pk, Ctx: ScriptContext>( + descriptor_key: Pk, + secp: &SecpCtx, +) -> Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError> { + let (key, key_map, valid_networks) = descriptor_key.into_descriptor_key()?.extract(secp)?; + let minisc = Miniscript::from_ast(Terminal::PkK(key))?; + + minisc.check_miniscript()?; + + Ok((minisc, key_map, valid_networks)) +} + +// Used internally by `bdk_wallet::fragment!` to build `pk_h()` fragments +#[doc(hidden)] +pub fn make_pkh, Ctx: ScriptContext>( + descriptor_key: Pk, + secp: &SecpCtx, +) -> Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError> { + let (key, key_map, valid_networks) = descriptor_key.into_descriptor_key()?.extract(secp)?; + let minisc = Miniscript::from_ast(Terminal::PkH(key))?; + + minisc.check_miniscript()?; + + Ok((minisc, key_map, valid_networks)) +} + +// Used internally by `bdk_wallet::fragment!` to build `multi()` fragments +#[doc(hidden)] +pub fn make_multi< + Pk: IntoDescriptorKey, + Ctx: ScriptContext, + V: Fn(usize, Vec) -> Terminal, +>( + thresh: usize, + variant: V, + pks: Vec, + secp: &SecpCtx, +) -> Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError> { + let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?; + let minisc = Miniscript::from_ast(variant(thresh, pks))?; + + minisc.check_miniscript()?; + + Ok((minisc, key_map, valid_networks)) +} + +// Used internally by `bdk_wallet::descriptor!` to build `sortedmulti()` fragments +#[doc(hidden)] +pub fn make_sortedmulti( + thresh: usize, + pks: Vec, + build_desc: F, + secp: &SecpCtx, +) -> Result<(Descriptor, KeyMap, ValidNetworks), DescriptorError> +where + Pk: IntoDescriptorKey, + Ctx: ScriptContext, + F: Fn( + usize, + Vec, + ) -> Result<(Descriptor, PhantomData), DescriptorError>, +{ + let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?; + let descriptor = build_desc(thresh, pks)?.0; + + Ok((descriptor, key_map, valid_networks)) +} + +/// The "identity" conversion is used internally by some `bdk_wallet::fragment`s +impl IntoDescriptorKey for DescriptorKey { + fn into_descriptor_key(self) -> Result, KeyError> { + Ok(self) + } +} + +impl IntoDescriptorKey for DescriptorPublicKey { + fn into_descriptor_key(self) -> Result, KeyError> { + let networks = match self { + DescriptorPublicKey::Single(_) => any_network(), + DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. }) + if xkey.network == Network::Bitcoin => + { + mainnet_network() + } + _ => test_networks(), + }; + + Ok(DescriptorKey::from_public(self, networks)) + } +} + +impl IntoDescriptorKey for PublicKey { + fn into_descriptor_key(self) -> Result, KeyError> { + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::FullKey(self), + origin: None, + }) + .into_descriptor_key() + } +} + +impl IntoDescriptorKey for XOnlyPublicKey { + fn into_descriptor_key(self) -> Result, KeyError> { + DescriptorPublicKey::Single(SinglePub { + key: SinglePubKey::XOnly(self), + origin: None, + }) + .into_descriptor_key() + } +} + +impl IntoDescriptorKey for DescriptorSecretKey { + fn into_descriptor_key(self) -> Result, KeyError> { + let networks = match &self { + DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => { + mainnet_network() + } + DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. }) + if xkey.network == Network::Bitcoin => + { + mainnet_network() + } + _ => test_networks(), + }; + + Ok(DescriptorKey::from_secret(self, networks)) + } +} + +impl IntoDescriptorKey for &'_ str { + fn into_descriptor_key(self) -> Result, KeyError> { + DescriptorSecretKey::from_str(self) + .map_err(|e| KeyError::Message(e.to_string()))? + .into_descriptor_key() + } +} + +impl IntoDescriptorKey for PrivateKey { + fn into_descriptor_key(self) -> Result, KeyError> { + DescriptorSecretKey::Single(SinglePriv { + key: self, + origin: None, + }) + .into_descriptor_key() + } +} + +/// Errors thrown while working with [`keys`](crate::keys) +#[derive(Debug)] +pub enum KeyError { + /// The key cannot exist in the given script context + InvalidScriptContext, + /// The key is not valid for the given network + InvalidNetwork, + /// The key has an invalid checksum + InvalidChecksum, + + /// Custom error message + Message(String), + + /// BIP32 error + Bip32(bitcoin::bip32::Error), + /// Miniscript error + Miniscript(miniscript::Error), +} + +impl From for KeyError { + fn from(err: miniscript::Error) -> Self { + KeyError::Miniscript(err) + } +} + +impl From for KeyError { + fn from(err: bip32::Error) -> Self { + KeyError::Bip32(err) + } +} + +impl fmt::Display for KeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidScriptContext => write!(f, "Invalid script context"), + Self::InvalidNetwork => write!(f, "Invalid network"), + Self::InvalidChecksum => write!(f, "Invalid checksum"), + Self::Message(err) => write!(f, "{}", err), + Self::Bip32(err) => write!(f, "BIP32 error: {}", err), + Self::Miniscript(err) => write!(f, "Miniscript error: {}", err), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for KeyError {} + +#[cfg(test)] +pub mod test { + use bitcoin::bip32; + + use super::*; + + pub const TEST_ENTROPY: [u8; 32] = [0xAA; 32]; + + #[test] + fn test_keys_generate_xprv() { + let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> = + bip32::Xpriv::generate_with_entropy_default(TEST_ENTROPY).unwrap(); + + assert_eq!(generated_xprv.valid_networks, any_network()); + assert_eq!(generated_xprv.to_string(), "xprv9s21ZrQH143K4Xr1cJyqTvuL2FWR8eicgY9boWqMBv8MDVUZ65AXHnzBrK1nyomu6wdcabRgmGTaAKawvhAno1V5FowGpTLVx3jxzE5uk3Q"); + } + + #[test] + fn test_keys_generate_wif() { + let generated_wif: GeneratedKey<_, miniscript::Segwitv0> = + bitcoin::PrivateKey::generate_with_entropy_default(TEST_ENTROPY).unwrap(); + + assert_eq!(generated_wif.valid_networks, any_network()); + assert_eq!( + generated_wif.to_string(), + "L2wTu6hQrnDMiFNWA5na6jB12ErGQqtXwqpSL7aWquJaZG8Ai3ch" + ); + } + + #[cfg(feature = "keys-bip39")] + #[test] + fn test_keys_wif_network_bip39() { + let xkey: ExtendedKey = bip39::Mnemonic::parse_in( + bip39::Language::English, + "jelly crash boy whisper mouse ecology tuna soccer memory million news short", + ) + .unwrap() + .into_extended_key() + .unwrap(); + let xprv = xkey.into_xprv(Network::Testnet).unwrap(); + + assert_eq!(xprv.network, Network::Testnet); + } +} diff --git a/crates/wallet/src/lib.rs b/crates/wallet/src/lib.rs new file mode 100644 index 00000000..f7c6f354 --- /dev/null +++ b/crates/wallet/src/lib.rs @@ -0,0 +1,47 @@ +#![doc = include_str!("../README.md")] +// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr( + docsrs, + doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png") +)] +#![no_std] +#![warn(missing_docs)] + +#[cfg(feature = "std")] +#[macro_use] +extern crate std; + +#[doc(hidden)] +#[macro_use] +pub extern crate alloc; + +pub extern crate bitcoin; +pub extern crate miniscript; +extern crate serde; +extern crate serde_json; + +#[cfg(feature = "keys-bip39")] +extern crate bip39; + +pub mod descriptor; +pub mod keys; +pub mod psbt; +pub(crate) mod types; +pub mod wallet; + +pub use descriptor::template; +pub use descriptor::HdKeyPaths; +pub use types::*; +pub use wallet::signer; +pub use wallet::signer::SignOptions; +pub use wallet::tx_builder::TxBuilder; +pub use wallet::Wallet; + +/// Get the version of BDK at runtime +pub fn version() -> &'static str { + env!("CARGO_PKG_VERSION", "unknown") +} + +pub use bdk_chain as chain; +pub(crate) use bdk_chain::collections; diff --git a/crates/wallet/src/psbt/mod.rs b/crates/wallet/src/psbt/mod.rs new file mode 100644 index 00000000..7a66989e --- /dev/null +++ b/crates/wallet/src/psbt/mod.rs @@ -0,0 +1,75 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Additional functions on the `rust-bitcoin` `Psbt` structure. + +use alloc::vec::Vec; +use bitcoin::Amount; +use bitcoin::FeeRate; +use bitcoin::Psbt; +use bitcoin::TxOut; + +// TODO upstream the functions here to `rust-bitcoin`? + +/// Trait to add functions to extract utxos and calculate fees. +pub trait PsbtUtils { + /// Get the `TxOut` for the specified input index, if it doesn't exist in the PSBT `None` is returned. + fn get_utxo_for(&self, input_index: usize) -> Option; + + /// The total transaction fee amount, sum of input amounts minus sum of output amounts, in sats. + /// If the PSBT is missing a TxOut for an input returns None. + fn fee_amount(&self) -> Option; + + /// The transaction's fee rate. This value will only be accurate if calculated AFTER the + /// `Psbt` is finalized and all witness/signature data is added to the + /// transaction. + /// If the PSBT is missing a TxOut for an input returns None. + fn fee_rate(&self) -> Option; +} + +impl PsbtUtils for Psbt { + fn get_utxo_for(&self, input_index: usize) -> Option { + let tx = &self.unsigned_tx; + let input = self.inputs.get(input_index)?; + + match (&input.witness_utxo, &input.non_witness_utxo) { + (Some(_), _) => input.witness_utxo.clone(), + (_, Some(_)) => input.non_witness_utxo.as_ref().map(|in_tx| { + in_tx.output[tx.input[input_index].previous_output.vout as usize].clone() + }), + _ => None, + } + } + + fn fee_amount(&self) -> Option { + let tx = &self.unsigned_tx; + let utxos: Option> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect(); + + utxos.map(|inputs| { + let input_amount: u64 = inputs.iter().map(|i| i.value.to_sat()).sum(); + let output_amount: u64 = self + .unsigned_tx + .output + .iter() + .map(|o| o.value.to_sat()) + .sum(); + input_amount + .checked_sub(output_amount) + .expect("input amount must be greater than output amount") + }) + } + + fn fee_rate(&self) -> Option { + let fee_amount = self.fee_amount(); + let weight = self.clone().extract_tx().ok()?.weight(); + fee_amount.map(|fee| Amount::from_sat(fee) / weight) + } +} diff --git a/crates/wallet/src/types.rs b/crates/wallet/src/types.rs new file mode 100644 index 00000000..4ce961b7 --- /dev/null +++ b/crates/wallet/src/types.rs @@ -0,0 +1,135 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use alloc::boxed::Box; +use core::convert::AsRef; + +use bdk_chain::ConfirmationTime; +use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut}; +use bitcoin::psbt; + +use serde::{Deserialize, Serialize}; + +/// Types of keychains +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub enum KeychainKind { + /// External keychain, used for deriving recipient addresses. + External = 0, + /// Internal keychain, used for deriving change addresses. + Internal = 1, +} + +impl KeychainKind { + /// Return [`KeychainKind`] as a byte + pub fn as_byte(&self) -> u8 { + match self { + KeychainKind::External => b'e', + KeychainKind::Internal => b'i', + } + } +} + +impl AsRef<[u8]> for KeychainKind { + fn as_ref(&self) -> &[u8] { + match self { + KeychainKind::External => b"e", + KeychainKind::Internal => b"i", + } + } +} + +/// An unspent output owned by a [`Wallet`]. +/// +/// [`Wallet`]: crate::Wallet +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +pub struct LocalOutput { + /// Reference to a transaction output + pub outpoint: OutPoint, + /// Transaction output + pub txout: TxOut, + /// Type of keychain + pub keychain: KeychainKind, + /// Whether this UTXO is spent or not + pub is_spent: bool, + /// The derivation index for the script pubkey in the wallet + pub derivation_index: u32, + /// The confirmation time for transaction containing this utxo + pub confirmation_time: ConfirmationTime, +} + +/// A [`Utxo`] with its `satisfaction_weight`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WeightedUtxo { + /// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to + /// properly maintain the feerate when adding this input to a transaction during coin selection. + /// + /// [weight units]: https://en.bitcoin.it/wiki/Weight_units + pub satisfaction_weight: usize, + /// The UTXO + pub utxo: Utxo, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// An unspent transaction output (UTXO). +pub enum Utxo { + /// A UTXO owned by the local wallet. + Local(LocalOutput), + /// A UTXO owned by another wallet. + Foreign { + /// The location of the output. + outpoint: OutPoint, + /// The nSequence value to set for this input. + sequence: Option, + /// The information about the input we require to add it to a PSBT. + // Box it to stop the type being too big. + psbt_input: Box, + }, +} + +impl Utxo { + /// Get the location of the UTXO + pub fn outpoint(&self) -> OutPoint { + match &self { + Utxo::Local(local) => local.outpoint, + Utxo::Foreign { outpoint, .. } => *outpoint, + } + } + + /// Get the `TxOut` of the UTXO + pub fn txout(&self) -> &TxOut { + match &self { + Utxo::Local(local) => &local.txout, + Utxo::Foreign { + outpoint, + psbt_input, + .. + } => { + if let Some(prev_tx) = &psbt_input.non_witness_utxo { + return &prev_tx.output[outpoint.vout as usize]; + } + + if let Some(txout) = &psbt_input.witness_utxo { + return txout; + } + + unreachable!("Foreign UTXOs will always have one of these set") + } + } + } + + /// Get the sequence number if an explicit sequence number has to be set for this input. + pub fn sequence(&self) -> Option { + match self { + Utxo::Local(_) => None, + Utxo::Foreign { sequence, .. } => *sequence, + } + } +} diff --git a/crates/wallet/src/wallet/coin_selection.rs b/crates/wallet/src/wallet/coin_selection.rs new file mode 100644 index 00000000..5df64796 --- /dev/null +++ b/crates/wallet/src/wallet/coin_selection.rs @@ -0,0 +1,1602 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Coin selection +//! +//! This module provides the trait [`CoinSelectionAlgorithm`] that can be implemented to +//! define custom coin selection algorithms. +//! +//! You can specify a custom coin selection algorithm through the [`coin_selection`] method on +//! [`TxBuilder`]. [`DefaultCoinSelectionAlgorithm`] aliases the coin selection algorithm that will +//! be used if it is not explicitly set. +//! +//! [`TxBuilder`]: super::tx_builder::TxBuilder +//! [`coin_selection`]: super::tx_builder::TxBuilder::coin_selection +//! +//! ## Example +//! +//! ``` +//! # use std::str::FromStr; +//! # use bitcoin::*; +//! # use bdk_wallet::wallet::{self, ChangeSet, coin_selection::*, coin_selection}; +//! # use bdk_wallet::wallet::error::CreateTxError; +//! # use bdk_persist::PersistBackend; +//! # use bdk_wallet::*; +//! # use bdk_wallet::wallet::coin_selection::decide_change; +//! # use anyhow::Error; +//! #[derive(Debug)] +//! struct AlwaysSpendEverything; +//! +//! impl CoinSelectionAlgorithm for AlwaysSpendEverything { +//! fn coin_select( +//! &self, +//! required_utxos: Vec, +//! optional_utxos: Vec, +//! fee_rate: FeeRate, +//! target_amount: u64, +//! drain_script: &Script, +//! ) -> Result { +//! let mut selected_amount = 0; +//! let mut additional_weight = Weight::ZERO; +//! let all_utxos_selected = required_utxos +//! .into_iter() +//! .chain(optional_utxos) +//! .scan( +//! (&mut selected_amount, &mut additional_weight), +//! |(selected_amount, additional_weight), weighted_utxo| { +//! **selected_amount += weighted_utxo.utxo.txout().value.to_sat(); +//! **additional_weight += Weight::from_wu( +//! (TxIn::default().segwit_weight().to_wu() +//! + weighted_utxo.satisfaction_weight as u64) +//! as u64, +//! ); +//! Some(weighted_utxo.utxo) +//! }, +//! ) +//! .collect::>(); +//! let additional_fees = (fee_rate * additional_weight).to_sat(); +//! let amount_needed_with_fees = additional_fees + target_amount; +//! if selected_amount < amount_needed_with_fees { +//! return Err(coin_selection::Error::InsufficientFunds { +//! needed: amount_needed_with_fees, +//! available: selected_amount, +//! }); +//! } +//! +//! let remaining_amount = selected_amount - amount_needed_with_fees; +//! +//! let excess = decide_change(remaining_amount, fee_rate, drain_script); +//! +//! Ok(CoinSelectionResult { +//! selected: all_utxos_selected, +//! fee_amount: additional_fees, +//! excess, +//! }) +//! } +//! } +//! +//! # let mut wallet = doctest_wallet!(); +//! // create wallet, sync, ... +//! +//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") +//! .unwrap() +//! .require_network(Network::Testnet) +//! .unwrap(); +//! let psbt = { +//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything); +//! builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); +//! builder.finish()? +//! }; +//! +//! // inspect, sign, broadcast, ... +//! +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +use crate::chain::collections::HashSet; +use crate::wallet::utils::IsDust; +use crate::Utxo; +use crate::WeightedUtxo; +use bitcoin::FeeRate; + +use alloc::vec::Vec; +use bitcoin::consensus::encode::serialize; +use bitcoin::OutPoint; +use bitcoin::TxIn; +use bitcoin::{Script, Weight}; + +use core::convert::TryInto; +use core::fmt::{self, Formatter}; +use rand::seq::SliceRandom; + +/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not +/// overridden +pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection; + +/// Errors that can be thrown by the [`coin_selection`](crate::wallet::coin_selection) module +#[derive(Debug)] +pub enum Error { + /// Wallet's UTXO set is not enough to cover recipient's requested plus fee + InsufficientFunds { + /// Sats needed for some transaction + needed: u64, + /// Sats available for spending + available: u64, + }, + /// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for + /// the desired outputs plus fee, if there is not such combination this error is thrown + BnBNoExactMatch, + /// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow + /// exponentially, thus a limit is set, and when hit, this error is thrown + BnBTotalTriesExceeded, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::InsufficientFunds { needed, available } => write!( + f, + "Insufficient funds: {} sat available of {} sat needed", + available, needed + ), + Self::BnBTotalTriesExceeded => { + write!(f, "Branch and bound coin selection: total tries exceeded") + } + Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + +#[derive(Debug)] +/// Remaining amount after performing coin selection +pub enum Excess { + /// It's not possible to create spendable output from excess using the current drain output + NoChange { + /// Threshold to consider amount as dust for this particular change script_pubkey + dust_threshold: u64, + /// Exceeding amount of current selection over outgoing value and fee costs + remaining_amount: u64, + /// The calculated fee for the drain TxOut with the selected script_pubkey + change_fee: u64, + }, + /// It's possible to create spendable output from excess using the current drain output + Change { + /// Effective amount available to create change after deducting the change output fee + amount: u64, + /// The deducted change output fee + fee: u64, + }, +} + +/// Result of a successful coin selection +#[derive(Debug)] +pub struct CoinSelectionResult { + /// List of outputs selected for use as inputs + pub selected: Vec, + /// Total fee amount for the selected utxos in satoshis + pub fee_amount: u64, + /// Remaining amount after deducing fees and outgoing outputs + pub excess: Excess, +} + +impl CoinSelectionResult { + /// The total value of the inputs selected. + pub fn selected_amount(&self) -> u64 { + self.selected.iter().map(|u| u.txout().value.to_sat()).sum() + } + + /// The total value of the inputs selected from the local wallet. + pub fn local_selected_amount(&self) -> u64 { + self.selected + .iter() + .filter_map(|u| match u { + Utxo::Local(_) => Some(u.txout().value.to_sat()), + _ => None, + }) + .sum() + } +} + +/// Trait for generalized coin selection algorithms +/// +/// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin +/// selection algorithm when it creates transactions. +/// +/// For an example see [this module](crate::wallet::coin_selection)'s documentation. +pub trait CoinSelectionAlgorithm: core::fmt::Debug { + /// Perform the coin selection + /// + /// - `database`: a reference to the wallet's database that can be used to lookup additional + /// details for a specific UTXO + /// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their + /// weight cost + /// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their + /// weight cost + /// - `fee_rate`: fee rate to use + /// - `target_amount`: the outgoing amount in satoshis and the fees already + /// accumulated from added outputs and transaction’s header. + /// - `drain_script`: the script to use in case of change + #[allow(clippy::too_many_arguments)] + fn coin_select( + &self, + required_utxos: Vec, + optional_utxos: Vec, + fee_rate: FeeRate, + target_amount: u64, + drain_script: &Script, + ) -> Result; +} + +/// Simple and dumb coin selection +/// +/// This coin selection algorithm sorts the available UTXOs by value and then picks them starting +/// from the largest ones until the required amount is reached. +#[derive(Debug, Default, Clone, Copy)] +pub struct LargestFirstCoinSelection; + +impl CoinSelectionAlgorithm for LargestFirstCoinSelection { + fn coin_select( + &self, + required_utxos: Vec, + mut optional_utxos: Vec, + fee_rate: FeeRate, + target_amount: u64, + drain_script: &Script, + ) -> Result { + // We put the "required UTXOs" first and make sure the optional UTXOs are sorted, + // initially smallest to largest, before being reversed with `.rev()`. + let utxos = { + optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value); + required_utxos + .into_iter() + .map(|utxo| (true, utxo)) + .chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo))) + }; + + select_sorted_utxos(utxos, fee_rate, target_amount, drain_script) + } +} + +/// OldestFirstCoinSelection always picks the utxo with the smallest blockheight to add to the selected coins next +/// +/// This coin selection algorithm sorts the available UTXOs by blockheight and then picks them starting +/// from the oldest ones until the required amount is reached. +#[derive(Debug, Default, Clone, Copy)] +pub struct OldestFirstCoinSelection; + +impl CoinSelectionAlgorithm for OldestFirstCoinSelection { + fn coin_select( + &self, + required_utxos: Vec, + mut optional_utxos: Vec, + fee_rate: FeeRate, + target_amount: u64, + drain_script: &Script, + ) -> Result { + // We put the "required UTXOs" first and make sure the optional UTXOs are sorted from + // oldest to newest according to blocktime + // For utxo that doesn't exist in DB, they will have lowest priority to be selected + let utxos = { + optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo { + Utxo::Local(local) => Some(local.confirmation_time), + Utxo::Foreign { .. } => None, + }); + + required_utxos + .into_iter() + .map(|utxo| (true, utxo)) + .chain(optional_utxos.into_iter().map(|utxo| (false, utxo))) + }; + + select_sorted_utxos(utxos, fee_rate, target_amount, drain_script) + } +} + +/// Decide if change can be created +/// +/// - `remaining_amount`: the amount in which the selected coins exceed the target amount +/// - `fee_rate`: required fee rate for the current selection +/// - `drain_script`: script to consider change creation +pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess { + // drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value) + let drain_output_len = serialize(drain_script).len() + 8usize; + let change_fee = + (fee_rate * Weight::from_vb(drain_output_len as u64).expect("overflow occurred")).to_sat(); + let drain_val = remaining_amount.saturating_sub(change_fee); + + if drain_val.is_dust(drain_script) { + let dust_threshold = drain_script.dust_value().to_sat(); + Excess::NoChange { + dust_threshold, + change_fee, + remaining_amount, + } + } else { + Excess::Change { + amount: drain_val, + fee: change_fee, + } + } +} + +fn select_sorted_utxos( + utxos: impl Iterator, + fee_rate: FeeRate, + target_amount: u64, + drain_script: &Script, +) -> Result { + let mut selected_amount = 0; + let mut fee_amount = 0; + let selected = utxos + .scan( + (&mut selected_amount, &mut fee_amount), + |(selected_amount, fee_amount), (must_use, weighted_utxo)| { + if must_use || **selected_amount < target_amount + **fee_amount { + **fee_amount += (fee_rate + * Weight::from_wu( + TxIn::default().segwit_weight().to_wu() + + weighted_utxo.satisfaction_weight as u64, + )) + .to_sat(); + **selected_amount += weighted_utxo.utxo.txout().value.to_sat(); + Some(weighted_utxo.utxo) + } else { + None + } + }, + ) + .collect::>(); + + let amount_needed_with_fees = target_amount + fee_amount; + if selected_amount < amount_needed_with_fees { + return Err(Error::InsufficientFunds { + needed: amount_needed_with_fees, + available: selected_amount, + }); + } + + let remaining_amount = selected_amount - amount_needed_with_fees; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); + + Ok(CoinSelectionResult { + selected, + fee_amount, + excess, + }) +} + +#[derive(Debug, Clone)] +// Adds fee information to an UTXO. +struct OutputGroup { + weighted_utxo: WeightedUtxo, + // Amount of fees for spending a certain utxo, calculated using a certain FeeRate + fee: u64, + // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it + effective_value: i64, +} + +impl OutputGroup { + fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self { + let fee = (fee_rate + * Weight::from_wu( + TxIn::default().segwit_weight().to_wu() + weighted_utxo.satisfaction_weight as u64, + )) + .to_sat(); + let effective_value = weighted_utxo.utxo.txout().value.to_sat() as i64 - fee as i64; + OutputGroup { + weighted_utxo, + fee, + effective_value, + } + } +} + +/// Branch and bound coin selection +/// +/// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: +#[derive(Debug, Clone)] +pub struct BranchAndBoundCoinSelection { + size_of_change: u64, +} + +impl Default for BranchAndBoundCoinSelection { + fn default() -> Self { + Self { + // P2WPKH cost of change -> value (8 bytes) + script len (1 bytes) + script (22 bytes) + size_of_change: 8 + 1 + 22, + } + } +} + +impl BranchAndBoundCoinSelection { + /// Create new instance with target size for change output + pub fn new(size_of_change: u64) -> Self { + Self { size_of_change } + } +} + +const BNB_TOTAL_TRIES: usize = 100_000; + +impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { + fn coin_select( + &self, + required_utxos: Vec, + optional_utxos: Vec, + fee_rate: FeeRate, + target_amount: u64, + drain_script: &Script, + ) -> Result { + // Mapping every (UTXO, usize) to an output group + let required_utxos: Vec = required_utxos + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + // Mapping every (UTXO, usize) to an output group, filtering UTXOs with a negative + // effective value + let optional_utxos: Vec = optional_utxos + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .filter(|u| u.effective_value.is_positive()) + .collect(); + + let curr_value = required_utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value); + + let curr_available_value = optional_utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value); + + let cost_of_change = + (Weight::from_vb(self.size_of_change).expect("overflow occurred") * fee_rate).to_sat(); + + // `curr_value` and `curr_available_value` are both the sum of *effective_values* of + // the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with + // negative effective value, so it will always be positive. + // + // Since we are required to spend the required UTXOs (curr_value) we have to consider + // all their effective values, even when negative, which means that curr_value could + // be negative as well. + // + // If the sum of curr_value and curr_available_value is negative or lower than our target, + // we can immediately exit with an error, as it's guaranteed we will never find a solution + // if we actually run the BnB. + let total_value: Result = (curr_available_value + curr_value).try_into(); + match total_value { + Ok(v) if v >= target_amount => {} + _ => { + // Assume we spend all the UTXOs we can (all the required + all the optional with + // positive effective value), sum their value and their fee cost. + let (utxo_fees, utxo_value) = required_utxos + .iter() + .chain(optional_utxos.iter()) + .fold((0, 0), |(mut fees, mut value), utxo| { + fees += utxo.fee; + value += utxo.weighted_utxo.utxo.txout().value.to_sat(); + + (fees, value) + }); + + // Add to the target the fee cost of the UTXOs + return Err(Error::InsufficientFunds { + needed: target_amount + utxo_fees, + available: utxo_value, + }); + } + } + + let target_amount = target_amount + .try_into() + .expect("Bitcoin amount to fit into i64"); + + if curr_value > target_amount { + // remaining_amount can't be negative as that would mean the + // selection wasn't successful + // target_amount = amount_needed + (fee_amount - vin_fees) + let remaining_amount = (curr_value - target_amount) as u64; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); + + return Ok(BranchAndBoundCoinSelection::calculate_cs_result( + vec![], + required_utxos, + excess, + )); + } + + Ok(self + .bnb( + required_utxos.clone(), + optional_utxos.clone(), + curr_value, + curr_available_value, + target_amount, + cost_of_change, + drain_script, + fee_rate, + ) + .unwrap_or_else(|_| { + self.single_random_draw( + required_utxos, + optional_utxos, + curr_value, + target_amount, + drain_script, + fee_rate, + ) + })) + } +} + +impl BranchAndBoundCoinSelection { + // TODO: make this more Rust-onic :) + // (And perhaps refactor with less arguments?) + #[allow(clippy::too_many_arguments)] + fn bnb( + &self, + required_utxos: Vec, + mut optional_utxos: Vec, + mut curr_value: i64, + mut curr_available_value: i64, + target_amount: i64, + cost_of_change: u64, + drain_script: &Script, + fee_rate: FeeRate, + ) -> Result { + // current_selection[i] will contain true if we are using optional_utxos[i], + // false otherwise. Note that current_selection.len() could be less than + // optional_utxos.len(), it just means that we still haven't decided if we should keep + // certain optional_utxos or not. + let mut current_selection: Vec = Vec::with_capacity(optional_utxos.len()); + + // Sort the utxo_pool + optional_utxos.sort_unstable_by_key(|a| a.effective_value); + optional_utxos.reverse(); + + // Contains the best selection we found + let mut best_selection = Vec::new(); + let mut best_selection_value = None; + + // Depth First search loop for choosing the UTXOs + for _ in 0..BNB_TOTAL_TRIES { + // Conditions for starting a backtrack + let mut backtrack = false; + // Cannot possibly reach target with the amount remaining in the curr_available_value, + // or the selected value is out of range. + // Go back and try other branch + if curr_value + curr_available_value < target_amount + || curr_value > target_amount + cost_of_change as i64 + { + backtrack = true; + } else if curr_value >= target_amount { + // Selected value is within range, there's no point in going forward. Start + // backtracking + backtrack = true; + + // If we found a solution better than the previous one, or if there wasn't previous + // solution, update the best solution + if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() { + best_selection.clone_from(¤t_selection); + best_selection_value = Some(curr_value); + } + + // If we found a perfect match, break here + if curr_value == target_amount { + break; + } + } + + // Backtracking, moving backwards + if backtrack { + // Walk backwards to find the last included UTXO that still needs to have its omission branch traversed. + while let Some(false) = current_selection.last() { + current_selection.pop(); + curr_available_value += optional_utxos[current_selection.len()].effective_value; + } + + if current_selection.last_mut().is_none() { + // We have walked back to the first utxo and no branch is untraversed. All solutions searched + // If best selection is empty, then there's no exact match + if best_selection.is_empty() { + return Err(Error::BnBNoExactMatch); + } + break; + } + + if let Some(c) = current_selection.last_mut() { + // Output was included on previous iterations, try excluding now. + *c = false; + } + + let utxo = &optional_utxos[current_selection.len() - 1]; + curr_value -= utxo.effective_value; + } else { + // Moving forwards, continuing down this branch + let utxo = &optional_utxos[current_selection.len()]; + + // Remove this utxo from the curr_available_value utxo amount + curr_available_value -= utxo.effective_value; + + // Inclusion branch first (Largest First Exploration) + current_selection.push(true); + curr_value += utxo.effective_value; + } + } + + // Check for solution + if best_selection.is_empty() { + return Err(Error::BnBTotalTriesExceeded); + } + + // Set output set + let selected_utxos = optional_utxos + .into_iter() + .zip(best_selection) + .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None }) + .collect::>(); + + let selected_amount = best_selection_value.unwrap(); + + // remaining_amount can't be negative as that would mean the + // selection wasn't successful + // target_amount = amount_needed + (fee_amount - vin_fees) + let remaining_amount = (selected_amount - target_amount) as u64; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); + + Ok(BranchAndBoundCoinSelection::calculate_cs_result( + selected_utxos, + required_utxos, + excess, + )) + } + + #[allow(clippy::too_many_arguments)] + fn single_random_draw( + &self, + required_utxos: Vec, + mut optional_utxos: Vec, + curr_value: i64, + target_amount: i64, + drain_script: &Script, + fee_rate: FeeRate, + ) -> CoinSelectionResult { + optional_utxos.shuffle(&mut rand::thread_rng()); + let selected_utxos = optional_utxos.into_iter().fold( + (curr_value, vec![]), + |(mut amount, mut utxos), utxo| { + if amount >= target_amount { + (amount, utxos) + } else { + amount += utxo.effective_value; + utxos.push(utxo); + (amount, utxos) + } + }, + ); + + // remaining_amount can't be negative as that would mean the + // selection wasn't successful + // target_amount = amount_needed + (fee_amount - vin_fees) + let remaining_amount = (selected_utxos.0 - target_amount) as u64; + + let excess = decide_change(remaining_amount, fee_rate, drain_script); + + BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess) + } + + fn calculate_cs_result( + mut selected_utxos: Vec, + mut required_utxos: Vec, + excess: Excess, + ) -> CoinSelectionResult { + selected_utxos.append(&mut required_utxos); + let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::(); + let selected = selected_utxos + .into_iter() + .map(|u| u.weighted_utxo.utxo) + .collect::>(); + + CoinSelectionResult { + selected, + fee_amount, + excess, + } + } +} + +/// Remove duplicate UTXOs. +/// +/// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept. +pub(crate) fn filter_duplicates(required: I, optional: I) -> (I, I) +where + I: IntoIterator + FromIterator, +{ + let mut visited = HashSet::::new(); + let required = required + .into_iter() + .filter(|utxo| visited.insert(utxo.utxo.outpoint())) + .collect::(); + let optional = optional + .into_iter() + .filter(|utxo| visited.insert(utxo.utxo.outpoint())) + .collect::(); + (required, optional) +} + +#[cfg(test)] +mod test { + use assert_matches::assert_matches; + use core::str::FromStr; + + use bdk_chain::ConfirmationTime; + use bitcoin::{Amount, ScriptBuf, TxIn, TxOut}; + + use super::*; + use crate::types::*; + use crate::wallet::coin_selection::filter_duplicates; + + use rand::rngs::StdRng; + use rand::seq::SliceRandom; + use rand::{Rng, RngCore, SeedableRng}; + + // signature len (1WU) + signature and sighash (72WU) + // + pubkey len (1WU) + pubkey (33WU) + const P2WPKH_SATISFACTION_SIZE: usize = 1 + 72 + 1 + 33; + + const FEE_AMOUNT: u64 = 50; + + fn utxo(value: u64, index: u32, confirmation_time: ConfirmationTime) -> WeightedUtxo { + assert!(index < 10); + let outpoint = OutPoint::from_str(&format!( + "000000000000000000000000000000000000000000000000000000000000000{}:0", + index + )) + .unwrap(); + WeightedUtxo { + satisfaction_weight: P2WPKH_SATISFACTION_SIZE, + utxo: Utxo::Local(LocalOutput { + outpoint, + txout: TxOut { + value: Amount::from_sat(value), + script_pubkey: ScriptBuf::new(), + }, + keychain: KeychainKind::External, + is_spent: false, + derivation_index: 42, + confirmation_time, + }), + } + } + + fn get_test_utxos() -> Vec { + vec![ + utxo(100_000, 0, ConfirmationTime::Unconfirmed { last_seen: 0 }), + utxo( + FEE_AMOUNT - 40, + 1, + ConfirmationTime::Unconfirmed { last_seen: 0 }, + ), + utxo(200_000, 2, ConfirmationTime::Unconfirmed { last_seen: 0 }), + ] + } + + fn get_oldest_first_test_utxos() -> Vec { + // ensure utxos are from different tx + let utxo1 = utxo( + 120_000, + 1, + ConfirmationTime::Confirmed { + height: 1, + time: 1231006505, + }, + ); + let utxo2 = utxo( + 80_000, + 2, + ConfirmationTime::Confirmed { + height: 2, + time: 1231006505, + }, + ); + let utxo3 = utxo( + 300_000, + 3, + ConfirmationTime::Confirmed { + height: 3, + time: 1231006505, + }, + ); + vec![utxo1, utxo2, utxo3] + } + + fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec { + let mut res = Vec::new(); + for i in 0..utxos_number { + res.push(WeightedUtxo { + satisfaction_weight: P2WPKH_SATISFACTION_SIZE, + utxo: Utxo::Local(LocalOutput { + outpoint: OutPoint::from_str(&format!( + "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}", + i + )) + .unwrap(), + txout: TxOut { + value: Amount::from_sat(rng.gen_range(0..200000000)), + script_pubkey: ScriptBuf::new(), + }, + keychain: KeychainKind::External, + is_spent: false, + derivation_index: rng.next_u32(), + confirmation_time: if rng.gen_bool(0.5) { + ConfirmationTime::Confirmed { + height: rng.next_u32(), + time: rng.next_u64(), + } + } else { + ConfirmationTime::Unconfirmed { last_seen: 0 } + }, + }), + }); + } + res + } + + fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec { + (0..utxos_number) + .map(|i| WeightedUtxo { + satisfaction_weight: P2WPKH_SATISFACTION_SIZE, + utxo: Utxo::Local(LocalOutput { + outpoint: OutPoint::from_str(&format!( + "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}", + i + )) + .unwrap(), + txout: TxOut { + value: Amount::from_sat(utxos_value), + script_pubkey: ScriptBuf::new(), + }, + keychain: KeychainKind::External, + is_spent: false, + derivation_index: 42, + confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 }, + }), + }) + .collect() + } + + fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec) -> u64 { + let utxos_picked_len = rng.gen_range(2..utxos.len() / 2); + utxos.shuffle(&mut rng); + utxos[..utxos_picked_len] + .iter() + .map(|u| u.utxo.txout().value.to_sat()) + .sum() + } + + #[test] + fn test_largest_first_coin_selection_success() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 250_000 + FEE_AMOUNT; + + let result = LargestFirstCoinSelection + .coin_select( + utxos, + vec![], + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 3); + assert_eq!(result.selected_amount(), 300_010); + assert_eq!(result.fee_amount, 204) + } + + #[test] + fn test_largest_first_coin_selection_use_all() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 20_000 + FEE_AMOUNT; + + let result = LargestFirstCoinSelection + .coin_select( + utxos, + vec![], + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 3); + assert_eq!(result.selected_amount(), 300_010); + assert_eq!(result.fee_amount, 204); + } + + #[test] + fn test_largest_first_coin_selection_use_only_necessary() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 20_000 + FEE_AMOUNT; + + let result = LargestFirstCoinSelection + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 1); + assert_eq!(result.selected_amount(), 200_000); + assert_eq!(result.fee_amount, 68); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_largest_first_coin_selection_insufficient_funds() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 500_000 + FEE_AMOUNT; + + LargestFirstCoinSelection + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_largest_first_coin_selection_insufficient_funds_high_fees() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 250_000 + FEE_AMOUNT; + + LargestFirstCoinSelection + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1000), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + fn test_oldest_first_coin_selection_success() { + let utxos = get_oldest_first_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 180_000 + FEE_AMOUNT; + + let result = OldestFirstCoinSelection + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 2); + assert_eq!(result.selected_amount(), 200_000); + assert_eq!(result.fee_amount, 136) + } + + #[test] + fn test_oldest_first_coin_selection_use_all() { + let utxos = get_oldest_first_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 20_000 + FEE_AMOUNT; + + let result = OldestFirstCoinSelection + .coin_select( + utxos, + vec![], + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 3); + assert_eq!(result.selected_amount(), 500_000); + assert_eq!(result.fee_amount, 204); + } + + #[test] + fn test_oldest_first_coin_selection_use_only_necessary() { + let utxos = get_oldest_first_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 20_000 + FEE_AMOUNT; + + let result = OldestFirstCoinSelection + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 1); + assert_eq!(result.selected_amount(), 120_000); + assert_eq!(result.fee_amount, 68); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_oldest_first_coin_selection_insufficient_funds() { + let utxos = get_oldest_first_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 600_000 + FEE_AMOUNT; + + OldestFirstCoinSelection + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_oldest_first_coin_selection_insufficient_funds_high_fees() { + let utxos = get_oldest_first_test_utxos(); + + let target_amount: u64 = utxos + .iter() + .map(|wu| wu.utxo.txout().value.to_sat()) + .sum::() + - 50; + let drain_script = ScriptBuf::default(); + + OldestFirstCoinSelection + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1000), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + fn test_bnb_coin_selection_success() { + // In this case bnb won't find a suitable match and single random draw will + // select three outputs + let utxos = generate_same_value_utxos(100_000, 20); + + let drain_script = ScriptBuf::default(); + + let target_amount = 250_000 + FEE_AMOUNT; + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 3); + assert_eq!(result.selected_amount(), 300_000); + assert_eq!(result.fee_amount, 204); + } + + #[test] + fn test_bnb_coin_selection_required_are_enough() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 20_000 + FEE_AMOUNT; + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + utxos.clone(), + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 3); + assert_eq!(result.selected_amount(), 300_010); + assert_eq!(result.fee_amount, 204); + } + + #[test] + fn test_bnb_coin_selection_optional_are_enough() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 299756 + FEE_AMOUNT; + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 2); + assert_eq!(result.selected_amount(), 300000); + assert_eq!(result.fee_amount, 136); + } + + #[test] + #[ignore] + fn test_bnb_coin_selection_required_not_enough() { + let utxos = get_test_utxos(); + + let required = vec![utxos[0].clone()]; + let mut optional = utxos[1..].to_vec(); + optional.push(utxo( + 500_000, + 3, + ConfirmationTime::Unconfirmed { last_seen: 0 }, + )); + + // Defensive assertions, for sanity and in case someone changes the test utxos vector. + let amount: u64 = required.iter().map(|u| u.utxo.txout().value.to_sat()).sum(); + assert_eq!(amount, 100_000); + let amount: u64 = optional.iter().map(|u| u.utxo.txout().value.to_sat()).sum(); + assert!(amount > 150_000); + let drain_script = ScriptBuf::default(); + + let target_amount = 150_000 + FEE_AMOUNT; + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + required, + optional, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + + assert_eq!(result.selected.len(), 2); + assert_eq!(result.selected_amount(), 300_000); + assert_eq!(result.fee_amount, 136); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_bnb_coin_selection_insufficient_funds() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 500_000 + FEE_AMOUNT; + + BranchAndBoundCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_bnb_coin_selection_insufficient_funds_high_fees() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 250_000 + FEE_AMOUNT; + + BranchAndBoundCoinSelection::default() + .coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1000), + target_amount, + &drain_script, + ) + .unwrap(); + } + + #[test] + fn test_bnb_coin_selection_check_fee_rate() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + let target_amount = 99932; // first utxo's effective value + let feerate = FeeRate::BROADCAST_MIN; + + let result = BranchAndBoundCoinSelection::new(0) + .coin_select(vec![], utxos, feerate, target_amount, &drain_script) + .unwrap(); + + assert_eq!(result.selected.len(), 1); + assert_eq!(result.selected_amount(), 100_000); + let input_weight = + TxIn::default().segwit_weight().to_wu() + P2WPKH_SATISFACTION_SIZE as u64; + // the final fee rate should be exactly the same as the fee rate given + let result_feerate = Amount::from_sat(result.fee_amount) / Weight::from_wu(input_weight); + assert_eq!(result_feerate, feerate); + } + + #[test] + fn test_bnb_coin_selection_exact_match() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + + for _i in 0..200 { + let mut optional_utxos = generate_random_utxos(&mut rng, 16); + let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos); + let drain_script = ScriptBuf::default(); + let result = BranchAndBoundCoinSelection::new(0) + .coin_select( + vec![], + optional_utxos, + FeeRate::ZERO, + target_amount, + &drain_script, + ) + .unwrap(); + assert_eq!(result.selected_amount(), target_amount); + } + } + + #[test] + #[should_panic(expected = "BnBNoExactMatch")] + fn test_bnb_function_no_exact_match() { + let fee_rate = FeeRate::from_sat_per_vb_unchecked(10); + let utxos: Vec = get_test_utxos() + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); + + let size_of_change = 31; + let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat(); + + let drain_script = ScriptBuf::default(); + let target_amount = 20_000 + FEE_AMOUNT; + BranchAndBoundCoinSelection::new(size_of_change) + .bnb( + vec![], + utxos, + 0, + curr_available_value, + target_amount as i64, + cost_of_change, + &drain_script, + fee_rate, + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "BnBTotalTriesExceeded")] + fn test_bnb_function_tries_exceeded() { + let fee_rate = FeeRate::from_sat_per_vb_unchecked(10); + let utxos: Vec = generate_same_value_utxos(100_000, 100_000) + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); + + let size_of_change = 31; + let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat(); + let target_amount = 20_000 + FEE_AMOUNT; + + let drain_script = ScriptBuf::default(); + + BranchAndBoundCoinSelection::new(size_of_change) + .bnb( + vec![], + utxos, + 0, + curr_available_value, + target_amount as i64, + cost_of_change, + &drain_script, + fee_rate, + ) + .unwrap(); + } + + // The match won't be exact but still in the range + #[test] + fn test_bnb_function_almost_exact_match_with_fees() { + let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); + let size_of_change = 31; + let cost_of_change = (Weight::from_vb_unchecked(size_of_change) * fee_rate).to_sat(); + + let utxos: Vec<_> = generate_same_value_utxos(50_000, 10) + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let curr_value = 0; + + let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); + + // 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) - + // cost_of_change + 5. + let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change as i64 + 5; + + let drain_script = ScriptBuf::default(); + + let result = BranchAndBoundCoinSelection::new(size_of_change) + .bnb( + vec![], + utxos, + curr_value, + curr_available_value, + target_amount, + cost_of_change, + &drain_script, + fee_rate, + ) + .unwrap(); + assert_eq!(result.selected_amount(), 100_000); + assert_eq!(result.fee_amount, 136); + } + + // TODO: bnb() function should be optimized, and this test should be done with more utxos + #[test] + fn test_bnb_function_exact_match_more_utxos() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let fee_rate = FeeRate::ZERO; + + for _ in 0..200 { + let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40) + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let curr_value = 0; + + let curr_available_value = optional_utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value); + + let target_amount = + optional_utxos[3].effective_value + optional_utxos[23].effective_value; + + let drain_script = ScriptBuf::default(); + + let result = BranchAndBoundCoinSelection::new(0) + .bnb( + vec![], + optional_utxos, + curr_value, + curr_available_value, + target_amount, + 0, + &drain_script, + fee_rate, + ) + .unwrap(); + assert_eq!(result.selected_amount(), target_amount as u64); + } + } + + #[test] + fn test_single_random_draw_function_success() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut utxos = generate_random_utxos(&mut rng, 300); + let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT; + + let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); + let utxos: Vec = utxos + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + let drain_script = ScriptBuf::default(); + + let result = BranchAndBoundCoinSelection::default().single_random_draw( + vec![], + utxos, + 0, + target_amount as i64, + &drain_script, + fee_rate, + ); + + assert!(result.selected_amount() > target_amount); + assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64); + } + + #[test] + fn test_bnb_exclude_negative_effective_value() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + + let selection = BranchAndBoundCoinSelection::default().coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(10), + 500_000, + &drain_script, + ); + + assert_matches!( + selection, + Err(Error::InsufficientFunds { + available: 300_000, + .. + }) + ); + } + + #[test] + fn test_bnb_include_negative_effective_value_when_required() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + + let (required, optional) = utxos.into_iter().partition( + |u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value.to_sat() < 1000), + ); + + let selection = BranchAndBoundCoinSelection::default().coin_select( + required, + optional, + FeeRate::from_sat_per_vb_unchecked(10), + 500_000, + &drain_script, + ); + + assert_matches!( + selection, + Err(Error::InsufficientFunds { + available: 300_010, + .. + }) + ); + } + + #[test] + fn test_bnb_sum_of_effective_value_negative() { + let utxos = get_test_utxos(); + let drain_script = ScriptBuf::default(); + + let selection = BranchAndBoundCoinSelection::default().coin_select( + utxos, + vec![], + FeeRate::from_sat_per_vb_unchecked(10_000), + 500_000, + &drain_script, + ); + + assert_matches!( + selection, + Err(Error::InsufficientFunds { + available: 300_010, + .. + }) + ); + } + + #[test] + fn test_filter_duplicates() { + fn utxo(txid: &str, value: u64) -> WeightedUtxo { + WeightedUtxo { + satisfaction_weight: 0, + utxo: Utxo::Local(LocalOutput { + outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0), + txout: TxOut { + value: Amount::from_sat(value), + script_pubkey: ScriptBuf::new(), + }, + keychain: KeychainKind::External, + is_spent: false, + derivation_index: 0, + confirmation_time: ConfirmationTime::Confirmed { + height: 12345, + time: 12345, + }, + }), + } + } + + fn to_utxo_vec(utxos: &[(&str, u64)]) -> Vec { + let mut v = utxos + .iter() + .map(|&(txid, value)| utxo(txid, value)) + .collect::>(); + v.sort_by_key(|u| u.utxo.outpoint()); + v + } + + struct TestCase<'a> { + name: &'a str, + required: &'a [(&'a str, u64)], + optional: &'a [(&'a str, u64)], + exp_required: &'a [(&'a str, u64)], + exp_optional: &'a [(&'a str, u64)], + } + + let test_cases = [ + TestCase { + name: "no_duplicates", + required: &[("A", 1000), ("B", 2100)], + optional: &[("C", 1000)], + exp_required: &[("A", 1000), ("B", 2100)], + exp_optional: &[("C", 1000)], + }, + TestCase { + name: "duplicate_required_utxos", + required: &[("A", 3000), ("B", 1200), ("C", 1234), ("A", 3000)], + optional: &[("D", 2100)], + exp_required: &[("A", 3000), ("B", 1200), ("C", 1234)], + exp_optional: &[("D", 2100)], + }, + TestCase { + name: "duplicate_optional_utxos", + required: &[("A", 3000), ("B", 1200)], + optional: &[("C", 5000), ("D", 1300), ("C", 5000)], + exp_required: &[("A", 3000), ("B", 1200)], + exp_optional: &[("C", 5000), ("D", 1300)], + }, + TestCase { + name: "duplicate_across_required_and_optional_utxos", + required: &[("A", 3000), ("B", 1200), ("C", 2100)], + optional: &[("A", 3000), ("D", 1200), ("E", 5000)], + exp_required: &[("A", 3000), ("B", 1200), ("C", 2100)], + exp_optional: &[("D", 1200), ("E", 5000)], + }, + ]; + + for (i, t) in test_cases.into_iter().enumerate() { + println!("Case {}: {}", i, t.name); + let (required, optional) = + filter_duplicates(to_utxo_vec(t.required), to_utxo_vec(t.optional)); + assert_eq!( + required, + to_utxo_vec(t.exp_required), + "[{}:{}] unexpected `required` result", + i, + t.name + ); + assert_eq!( + optional, + to_utxo_vec(t.exp_optional), + "[{}:{}] unexpected `optional` result", + i, + t.name + ); + } + } +} diff --git a/crates/wallet/src/wallet/error.rs b/crates/wallet/src/wallet/error.rs new file mode 100644 index 00000000..eaf811d6 --- /dev/null +++ b/crates/wallet/src/wallet/error.rs @@ -0,0 +1,291 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet) + +use crate::descriptor::policy::PolicyError; +use crate::descriptor::DescriptorError; +use crate::wallet::coin_selection; +use crate::{descriptor, KeychainKind}; +use alloc::string::String; +use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid}; +use core::fmt; + +/// Errors returned by miniscript when updating inconsistent PSBTs +#[derive(Debug, Clone)] +pub enum MiniscriptPsbtError { + /// Descriptor key conversion error + Conversion(miniscript::descriptor::ConversionError), + /// Return error type for PsbtExt::update_input_with_descriptor + UtxoUpdate(miniscript::psbt::UtxoUpdateError), + /// Return error type for PsbtExt::update_output_with_descriptor + OutputUpdate(miniscript::psbt::OutputUpdateError), +} + +impl fmt::Display for MiniscriptPsbtError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Conversion(err) => write!(f, "Conversion error: {}", err), + Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err), + Self::OutputUpdate(err) => write!(f, "Output update error: {}", err), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for MiniscriptPsbtError {} + +#[derive(Debug)] +/// Error returned from [`TxBuilder::finish`] +/// +/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish +pub enum CreateTxError { + /// There was a problem with the descriptors passed in + Descriptor(DescriptorError), + /// We were unable to load wallet data from or write wallet data to the persistence backend + Persist(anyhow::Error), + /// There was a problem while extracting and manipulating policies + Policy(PolicyError), + /// Spending policy is not compatible with this [`KeychainKind`] + SpendingPolicyRequired(KeychainKind), + /// Requested invalid transaction version '0' + Version0, + /// Requested transaction version `1`, but at least `2` is needed to use OP_CSV + Version1Csv, + /// Requested `LockTime` is less than is required to spend from this script + LockTime { + /// Requested `LockTime` + requested: absolute::LockTime, + /// Required `LockTime` + required: absolute::LockTime, + }, + /// Cannot enable RBF with a `Sequence` >= 0xFFFFFFFE + RbfSequence, + /// Cannot enable RBF with `Sequence` given a required OP_CSV + RbfSequenceCsv { + /// Given RBF `Sequence` + rbf: Sequence, + /// Required OP_CSV `Sequence` + csv: Sequence, + }, + /// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee + FeeTooLow { + /// Required fee absolute value (satoshi) + required: u64, + }, + /// When bumping a tx the fee rate requested is lower than required + FeeRateTooLow { + /// Required fee rate + required: bitcoin::FeeRate, + }, + /// `manually_selected_only` option is selected but no utxo has been passed + NoUtxosSelected, + /// Output created is under the dust limit, 546 satoshis + OutputBelowDustLimit(usize), + /// The `change_policy` was set but the wallet does not have a change_descriptor + ChangePolicyDescriptor, + /// There was an error with coin selection + CoinSelection(coin_selection::Error), + /// Wallet's UTXO set is not enough to cover recipient's requested plus fee + InsufficientFunds { + /// Sats needed for some transaction + needed: u64, + /// Sats available for spending + available: u64, + }, + /// Cannot build a tx without recipients + NoRecipients, + /// Partially signed bitcoin transaction error + Psbt(psbt::Error), + /// In order to use the [`TxBuilder::add_global_xpubs`] option every extended + /// key in the descriptor must either be a master key itself (having depth = 0) or have an + /// explicit origin provided + /// + /// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs + MissingKeyOrigin(String), + /// Happens when trying to spend an UTXO that is not in the internal database + UnknownUtxo, + /// Missing non_witness_utxo on foreign utxo for given `OutPoint` + MissingNonWitnessUtxo(OutPoint), + /// Miniscript PSBT error + MiniscriptPsbt(MiniscriptPsbtError), +} + +impl fmt::Display for CreateTxError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Descriptor(e) => e.fmt(f), + Self::Persist(e) => { + write!( + f, + "failed to load wallet data from or write wallet data to persistence backend: {}", + e + ) + } + Self::Policy(e) => e.fmt(f), + CreateTxError::SpendingPolicyRequired(keychain_kind) => { + write!(f, "Spending policy required: {:?}", keychain_kind) + } + CreateTxError::Version0 => { + write!(f, "Invalid version `0`") + } + CreateTxError::Version1Csv => { + write!( + f, + "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV" + ) + } + CreateTxError::LockTime { + requested, + required, + } => { + write!(f, "TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", required, requested) + } + CreateTxError::RbfSequence => { + write!(f, "Cannot enable RBF with a nSequence >= 0xFFFFFFFE") + } + CreateTxError::RbfSequenceCsv { rbf, csv } => { + write!( + f, + "Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`", + rbf, csv + ) + } + CreateTxError::FeeTooLow { required } => { + write!(f, "Fee to low: required {} sat", required) + } + CreateTxError::FeeRateTooLow { required } => { + write!( + f, + // Note: alternate fmt as sat/vb (ceil) available in bitcoin-0.31 + //"Fee rate too low: required {required:#}" + "Fee rate too low: required {} sat/vb", + crate::floating_rate!(required) + ) + } + CreateTxError::NoUtxosSelected => { + write!(f, "No UTXO selected") + } + CreateTxError::OutputBelowDustLimit(limit) => { + write!(f, "Output below the dust limit: {}", limit) + } + CreateTxError::ChangePolicyDescriptor => { + write!( + f, + "The `change_policy` can be set only if the wallet has a change_descriptor" + ) + } + CreateTxError::CoinSelection(e) => e.fmt(f), + CreateTxError::InsufficientFunds { needed, available } => { + write!( + f, + "Insufficient funds: {} sat available of {} sat needed", + available, needed + ) + } + CreateTxError::NoRecipients => { + write!(f, "Cannot build tx without recipients") + } + CreateTxError::Psbt(e) => e.fmt(f), + CreateTxError::MissingKeyOrigin(err) => { + write!(f, "Missing key origin: {}", err) + } + CreateTxError::UnknownUtxo => { + write!(f, "UTXO not found in the internal database") + } + CreateTxError::MissingNonWitnessUtxo(outpoint) => { + write!(f, "Missing non_witness_utxo on foreign utxo {}", outpoint) + } + CreateTxError::MiniscriptPsbt(err) => { + write!(f, "Miniscript PSBT error: {}", err) + } + } + } +} + +impl From for CreateTxError { + fn from(err: descriptor::error::Error) -> Self { + CreateTxError::Descriptor(err) + } +} + +impl From for CreateTxError { + fn from(err: PolicyError) -> Self { + CreateTxError::Policy(err) + } +} + +impl From for CreateTxError { + fn from(err: MiniscriptPsbtError) -> Self { + CreateTxError::MiniscriptPsbt(err) + } +} + +impl From for CreateTxError { + fn from(err: psbt::Error) -> Self { + CreateTxError::Psbt(err) + } +} + +impl From for CreateTxError { + fn from(err: coin_selection::Error) -> Self { + CreateTxError::CoinSelection(err) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CreateTxError {} + +#[derive(Debug)] +/// Error returned from [`Wallet::build_fee_bump`] +/// +/// [`Wallet::build_fee_bump`]: super::Wallet::build_fee_bump +pub enum BuildFeeBumpError { + /// Happens when trying to spend an UTXO that is not in the internal database + UnknownUtxo(OutPoint), + /// Thrown when a tx is not found in the internal database + TransactionNotFound(Txid), + /// Happens when trying to bump a transaction that is already confirmed + TransactionConfirmed(Txid), + /// Trying to replace a tx that has a sequence >= `0xFFFFFFFE` + IrreplaceableTransaction(Txid), + /// Node doesn't have data to estimate a fee rate + FeeRateUnavailable, +} + +impl fmt::Display for BuildFeeBumpError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnknownUtxo(outpoint) => write!( + f, + "UTXO not found in the internal database with txid: {}, vout: {}", + outpoint.txid, outpoint.vout + ), + Self::TransactionNotFound(txid) => { + write!( + f, + "Transaction not found in the internal database with txid: {}", + txid + ) + } + Self::TransactionConfirmed(txid) => { + write!(f, "Transaction already confirmed with txid: {}", txid) + } + Self::IrreplaceableTransaction(txid) => { + write!(f, "Transaction can't be replaced with txid: {}", txid) + } + Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for BuildFeeBumpError {} diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs new file mode 100644 index 00000000..95d91193 --- /dev/null +++ b/crates/wallet/src/wallet/export.rs @@ -0,0 +1,341 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Wallet export +//! +//! This modules implements the wallet export format used by [FullyNoded](https://github.com/Fonta1n3/FullyNoded/blob/10b7808c8b929b171cca537fb50522d015168ac9/Docs/Wallets/Wallet-Export-Spec.md). +//! +//! ## Examples +//! +//! ### Import from JSON +//! +//! ``` +//! # use std::str::FromStr; +//! # use bitcoin::*; +//! # use bdk_wallet::wallet::export::*; +//! # use bdk_wallet::*; +//! let import = r#"{ +//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)", +//! "blockheight":1782088, +//! "label":"testnet" +//! }"#; +//! +//! let import = FullyNodedExport::from_str(import)?; +//! let wallet = Wallet::new_no_persist( +//! &import.descriptor(), +//! import.change_descriptor().as_ref(), +//! Network::Testnet, +//! )?; +//! # Ok::<_, Box>(()) +//! ``` +//! +//! ### Export a `Wallet` +//! ``` +//! # use bitcoin::*; +//! # use bdk_wallet::wallet::export::*; +//! # use bdk_wallet::*; +//! let wallet = Wallet::new_no_persist( +//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)", +//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"), +//! Network::Testnet, +//! )?; +//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap(); +//! +//! println!("Exported: {}", export.to_string()); +//! # Ok::<_, Box>(()) +//! ``` + +use alloc::string::String; +use core::fmt; +use core::str::FromStr; +use serde::{Deserialize, Serialize}; + +use miniscript::descriptor::{ShInner, WshInner}; +use miniscript::{Descriptor, ScriptContext, Terminal}; + +use crate::types::KeychainKind; +use crate::wallet::Wallet; + +/// Alias for [`FullyNodedExport`] +#[deprecated(since = "0.18.0", note = "Please use [`FullyNodedExport`] instead")] +pub type WalletExport = FullyNodedExport; + +/// Structure that contains the export of a wallet +/// +/// For a usage example see [this module](crate::wallet::export)'s documentation. +#[derive(Debug, Serialize, Deserialize)] +pub struct FullyNodedExport { + descriptor: String, + /// Earliest block to rescan when looking for the wallet's transactions + pub blockheight: u32, + /// Arbitrary label for the wallet + pub label: String, +} + +impl fmt::Display for FullyNodedExport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", serde_json::to_string(self).unwrap()) + } +} + +impl FromStr for FullyNodedExport { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + +fn remove_checksum(s: String) -> String { + s.split_once('#').map(|(a, _)| String::from(a)).unwrap() +} + +impl FullyNodedExport { + /// Export a wallet + /// + /// This function returns an error if it determines that the `wallet`'s descriptor(s) are not + /// supported by Bitcoin Core or don't follow the standard derivation paths defined by BIP44 + /// and others. + /// + /// If `include_blockheight` is `true`, this function will look into the `wallet`'s database + /// for the oldest transaction it knows and use that as the earliest block to rescan. + /// + /// If the database is empty or `include_blockheight` is false, the `blockheight` field + /// returned will be `0`. + pub fn export_wallet( + wallet: &Wallet, + label: &str, + include_blockheight: bool, + ) -> Result { + let descriptor = wallet + .get_descriptor_for_keychain(KeychainKind::External) + .to_string_with_secret( + &wallet + .get_signers(KeychainKind::External) + .as_key_map(wallet.secp_ctx()), + ); + let descriptor = remove_checksum(descriptor); + Self::is_compatible_with_core(&descriptor)?; + + let blockheight = if include_blockheight { + wallet.transactions().next().map_or(0, |canonical_tx| { + match canonical_tx.chain_position { + bdk_chain::ChainPosition::Confirmed(a) => a.confirmation_height, + bdk_chain::ChainPosition::Unconfirmed(_) => 0, + } + }) + } else { + 0 + }; + + let export = FullyNodedExport { + descriptor, + label: label.into(), + blockheight, + }; + + let change_descriptor = match wallet.public_descriptor(KeychainKind::Internal).is_some() { + false => None, + true => { + let descriptor = wallet + .get_descriptor_for_keychain(KeychainKind::Internal) + .to_string_with_secret( + &wallet + .get_signers(KeychainKind::Internal) + .as_key_map(wallet.secp_ctx()), + ); + Some(remove_checksum(descriptor)) + } + }; + if export.change_descriptor() != change_descriptor { + return Err("Incompatible change descriptor"); + } + + Ok(export) + } + + fn is_compatible_with_core(descriptor: &str) -> Result<(), &'static str> { + fn check_ms( + terminal: &Terminal, + ) -> Result<(), &'static str> { + if let Terminal::Multi(_, _) = terminal { + Ok(()) + } else { + Err("The descriptor contains operators not supported by Bitcoin Core") + } + } + + // pkh(), wpkh(), sh(wpkh()) are always fine, as well as multi() and sortedmulti() + match Descriptor::::from_str(descriptor).map_err(|_| "Invalid descriptor")? { + Descriptor::Pkh(_) | Descriptor::Wpkh(_) => Ok(()), + Descriptor::Sh(sh) => match sh.as_inner() { + ShInner::Wpkh(_) => Ok(()), + ShInner::SortedMulti(_) => Ok(()), + ShInner::Wsh(wsh) => match wsh.as_inner() { + WshInner::SortedMulti(_) => Ok(()), + WshInner::Ms(ms) => check_ms(&ms.node), + }, + ShInner::Ms(ms) => check_ms(&ms.node), + }, + Descriptor::Wsh(wsh) => match wsh.as_inner() { + WshInner::SortedMulti(_) => Ok(()), + WshInner::Ms(ms) => check_ms(&ms.node), + }, + _ => Err("The descriptor is not compatible with Bitcoin Core"), + } + } + + /// Return the external descriptor + pub fn descriptor(&self) -> String { + self.descriptor.clone() + } + + /// Return the internal descriptor, if present + pub fn change_descriptor(&self) -> Option { + let replaced = self.descriptor.replace("/0/*", "/1/*"); + + if replaced != self.descriptor { + Some(replaced) + } else { + None + } + } +} + +#[cfg(test)] +mod test { + use core::str::FromStr; + + use crate::std::string::ToString; + use bdk_chain::{BlockId, ConfirmationTime}; + use bitcoin::hashes::Hash; + use bitcoin::{transaction, BlockHash, Network, Transaction}; + + use super::*; + use crate::wallet::Wallet; + + fn get_test_wallet( + descriptor: &str, + change_descriptor: Option<&str>, + network: Network, + ) -> Wallet { + let mut wallet = Wallet::new_no_persist(descriptor, change_descriptor, network).unwrap(); + let transaction = Transaction { + input: vec![], + output: vec![], + version: transaction::Version::non_standard(0), + lock_time: bitcoin::absolute::LockTime::ZERO, + }; + wallet + .insert_checkpoint(BlockId { + height: 5001, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_tx( + transaction, + ConfirmationTime::Confirmed { + height: 5000, + time: 0, + }, + ) + .unwrap(); + wallet + } + + #[test] + fn test_export_bip44() { + let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; + let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; + + let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin); + let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + + assert_eq!(export.descriptor(), descriptor); + assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); + assert_eq!(export.blockheight, 5000); + assert_eq!(export.label, "Test Label"); + } + + #[test] + #[should_panic(expected = "Incompatible change descriptor")] + fn test_export_no_change() { + // This wallet explicitly doesn't have a change descriptor. It should be impossible to + // export, because exporting this kind of external descriptor normally implies the + // existence of an internal descriptor + + let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; + + let wallet = get_test_wallet(descriptor, None, Network::Bitcoin); + FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + } + + #[test] + #[should_panic(expected = "Incompatible change descriptor")] + fn test_export_incompatible_change() { + // This wallet has a change descriptor, but the derivation path is not in the "standard" + // bip44/49/etc format + + let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; + let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)"; + + let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin); + FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + } + + #[test] + fn test_export_multi() { + let descriptor = "wsh(multi(2,\ + [73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*,\ + [f9f62194/48'/0'/0'/2']tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/0/*,\ + [c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/0/*\ + ))"; + let change_descriptor = "wsh(multi(2,\ + [73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/1/*,\ + [f9f62194/48'/0'/0'/2']tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/1/*,\ + [c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\ + ))"; + + let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Testnet); + let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + + assert_eq!(export.descriptor(), descriptor); + assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); + assert_eq!(export.blockheight, 5000); + assert_eq!(export.label, "Test Label"); + } + + #[test] + fn test_export_to_json() { + let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; + let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; + + let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin); + let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + + assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}"); + } + + #[test] + fn test_export_from_json() { + let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; + let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; + + let import_str = "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}"; + let export = FullyNodedExport::from_str(import_str).unwrap(); + + assert_eq!(export.descriptor(), descriptor); + assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); + assert_eq!(export.blockheight, 5000); + assert_eq!(export.label, "Test Label"); + } +} diff --git a/crates/wallet/src/wallet/hardwaresigner.rs b/crates/wallet/src/wallet/hardwaresigner.rs new file mode 100644 index 00000000..b9bff5ad --- /dev/null +++ b/crates/wallet/src/wallet/hardwaresigner.rs @@ -0,0 +1,98 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! HWI Signer +//! +//! This module contains HWISigner, an implementation of a [TransactionSigner] to be +//! used with hardware wallets. +//! ```no_run +//! # use bdk_wallet::bitcoin::Network; +//! # use bdk_wallet::signer::SignerOrdering; +//! # use bdk_wallet::wallet::hardwaresigner::HWISigner; +//! # use bdk_wallet::wallet::AddressIndex::New; +//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet}; +//! # use hwi::HWIClient; +//! # use std::sync::Arc; +//! # +//! # fn main() -> Result<(), Box> { +//! let mut devices = HWIClient::enumerate()?; +//! if devices.is_empty() { +//! panic!("No devices found!"); +//! } +//! let first_device = devices.remove(0)?; +//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?; +//! +//! # let mut wallet = Wallet::new_no_persist( +//! # "", +//! # None, +//! # Network::Testnet, +//! # )?; +//! # +//! // Adding the hardware signer to the BDK wallet +//! wallet.add_signer( +//! KeychainKind::External, +//! SignerOrdering(200), +//! Arc::new(custom_signer), +//! ); +//! +//! # Ok(()) +//! # } +//! ``` + +use bitcoin::bip32::Fingerprint; +use bitcoin::secp256k1::{All, Secp256k1}; +use bitcoin::Psbt; + +use hwi::error::Error; +use hwi::types::{HWIChain, HWIDevice}; +use hwi::HWIClient; + +use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner}; + +#[derive(Debug)] +/// Custom signer for Hardware Wallets +/// +/// This ignores `sign_options` and leaves the decisions up to the hardware wallet. +pub struct HWISigner { + fingerprint: Fingerprint, + client: HWIClient, +} + +impl HWISigner { + /// Create a instance from the specified device and chain + pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result { + let client = HWIClient::get_client(device, false, chain)?; + Ok(HWISigner { + fingerprint: device.fingerprint, + client, + }) + } +} + +impl SignerCommon for HWISigner { + fn id(&self, _secp: &Secp256k1) -> SignerId { + SignerId::Fingerprint(self.fingerprint) + } +} + +/// This implementation ignores `sign_options` +impl TransactionSigner for HWISigner { + fn sign_transaction( + &self, + psbt: &mut Psbt, + _sign_options: &crate::SignOptions, + _secp: &crate::wallet::utils::SecpCtx, + ) -> Result<(), SignerError> { + psbt.combine(self.client.sign_tx(psbt)?.psbt) + .expect("Failed to combine HW signed psbt with passed PSBT"); + Ok(()) + } +} diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs new file mode 100644 index 00000000..61ec5893 --- /dev/null +++ b/crates/wallet/src/wallet/mod.rs @@ -0,0 +1,2708 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Wallet +//! +//! This module defines the [`Wallet`]. +use crate::collections::{BTreeMap, HashMap}; +use alloc::{ + boxed::Box, + string::{String, ToString}, + sync::Arc, + vec::Vec, +}; +pub use bdk_chain::keychain::Balance; +use bdk_chain::{ + indexed_tx_graph, + keychain::{self, KeychainTxOutIndex}, + local_chain::{ + self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain, + }, + spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, + tx_graph::{CanonicalTx, TxGraph}, + Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut, + IndexedTxGraph, +}; +use bdk_persist::{Persist, PersistBackend}; +use bitcoin::secp256k1::{All, Secp256k1}; +use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; +use bitcoin::{ + absolute, psbt, Address, Block, FeeRate, Network, OutPoint, Script, ScriptBuf, Sequence, + Transaction, TxOut, Txid, Witness, +}; +use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt}; +use bitcoin::{constants::genesis_block, Amount}; +use core::fmt; +use core::ops::Deref; +use descriptor::error::Error as DescriptorError; +use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}; + +use bdk_chain::tx_graph::CalculateFeeError; + +pub mod coin_selection; +pub mod export; +pub mod signer; +pub mod tx_builder; +pub(crate) mod utils; + +pub mod error; + +pub use utils::IsDust; + +use coin_selection::DefaultCoinSelectionAlgorithm; +use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner}; +use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams}; +use utils::{check_nsequence_rbf, After, Older, SecpCtx}; + +use crate::descriptor::policy::BuildSatisfaction; +use crate::descriptor::{ + self, calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta, + ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils, +}; +use crate::psbt::PsbtUtils; +use crate::signer::SignerError; +use crate::types::*; +use crate::wallet::coin_selection::Excess::{Change, NoChange}; +use crate::wallet::error::{BuildFeeBumpError, CreateTxError, MiniscriptPsbtError}; + +const COINBASE_MATURITY: u32 = 100; + +/// A Bitcoin wallet +/// +/// The `Wallet` acts as a way of coherently interfacing with output descriptors and related transactions. +/// Its main components are: +/// +/// 1. output *descriptors* from which it can derive addresses. +/// 2. [`signer`]s that can contribute signatures to addresses instantiated from the descriptors. +/// +/// [`signer`]: crate::signer +#[derive(Debug)] +pub struct Wallet { + signers: Arc, + change_signers: Arc, + chain: LocalChain, + indexed_graph: IndexedTxGraph>, + persist: Persist, + network: Network, + secp: SecpCtx, +} + +/// An update to [`Wallet`]. +/// +/// It updates [`bdk_chain::keychain::KeychainTxOutIndex`], [`bdk_chain::TxGraph`] and [`local_chain::LocalChain`] atomically. +#[derive(Debug, Clone, Default)] +pub struct Update { + /// Contains the last active derivation indices per keychain (`K`), which is used to update the + /// [`KeychainTxOutIndex`]. + pub last_active_indices: BTreeMap, + + /// Update for the wallet's internal [`TxGraph`]. + pub graph: TxGraph, + + /// Update for the wallet's internal [`LocalChain`]. + /// + /// [`LocalChain`]: local_chain::LocalChain + pub chain: Option, +} + +impl From> for Update { + fn from(value: FullScanResult) -> Self { + Self { + last_active_indices: value.last_active_indices, + graph: value.graph_update, + chain: Some(value.chain_update), + } + } +} + +impl From for Update { + fn from(value: SyncResult) -> Self { + Self { + last_active_indices: BTreeMap::new(), + graph: value.graph_update, + chain: Some(value.chain_update), + } + } +} + +/// The changes made to a wallet by applying an [`Update`]. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)] +pub struct ChangeSet { + /// Changes to the [`LocalChain`]. + /// + /// [`LocalChain`]: local_chain::LocalChain + pub chain: local_chain::ChangeSet, + + /// Changes to [`IndexedTxGraph`]. + /// + /// [`IndexedTxGraph`]: bdk_chain::indexed_tx_graph::IndexedTxGraph + pub indexed_tx_graph: indexed_tx_graph::ChangeSet< + ConfirmationTimeHeightAnchor, + keychain::ChangeSet, + >, + + /// Stores the network type of the wallet. + pub network: Option, +} + +impl Append for ChangeSet { + fn append(&mut self, other: Self) { + Append::append(&mut self.chain, other.chain); + Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph); + if other.network.is_some() { + debug_assert!( + self.network.is_none() || self.network == other.network, + "network type must be consistent" + ); + self.network = other.network; + } + } + + fn is_empty(&self) -> bool { + self.chain.is_empty() && self.indexed_tx_graph.is_empty() + } +} + +impl From for ChangeSet { + fn from(chain: local_chain::ChangeSet) -> Self { + Self { + chain, + ..Default::default() + } + } +} + +impl + From< + indexed_tx_graph::ChangeSet< + ConfirmationTimeHeightAnchor, + keychain::ChangeSet, + >, + > for ChangeSet +{ + fn from( + indexed_tx_graph: indexed_tx_graph::ChangeSet< + ConfirmationTimeHeightAnchor, + keychain::ChangeSet, + >, + ) -> Self { + Self { + indexed_tx_graph, + ..Default::default() + } + } +} + +/// A derived address and the index it was found at. +/// For convenience this automatically derefs to `Address` +#[derive(Debug, PartialEq, Eq)] +pub struct AddressInfo { + /// Child index of this address + pub index: u32, + /// Address + pub address: Address, + /// Type of keychain + pub keychain: KeychainKind, +} + +impl Deref for AddressInfo { + type Target = Address; + + fn deref(&self) -> &Self::Target { + &self.address + } +} + +impl fmt::Display for AddressInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.address) + } +} + +impl Wallet { + /// Creates a wallet that does not persist data. + pub fn new_no_persist( + descriptor: E, + change_descriptor: Option, + network: Network, + ) -> Result { + Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e { + NewError::NonEmptyDatabase => unreachable!("mock-database cannot have data"), + NewError::Descriptor(e) => e, + NewError::Persist(_) => unreachable!("mock-write must always succeed"), + }) + } + + /// Creates a wallet that does not persist data, with a custom genesis hash. + pub fn new_no_persist_with_genesis_hash( + descriptor: E, + change_descriptor: Option, + network: Network, + genesis_hash: BlockHash, + ) -> Result { + Self::new_with_genesis_hash(descriptor, change_descriptor, (), network, genesis_hash) + .map_err(|e| match e { + NewError::NonEmptyDatabase => unreachable!("mock-database cannot have data"), + NewError::Descriptor(e) => e, + NewError::Persist(_) => unreachable!("mock-write must always succeed"), + }) + } +} + +/// The error type when constructing a fresh [`Wallet`]. +/// +/// Methods [`new`] and [`new_with_genesis_hash`] may return this error. +/// +/// [`new`]: Wallet::new +/// [`new_with_genesis_hash`]: Wallet::new_with_genesis_hash +#[derive(Debug)] +pub enum NewError { + /// Database already has data. + NonEmptyDatabase, + /// There was problem with the passed-in descriptor(s). + Descriptor(crate::descriptor::DescriptorError), + /// We were unable to write the wallet's data to the persistence backend. + Persist(anyhow::Error), +} + +impl fmt::Display for NewError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NewError::NonEmptyDatabase => write!( + f, + "database already has data - use `load` or `new_or_load` methods instead" + ), + NewError::Descriptor(e) => e.fmt(f), + NewError::Persist(e) => e.fmt(f), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for NewError {} + +/// The error type when loading a [`Wallet`] from persistence. +/// +/// Method [`load`] may return this error. +/// +/// [`load`]: Wallet::load +#[derive(Debug)] +pub enum LoadError { + /// There was a problem with the passed-in descriptor(s). + Descriptor(crate::descriptor::DescriptorError), + /// Loading data from the persistence backend failed. + Persist(anyhow::Error), + /// Wallet not initialized, persistence backend is empty. + NotInitialized, + /// Data loaded from persistence is missing network type. + MissingNetwork, + /// Data loaded from persistence is missing genesis hash. + MissingGenesis, + /// Data loaded from persistence is missing descriptor. + MissingDescriptor, +} + +impl fmt::Display for LoadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LoadError::Descriptor(e) => e.fmt(f), + LoadError::Persist(e) => e.fmt(f), + LoadError::NotInitialized => { + write!(f, "wallet is not initialized, persistence backend is empty") + } + LoadError::MissingNetwork => write!(f, "loaded data is missing network type"), + LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"), + LoadError::MissingDescriptor => write!(f, "loaded data is missing descriptor"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for LoadError {} + +/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existent. +/// +/// Methods [`new_or_load`] and [`new_or_load_with_genesis_hash`] may return this error. +/// +/// [`new_or_load`]: Wallet::new_or_load +/// [`new_or_load_with_genesis_hash`]: Wallet::new_or_load_with_genesis_hash +#[derive(Debug)] +pub enum NewOrLoadError { + /// There is a problem with the passed-in descriptor. + Descriptor(crate::descriptor::DescriptorError), + /// Either writing to or loading from the persistence backend failed. + Persist(anyhow::Error), + /// Wallet is not initialized, persistence backend is empty. + NotInitialized, + /// The loaded genesis hash does not match what was provided. + LoadedGenesisDoesNotMatch { + /// The expected genesis block hash. + expected: BlockHash, + /// The block hash loaded from persistence. + got: Option, + }, + /// The loaded network type does not match what was provided. + LoadedNetworkDoesNotMatch { + /// The expected network type. + expected: Network, + /// The network type loaded from persistence. + got: Option, + }, + /// The loaded desccriptor does not match what was provided. + LoadedDescriptorDoesNotMatch { + /// The descriptor loaded from persistence. + got: Option, + /// The keychain of the descriptor not matching + keychain: KeychainKind, + }, +} + +impl fmt::Display for NewOrLoadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NewOrLoadError::Descriptor(e) => e.fmt(f), + NewOrLoadError::Persist(e) => write!( + f, + "failed to either write to or load from persistence, {}", + e + ), + NewOrLoadError::NotInitialized => { + write!(f, "wallet is not initialized, persistence backend is empty") + } + NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => { + write!(f, "loaded genesis hash is not {}, got {:?}", expected, got) + } + NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => { + write!(f, "loaded network type is not {}, got {:?}", expected, got) + } + NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => { + write!( + f, + "loaded descriptor is different from what was provided, got {:?} for keychain {:?}", + got, keychain + ) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for NewOrLoadError {} + +/// An error that may occur when inserting a transaction into [`Wallet`]. +#[derive(Debug)] +pub enum InsertTxError { + /// The error variant that occurs when the caller attempts to insert a transaction with a + /// confirmation height that is greater than the internal chain tip. + ConfirmationHeightCannotBeGreaterThanTip { + /// The internal chain's tip height. + tip_height: u32, + /// The introduced transaction's confirmation height. + tx_height: u32, + }, +} + +impl fmt::Display for InsertTxError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InsertTxError::ConfirmationHeightCannotBeGreaterThanTip { + tip_height, + tx_height, + } => { + write!(f, "cannot insert tx with confirmation height ({}) higher than internal tip height ({})", tx_height, tip_height) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InsertTxError {} + +/// An error that may occur when applying a block to [`Wallet`]. +#[derive(Debug)] +pub enum ApplyBlockError { + /// Occurs when the update chain cannot connect with original chain. + CannotConnect(CannotConnectError), + /// Occurs when the `connected_to` hash does not match the hash derived from `block`. + UnexpectedConnectedToHash { + /// Block hash of `connected_to`. + connected_to_hash: BlockHash, + /// Expected block hash of `connected_to`, as derived from `block`. + expected_hash: BlockHash, + }, +} + +impl fmt::Display for ApplyBlockError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ApplyBlockError::CannotConnect(err) => err.fmt(f), + ApplyBlockError::UnexpectedConnectedToHash { + expected_hash: block_hash, + connected_to_hash: checkpoint_hash, + } => write!( + f, + "`connected_to` hash {} differs from the expected hash {} (which is derived from `block`)", + checkpoint_hash, block_hash + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ApplyBlockError {} + +impl Wallet { + /// Initialize an empty [`Wallet`]. + pub fn new( + descriptor: E, + change_descriptor: Option, + db: impl PersistBackend + Send + Sync + 'static, + network: Network, + ) -> Result { + let genesis_hash = genesis_block(network).block_hash(); + Self::new_with_genesis_hash(descriptor, change_descriptor, db, network, genesis_hash) + } + + /// Initialize an empty [`Wallet`] with a custom genesis hash. + /// + /// This is like [`Wallet::new`] with an additional `genesis_hash` parameter. This is useful + /// for syncing from alternative networks. + pub fn new_with_genesis_hash( + descriptor: E, + change_descriptor: Option, + mut db: impl PersistBackend + Send + Sync + 'static, + network: Network, + genesis_hash: BlockHash, + ) -> Result { + if let Ok(changeset) = db.load_from_persistence() { + if changeset.is_some() { + return Err(NewError::NonEmptyDatabase); + } + } + let secp = Secp256k1::new(); + let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); + let mut index = KeychainTxOutIndex::::default(); + + let (signers, change_signers) = + create_signers(&mut index, &secp, descriptor, change_descriptor, network) + .map_err(NewError::Descriptor)?; + + let indexed_graph = IndexedTxGraph::new(index); + + let mut persist = Persist::new(db); + persist.stage(ChangeSet { + chain: chain_changeset, + indexed_tx_graph: indexed_graph.initial_changeset(), + network: Some(network), + }); + persist.commit().map_err(NewError::Persist)?; + + Ok(Wallet { + signers, + change_signers, + network, + chain, + indexed_graph, + persist, + secp, + }) + } + + /// Load [`Wallet`] from the given persistence backend. + /// + /// Note that the descriptor secret keys are not persisted to the db; this means that after + /// calling this method the [`Wallet`] **won't** know the secret keys, and as such, won't be + /// able to sign transactions. + /// + /// If you wish to use the wallet to sign transactions, you need to add the secret keys + /// manually to the [`Wallet`]: + /// + /// ```rust,no_run + /// # use bdk_wallet::Wallet; + /// # use bdk_wallet::signer::{SignersContainer, SignerOrdering}; + /// # use bdk_wallet::descriptor::Descriptor; + /// # use bitcoin::key::Secp256k1; + /// # use bdk_wallet::KeychainKind; + /// # use bdk_file_store::Store; + /// # + /// # fn main() -> Result<(), anyhow::Error> { + /// # let temp_dir = tempfile::tempdir().expect("must create tempdir"); + /// # let file_path = temp_dir.path().join("store.db"); + /// # let db: Store = Store::create_new(&[], &file_path).expect("must create db"); + /// let secp = Secp256k1::new(); + /// + /// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap(); + /// let (internal_descriptor, internal_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)").unwrap(); + /// + /// let external_signer_container = SignersContainer::build(external_keymap, &external_descriptor, &secp); + /// let internal_signer_container = SignersContainer::build(internal_keymap, &internal_descriptor, &secp); + /// + /// let mut wallet = Wallet::load(db)?; + /// + /// external_signer_container.signers().into_iter() + /// .for_each(|s| wallet.add_signer(KeychainKind::External, SignerOrdering::default(), s.clone())); + /// internal_signer_container.signers().into_iter() + /// .for_each(|s| wallet.add_signer(KeychainKind::Internal, SignerOrdering::default(), s.clone())); + /// # Ok(()) + /// # } + /// ``` + /// + /// Alternatively, you can call [`Wallet::new_or_load`], which will add the private keys of the + /// passed-in descriptors to the [`Wallet`]. + pub fn load( + mut db: impl PersistBackend + Send + Sync + 'static, + ) -> Result { + let changeset = db + .load_from_persistence() + .map_err(LoadError::Persist)? + .ok_or(LoadError::NotInitialized)?; + Self::load_from_changeset(db, changeset) + } + + fn load_from_changeset( + db: impl PersistBackend + Send + Sync + 'static, + changeset: ChangeSet, + ) -> Result { + let secp = Secp256k1::new(); + let network = changeset.network.ok_or(LoadError::MissingNetwork)?; + let chain = + LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?; + let mut index = KeychainTxOutIndex::::default(); + let descriptor = changeset + .indexed_tx_graph + .indexer + .keychains_added + .get(&KeychainKind::External) + .ok_or(LoadError::MissingDescriptor)? + .clone(); + let change_descriptor = changeset + .indexed_tx_graph + .indexer + .keychains_added + .get(&KeychainKind::Internal) + .cloned(); + + let (signers, change_signers) = + create_signers(&mut index, &secp, descriptor, change_descriptor, network) + .expect("Can't fail: we passed in valid descriptors, recovered from the changeset"); + + let mut indexed_graph = IndexedTxGraph::new(index); + indexed_graph.apply_changeset(changeset.indexed_tx_graph); + + let persist = Persist::new(db); + + Ok(Wallet { + signers, + change_signers, + chain, + indexed_graph, + persist, + network, + secp, + }) + } + + /// Either loads [`Wallet`] from persistence, or initializes it if it does not exist. + /// + /// This method will fail if the loaded [`Wallet`] has different parameters to those provided. + pub fn new_or_load( + descriptor: E, + change_descriptor: Option, + db: impl PersistBackend + Send + Sync + 'static, + network: Network, + ) -> Result { + let genesis_hash = genesis_block(network).block_hash(); + Self::new_or_load_with_genesis_hash( + descriptor, + change_descriptor, + db, + network, + genesis_hash, + ) + } + + /// Either loads [`Wallet`] from persistence, or initializes it if it does not exist, using the + /// provided descriptor, change descriptor, network, and custom genesis hash. + /// + /// This method will fail if the loaded [`Wallet`] has different parameters to those provided. + /// This is like [`Wallet::new_or_load`] with an additional `genesis_hash` parameter. This is + /// useful for syncing from alternative networks. + pub fn new_or_load_with_genesis_hash( + descriptor: E, + change_descriptor: Option, + mut db: impl PersistBackend + Send + Sync + 'static, + network: Network, + genesis_hash: BlockHash, + ) -> Result { + let changeset = db + .load_from_persistence() + .map_err(NewOrLoadError::Persist)?; + match changeset { + Some(changeset) => { + let mut wallet = Self::load_from_changeset(db, changeset).map_err(|e| match e { + LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e), + LoadError::Persist(e) => NewOrLoadError::Persist(e), + LoadError::NotInitialized => NewOrLoadError::NotInitialized, + LoadError::MissingNetwork => NewOrLoadError::LoadedNetworkDoesNotMatch { + expected: network, + got: None, + }, + LoadError::MissingGenesis => NewOrLoadError::LoadedGenesisDoesNotMatch { + expected: genesis_hash, + got: None, + }, + LoadError::MissingDescriptor => NewOrLoadError::LoadedDescriptorDoesNotMatch { + got: None, + keychain: KeychainKind::External, + }, + })?; + if wallet.network != network { + return Err(NewOrLoadError::LoadedNetworkDoesNotMatch { + expected: network, + got: Some(wallet.network), + }); + } + if wallet.chain.genesis_hash() != genesis_hash { + return Err(NewOrLoadError::LoadedGenesisDoesNotMatch { + expected: genesis_hash, + got: Some(wallet.chain.genesis_hash()), + }); + } + + let (expected_descriptor, expected_descriptor_keymap) = descriptor + .into_wallet_descriptor(&wallet.secp, network) + .map_err(NewOrLoadError::Descriptor)?; + let wallet_descriptor = wallet.public_descriptor(KeychainKind::External).cloned(); + if wallet_descriptor != Some(expected_descriptor.clone()) { + return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch { + got: wallet_descriptor, + keychain: KeychainKind::External, + }); + } + // if expected descriptor has private keys add them as new signers + if !expected_descriptor_keymap.is_empty() { + let signer_container = SignersContainer::build( + expected_descriptor_keymap, + &expected_descriptor, + &wallet.secp, + ); + signer_container.signers().into_iter().for_each(|signer| { + wallet.add_signer( + KeychainKind::External, + SignerOrdering::default(), + signer.clone(), + ) + }); + } + + let expected_change_descriptor = if let Some(c) = change_descriptor { + Some( + c.into_wallet_descriptor(&wallet.secp, network) + .map_err(NewOrLoadError::Descriptor)?, + ) + } else { + None + }; + let wallet_change_descriptor = + wallet.public_descriptor(KeychainKind::Internal).cloned(); + + match (expected_change_descriptor, wallet_change_descriptor) { + (Some((expected_descriptor, expected_keymap)), Some(wallet_descriptor)) + if wallet_descriptor == expected_descriptor => + { + // if expected change descriptor has private keys add them as new signers + if !expected_keymap.is_empty() { + let signer_container = SignersContainer::build( + expected_keymap, + &expected_descriptor, + &wallet.secp, + ); + signer_container.signers().into_iter().for_each(|signer| { + wallet.add_signer( + KeychainKind::Internal, + SignerOrdering::default(), + signer.clone(), + ) + }); + } + } + (None, None) => (), + (_, wallet_descriptor) => { + return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch { + got: wallet_descriptor, + keychain: KeychainKind::Internal, + }); + } + } + + Ok(wallet) + } + None => Self::new_with_genesis_hash( + descriptor, + change_descriptor, + db, + network, + genesis_hash, + ) + .map_err(|e| match e { + NewError::NonEmptyDatabase => { + unreachable!("database is already checked to have no data") + } + NewError::Descriptor(e) => NewOrLoadError::Descriptor(e), + NewError::Persist(e) => NewOrLoadError::Persist(e), + }), + } + } + + /// Get the Bitcoin network the wallet is using. + pub fn network(&self) -> Network { + self.network + } + + /// Iterator over all keychains in this wallet + pub fn keychains(&self) -> impl Iterator { + self.indexed_graph.index.keychains() + } + + /// Peek an address of the given `keychain` at `index` without revealing it. + /// + /// For non-wildcard descriptors this returns the same address at every provided index. + /// + /// # Panics + /// + /// This panics when the caller requests for an address of derivation index greater than the + /// [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) max index. + pub fn peek_address(&self, keychain: KeychainKind, mut index: u32) -> AddressInfo { + let keychain = self.map_keychain(keychain); + let mut spk_iter = self + .indexed_graph + .index + .unbounded_spk_iter(&keychain) + .expect("Must exist (we called map_keychain)"); + if !spk_iter.descriptor().has_wildcard() { + index = 0; + } + let (index, spk) = spk_iter + .nth(index as usize) + .expect("derivation index is out of bounds"); + + AddressInfo { + index, + address: Address::from_script(&spk, self.network).expect("must have address form"), + keychain, + } + } + + /// Attempt to reveal the next address of the given `keychain`. + /// + /// This will increment the internal derivation index. If the keychain's descriptor doesn't + /// contain a wildcard or every address is already revealed up to the maximum derivation + /// index defined in [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki), + /// then returns the last revealed address. + /// + /// # Errors + /// + /// If writing to persistent storage fails. + pub fn reveal_next_address(&mut self, keychain: KeychainKind) -> anyhow::Result { + let keychain = self.map_keychain(keychain); + let ((index, spk), index_changeset) = self + .indexed_graph + .index + .reveal_next_spk(&keychain) + .expect("Must exist (we called map_keychain)"); + + self.persist + .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?; + + Ok(AddressInfo { + index, + address: Address::from_script(spk, self.network).expect("must have address form"), + keychain, + }) + } + + /// Reveal addresses up to and including the target `index` and return an iterator + /// of newly revealed addresses. + /// + /// If the target `index` is unreachable, we make a best effort to reveal up to the last + /// possible index. If all addresses up to the given `index` are already revealed, then + /// no new addresses are returned. + /// + /// # Errors + /// + /// If writing to persistent storage fails. + pub fn reveal_addresses_to( + &mut self, + keychain: KeychainKind, + index: u32, + ) -> anyhow::Result + '_> { + let keychain = self.map_keychain(keychain); + let (spk_iter, index_changeset) = self + .indexed_graph + .index + .reveal_to_target(&keychain, index) + .expect("must exist (we called map_keychain)"); + + self.persist + .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?; + + Ok(spk_iter.map(move |(index, spk)| AddressInfo { + index, + address: Address::from_script(&spk, self.network).expect("must have address form"), + keychain, + })) + } + + /// Get the next unused address for the given `keychain`, i.e. the address with the lowest + /// derivation index that hasn't been used. + /// + /// This will attempt to derive and reveal a new address if no newly revealed addresses + /// are available. See also [`reveal_next_address`](Self::reveal_next_address). + /// + /// # Errors + /// + /// If writing to persistent storage fails. + pub fn next_unused_address(&mut self, keychain: KeychainKind) -> anyhow::Result { + let keychain = self.map_keychain(keychain); + let ((index, spk), index_changeset) = self + .indexed_graph + .index + .next_unused_spk(&keychain) + .expect("must exist (we called map_keychain)"); + + self.persist + .stage_and_commit(indexed_tx_graph::ChangeSet::from(index_changeset).into())?; + + Ok(AddressInfo { + index, + address: Address::from_script(spk, self.network).expect("must have address form"), + keychain, + }) + } + + /// Marks an address used of the given `keychain` at `index`. + /// + /// Returns whether the given index was present and then removed from the unused set. + pub fn mark_used(&mut self, keychain: KeychainKind, index: u32) -> bool { + self.indexed_graph.index.mark_used(keychain, index) + } + + /// Undoes the effect of [`mark_used`] and returns whether the `index` was inserted + /// back into the unused set. + /// + /// Since this is only a superficial marker, it will have no effect if the address at the given + /// `index` was actually used, i.e. the wallet has previously indexed a tx output for the + /// derived spk. + /// + /// [`mark_used`]: Self::mark_used + pub fn unmark_used(&mut self, keychain: KeychainKind, index: u32) -> bool { + self.indexed_graph.index.unmark_used(keychain, index) + } + + /// List addresses that are revealed but unused. + /// + /// Note if the returned iterator is empty you can reveal more addresses + /// by using [`reveal_next_address`](Self::reveal_next_address) or + /// [`reveal_addresses_to`](Self::reveal_addresses_to). + pub fn list_unused_addresses( + &self, + keychain: KeychainKind, + ) -> impl DoubleEndedIterator + '_ { + let keychain = self.map_keychain(keychain); + self.indexed_graph + .index + .unused_keychain_spks(&keychain) + .map(move |(index, spk)| AddressInfo { + index, + address: Address::from_script(spk, self.network).expect("must have address form"), + keychain, + }) + } + + /// Return whether or not a `script` is part of this wallet (either internal or external) + pub fn is_mine(&self, script: &Script) -> bool { + self.indexed_graph.index.index_of_spk(script).is_some() + } + + /// Finds how the wallet derived the script pubkey `spk`. + /// + /// Will only return `Some(_)` if the wallet has given out the spk. + pub fn derivation_of_spk(&self, spk: &Script) -> Option<(KeychainKind, u32)> { + self.indexed_graph.index.index_of_spk(spk) + } + + /// Return the list of unspent outputs of this wallet + pub fn list_unspent(&self) -> impl Iterator + '_ { + self.indexed_graph + .graph() + .filter_chain_unspents( + &self.chain, + self.chain.tip().block_id(), + self.indexed_graph.index.outpoints(), + ) + .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) + } + + /// List all relevant outputs (includes both spent and unspent, confirmed and unconfirmed). + /// + /// To list only unspent outputs (UTXOs), use [`Wallet::list_unspent`] instead. + pub fn list_output(&self) -> impl Iterator + '_ { + self.indexed_graph + .graph() + .filter_chain_txouts( + &self.chain, + self.chain.tip().block_id(), + self.indexed_graph.index.outpoints(), + ) + .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) + } + + /// Get all the checkpoints the wallet is currently storing indexed by height. + pub fn checkpoints(&self) -> CheckPointIter { + self.chain.iter_checkpoints() + } + + /// Returns the latest checkpoint. + pub fn latest_checkpoint(&self) -> CheckPoint { + self.chain.tip() + } + + /// Get unbounded script pubkey iterators for both `Internal` and `External` keychains. + /// + /// This is intended to be used when doing a full scan of your addresses (e.g. after restoring + /// from seed words). You pass the `BTreeMap` of iterators to a blockchain data source (e.g. + /// electrum server) which will go through each address until it reaches a *stop gap*. + /// + /// Note carefully that iterators go over **all** script pubkeys on the keychains (not what + /// script pubkeys the wallet is storing internally). + pub fn all_unbounded_spk_iters( + &self, + ) -> BTreeMap + Clone> { + self.indexed_graph.index.all_unbounded_spk_iters() + } + + /// Get an unbounded script pubkey iterator for the given `keychain`. + /// + /// See [`all_unbounded_spk_iters`] for more documentation + /// + /// [`all_unbounded_spk_iters`]: Self::all_unbounded_spk_iters + pub fn unbounded_spk_iter( + &self, + keychain: KeychainKind, + ) -> impl Iterator + Clone { + let keychain = self.map_keychain(keychain); + self.indexed_graph + .index + .unbounded_spk_iter(&keychain) + .expect("Must exist (we called map_keychain)") + } + + /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the + /// wallet's database. + pub fn get_utxo(&self, op: OutPoint) -> Option { + let (keychain, index, _) = self.indexed_graph.index.txout(op)?; + self.indexed_graph + .graph() + .filter_chain_unspents( + &self.chain, + self.chain.tip().block_id(), + core::iter::once(((), op)), + ) + .map(|(_, full_txo)| new_local_utxo(keychain, index, full_txo)) + .next() + } + + /// Inserts a [`TxOut`] at [`OutPoint`] into the wallet's transaction graph. + /// + /// This is used for providing a previous output's value so that we can use [`calculate_fee`] + /// or [`calculate_fee_rate`] on a given transaction. Outputs inserted with this method will + /// not be returned in [`list_unspent`] or [`list_output`]. + /// + /// Any inserted `TxOut`s are not persisted until [`commit`] is called. + /// + /// **WARNING:** This should only be used to add `TxOut`s that the wallet does not own. Only + /// insert `TxOut`s that you trust the values for! + /// + /// [`calculate_fee`]: Self::calculate_fee + /// [`calculate_fee_rate`]: Self::calculate_fee_rate + /// [`list_unspent`]: Self::list_unspent + /// [`list_output`]: Self::list_output + /// [`commit`]: Self::commit + pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) { + let additions = self.indexed_graph.insert_txout(outpoint, txout); + self.persist.stage(ChangeSet::from(additions)); + } + + /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction. + /// + /// To calculate the fee for a [`Transaction`] with inputs not owned by this wallet you must + /// manually insert the TxOut(s) into the tx graph using the [`insert_txout`] function. + /// + /// Note `tx` does not have to be in the graph for this to work. + /// + /// # Examples + /// + /// ```rust, no_run + /// # use bitcoin::Txid; + /// # use bdk_wallet::Wallet; + /// # let mut wallet: Wallet = todo!(); + /// # let txid:Txid = todo!(); + /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + /// let fee = wallet.calculate_fee(&tx).expect("fee"); + /// ``` + /// + /// ```rust, no_run + /// # use bitcoin::Psbt; + /// # use bdk_wallet::Wallet; + /// # let mut wallet: Wallet = todo!(); + /// # let mut psbt: Psbt = todo!(); + /// let tx = &psbt.clone().extract_tx().expect("tx"); + /// let fee = wallet.calculate_fee(tx).expect("fee"); + /// ``` + /// [`insert_txout`]: Self::insert_txout + pub fn calculate_fee(&self, tx: &Transaction) -> Result { + self.indexed_graph.graph().calculate_fee(tx) + } + + /// Calculate the [`FeeRate`] for a given transaction. + /// + /// To calculate the fee rate for a [`Transaction`] with inputs not owned by this wallet you must + /// manually insert the TxOut(s) into the tx graph using the [`insert_txout`] function. + /// + /// Note `tx` does not have to be in the graph for this to work. + /// + /// # Examples + /// + /// ```rust, no_run + /// # use bitcoin::Txid; + /// # use bdk_wallet::Wallet; + /// # let mut wallet: Wallet = todo!(); + /// # let txid:Txid = todo!(); + /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + /// let fee_rate = wallet.calculate_fee_rate(&tx).expect("fee rate"); + /// ``` + /// + /// ```rust, no_run + /// # use bitcoin::Psbt; + /// # use bdk_wallet::Wallet; + /// # let mut wallet: Wallet = todo!(); + /// # let mut psbt: Psbt = todo!(); + /// let tx = &psbt.clone().extract_tx().expect("tx"); + /// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate"); + /// ``` + /// [`insert_txout`]: Self::insert_txout + pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result { + self.calculate_fee(tx) + .map(|fee| Amount::from_sat(fee) / tx.weight()) + } + + /// Compute the `tx`'s sent and received [`Amount`]s. + /// + /// This method returns a tuple `(sent, received)`. Sent is the sum of the txin amounts + /// that spend from previous txouts tracked by this wallet. Received is the summation + /// of this tx's outputs that send to script pubkeys tracked by this wallet. + /// + /// # Examples + /// + /// ```rust, no_run + /// # use bitcoin::Txid; + /// # use bdk_wallet::Wallet; + /// # let mut wallet: Wallet = todo!(); + /// # let txid:Txid = todo!(); + /// let tx = wallet.get_tx(txid).expect("tx exists").tx_node.tx; + /// let (sent, received) = wallet.sent_and_received(&tx); + /// ``` + /// + /// ```rust, no_run + /// # use bitcoin::Psbt; + /// # use bdk_wallet::Wallet; + /// # let mut wallet: Wallet = todo!(); + /// # let mut psbt: Psbt = todo!(); + /// let tx = &psbt.clone().extract_tx().expect("tx"); + /// let (sent, received) = wallet.sent_and_received(tx); + /// ``` + pub fn sent_and_received(&self, tx: &Transaction) -> (Amount, Amount) { + self.indexed_graph.index.sent_and_received(tx, ..) + } + + /// Get a single transaction from the wallet as a [`CanonicalTx`] (if the transaction exists). + /// + /// `CanonicalTx` contains the full transaction alongside meta-data such as: + /// * Blocks that the transaction is [`Anchor`]ed in. These may or may not be blocks that exist + /// in the best chain. + /// * The [`ChainPosition`] of the transaction in the best chain - whether the transaction is + /// confirmed or unconfirmed. If the transaction is confirmed, the anchor which proves the + /// confirmation is provided. If the transaction is unconfirmed, the unix timestamp of when + /// the transaction was last seen in the mempool is provided. + /// + /// ```rust, no_run + /// use bdk_chain::Anchor; + /// use bdk_wallet::{chain::ChainPosition, Wallet}; + /// # let wallet: Wallet = todo!(); + /// # let my_txid: bitcoin::Txid = todo!(); + /// + /// let canonical_tx = wallet.get_tx(my_txid).expect("panic if tx does not exist"); + /// + /// // get reference to full transaction + /// println!("my tx: {:#?}", canonical_tx.tx_node.tx); + /// + /// // list all transaction anchors + /// for anchor in canonical_tx.tx_node.anchors { + /// println!( + /// "tx is anchored by block of hash {}", + /// anchor.anchor_block().hash + /// ); + /// } + /// + /// // get confirmation status of transaction + /// match canonical_tx.chain_position { + /// ChainPosition::Confirmed(anchor) => println!( + /// "tx is confirmed at height {}, we know this since {}:{} is in the best chain", + /// anchor.confirmation_height, anchor.anchor_block.height, anchor.anchor_block.hash, + /// ), + /// ChainPosition::Unconfirmed(last_seen) => println!( + /// "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain", + /// last_seen, + /// ), + /// } + /// ``` + /// + /// [`Anchor`]: bdk_chain::Anchor + pub fn get_tx( + &self, + txid: Txid, + ) -> Option, ConfirmationTimeHeightAnchor>> { + let graph = self.indexed_graph.graph(); + + Some(CanonicalTx { + chain_position: graph.get_chain_position( + &self.chain, + self.chain.tip().block_id(), + txid, + )?, + tx_node: graph.get_tx_node(txid)?, + }) + } + + /// Add a new checkpoint to the wallet's internal view of the chain. + /// This stages but does not [`commit`] the change. + /// + /// Returns whether anything changed with the insertion (e.g. `false` if checkpoint was already + /// there). + /// + /// [`commit`]: Self::commit + pub fn insert_checkpoint( + &mut self, + block_id: BlockId, + ) -> Result { + let changeset = self.chain.insert_block(block_id)?; + let changed = !changeset.is_empty(); + self.persist.stage(changeset.into()); + Ok(changed) + } + + /// Add a transaction to the wallet's internal view of the chain. This stages but does not + /// [`commit`] the change. + /// + /// Returns whether anything changed with the transaction insertion (e.g. `false` if the + /// transaction was already inserted at the same position). + /// + /// A `tx` can be rejected if `position` has a height greater than the [`latest_checkpoint`]. + /// Therefore you should use [`insert_checkpoint`] to insert new checkpoints before manually + /// inserting new transactions. + /// + /// **WARNING:** If `position` is confirmed, we anchor the `tx` to a the lowest checkpoint that + /// is >= the `position`'s height. The caller is responsible for ensuring the `tx` exists in our + /// local view of the best chain's history. + /// + /// [`commit`]: Self::commit + /// [`latest_checkpoint`]: Self::latest_checkpoint + /// [`insert_checkpoint`]: Self::insert_checkpoint + pub fn insert_tx( + &mut self, + tx: Transaction, + position: ConfirmationTime, + ) -> Result { + let (anchor, last_seen) = match position { + ConfirmationTime::Confirmed { height, time } => { + // anchor tx to checkpoint with lowest height that is >= position's height + let anchor = self + .chain + .range(height..) + .last() + .ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip { + tip_height: self.chain.tip().height(), + tx_height: height, + }) + .map(|anchor_cp| ConfirmationTimeHeightAnchor { + anchor_block: anchor_cp.block_id(), + confirmation_height: height, + confirmation_time: time, + })?; + + (Some(anchor), None) + } + ConfirmationTime::Unconfirmed { last_seen } => (None, Some(last_seen)), + }; + + let mut changeset = ChangeSet::default(); + let txid = tx.txid(); + changeset.append(self.indexed_graph.insert_tx(tx).into()); + if let Some(anchor) = anchor { + changeset.append(self.indexed_graph.insert_anchor(txid, anchor).into()); + } + if let Some(last_seen) = last_seen { + changeset.append(self.indexed_graph.insert_seen_at(txid, last_seen).into()); + } + + let changed = !changeset.is_empty(); + self.persist.stage(changeset); + Ok(changed) + } + + /// Iterate over the transactions in the wallet. + pub fn transactions( + &self, + ) -> impl Iterator, ConfirmationTimeHeightAnchor>> + '_ + { + self.indexed_graph + .graph() + .list_chain_txs(&self.chain, self.chain.tip().block_id()) + } + + /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature + /// values. + pub fn get_balance(&self) -> Balance { + self.indexed_graph.graph().balance( + &self.chain, + self.chain.tip().block_id(), + self.indexed_graph.index.outpoints(), + |&(k, _), _| k == KeychainKind::Internal, + ) + } + + /// Add an external signer + /// + /// See [the `signer` module](signer) for an example. + pub fn add_signer( + &mut self, + keychain: KeychainKind, + ordering: SignerOrdering, + signer: Arc, + ) { + let signers = match keychain { + KeychainKind::External => Arc::make_mut(&mut self.signers), + KeychainKind::Internal => Arc::make_mut(&mut self.change_signers), + }; + + signers.add_external(signer.id(&self.secp), ordering, signer); + } + + /// Get the signers + /// + /// ## Example + /// + /// ``` + /// # use bdk_wallet::{Wallet, KeychainKind}; + /// # use bdk_wallet::bitcoin::Network; + /// let wallet = Wallet::new_no_persist("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet)?; + /// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) { + /// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/* + /// println!("secret_key: {}", secret_key); + /// } + /// + /// Ok::<(), Box>(()) + /// ``` + pub fn get_signers(&self, keychain: KeychainKind) -> Arc { + match keychain { + KeychainKind::External => Arc::clone(&self.signers), + KeychainKind::Internal => Arc::clone(&self.change_signers), + } + } + + /// Start building a transaction. + /// + /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction. + /// + /// ## Example + /// + /// ``` + /// # use std::str::FromStr; + /// # use bitcoin::*; + /// # use bdk_wallet::*; + /// # use bdk_wallet::wallet::ChangeSet; + /// # use bdk_wallet::wallet::error::CreateTxError; + /// # use bdk_persist::PersistBackend; + /// # use anyhow::Error; + /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// # let mut wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); + /// let psbt = { + /// let mut builder = wallet.build_tx(); + /// builder + /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); + /// builder.finish()? + /// }; + /// + /// // sign and broadcast ... + /// # Ok::<(), anyhow::Error>(()) + /// ``` + /// + /// [`TxBuilder`]: crate::TxBuilder + pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm, CreateTx> { + TxBuilder { + wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), + params: TxParams::default(), + coin_selection: DefaultCoinSelectionAlgorithm::default(), + phantom: core::marker::PhantomData, + } + } + + pub(crate) fn create_tx( + &mut self, + coin_selection: Cs, + params: TxParams, + ) -> Result { + let keychains: BTreeMap<_, _> = self.indexed_graph.index.keychains().collect(); + let external_descriptor = keychains.get(&KeychainKind::External).expect("must exist"); + let internal_descriptor = keychains.get(&KeychainKind::Internal); + + let external_policy = external_descriptor + .extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)? + .unwrap(); + let internal_policy = internal_descriptor + .as_ref() + .map(|desc| { + Ok::<_, CreateTxError>( + desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? + .unwrap(), + ) + }) + .transpose()?; + + // The policy allows spending external outputs, but it requires a policy path that hasn't been + // provided + if params.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange + && external_policy.requires_path() + && params.external_policy_path.is_none() + { + return Err(CreateTxError::SpendingPolicyRequired( + KeychainKind::External, + )); + }; + // Same for the internal_policy path, if present + if let Some(internal_policy) = &internal_policy { + if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden + && internal_policy.requires_path() + && params.internal_policy_path.is_none() + { + return Err(CreateTxError::SpendingPolicyRequired( + KeychainKind::Internal, + )); + }; + } + + let external_requirements = external_policy.get_condition( + params + .external_policy_path + .as_ref() + .unwrap_or(&BTreeMap::new()), + )?; + let internal_requirements = internal_policy + .map(|policy| { + Ok::<_, CreateTxError>( + policy.get_condition( + params + .internal_policy_path + .as_ref() + .unwrap_or(&BTreeMap::new()), + )?, + ) + }) + .transpose()?; + + let requirements = + external_requirements.merge(&internal_requirements.unwrap_or_default())?; + + let version = match params.version { + Some(tx_builder::Version(0)) => return Err(CreateTxError::Version0), + Some(tx_builder::Version(1)) if requirements.csv.is_some() => { + return Err(CreateTxError::Version1Csv) + } + Some(tx_builder::Version(x)) => x, + None if requirements.csv.is_some() => 2, + None => 1, + }; + + // We use a match here instead of a unwrap_or_else as it's way more readable :) + let current_height = match params.current_height { + // If they didn't tell us the current height, we assume it's the latest sync height. + None => { + let tip_height = self.chain.tip().height(); + absolute::LockTime::from_height(tip_height).expect("invalid height") + } + Some(h) => h, + }; + + let lock_time = match params.locktime { + // When no nLockTime is specified, we try to prevent fee sniping, if possible + None => { + // Fee sniping can be partially prevented by setting the timelock + // to current_height. If we don't know the current_height, + // we default to 0. + let fee_sniping_height = current_height; + + // We choose the biggest between the required nlocktime and the fee sniping + // height + match requirements.timelock { + // No requirement, just use the fee_sniping_height + None => fee_sniping_height, + // There's a block-based requirement, but the value is lower than the fee_sniping_height + Some(value @ absolute::LockTime::Blocks(_)) if value < fee_sniping_height => { + fee_sniping_height + } + // There's a time-based requirement or a block-based requirement greater + // than the fee_sniping_height use that value + Some(value) => value, + } + } + // Specific nLockTime required and we have no constraints, so just set to that value + Some(x) if requirements.timelock.is_none() => x, + // Specific nLockTime required and it's compatible with the constraints + Some(x) + if requirements.timelock.unwrap().is_same_unit(x) + && x >= requirements.timelock.unwrap() => + { + x + } + // Invalid nLockTime required + Some(x) => { + return Err(CreateTxError::LockTime { + requested: x, + required: requirements.timelock.unwrap(), + }) + } + }; + + // The nSequence to be by default for inputs unless an explicit sequence is specified. + let n_sequence = match (params.rbf, requirements.csv) { + // No RBF or CSV but there's an nLockTime, so the nSequence cannot be final + (None, None) if lock_time != absolute::LockTime::ZERO => { + Sequence::ENABLE_LOCKTIME_NO_RBF + } + // No RBF, CSV or nLockTime, make the transaction final + (None, None) => Sequence::MAX, + + // No RBF requested, use the value from CSV. Note that this value is by definition + // non-final, so even if a timelock is enabled this nSequence is fine, hence why we + // don't bother checking for it here. The same is true for all the other branches below + (None, Some(csv)) => csv, + + // RBF with a specific value but that value is too high + (Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => { + return Err(CreateTxError::RbfSequence) + } + // RBF with a specific value requested, but the value is incompatible with CSV + (Some(tx_builder::RbfValue::Value(rbf)), Some(csv)) + if !check_nsequence_rbf(rbf, csv) => + { + return Err(CreateTxError::RbfSequenceCsv { rbf, csv }) + } + + // RBF enabled with the default value with CSV also enabled. CSV takes precedence + (Some(tx_builder::RbfValue::Default), Some(csv)) => csv, + // Valid RBF, either default or with a specific value. We ignore the `CSV` value + // because we've already checked it before + (Some(rbf), _) => rbf.get_value(), + }; + + let (fee_rate, mut fee_amount) = match params.fee_policy.unwrap_or_default() { + //FIXME: see https://github.com/bitcoindevkit/bdk/issues/256 + FeePolicy::FeeAmount(fee) => { + if let Some(previous_fee) = params.bumping_fee { + if fee < previous_fee.absolute { + return Err(CreateTxError::FeeTooLow { + required: previous_fee.absolute, + }); + } + } + (FeeRate::ZERO, fee) + } + FeePolicy::FeeRate(rate) => { + if let Some(previous_fee) = params.bumping_fee { + let required_feerate = FeeRate::from_sat_per_kwu( + previous_fee.rate.to_sat_per_kwu() + + FeeRate::BROADCAST_MIN.to_sat_per_kwu(), // +1 sat/vb + ); + if rate < required_feerate { + return Err(CreateTxError::FeeRateTooLow { + required: required_feerate, + }); + } + } + (rate, 0) + } + }; + + let mut tx = Transaction { + version: transaction::Version::non_standard(version), + lock_time, + input: vec![], + output: vec![], + }; + + if params.manually_selected_only && params.utxos.is_empty() { + return Err(CreateTxError::NoUtxosSelected); + } + + // we keep it as a float while we accumulate it, and only round it at the end + let mut outgoing: u64 = 0; + let mut received: u64 = 0; + + let recipients = params.recipients.iter().map(|(r, v)| (r, *v)); + + for (index, (script_pubkey, value)) in recipients.enumerate() { + if !params.allow_dust + && value.is_dust(script_pubkey) + && !script_pubkey.is_provably_unspendable() + { + return Err(CreateTxError::OutputBelowDustLimit(index)); + } + + if self.is_mine(script_pubkey) { + received += value; + } + + let new_out = TxOut { + script_pubkey: script_pubkey.clone(), + value: Amount::from_sat(value), + }; + + tx.output.push(new_out); + + outgoing += value; + } + + fee_amount += (fee_rate * tx.weight()).to_sat(); + + if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed + && internal_descriptor.is_none() + { + return Err(CreateTxError::ChangePolicyDescriptor); + } + + let (required_utxos, optional_utxos) = + self.preselect_utxos(¶ms, Some(current_height.to_consensus_u32())); + + // get drain script + let drain_script = match params.drain_to { + Some(ref drain_recipient) => drain_recipient.clone(), + None => { + let change_keychain = self.map_keychain(KeychainKind::Internal); + let ((index, spk), index_changeset) = self + .indexed_graph + .index + .next_unused_spk(&change_keychain) + .expect("Keychain exists (we called map_keychain)"); + let spk = spk.into(); + self.indexed_graph.index.mark_used(change_keychain, index); + self.persist + .stage(ChangeSet::from(indexed_tx_graph::ChangeSet::from( + index_changeset, + ))); + self.persist.commit().map_err(CreateTxError::Persist)?; + spk + } + }; + + let (required_utxos, optional_utxos) = + coin_selection::filter_duplicates(required_utxos, optional_utxos); + + let coin_selection = coin_selection.coin_select( + required_utxos, + optional_utxos, + fee_rate, + outgoing + fee_amount, + &drain_script, + )?; + fee_amount += coin_selection.fee_amount; + let excess = &coin_selection.excess; + + tx.input = coin_selection + .selected + .iter() + .map(|u| bitcoin::TxIn { + previous_output: u.outpoint(), + script_sig: ScriptBuf::default(), + sequence: u.sequence().unwrap_or(n_sequence), + witness: Witness::new(), + }) + .collect(); + + if tx.output.is_empty() { + // Uh oh, our transaction has no outputs. + // We allow this when: + // - We have a drain_to address and the utxos we must spend (this happens, + // for example, when we RBF) + // - We have a drain_to address and drain_wallet set + // Otherwise, we don't know who we should send the funds to, and how much + // we should send! + if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) { + if let NoChange { + dust_threshold, + remaining_amount, + change_fee, + } = excess + { + return Err(CreateTxError::InsufficientFunds { + needed: *dust_threshold, + available: remaining_amount.saturating_sub(*change_fee), + }); + } + } else { + return Err(CreateTxError::NoRecipients); + } + } + + match excess { + NoChange { + remaining_amount, .. + } => fee_amount += remaining_amount, + Change { amount, fee } => { + if self.is_mine(&drain_script) { + received += amount; + } + fee_amount += fee; + + // create drain output + let drain_output = TxOut { + value: Amount::from_sat(*amount), + script_pubkey: drain_script, + }; + + // TODO: We should pay attention when adding a new output: this might increase + // the length of the "number of vouts" parameter by 2 bytes, potentially making + // our feerate too low + tx.output.push(drain_output); + } + }; + + // sort input/outputs according to the chosen algorithm + params.ordering.sort_tx(&mut tx); + + let psbt = self.complete_transaction(tx, coin_selection.selected, params)?; + Ok(psbt) + } + + /// Bump the fee of a transaction previously created with this wallet. + /// + /// Returns an error if the transaction is already confirmed or doesn't explicitly signal + /// *replace by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`] + /// pre-populated with the inputs and outputs of the original transaction. + /// + /// ## Example + /// + /// ```no_run + /// # // TODO: remove norun -- bumping fee seems to need the tx in the wallet database first. + /// # use std::str::FromStr; + /// # use bitcoin::*; + /// # use bdk_wallet::*; + /// # use bdk_wallet::wallet::ChangeSet; + /// # use bdk_wallet::wallet::error::CreateTxError; + /// # use bdk_persist::PersistBackend; + /// # use anyhow::Error; + /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// # let mut wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); + /// let mut psbt = { + /// let mut builder = wallet.build_tx(); + /// builder + /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)) + /// .enable_rbf(); + /// builder.finish()? + /// }; + /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; + /// let tx = psbt.clone().extract_tx().expect("tx"); + /// // broadcast tx but it's taking too long to confirm so we want to bump the fee + /// let mut psbt = { + /// let mut builder = wallet.build_fee_bump(tx.txid())?; + /// builder + /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")); + /// builder.finish()? + /// }; + /// + /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; + /// let fee_bumped_tx = psbt.extract_tx(); + /// // broadcast fee_bumped_tx to replace original + /// # Ok::<(), anyhow::Error>(()) + /// ``` + // TODO: support for merging multiple transactions while bumping the fees + pub fn build_fee_bump( + &mut self, + txid: Txid, + ) -> Result, BuildFeeBumpError> { + let graph = self.indexed_graph.graph(); + let txout_index = &self.indexed_graph.index; + let chain_tip = self.chain.tip().block_id(); + + let mut tx = graph + .get_tx(txid) + .ok_or(BuildFeeBumpError::TransactionNotFound(txid))? + .as_ref() + .clone(); + + let pos = graph + .get_chain_position(&self.chain, chain_tip, txid) + .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?; + if let ChainPosition::Confirmed(_) = pos { + return Err(BuildFeeBumpError::TransactionConfirmed(txid)); + } + + if !tx + .input + .iter() + .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD) + { + return Err(BuildFeeBumpError::IrreplaceableTransaction(tx.txid())); + } + + let fee = self + .calculate_fee(&tx) + .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?; + let fee_rate = self + .calculate_fee_rate(&tx) + .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?; + + // remove the inputs from the tx and process them + let original_txin = tx.input.drain(..).collect::>(); + let original_utxos = original_txin + .iter() + .map(|txin| -> Result<_, BuildFeeBumpError> { + let prev_tx = graph + .get_tx(txin.previous_output.txid) + .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?; + let txout = &prev_tx.output[txin.previous_output.vout as usize]; + + let confirmation_time: ConfirmationTime = graph + .get_chain_position(&self.chain, chain_tip, txin.previous_output.txid) + .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))? + .cloned() + .into(); + + let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) { + Some((keychain, derivation_index)) => { + let satisfaction_weight = self + .get_descriptor_for_keychain(keychain) + .max_weight_to_satisfy() + .unwrap(); + WeightedUtxo { + utxo: Utxo::Local(LocalOutput { + outpoint: txin.previous_output, + txout: txout.clone(), + keychain, + is_spent: true, + derivation_index, + confirmation_time, + }), + satisfaction_weight, + } + } + None => { + let satisfaction_weight = + serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(); + WeightedUtxo { + utxo: Utxo::Foreign { + outpoint: txin.previous_output, + sequence: Some(txin.sequence), + psbt_input: Box::new(psbt::Input { + witness_utxo: Some(txout.clone()), + non_witness_utxo: Some(prev_tx.as_ref().clone()), + ..Default::default() + }), + }, + satisfaction_weight, + } + } + }; + + Ok(weighted_utxo) + }) + .collect::, _>>()?; + + if tx.output.len() > 1 { + let mut change_index = None; + for (index, txout) in tx.output.iter().enumerate() { + let change_type = self.map_keychain(KeychainKind::Internal); + match txout_index.index_of_spk(&txout.script_pubkey) { + Some((keychain, _)) if keychain == change_type => change_index = Some(index), + _ => {} + } + } + + if let Some(change_index) = change_index { + tx.output.remove(change_index); + } + } + + let params = TxParams { + // TODO: figure out what rbf option should be? + version: Some(tx_builder::Version(tx.version.0)), + recipients: tx + .output + .into_iter() + .map(|txout| (txout.script_pubkey, txout.value.to_sat())) + .collect(), + utxos: original_utxos, + bumping_fee: Some(tx_builder::PreviousFee { + absolute: fee, + rate: fee_rate, + }), + ..Default::default() + }; + + Ok(TxBuilder { + wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), + params, + coin_selection: DefaultCoinSelectionAlgorithm::default(), + phantom: core::marker::PhantomData, + }) + } + + /// Sign a transaction with all the wallet's signers, in the order specified by every signer's + /// [`SignerOrdering`]. This function returns the `Result` type with an encapsulated `bool` that has the value true if the PSBT was finalized, or false otherwise. + /// + /// 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 + /// + /// ``` + /// # use std::str::FromStr; + /// # use bitcoin::*; + /// # use bdk_wallet::*; + /// # use bdk_wallet::wallet::ChangeSet; + /// # use bdk_wallet::wallet::error::CreateTxError; + /// # use bdk_persist::PersistBackend; + /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// # let mut wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); + /// let mut psbt = { + /// let mut builder = wallet.build_tx(); + /// builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); + /// builder.finish()? + /// }; + /// let finalized = wallet.sign(&mut psbt, SignOptions::default())?; + /// assert!(finalized, "we should have signed all the inputs"); + /// # Ok::<(),anyhow::Error>(()) + pub fn sign(&self, psbt: &mut Psbt, sign_options: SignOptions) -> Result { + // This adds all the PSBT metadata for the inputs, which will help us later figure out how + // to derive our keys + self.update_psbt_with_descriptor(psbt) + .map_err(SignerError::MiniscriptPsbt)?; + + // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones) + // 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()) + .filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none()) + .any(|i| i.non_witness_utxo.is_none()) + { + return Err(SignerError::MissingNonWitnessUtxo); + } + + // If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input + // is using `SIGHASH_ALL` or `SIGHASH_DEFAULT` for taproot + if !sign_options.allow_all_sighashes + && !psbt.inputs.iter().all(|i| { + i.sighash_type.is_none() + || i.sighash_type == Some(EcdsaSighashType::All.into()) + || i.sighash_type == Some(TapSighashType::All.into()) + || i.sighash_type == Some(TapSighashType::Default.into()) + }) + { + return Err(SignerError::NonStandardSighash); + } + + for signer in self + .signers + .signers() + .iter() + .chain(self.change_signers.signers().iter()) + { + signer.sign_transaction(psbt, &sign_options, &self.secp)?; + } + + // attempt to finalize + if sign_options.try_finalize { + self.finalize_psbt(psbt, sign_options) + } else { + Ok(false) + } + } + + /// Return the spending policies for the wallet's descriptor + pub fn policies(&self, keychain: KeychainKind) -> Result, DescriptorError> { + let signers = match keychain { + KeychainKind::External => &self.signers, + KeychainKind::Internal => &self.change_signers, + }; + + match self.public_descriptor(keychain) { + Some(desc) => Ok(desc.extract_policy(signers, BuildSatisfaction::None, &self.secp)?), + None => Ok(None), + } + } + + /// Return the "public" version of the wallet's descriptor, meaning a new descriptor that has + /// the same structure but with every secret key removed + /// + /// This can be used to build a watch-only version of a wallet + pub fn public_descriptor(&self, keychain: KeychainKind) -> Option<&ExtendedDescriptor> { + self.indexed_graph + .index + .keychains() + .find(|(k, _)| *k == &keychain) + .map(|(_, d)| d) + } + + /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass + /// validation and construct the respective `scriptSig` or `scriptWitness`. Please refer to + /// [BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Input_Finalizer) + /// for further information. + /// + /// Returns `true` if the PSBT could be finalized, and `false` otherwise. + /// + /// The [`SignOptions`] can be used to tweak the behavior of the finalizer. + pub fn finalize_psbt( + &self, + psbt: &mut Psbt, + sign_options: SignOptions, + ) -> Result { + let chain_tip = self.chain.tip().block_id(); + + let tx = &psbt.unsigned_tx; + let mut finished = true; + + for (n, input) in tx.input.iter().enumerate() { + let psbt_input = &psbt + .inputs + .get(n) + .ok_or(SignerError::InputIndexOutOfRange)?; + if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() { + continue; + } + let confirmation_height = self + .indexed_graph + .graph() + .get_chain_position(&self.chain, chain_tip, input.previous_output.txid) + .map(|chain_position| match chain_position { + ChainPosition::Confirmed(a) => a.confirmation_height, + ChainPosition::Unconfirmed(_) => u32::MAX, + }); + let current_height = sign_options + .assume_height + .unwrap_or_else(|| self.chain.tip().height()); + + // - Try to derive the descriptor by looking at the txout. If it's in our database, we + // know exactly which `keychain` to use, and which derivation index it is + // - If that fails, try to derive it by looking at the psbt input: the complete logic + // is in `src/descriptor/mod.rs`, but it will basically look at `bip32_derivation`, + // `redeem_script` and `witness_script` to determine the right derivation + // - If that also fails, it will try it on the internal descriptor, if present + let desc = psbt + .get_utxo_for(n) + .and_then(|txout| self.get_descriptor_for_txout(&txout)) + .or_else(|| { + self.indexed_graph.index.keychains().find_map(|(_, desc)| { + desc.derive_from_psbt_input(psbt_input, psbt.get_utxo_for(n), &self.secp) + }) + }); + + match desc { + Some(desc) => { + let mut tmp_input = bitcoin::TxIn::default(); + match desc.satisfy( + &mut tmp_input, + ( + PsbtInputSatisfier::new(psbt, n), + After::new(Some(current_height), false), + Older::new(Some(current_height), confirmation_height, false), + ), + ) { + Ok(_) => { + let psbt_input = &mut psbt.inputs[n]; + psbt_input.final_script_sig = Some(tmp_input.script_sig); + psbt_input.final_script_witness = Some(tmp_input.witness); + if sign_options.remove_partial_sigs { + psbt_input.partial_sigs.clear(); + } + if sign_options.remove_taproot_extras { + // We just constructed the final witness, clear these fields. + psbt_input.tap_key_sig = None; + psbt_input.tap_script_sigs.clear(); + psbt_input.tap_scripts.clear(); + psbt_input.tap_key_origins.clear(); + psbt_input.tap_internal_key = None; + psbt_input.tap_merkle_root = None; + } + } + Err(_) => finished = false, + } + } + None => finished = false, + } + } + + if finished && sign_options.remove_taproot_extras { + for output in &mut psbt.outputs { + output.tap_key_origins.clear(); + } + } + + Ok(finished) + } + + /// Return the secp256k1 context used for all signing operations + pub fn secp_ctx(&self) -> &SecpCtx { + &self.secp + } + + /// Returns the descriptor used to create addresses for a particular `keychain`. + pub fn get_descriptor_for_keychain(&self, keychain: KeychainKind) -> &ExtendedDescriptor { + self.public_descriptor(self.map_keychain(keychain)) + .expect("we mapped it to external if it doesn't exist") + } + + /// The derivation index of this wallet. It will return `None` if it has not derived any addresses. + /// Otherwise, it will return the index of the highest address it has derived. + pub fn derivation_index(&self, keychain: KeychainKind) -> Option { + self.indexed_graph.index.last_revealed_index(&keychain) + } + + /// The index of the next address that you would get if you were to ask the wallet for a new address + pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 { + let keychain = self.map_keychain(keychain); + self.indexed_graph + .index + .next_index(&keychain) + .expect("Keychain must exist (we called map_keychain)") + .0 + } + + /// Informs the wallet that you no longer intend to broadcast a tx that was built from it. + /// + /// This frees up the change address used when creating the tx for use in future transactions. + // TODO: Make this free up reserved utxos when that's implemented + pub fn cancel_tx(&mut self, tx: &Transaction) { + let txout_index = &mut self.indexed_graph.index; + for txout in &tx.output { + if let Some((keychain, index)) = txout_index.index_of_spk(&txout.script_pubkey) { + // NOTE: unmark_used will **not** make something unused if it has actually been used + // by a tx in the tracker. It only removes the superficial marking. + txout_index.unmark_used(keychain, index); + } + } + } + + fn map_keychain(&self, keychain: KeychainKind) -> KeychainKind { + if keychain == KeychainKind::Internal + && self.public_descriptor(KeychainKind::Internal).is_none() + { + KeychainKind::External + } else { + keychain + } + } + + fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { + let (keychain, child) = self + .indexed_graph + .index + .index_of_spk(&txout.script_pubkey)?; + let descriptor = self.get_descriptor_for_keychain(keychain); + descriptor.at_derivation_index(child).ok() + } + + fn get_available_utxos(&self) -> Vec<(LocalOutput, usize)> { + self.list_unspent() + .map(|utxo| { + let keychain = utxo.keychain; + (utxo, { + self.get_descriptor_for_keychain(keychain) + .max_weight_to_satisfy() + .unwrap() + }) + }) + .collect() + } + + /// Given the options returns the list of utxos that must be used to form the + /// transaction and any further that may be used if needed. + fn preselect_utxos( + &self, + params: &TxParams, + current_height: Option, + ) -> (Vec, Vec) { + let TxParams { + change_policy, + unspendable, + utxos, + drain_wallet, + manually_selected_only, + bumping_fee, + .. + } = params; + + let manually_selected = utxos.clone(); + // we mandate confirmed transactions if we're bumping the fee + let must_only_use_confirmed_tx = bumping_fee.is_some(); + let must_use_all_available = *drain_wallet; + + let chain_tip = self.chain.tip().block_id(); + // must_spend <- manually selected utxos + // may_spend <- all other available utxos + let mut may_spend = self.get_available_utxos(); + + may_spend.retain(|may_spend| { + !manually_selected + .iter() + .any(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint) + }); + let mut must_spend = manually_selected; + + // NOTE: we are intentionally ignoring `unspendable` here. i.e manual + // selection overrides unspendable. + if *manually_selected_only { + return (must_spend, vec![]); + } + + let satisfies_confirmed = may_spend + .iter() + .map(|u| -> bool { + let txid = u.0.outpoint.txid; + let tx = match self.indexed_graph.graph().get_tx(txid) { + Some(tx) => tx, + None => return false, + }; + let confirmation_time: ConfirmationTime = match self + .indexed_graph + .graph() + .get_chain_position(&self.chain, chain_tip, txid) + { + Some(chain_position) => chain_position.cloned().into(), + None => return false, + }; + + // Whether the UTXO is mature and, if needed, confirmed + let mut spendable = true; + if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() { + return false; + } + if tx.is_coinbase() { + debug_assert!( + confirmation_time.is_confirmed(), + "coinbase must always be confirmed" + ); + if let Some(current_height) = current_height { + match confirmation_time { + ConfirmationTime::Confirmed { height, .. } => { + // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375 + spendable &= + (current_height.saturating_sub(height)) >= COINBASE_MATURITY; + } + ConfirmationTime::Unconfirmed { .. } => spendable = false, + } + } + } + spendable + }) + .collect::>(); + + let mut i = 0; + may_spend.retain(|u| { + let retain = change_policy.is_satisfied_by(&u.0) + && !unspendable.contains(&u.0.outpoint) + && satisfies_confirmed[i]; + i += 1; + retain + }); + + let mut may_spend = may_spend + .into_iter() + .map(|(local_utxo, satisfaction_weight)| WeightedUtxo { + satisfaction_weight, + utxo: Utxo::Local(local_utxo), + }) + .collect(); + + if must_use_all_available { + must_spend.append(&mut may_spend); + } + + (must_spend, may_spend) + } + + fn complete_transaction( + &self, + tx: Transaction, + selected: Vec, + params: TxParams, + ) -> Result { + let mut psbt = Psbt::from_unsigned_tx(tx)?; + + if params.add_global_xpubs { + let all_xpubs = self + .keychains() + .flat_map(|(_, desc)| desc.get_extended_keys()) + .collect::>(); + + for xpub in all_xpubs { + let origin = match xpub.origin { + Some(origin) => origin, + None if xpub.xkey.depth == 0 => { + (xpub.root_fingerprint(&self.secp), vec![].into()) + } + _ => return Err(CreateTxError::MissingKeyOrigin(xpub.xkey.to_string())), + }; + + psbt.xpub.insert(xpub.xkey, origin); + } + } + + let mut lookup_output = selected + .into_iter() + .map(|utxo| (utxo.outpoint(), utxo)) + .collect::>(); + + // add metadata for the inputs + for (psbt_input, input) in psbt.inputs.iter_mut().zip(psbt.unsigned_tx.input.iter()) { + let utxo = match lookup_output.remove(&input.previous_output) { + Some(utxo) => utxo, + None => continue, + }; + + match utxo { + Utxo::Local(utxo) => { + *psbt_input = + match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) { + Ok(psbt_input) => psbt_input, + Err(e) => match e { + CreateTxError::UnknownUtxo => psbt::Input { + sighash_type: params.sighash, + ..psbt::Input::default() + }, + _ => return Err(e), + }, + } + } + Utxo::Foreign { + outpoint, + psbt_input: foreign_psbt_input, + .. + } => { + let is_taproot = foreign_psbt_input + .witness_utxo + .as_ref() + .map(|txout| txout.script_pubkey.is_p2tr()) + .unwrap_or(false); + if !is_taproot + && !params.only_witness_utxo + && foreign_psbt_input.non_witness_utxo.is_none() + { + return Err(CreateTxError::MissingNonWitnessUtxo(outpoint)); + } + *psbt_input = *foreign_psbt_input; + } + } + } + + self.update_psbt_with_descriptor(&mut psbt)?; + + Ok(psbt) + } + + /// get the corresponding PSBT Input for a LocalUtxo + pub fn get_psbt_input( + &self, + utxo: LocalOutput, + sighash_type: Option, + only_witness_utxo: bool, + ) -> Result { + // Try to find the prev_script in our db to figure out if this is internal or external, + // and the derivation index + let (keychain, child) = self + .indexed_graph + .index + .index_of_spk(&utxo.txout.script_pubkey) + .ok_or(CreateTxError::UnknownUtxo)?; + + let mut psbt_input = psbt::Input { + sighash_type, + ..psbt::Input::default() + }; + + let desc = self.get_descriptor_for_keychain(keychain); + let derived_descriptor = desc + .at_derivation_index(child) + .expect("child can't be hardened"); + + psbt_input + .update_with_descriptor_unchecked(&derived_descriptor) + .map_err(MiniscriptPsbtError::Conversion)?; + + let prev_output = utxo.outpoint; + if let Some(prev_tx) = self.indexed_graph.graph().get_tx(prev_output.txid) { + if desc.is_witness() || desc.is_taproot() { + psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); + } + if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) { + psbt_input.non_witness_utxo = Some(prev_tx.as_ref().clone()); + } + } + Ok(psbt_input) + } + + fn update_psbt_with_descriptor(&self, psbt: &mut Psbt) -> Result<(), MiniscriptPsbtError> { + // We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all + // the input utxos and outputs + let utxos = (0..psbt.inputs.len()) + .filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo))) + .chain( + psbt.unsigned_tx + .output + .iter() + .enumerate() + .map(|(i, out)| (false, i, out.clone())), + ) + .collect::>(); + + // Try to figure out the keychain and derivation for every input and output + for (is_input, index, out) in utxos.into_iter() { + if let Some((keychain, child)) = + self.indexed_graph.index.index_of_spk(&out.script_pubkey) + { + let desc = self.get_descriptor_for_keychain(keychain); + let desc = desc + .at_derivation_index(child) + .expect("child can't be hardened"); + + if is_input { + psbt.update_input_with_descriptor(index, &desc) + .map_err(MiniscriptPsbtError::UtxoUpdate)?; + } else { + psbt.update_output_with_descriptor(index, &desc) + .map_err(MiniscriptPsbtError::OutputUpdate)?; + } + } + } + + Ok(()) + } + + /// Return the checksum of the public descriptor associated to `keychain` + /// + /// Internally calls [`Self::get_descriptor_for_keychain`] to fetch the right descriptor + pub fn descriptor_checksum(&self, keychain: KeychainKind) -> String { + self.get_descriptor_for_keychain(keychain) + .to_string() + .split_once('#') + .unwrap() + .1 + .to_string() + } + + /// Applies an update to the wallet and stages the changes (but does not [`commit`] them). + /// + /// Usually you create an `update` by interacting with some blockchain data source and inserting + /// transactions related to your wallet into it. + /// + /// [`commit`]: Self::commit + pub fn apply_update(&mut self, update: impl Into) -> Result<(), CannotConnectError> { + let update = update.into(); + let mut changeset = match update.chain { + Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?), + None => ChangeSet::default(), + }; + + let (_, index_changeset) = self + .indexed_graph + .index + .reveal_to_target_multi(&update.last_active_indices); + changeset.append(ChangeSet::from(indexed_tx_graph::ChangeSet::from( + index_changeset, + ))); + changeset.append(ChangeSet::from( + self.indexed_graph.apply_update(update.graph), + )); + self.persist.stage(changeset); + Ok(()) + } + + /// Commits all currently [`staged`] changed to the persistence backend returning and error when + /// this fails. + /// + /// This returns whether the `update` resulted in any changes. + /// + /// [`staged`]: Self::staged + pub fn commit(&mut self) -> anyhow::Result { + self.persist.commit().map(|c| c.is_some()) + } + + /// Returns the changes that will be committed with the next call to [`commit`]. + /// + /// [`commit`]: Self::commit + pub fn staged(&self) -> &ChangeSet { + self.persist.staged() + } + + /// Get a reference to the inner [`TxGraph`]. + pub fn tx_graph(&self) -> &TxGraph { + self.indexed_graph.graph() + } + + /// Get a reference to the inner [`KeychainTxOutIndex`]. + pub fn spk_index(&self) -> &KeychainTxOutIndex { + &self.indexed_graph.index + } + + /// Get a reference to the inner [`LocalChain`]. + pub fn local_chain(&self) -> &LocalChain { + &self.chain + } + + /// Introduces a `block` of `height` to the wallet, and tries to connect it to the + /// `prev_blockhash` of the block's header. + /// + /// This is a convenience method that is equivalent to calling [`apply_block_connected_to`] + /// with `prev_blockhash` and `height-1` as the `connected_to` parameter. + /// + /// [`apply_block_connected_to`]: Self::apply_block_connected_to + pub fn apply_block(&mut self, block: &Block, height: u32) -> Result<(), CannotConnectError> { + let connected_to = match height.checked_sub(1) { + Some(prev_height) => BlockId { + height: prev_height, + hash: block.header.prev_blockhash, + }, + None => BlockId { + height, + hash: block.block_hash(), + }, + }; + self.apply_block_connected_to(block, height, connected_to) + .map_err(|err| match err { + ApplyHeaderError::InconsistentBlocks => { + unreachable!("connected_to is derived from the block so must be consistent") + } + ApplyHeaderError::CannotConnect(err) => err, + }) + } + + /// Applies relevant transactions from `block` of `height` to the wallet, and connects the + /// block to the internal chain. + /// + /// The `connected_to` parameter informs the wallet how this block connects to the internal + /// [`LocalChain`]. Relevant transactions are filtered from the `block` and inserted into the + /// internal [`TxGraph`]. + pub fn apply_block_connected_to( + &mut self, + block: &Block, + height: u32, + connected_to: BlockId, + ) -> Result<(), ApplyHeaderError> { + let mut changeset = ChangeSet::default(); + changeset.append( + self.chain + .apply_header_connected_to(&block.header, height, connected_to)? + .into(), + ); + changeset.append( + self.indexed_graph + .apply_block_relevant(block, height) + .into(), + ); + self.persist.stage(changeset); + Ok(()) + } + + /// Apply relevant unconfirmed transactions to the wallet. + /// + /// Transactions that are not relevant are filtered out. + /// + /// This method takes in an iterator of `(tx, last_seen)` where `last_seen` is the timestamp of + /// when the transaction was last seen in the mempool. This is used for conflict resolution + /// when there is conflicting unconfirmed transactions. The transaction with the later + /// `last_seen` is prioritized. + pub fn apply_unconfirmed_txs<'t>( + &mut self, + unconfirmed_txs: impl IntoIterator, + ) { + let indexed_graph_changeset = self + .indexed_graph + .batch_insert_relevant_unconfirmed(unconfirmed_txs); + self.persist.stage(ChangeSet::from(indexed_graph_changeset)); + } +} + +/// Methods to construct sync/full-scan requests for spk-based chain sources. +impl Wallet { + /// Create a partial [`SyncRequest`] for this wallet for all revealed spks. + /// + /// This is the first step when performing a spk-based wallet partial sync, the returned + /// [`SyncRequest`] collects all revealed script pubkeys from the wallet keychain needed to + /// start a blockchain sync with a spk based blockchain client. + pub fn start_sync_with_revealed_spks(&self) -> SyncRequest { + SyncRequest::from_chain_tip(self.chain.tip()) + .cache_graph_txs(self.tx_graph()) + .populate_with_revealed_spks(&self.indexed_graph.index, ..) + } + + /// Create a [`FullScanRequest] for this wallet. + /// + /// This is the first step when performing a spk-based wallet full scan, the returned + /// [`FullScanRequest] collects iterators for the wallet's keychain script pub keys needed to + /// start a blockchain full scan with a spk based blockchain client. + /// + /// This operation is generally only used when importing or restoring a previously used wallet + /// in which the list of used scripts is not known. + pub fn start_full_scan(&self) -> FullScanRequest { + FullScanRequest::from_keychain_txout_index(self.chain.tip(), &self.indexed_graph.index) + .cache_graph_txs(self.tx_graph()) + } +} + +impl AsRef> for Wallet { + fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { + self.indexed_graph.graph() + } +} + +/// Deterministically generate a unique name given the descriptors defining the wallet +/// +/// Compatible with [`wallet_name_from_descriptor`] +pub fn wallet_name_from_descriptor( + descriptor: T, + change_descriptor: Option, + network: Network, + secp: &SecpCtx, +) -> Result +where + T: IntoWalletDescriptor, +{ + //TODO check descriptors contains only public keys + let descriptor = descriptor + .into_wallet_descriptor(secp, network)? + .0 + .to_string(); + let mut wallet_name = calc_checksum(&descriptor[..descriptor.find('#').unwrap()])?; + if let Some(change_descriptor) = change_descriptor { + let change_descriptor = change_descriptor + .into_wallet_descriptor(secp, network)? + .0 + .to_string(); + wallet_name.push_str( + calc_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(), + ); + } + + Ok(wallet_name) +} + +fn new_local_utxo( + keychain: KeychainKind, + derivation_index: u32, + full_txo: FullTxOut, +) -> LocalOutput { + LocalOutput { + outpoint: full_txo.outpoint, + txout: full_txo.txout, + is_spent: full_txo.spent_by.is_some(), + confirmation_time: full_txo.chain_position.into(), + keychain, + derivation_index, + } +} + +fn create_signers( + index: &mut KeychainTxOutIndex, + secp: &Secp256k1, + descriptor: E, + change_descriptor: Option, + network: Network, +) -> Result<(Arc, Arc), crate::descriptor::error::Error> { + let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?; + let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp)); + let _ = index.insert_descriptor(KeychainKind::External, descriptor); + + let change_signers = match change_descriptor { + Some(descriptor) => { + let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?; + let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp)); + let _ = index.insert_descriptor(KeychainKind::Internal, descriptor); + signers + } + None => Arc::new(SignersContainer::new()), + }; + + Ok((signers, change_signers)) +} + +/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb. +#[macro_export] +#[doc(hidden)] +macro_rules! floating_rate { + ($rate:expr) => {{ + use $crate::bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; + // sat_kwu / 250.0 -> sat_vb + $rate.to_sat_per_kwu() as f64 / ((1000 / WITNESS_SCALE_FACTOR) as f64) + }}; +} + +#[macro_export] +#[doc(hidden)] +/// Macro for getting a wallet for use in a doctest +macro_rules! doctest_wallet { + () => {{ + use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash}; + use $crate::chain::{ConfirmationTime, BlockId}; + use $crate::{KeychainKind, wallet::Wallet}; + let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; + let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)"; + + let mut wallet = Wallet::new_no_persist( + descriptor, + Some(change_descriptor), + Network::Regtest, + ) + .unwrap(); + let address = wallet.peek_address(KeychainKind::External, 0).address; + let tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + value: Amount::from_sat(500_000), + script_pubkey: address.script_pubkey(), + }], + }; + let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() }); + let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed { + height: 500, + time: 50_000 + }); + + wallet + }} +} diff --git a/crates/wallet/src/wallet/signer.rs b/crates/wallet/src/wallet/signer.rs new file mode 100644 index 00000000..51420caa --- /dev/null +++ b/crates/wallet/src/wallet/signer.rs @@ -0,0 +1,1193 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Generalized signers +//! +//! This module provides the ability to add customized signers to a [`Wallet`](super::Wallet) +//! through the [`Wallet::add_signer`](super::Wallet::add_signer) function. +//! +//! ``` +//! # use alloc::sync::Arc; +//! # use core::str::FromStr; +//! # use bitcoin::secp256k1::{Secp256k1, All}; +//! # use bitcoin::*; +//! # use bdk_wallet::signer::*; +//! # use bdk_wallet::*; +//! # #[derive(Debug)] +//! # struct CustomHSM; +//! # impl CustomHSM { +//! # fn hsm_sign_input(&self, _psbt: &mut Psbt, _input: usize) -> Result<(), SignerError> { +//! # Ok(()) +//! # } +//! # fn connect() -> Self { +//! # CustomHSM +//! # } +//! # fn get_id(&self) -> SignerId { +//! # SignerId::Dummy(0) +//! # } +//! # } +//! #[derive(Debug)] +//! struct CustomSigner { +//! device: CustomHSM, +//! } +//! +//! impl CustomSigner { +//! fn connect() -> Self { +//! CustomSigner { device: CustomHSM::connect() } +//! } +//! } +//! +//! impl SignerCommon for CustomSigner { +//! fn id(&self, _secp: &Secp256k1) -> SignerId { +//! self.device.get_id() +//! } +//! } +//! +//! impl InputSigner for CustomSigner { +//! fn sign_input( +//! &self, +//! psbt: &mut Psbt, +//! input_index: usize, +//! _sign_options: &SignOptions, +//! _secp: &Secp256k1, +//! ) -> Result<(), SignerError> { +//! self.device.hsm_sign_input(psbt, input_index)?; +//! +//! Ok(()) +//! } +//! } +//! +//! let custom_signer = CustomSigner::connect(); +//! +//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; +//! let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet)?; +//! wallet.add_signer( +//! KeychainKind::External, +//! SignerOrdering(200), +//! Arc::new(custom_signer) +//! ); +//! +//! # Ok::<_, anyhow::Error>(()) +//! ``` + +use crate::collections::BTreeMap; +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec::Vec; +use core::cmp::Ordering; +use core::fmt; +use core::ops::{Bound::Included, Deref}; + +use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, Xpriv}; +use bitcoin::hashes::hash160; +use bitcoin::secp256k1::Message; +use bitcoin::sighash::{EcdsaSighashType, TapSighash, TapSighashType}; +use bitcoin::{ecdsa, psbt, sighash, taproot}; +use bitcoin::{key::TapTweak, key::XOnlyPublicKey, secp256k1}; +use bitcoin::{PrivateKey, Psbt, PublicKey}; + +use miniscript::descriptor::{ + Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, + InnerXKey, KeyMap, SinglePriv, SinglePubKey, +}; +use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey}; + +use super::utils::SecpCtx; +use crate::descriptor::{DescriptorMeta, XKeyUtils}; +use crate::psbt::PsbtUtils; +use crate::wallet::error::MiniscriptPsbtError; + +/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among +/// multiple of them +#[derive(Debug, Clone, Ord, PartialOrd, PartialEq, Eq, Hash)] +pub enum SignerId { + /// Bitcoin HASH160 (RIPEMD160 after SHA256) hash of an ECDSA public key + PkHash(hash160::Hash), + /// The fingerprint of a BIP32 extended key + Fingerprint(Fingerprint), + /// Dummy identifier + Dummy(u64), +} + +impl From for SignerId { + fn from(hash: hash160::Hash) -> SignerId { + SignerId::PkHash(hash) + } +} + +impl From for SignerId { + fn from(fing: Fingerprint) -> SignerId { + SignerId::Fingerprint(fing) + } +} + +/// Signing error +#[derive(Debug)] +pub enum SignerError { + /// The private key is missing for the required public key + MissingKey, + /// The private key in use has the right fingerprint but derives differently than expected + InvalidKey, + /// The user canceled the operation + UserCanceled, + /// Input index is out of range + InputIndexOutOfRange, + /// The `non_witness_utxo` field of the transaction is required to sign this input + MissingNonWitnessUtxo, + /// The `non_witness_utxo` specified is invalid + InvalidNonWitnessUtxo, + /// The `witness_utxo` field of the transaction is required to sign this input + MissingWitnessUtxo, + /// The `witness_script` field of the transaction is required to sign this input + MissingWitnessScript, + /// The fingerprint and derivation path are missing from the psbt input + MissingHdKeypath, + /// The psbt contains a non-`SIGHASH_ALL` sighash in one of its input and the user hasn't + /// explicitly allowed them + /// + /// To enable signing transactions with non-standard sighashes set + /// [`SignOptions::allow_all_sighashes`] to `true`. + NonStandardSighash, + /// Invalid SIGHASH for the signing context in use + InvalidSighash, + /// Error while computing the hash to sign + SighashError(sighash::Error), + /// Miniscript PSBT error + MiniscriptPsbt(MiniscriptPsbtError), + /// To be used only by external libraries implementing [`InputSigner`] or + /// [`TransactionSigner`], so that they can return their own custom errors, without having to + /// modify [`SignerError`] in BDK. + External(String), +} + +impl From for SignerError { + fn from(e: sighash::Error) -> Self { + SignerError::SighashError(e) + } +} + +impl fmt::Display for SignerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingKey => write!(f, "Missing private key"), + Self::InvalidKey => write!(f, "The private key in use has the right fingerprint but derives differently than expected"), + Self::UserCanceled => write!(f, "The user canceled the operation"), + Self::InputIndexOutOfRange => write!(f, "Input index out of range"), + Self::MissingNonWitnessUtxo => write!(f, "Missing non-witness UTXO"), + Self::InvalidNonWitnessUtxo => write!(f, "Invalid non-witness UTXO"), + Self::MissingWitnessUtxo => write!(f, "Missing witness UTXO"), + Self::MissingWitnessScript => write!(f, "Missing witness script"), + Self::MissingHdKeypath => write!(f, "Missing fingerprint and derivation path"), + Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"), + Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"), + Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err), + Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err), + Self::External(err) => write!(f, "{}", err), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for SignerError {} + +/// Signing context +/// +/// Used by our software signers to determine the type of signatures to make +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignerContext { + /// Legacy context + Legacy, + /// Segwit v0 context (BIP 143) + Segwitv0, + /// Taproot context (BIP 340) + Tap { + /// Whether the signer can sign for the internal key or not + is_internal_key: bool, + }, +} + +/// Wrapper to pair a signer with its context +#[derive(Debug, Clone)] +pub struct SignerWrapper { + signer: S, + ctx: SignerContext, +} + +impl SignerWrapper { + /// Create a wrapped signer from a signer and a context + pub fn new(signer: S, ctx: SignerContext) -> Self { + SignerWrapper { signer, ctx } + } +} + +impl Deref for SignerWrapper { + type Target = S; + + fn deref(&self) -> &Self::Target { + &self.signer + } +} + +/// Common signer methods +pub trait SignerCommon: fmt::Debug + Send + Sync { + /// Return the [`SignerId`] for this signer + /// + /// The [`SignerId`] can be used to lookup a signer in the [`Wallet`](crate::Wallet)'s signers map or to + /// compare two signers. + fn id(&self, secp: &SecpCtx) -> SignerId; + + /// Return the secret key for the signer + /// + /// This is used internally to reconstruct the original descriptor that may contain secrets. + /// External signers that are meant to keep key isolated should just return `None` here (which + /// is the default for this method, if not overridden). + fn descriptor_secret_key(&self) -> Option { + None + } +} + +/// PSBT Input signer +/// +/// This trait can be implemented to provide custom signers to the wallet. If the signer supports signing +/// individual inputs, this trait should be implemented and BDK will provide automatically an implementation +/// for [`TransactionSigner`]. +pub trait InputSigner: SignerCommon { + /// Sign a single psbt input + fn sign_input( + &self, + psbt: &mut Psbt, + input_index: usize, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> Result<(), SignerError>; +} + +/// PSBT signer +/// +/// This trait can be implemented when the signer can't sign inputs individually, but signs the whole transaction +/// at once. +pub trait TransactionSigner: SignerCommon { + /// Sign all the inputs of the psbt + fn sign_transaction( + &self, + psbt: &mut Psbt, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> Result<(), SignerError>; +} + +impl TransactionSigner for T { + fn sign_transaction( + &self, + psbt: &mut Psbt, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> Result<(), SignerError> { + for input_index in 0..psbt.inputs.len() { + self.sign_input(psbt, input_index, sign_options, secp)?; + } + + Ok(()) + } +} + +impl SignerCommon for SignerWrapper> { + fn id(&self, secp: &SecpCtx) -> SignerId { + SignerId::from(self.root_fingerprint(secp)) + } + + fn descriptor_secret_key(&self) -> Option { + Some(DescriptorSecretKey::XPrv(self.signer.clone())) + } +} + +impl InputSigner for SignerWrapper> { + fn sign_input( + &self, + psbt: &mut Psbt, + input_index: usize, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> Result<(), SignerError> { + if input_index >= psbt.inputs.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 tap_key_origins = psbt.inputs[input_index] + .tap_key_origins + .iter() + .map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource)); + let (public_key, full_path) = match psbt.inputs[input_index] + .bip32_derivation + .iter() + .map(|(pk, keysource)| (SinglePubKey::FullKey(PublicKey::new(*pk)), keysource)) + .chain(tap_key_origins) + .find_map(|(pk, keysource)| { + if self.matches(keysource, secp).is_some() { + Some((pk, keysource.1.clone())) + } else { + None + } + }) { + Some((pk, full_path)) => (pk, full_path), + None => return Ok(()), + }; + + let derived_key = match self.origin.clone() { + Some((_fingerprint, origin_path)) => { + let deriv_path = DerivationPath::from( + &full_path.into_iter().cloned().collect::>() + [origin_path.len()..], + ); + self.xkey.derive_priv(secp, &deriv_path).unwrap() + } + None => self.xkey.derive_priv(secp, &full_path).unwrap(), + }; + + let computed_pk = secp256k1::PublicKey::from_secret_key(secp, &derived_key.private_key); + let valid_key = match public_key { + SinglePubKey::FullKey(pk) if pk.inner == computed_pk => true, + SinglePubKey::XOnly(x_only) if XOnlyPublicKey::from(computed_pk) == x_only => true, + _ => false, + }; + if !valid_key { + Err(SignerError::InvalidKey) + } else { + // HD wallets imply compressed keys + let priv_key = PrivateKey { + compressed: true, + network: self.xkey.network, + inner: derived_key.private_key, + }; + + SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, sign_options, secp) + } + } +} + +fn multikey_to_xkeys( + multikey: DescriptorMultiXKey, +) -> Vec> { + multikey + .derivation_paths + .into_paths() + .into_iter() + .map(|derivation_path| DescriptorXKey { + origin: multikey.origin.clone(), + xkey: multikey.xkey.clone(), + derivation_path, + wildcard: multikey.wildcard, + }) + .collect() +} + +impl SignerCommon for SignerWrapper> { + fn id(&self, secp: &SecpCtx) -> SignerId { + SignerId::from(self.root_fingerprint(secp)) + } + + fn descriptor_secret_key(&self) -> Option { + Some(DescriptorSecretKey::MultiXPrv(self.signer.clone())) + } +} + +impl InputSigner for SignerWrapper> { + fn sign_input( + &self, + psbt: &mut Psbt, + input_index: usize, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> Result<(), SignerError> { + let xkeys = multikey_to_xkeys(self.signer.clone()); + for xkey in xkeys { + SignerWrapper::new(xkey, self.ctx).sign_input(psbt, input_index, sign_options, secp)? + } + Ok(()) + } +} + +impl SignerCommon for SignerWrapper { + fn id(&self, secp: &SecpCtx) -> SignerId { + SignerId::from(self.public_key(secp).to_pubkeyhash(SigType::Ecdsa)) + } + + fn descriptor_secret_key(&self) -> Option { + Some(DescriptorSecretKey::Single(SinglePriv { + key: self.signer, + origin: None, + })) + } +} + +impl InputSigner for SignerWrapper { + fn sign_input( + &self, + psbt: &mut Psbt, + input_index: usize, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> Result<(), SignerError> { + if input_index >= psbt.inputs.len() || input_index >= psbt.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 = PublicKey::from_private_key(secp, self); + let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner); + + if let SignerContext::Tap { is_internal_key } = self.ctx { + if let Some(psbt_internal_key) = psbt.inputs[input_index].tap_internal_key { + if is_internal_key + && psbt.inputs[input_index].tap_key_sig.is_none() + && sign_options.sign_with_tap_internal_key + && x_only_pubkey == psbt_internal_key + { + let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?; + sign_psbt_schnorr( + &self.inner, + x_only_pubkey, + None, + &mut psbt.inputs[input_index], + hash, + hash_ty, + secp, + ); + } + } + + if let Some((leaf_hashes, _)) = + psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey) + { + let leaf_hashes = leaf_hashes + .iter() + .filter(|lh| { + // Removing the leaves we shouldn't sign for + let should_sign = match &sign_options.tap_leaves_options { + TapLeavesOptions::All => true, + TapLeavesOptions::Include(v) => v.contains(lh), + TapLeavesOptions::Exclude(v) => !v.contains(lh), + TapLeavesOptions::None => false, + }; + // Filtering out the leaves without our key + should_sign + && !psbt.inputs[input_index] + .tap_script_sigs + .contains_key(&(x_only_pubkey, **lh)) + }) + .cloned() + .collect::>(); + for lh in leaf_hashes { + let (hash, hash_ty) = Tap::sighash(psbt, input_index, Some(lh))?; + sign_psbt_schnorr( + &self.inner, + x_only_pubkey, + Some(lh), + &mut psbt.inputs[input_index], + hash, + hash_ty, + secp, + ); + } + } + + return Ok(()); + } + + if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) { + return Ok(()); + } + + let (hash, hash_ty) = match self.ctx { + SignerContext::Segwitv0 => { + let (h, t) = Segwitv0::sighash(psbt, input_index, ())?; + let h = h.to_raw_hash(); + (h, t) + } + SignerContext::Legacy => { + let (h, t) = Legacy::sighash(psbt, input_index, ())?; + let h = h.to_raw_hash(); + (h, t) + } + _ => return Ok(()), // handled above + }; + sign_psbt_ecdsa( + &self.inner, + pubkey, + &mut psbt.inputs[input_index], + hash, + hash_ty, + secp, + sign_options.allow_grinding, + ); + + Ok(()) + } +} + +fn sign_psbt_ecdsa( + secret_key: &secp256k1::SecretKey, + pubkey: PublicKey, + psbt_input: &mut psbt::Input, + hash: impl bitcoin::hashes::Hash + bitcoin::secp256k1::ThirtyTwoByteHash, + hash_ty: EcdsaSighashType, + secp: &SecpCtx, + allow_grinding: bool, +) { + let msg = &Message::from(hash); + let sig = if allow_grinding { + secp.sign_ecdsa_low_r(msg, secret_key) + } else { + secp.sign_ecdsa(msg, secret_key) + }; + secp.verify_ecdsa(msg, &sig, &pubkey.inner) + .expect("invalid or corrupted ecdsa signature"); + + let final_signature = ecdsa::Signature { sig, hash_ty }; + psbt_input.partial_sigs.insert(pubkey, final_signature); +} + +// Calling this with `leaf_hash` = `None` will sign for key-spend +fn sign_psbt_schnorr( + secret_key: &secp256k1::SecretKey, + pubkey: XOnlyPublicKey, + leaf_hash: Option, + psbt_input: &mut psbt::Input, + hash: TapSighash, + hash_ty: TapSighashType, + secp: &SecpCtx, +) { + let keypair = secp256k1::Keypair::from_seckey_slice(secp, secret_key.as_ref()).unwrap(); + let keypair = match leaf_hash { + None => keypair + .tap_tweak(secp, psbt_input.tap_merkle_root) + .to_inner(), + Some(_) => keypair, // no tweak for script spend + }; + + let msg = &Message::from(hash); + let sig = secp.sign_schnorr(msg, &keypair); + secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair).0) + .expect("invalid or corrupted schnorr signature"); + + let final_signature = taproot::Signature { sig, hash_ty }; + + if let Some(lh) = leaf_hash { + psbt_input + .tap_script_sigs + .insert((pubkey, lh), final_signature); + } else { + psbt_input.tap_key_sig = Some(final_signature); + } +} + +/// Defines the order in which signers are called +/// +/// The default value is `100`. Signers with an ordering above that will be called later, +/// and they will thus see the partial signatures added to the transaction once they get to sign +/// themselves. +#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq)] +pub struct SignerOrdering(pub usize); + +impl Default for SignerOrdering { + fn default() -> Self { + SignerOrdering(100) + } +} + +#[derive(Debug, Clone)] +struct SignersContainerKey { + id: SignerId, + ordering: SignerOrdering, +} + +impl From<(SignerId, SignerOrdering)> for SignersContainerKey { + fn from(tuple: (SignerId, SignerOrdering)) -> Self { + SignersContainerKey { + id: tuple.0, + ordering: tuple.1, + } + } +} + +/// Container for multiple signers +#[derive(Debug, Default, Clone)] +pub struct SignersContainer(BTreeMap>); + +impl SignersContainer { + /// Create a map of public keys to secret keys + pub fn as_key_map(&self, secp: &SecpCtx) -> KeyMap { + self.0 + .values() + .filter_map(|signer| signer.descriptor_secret_key()) + .filter_map(|secret| secret.to_public(secp).ok().map(|public| (public, secret))) + .collect() + } + + /// Build a new signer container from a [`KeyMap`] + /// + /// Also looks at the corresponding descriptor to determine the [`SignerContext`] to attach to + /// the signers + pub fn build( + keymap: KeyMap, + descriptor: &Descriptor, + secp: &SecpCtx, + ) -> SignersContainer { + let mut container = SignersContainer::new(); + + for (pubkey, secret) in keymap { + let ctx = match descriptor { + Descriptor::Tr(tr) => SignerContext::Tap { + is_internal_key: tr.internal_key() == &pubkey, + }, + _ if descriptor.is_witness() => SignerContext::Segwitv0, + _ => SignerContext::Legacy, + }; + + match secret { + DescriptorSecretKey::Single(private_key) => container.add_external( + SignerId::from( + private_key + .key + .public_key(secp) + .to_pubkeyhash(SigType::Ecdsa), + ), + SignerOrdering::default(), + Arc::new(SignerWrapper::new(private_key.key, ctx)), + ), + DescriptorSecretKey::XPrv(xprv) => container.add_external( + SignerId::from(xprv.root_fingerprint(secp)), + SignerOrdering::default(), + Arc::new(SignerWrapper::new(xprv, ctx)), + ), + DescriptorSecretKey::MultiXPrv(xprv) => container.add_external( + SignerId::from(xprv.root_fingerprint(secp)), + SignerOrdering::default(), + Arc::new(SignerWrapper::new(xprv, ctx)), + ), + }; + } + + container + } +} + +impl SignersContainer { + /// Default constructor + pub fn new() -> Self { + SignersContainer(Default::default()) + } + + /// Adds an external signer to the container for the specified id. Optionally returns the + /// signer that was previously in the container, if any + pub fn add_external( + &mut self, + id: SignerId, + ordering: SignerOrdering, + signer: Arc, + ) -> Option> { + self.0.insert((id, ordering).into(), signer) + } + + /// Removes a signer from the container and returns it + pub fn remove( + &mut self, + id: SignerId, + ordering: SignerOrdering, + ) -> Option> { + self.0.remove(&(id, ordering).into()) + } + + /// Returns the list of identifiers of all the signers in the container + pub fn ids(&self) -> Vec<&SignerId> { + self.0 + .keys() + .map(|SignersContainerKey { id, .. }| id) + .collect() + } + + /// Returns the list of signers in the container, sorted by lowest to highest `ordering` + pub fn signers(&self) -> Vec<&Arc> { + self.0.values().collect() + } + + /// Finds the signer with lowest ordering for a given id in the container. + pub fn find(&self, id: SignerId) -> Option<&Arc> { + self.0 + .range(( + Included(&(id.clone(), SignerOrdering(0)).into()), + Included(&(id.clone(), SignerOrdering(usize::MAX)).into()), + )) + .filter(|(k, _)| k.id == id) + .map(|(_, v)| v) + .next() + } +} + +/// 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 should 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: + 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, + + /// Whether the signer should use the `sighash_type` set in the PSBT when signing, no matter + /// what its value is + /// + /// Defaults to `false` which will only allow signing using `SIGHASH_ALL`. + pub allow_all_sighashes: bool, + + /// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT. + /// + /// Defaults to `true` which will remove partial signatures during finalization. + pub remove_partial_sigs: bool, + + /// Whether to remove taproot specific fields from the PSBT on finalization. + /// + /// For inputs this includes the taproot internal key, merkle root, and individual + /// scripts and signatures. For both inputs and outputs it includes key origin info. + /// + /// Defaults to `true` which will remove all of the above mentioned fields when finalizing. + /// + /// See [`BIP371`](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) for details. + pub remove_taproot_extras: bool, + + /// Whether to try finalizing the PSBT after the inputs are signed. + /// + /// Defaults to `true` which will try finalizing PSBT after inputs are signed. + pub try_finalize: bool, + + /// Specifies which Taproot script-spend leaves we should sign for. This option is + /// ignored if we're signing a non-taproot PSBT. + /// + /// Defaults to All, i.e., the wallet will sign all the leaves it has a key for. + pub tap_leaves_options: TapLeavesOptions, + + /// Whether we should try to sign a taproot transaction with the taproot internal key + /// or not. This option is ignored if we're signing a non-taproot PSBT. + /// + /// Defaults to `true`, i.e., we always try to sign with the taproot internal key. + pub sign_with_tap_internal_key: bool, + + /// Whether we should grind ECDSA signature to ensure signing with low r + /// or not. + /// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r. + pub allow_grinding: bool, +} + +/// Customize which taproot script-path leaves the signer should sign. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub enum TapLeavesOptions { + /// The signer will sign all the leaves it has a key for. + #[default] + All, + /// The signer won't sign leaves other than the ones specified. Note that it could still ignore + /// some of the specified leaves, if it doesn't have the right key to sign them. + Include(Vec), + /// The signer won't sign the specified leaves. + Exclude(Vec), + /// The signer won't sign any leaf. + None, +} + +impl Default for SignOptions { + fn default() -> Self { + SignOptions { + trust_witness_utxo: false, + assume_height: None, + allow_all_sighashes: false, + remove_partial_sigs: true, + remove_taproot_extras: true, + try_finalize: true, + tap_leaves_options: TapLeavesOptions::default(), + sign_with_tap_internal_key: true, + allow_grinding: true, + } + } +} + +pub(crate) trait ComputeSighash { + type Extra; + type Sighash; + type SighashType; + + fn sighash( + psbt: &Psbt, + input_index: usize, + extra: Self::Extra, + ) -> Result<(Self::Sighash, Self::SighashType), SignerError>; +} + +impl ComputeSighash for Legacy { + type Extra = (); + type Sighash = sighash::LegacySighash; + type SighashType = EcdsaSighashType; + + fn sighash( + psbt: &Psbt, + input_index: usize, + _extra: (), + ) -> Result<(Self::Sighash, Self::SighashType), SignerError> { + if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { + return Err(SignerError::InputIndexOutOfRange); + } + + let psbt_input = &psbt.inputs[input_index]; + let tx_input = &psbt.unsigned_tx.input[input_index]; + + let sighash = psbt_input + .sighash_type + .unwrap_or_else(|| EcdsaSighashType::All.into()) + .ecdsa_hash_ty() + .map_err(|_| SignerError::InvalidSighash)?; + let script = match psbt_input.redeem_script { + Some(ref redeem_script) => redeem_script.clone(), + None => { + let non_witness_utxo = psbt_input + .non_witness_utxo + .as_ref() + .ok_or(SignerError::MissingNonWitnessUtxo)?; + let prev_out = non_witness_utxo + .output + .get(tx_input.previous_output.vout as usize) + .ok_or(SignerError::InvalidNonWitnessUtxo)?; + + prev_out.script_pubkey.clone() + } + }; + + Ok(( + sighash::SighashCache::new(&psbt.unsigned_tx).legacy_signature_hash( + input_index, + &script, + sighash.to_u32(), + )?, + sighash, + )) + } +} + +impl ComputeSighash for Segwitv0 { + type Extra = (); + type Sighash = sighash::SegwitV0Sighash; + type SighashType = EcdsaSighashType; + + fn sighash( + psbt: &Psbt, + input_index: usize, + _extra: (), + ) -> Result<(Self::Sighash, Self::SighashType), SignerError> { + if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { + return Err(SignerError::InputIndexOutOfRange); + } + + let psbt_input = &psbt.inputs[input_index]; + let tx_input = &psbt.unsigned_tx.input[input_index]; + + let sighash_type = psbt_input + .sighash_type + .unwrap_or_else(|| EcdsaSighashType::All.into()) + .ecdsa_hash_ty() + .map_err(|_| SignerError::InvalidSighash)?; + + // 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 mut sighasher = sighash::SighashCache::new(&psbt.unsigned_tx); + + let sighash = match psbt_input.witness_script { + Some(ref witness_script) => { + sighasher.p2wsh_signature_hash(input_index, witness_script, value, sighash_type)? + } + None => { + if utxo.script_pubkey.is_p2wpkh() { + sighasher.p2wpkh_signature_hash( + input_index, + &utxo.script_pubkey, + value, + sighash_type, + )? + } else if psbt_input + .redeem_script + .as_ref() + .map(|s| s.is_p2wpkh()) + .unwrap_or(false) + { + let script_pubkey = psbt_input.redeem_script.as_ref().unwrap(); + sighasher.p2wpkh_signature_hash( + input_index, + script_pubkey, + value, + sighash_type, + )? + } else { + return Err(SignerError::MissingWitnessScript); + } + } + }; + Ok((sighash, sighash_type)) + } +} + +impl ComputeSighash for Tap { + type Extra = Option; + type Sighash = TapSighash; + type SighashType = TapSighashType; + + fn sighash( + psbt: &Psbt, + input_index: usize, + extra: Self::Extra, + ) -> Result<(Self::Sighash, TapSighashType), SignerError> { + if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { + return Err(SignerError::InputIndexOutOfRange); + } + + let psbt_input = &psbt.inputs[input_index]; + + let sighash_type = psbt_input + .sighash_type + .unwrap_or_else(|| TapSighashType::Default.into()) + .taproot_hash_ty() + .map_err(|_| SignerError::InvalidSighash)?; + let witness_utxos = (0..psbt.inputs.len()) + .map(|i| psbt.get_utxo_for(i)) + .collect::>(); + let mut all_witness_utxos = vec![]; + + let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx); + let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0; + let prevouts = if is_anyone_can_pay { + sighash::Prevouts::One( + input_index, + witness_utxos[input_index] + .as_ref() + .ok_or(SignerError::MissingWitnessUtxo)?, + ) + } else if witness_utxos.iter().all(Option::is_some) { + all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref())); + sighash::Prevouts::All(&all_witness_utxos) + } else { + return Err(SignerError::MissingWitnessUtxo); + }; + + // Assume no OP_CODESEPARATOR + let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF)); + + Ok(( + cache.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)?, + sighash_type, + )) + } +} + +impl PartialOrd for SignersContainerKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for SignersContainerKey { + fn cmp(&self, other: &Self) -> Ordering { + self.ordering + .cmp(&other.ordering) + .then(self.id.cmp(&other.id)) + } +} + +impl PartialEq for SignersContainerKey { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.ordering == other.ordering + } +} + +impl Eq for SignersContainerKey {} + +#[cfg(test)] +mod signers_container_tests { + use super::*; + use crate::descriptor; + use crate::descriptor::IntoWalletDescriptor; + use crate::keys::{DescriptorKey, IntoDescriptorKey}; + use assert_matches::assert_matches; + use bitcoin::bip32; + use bitcoin::secp256k1::{All, Secp256k1}; + use bitcoin::Network; + use core::str::FromStr; + use miniscript::ScriptContext; + + fn is_equal(this: &Arc, that: &Arc) -> bool { + let secp = Secp256k1::new(); + this.id(&secp) == that.id(&secp) + } + + // Signers added with the same ordering (like `Ordering::default`) created from `KeyMap` + // should be preserved and not overwritten. + // This happens usually when a set of signers is created from a descriptor with private keys. + #[test] + fn signers_with_same_ordering() { + let secp = Secp256k1::new(); + + let (prvkey1, _, _) = setup_keys(TPRV0_STR); + let (prvkey2, _, _) = setup_keys(TPRV1_STR); + let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let signers = SignersContainer::build(keymap, &wallet_desc, &secp); + assert_eq!(signers.ids().len(), 2); + + let signers = signers.signers(); + assert_eq!(signers.len(), 2); + } + + #[test] + fn signers_sorted_by_ordering() { + let mut signers = SignersContainer::new(); + let signer1 = Arc::new(DummySigner { number: 1 }); + let signer2 = Arc::new(DummySigner { number: 2 }); + let signer3 = Arc::new(DummySigner { number: 3 }); + + // Mixed order insertions verifies we are not inserting at head or tail. + signers.add_external(SignerId::Dummy(2), SignerOrdering(2), signer2.clone()); + signers.add_external(SignerId::Dummy(1), SignerOrdering(1), signer1.clone()); + signers.add_external(SignerId::Dummy(3), SignerOrdering(3), signer3.clone()); + + // Check that signers are sorted from lowest to highest ordering + let signers = signers.signers(); + + assert!(is_equal(signers[0], &signer1)); + assert!(is_equal(signers[1], &signer2)); + assert!(is_equal(signers[2], &signer3)); + } + + #[test] + fn find_signer_by_id() { + let mut signers = SignersContainer::new(); + let signer1 = Arc::new(DummySigner { number: 1 }); + let signer2 = Arc::new(DummySigner { number: 2 }); + let signer3 = Arc::new(DummySigner { number: 3 }); + let signer4 = Arc::new(DummySigner { number: 3 }); // Same ID as `signer3` but will use lower ordering. + + let id1 = SignerId::Dummy(1); + let id2 = SignerId::Dummy(2); + let id3 = SignerId::Dummy(3); + let id_nonexistent = SignerId::Dummy(999); + + signers.add_external(id1.clone(), SignerOrdering(1), signer1.clone()); + signers.add_external(id2.clone(), SignerOrdering(2), signer2.clone()); + signers.add_external(id3.clone(), SignerOrdering(3), signer3.clone()); + + assert_matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1)); + assert_matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2)); + assert_matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3)); + + // The `signer4` has the same ID as `signer3` but lower ordering. + // It should be found by `id3` instead of `signer3`. + signers.add_external(id3.clone(), SignerOrdering(2), signer4.clone()); + assert_matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4)); + + // Can't find anything with ID that doesn't exist + assert_matches!(signers.find(id_nonexistent), None); + } + + #[derive(Debug, Clone, Copy)] + struct DummySigner { + number: u64, + } + + impl SignerCommon for DummySigner { + fn id(&self, _secp: &SecpCtx) -> SignerId { + SignerId::Dummy(self.number) + } + } + + impl TransactionSigner for DummySigner { + fn sign_transaction( + &self, + _psbt: &mut Psbt, + _sign_options: &SignOptions, + _secp: &SecpCtx, + ) -> Result<(), SignerError> { + Ok(()) + } + } + + const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf"; + const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N"; + + const PATH: &str = "m/44'/1'/0'/0"; + + fn setup_keys( + tprv: &str, + ) -> (DescriptorKey, DescriptorKey, Fingerprint) { + let secp: Secp256k1 = Secp256k1::new(); + let path = bip32::DerivationPath::from_str(PATH).unwrap(); + let tprv = bip32::Xpriv::from_str(tprv).unwrap(); + let tpub = bip32::Xpub::from_priv(&secp, &tprv); + let fingerprint = tprv.fingerprint(&secp); + let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap(); + let pubkey = (tpub, path).into_descriptor_key().unwrap(); + + (prvkey, pubkey, fingerprint) + } +} diff --git a/crates/wallet/src/wallet/tx_builder.rs b/crates/wallet/src/wallet/tx_builder.rs new file mode 100644 index 00000000..28b70a8b --- /dev/null +++ b/crates/wallet/src/wallet/tx_builder.rs @@ -0,0 +1,1083 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Transaction builder +//! +//! ## Example +//! +//! ``` +//! # use std::str::FromStr; +//! # use bitcoin::*; +//! # use bdk_wallet::*; +//! # use bdk_wallet::wallet::ChangeSet; +//! # use bdk_wallet::wallet::error::CreateTxError; +//! # use bdk_wallet::wallet::tx_builder::CreateTx; +//! # use bdk_persist::PersistBackend; +//! # use anyhow::Error; +//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); +//! # let mut wallet = doctest_wallet!(); +//! // create a TxBuilder from a wallet +//! let mut tx_builder = wallet.build_tx(); +//! +//! tx_builder +//! // Create a transaction with one output to `to_address` of 50_000 satoshi +//! .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)) +//! // With a custom fee rate of 5.0 satoshi/vbyte +//! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")) +//! // Only spend non-change outputs +//! .do_not_spend_change() +//! // Turn on RBF signaling +//! .enable_rbf(); +//! let psbt = tx_builder.finish()?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; +use core::cell::RefCell; +use core::fmt; +use core::marker::PhantomData; + +use bitcoin::psbt::{self, Psbt}; +use bitcoin::script::PushBytes; +use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid}; + +use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; +use super::{CreateTxError, Wallet}; +use crate::collections::{BTreeMap, HashSet}; +use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; + +/// Context in which the [`TxBuilder`] is valid +pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {} + +/// Marker type to indicate the [`TxBuilder`] is being used to create a new transaction (as opposed +/// to bumping the fee of an existing one). +#[derive(Debug, Default, Clone)] +pub struct CreateTx; +impl TxBuilderContext for CreateTx {} + +/// Marker type to indicate the [`TxBuilder`] is being used to bump the fee of an existing transaction. +#[derive(Debug, Default, Clone)] +pub struct BumpFee; +impl TxBuilderContext for BumpFee {} + +/// A transaction builder +/// +/// A `TxBuilder` is created by calling [`build_tx`] or [`build_fee_bump`] on a wallet. After +/// assigning it, you set options on it until finally calling [`finish`] to consume the builder and +/// generate the transaction. +/// +/// Each option setting method on `TxBuilder` takes and returns `&mut self` so you can chain calls +/// as in the following example: +/// +/// ``` +/// # use bdk_wallet::*; +/// # use bdk_wallet::wallet::tx_builder::*; +/// # use bitcoin::*; +/// # use core::str::FromStr; +/// # use bdk_wallet::wallet::ChangeSet; +/// # use bdk_wallet::wallet::error::CreateTxError; +/// # use bdk_persist::PersistBackend; +/// # use anyhow::Error; +/// # let mut wallet = doctest_wallet!(); +/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); +/// # let addr2 = addr1.clone(); +/// // chaining +/// let psbt1 = { +/// let mut builder = wallet.build_tx(); +/// builder +/// .ordering(TxOrdering::Untouched) +/// .add_recipient(addr1.script_pubkey(), Amount::from_sat(50_000)) +/// .add_recipient(addr2.script_pubkey(), Amount::from_sat(50_000)); +/// builder.finish()? +/// }; +/// +/// // non-chaining +/// let psbt2 = { +/// let mut builder = wallet.build_tx(); +/// builder.ordering(TxOrdering::Untouched); +/// for addr in &[addr1, addr2] { +/// builder.add_recipient(addr.script_pubkey(), Amount::from_sat(50_000)); +/// } +/// builder.finish()? +/// }; +/// +/// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]); +/// # Ok::<(), anyhow::Error>(()) +/// ``` +/// +/// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`. +/// This means it is usually best to call [`coin_selection`] on the return value of `build_tx` before assigning it. +/// +/// For further examples see [this module](super::tx_builder)'s documentation; +/// +/// [`build_tx`]: Wallet::build_tx +/// [`build_fee_bump`]: Wallet::build_fee_bump +/// [`finish`]: Self::finish +/// [`coin_selection`]: Self::coin_selection +#[derive(Debug)] +pub struct TxBuilder<'a, Cs, Ctx> { + pub(crate) wallet: Rc>, + pub(crate) params: TxParams, + pub(crate) coin_selection: Cs, + pub(crate) phantom: PhantomData, +} + +/// The parameters for transaction creation sans coin selection algorithm. +//TODO: TxParams should eventually be exposed publicly. +#[derive(Default, Debug, Clone)] +pub(crate) struct TxParams { + pub(crate) recipients: Vec<(ScriptBuf, u64)>, + pub(crate) drain_wallet: bool, + pub(crate) drain_to: Option, + pub(crate) fee_policy: Option, + pub(crate) internal_policy_path: Option>>, + pub(crate) external_policy_path: Option>>, + pub(crate) utxos: Vec, + pub(crate) unspendable: HashSet, + pub(crate) manually_selected_only: bool, + pub(crate) sighash: Option, + pub(crate) ordering: TxOrdering, + pub(crate) locktime: Option, + pub(crate) rbf: Option, + pub(crate) version: Option, + pub(crate) change_policy: ChangeSpendPolicy, + 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, + pub(crate) current_height: Option, + pub(crate) allow_dust: bool, +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct PreviousFee { + pub absolute: u64, + pub rate: FeeRate, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum FeePolicy { + FeeRate(FeeRate), + FeeAmount(u64), +} + +impl Default for FeePolicy { + fn default() -> Self { + FeePolicy::FeeRate(FeeRate::BROADCAST_MIN) + } +} + +impl<'a, Cs: Clone, Ctx> Clone for TxBuilder<'a, Cs, Ctx> { + fn clone(&self) -> Self { + TxBuilder { + wallet: self.wallet.clone(), + params: self.params.clone(), + coin_selection: self.coin_selection.clone(), + phantom: PhantomData, + } + } +} + +// methods supported by both contexts, for any CoinSelectionAlgorithm +impl<'a, Cs, Ctx> TxBuilder<'a, Cs, Ctx> { + /// Set a custom fee rate. + /// + /// This method sets the mining fee paid by the transaction as a rate on its size. + /// This means that the total fee paid is equal to `fee_rate` times the size + /// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default + /// relay policy. + /// + /// Note that this is really a minimum feerate -- it's possible to + /// overshoot it slightly since adding a change output to drain the remaining + /// excess might not be viable. + pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self { + self.params.fee_policy = Some(FeePolicy::FeeRate(fee_rate)); + self + } + + /// Set an absolute fee + /// The fee_absolute method refers to the absolute transaction fee in satoshis (sats). + /// If anyone sets both the fee_absolute method and the fee_rate method, + /// the FeePolicy enum will be set by whichever method was called last, + /// as the FeeRate and FeeAmount are mutually exclusive. + /// + /// Note that this is really a minimum absolute fee -- it's possible to + /// overshoot it slightly since adding a change output to drain the remaining + /// excess might not be viable. + pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self { + self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount)); + self + } + + /// Set the policy path to use while creating the transaction for a given keychain. + /// + /// This method accepts a map where the key is the policy node id (see + /// [`Policy::id`](crate::descriptor::Policy::id)) and the value is the list of the indexes of + /// the items that are intended to be satisfied from the policy node (see + /// [`SatisfiableItem::Thresh::items`](crate::descriptor::policy::SatisfiableItem::Thresh::items)). + /// + /// ## Example + /// + /// An example of when the policy path is needed is the following descriptor: + /// `wsh(thresh(2,pk(A),sj:and_v(v:pk(B),n:older(6)),snj:and_v(v:pk(C),after(630000))))`, + /// derived from the miniscript policy `thresh(2,pk(A),and(pk(B),older(6)),and(pk(C),after(630000)))`. + /// It declares three descriptor fragments, and at the top level it uses `thresh()` to + /// ensure that at least two of them are satisfied. The individual fragments are: + /// + /// 1. `pk(A)` + /// 2. `and(pk(B),older(6))` + /// 3. `and(pk(C),after(630000))` + /// + /// When those conditions are combined in pairs, it's clear that the transaction needs to be created + /// differently depending on how the user intends to satisfy the policy afterwards: + /// + /// * If fragments `1` and `2` are used, the transaction will need to use a specific + /// `n_sequence` in order to spend an `OP_CSV` branch. + /// * If fragments `1` and `3` are used, the transaction will need to use a specific `locktime` + /// in order to spend an `OP_CLTV` branch. + /// * If fragments `2` and `3` are used, the transaction will need both. + /// + /// When the spending policy is represented as a tree (see + /// [`Wallet::policies`](super::Wallet::policies)), every node + /// is assigned a unique identifier that can be used in the policy path to specify which of + /// the node's children the user intends to satisfy: for instance, assuming the `thresh()` + /// root node of this example has an id of `aabbccdd`, the policy path map would look like: + /// + /// `{ "aabbccdd" => [0, 1] }` + /// + /// where the key is the node's id, and the value is a list of the children that should be + /// used, in no particular order. + /// + /// If a particularly complex descriptor has multiple ambiguous thresholds in its structure, + /// multiple entries can be added to the map, one for each node that requires an explicit path. + /// + /// ``` + /// # use std::str::FromStr; + /// # use std::collections::BTreeMap; + /// # use bitcoin::*; + /// # use bdk_wallet::*; + /// # let to_address = + /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") + /// .unwrap() + /// .assume_checked(); + /// # let mut wallet = doctest_wallet!(); + /// let mut path = BTreeMap::new(); + /// path.insert("aabbccdd".to_string(), vec![0, 1]); + /// + /// let builder = wallet + /// .build_tx() + /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)) + /// .policy_path(path, KeychainKind::External); + /// + /// # Ok::<(), anyhow::Error>(()) + /// ``` + pub fn policy_path( + &mut self, + policy_path: BTreeMap>, + keychain: KeychainKind, + ) -> &mut Self { + let to_update = match keychain { + KeychainKind::Internal => &mut self.params.internal_policy_path, + KeychainKind::External => &mut self.params.external_policy_path, + }; + + *to_update = Some(policy_path); + self + } + + /// Add the list of outpoints to the internal list of UTXOs that **must** be spent. + /// + /// If an error occurs while adding any of the UTXOs then none of them are added and the error is returned. + /// + /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in + /// the "utxos" and the "unspendable" list, it will be spent. + pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, AddUtxoError> { + { + let wallet = self.wallet.borrow(); + let utxos = outpoints + .iter() + .map(|outpoint| { + wallet + .get_utxo(*outpoint) + .ok_or(AddUtxoError::UnknownUtxo(*outpoint)) + }) + .collect::, _>>()?; + + for utxo in utxos { + let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain); + let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap(); + self.params.utxos.push(WeightedUtxo { + satisfaction_weight, + utxo: Utxo::Local(utxo), + }); + } + } + + Ok(self) + } + + /// Add a utxo to the internal list of utxos that **must** be spent + /// + /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in + /// the "utxos" and the "unspendable" list, it will be spent. + pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, AddUtxoError> { + self.add_utxos(&[outpoint]) + } + + /// Add a foreign UTXO i.e. a UTXO not owned by this wallet. + /// + /// At a minimum to add a foreign UTXO we need: + /// + /// 1. `outpoint`: To add it to the raw transaction. + /// 2. `psbt_input`: To know the value. + /// 3. `satisfaction_weight`: To know how much weight/vbytes the input will add to the transaction for fee calculation. + /// + /// There are several security concerns about adding foreign UTXOs that application + /// developers should consider. First, how do you know the value of the input is correct? If a + /// `non_witness_utxo` is provided in the `psbt_input` then this method implicitly verifies the + /// value by checking it against the transaction. If only a `witness_utxo` is provided then this + /// method doesn't verify the value but just takes it as a given -- it is up to you to check + /// that whoever sent you the `input_psbt` was not lying! + /// + /// Secondly, you must somehow provide `satisfaction_weight` of the input. Depending on your + /// application it may be important that this be known precisely. If not, a malicious + /// counterparty may fool you into putting in a value that is too low, giving the transaction a + /// lower than expected feerate. They could also fool you into putting a value that is too high + /// causing you to pay a fee that is too high. The party who is broadcasting the transaction can + /// of course check the real input weight matches the expected weight prior to broadcasting. + /// + /// To guarantee the `max_weight_to_satisfy` is correct, you can require the party providing the + /// `psbt_input` provide a miniscript descriptor for the input so you can check it against the + /// `script_pubkey` and then ask it for the [`max_weight_to_satisfy`]. + /// + /// This is an **EXPERIMENTAL** feature, API and other major changes are expected. + /// + /// In order to use [`Wallet::calculate_fee`] or [`Wallet::calculate_fee_rate`] for a transaction + /// created with foreign UTXO(s) you must manually insert the corresponding TxOut(s) into the tx + /// graph using the [`Wallet::insert_txout`] function. + /// + /// # Errors + /// + /// This method returns errors in the following circumstances: + /// + /// 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 unless you set [`only_witness_utxo`] any non-taproot `psbt_input` you pass to this + /// method must have `non_witness_utxo` set otherwise you will get an error when [`finish`] + /// is called. + /// + /// [`only_witness_utxo`]: Self::only_witness_utxo + /// [`finish`]: Self::finish + /// [`max_weight_to_satisfy`]: miniscript::Descriptor::max_weight_to_satisfy + pub fn add_foreign_utxo( + &mut self, + outpoint: OutPoint, + psbt_input: psbt::Input, + satisfaction_weight: usize, + ) -> Result<&mut Self, AddForeignUtxoError> { + self.add_foreign_utxo_with_sequence( + outpoint, + psbt_input, + satisfaction_weight, + Sequence::MAX, + ) + } + + /// Same as [add_foreign_utxo](TxBuilder::add_foreign_utxo) but allows to set the nSequence value. + pub fn add_foreign_utxo_with_sequence( + &mut self, + outpoint: OutPoint, + psbt_input: psbt::Input, + satisfaction_weight: usize, + sequence: Sequence, + ) -> Result<&mut Self, AddForeignUtxoError> { + if psbt_input.witness_utxo.is_none() { + match psbt_input.non_witness_utxo.as_ref() { + Some(tx) => { + if tx.txid() != outpoint.txid { + return Err(AddForeignUtxoError::InvalidTxid { + input_txid: tx.txid(), + foreign_utxo: outpoint, + }); + } + if tx.output.len() <= outpoint.vout as usize { + return Err(AddForeignUtxoError::InvalidOutpoint(outpoint)); + } + } + None => { + return Err(AddForeignUtxoError::MissingUtxo); + } + } + } + + self.params.utxos.push(WeightedUtxo { + satisfaction_weight, + utxo: Utxo::Foreign { + outpoint, + sequence: Some(sequence), + psbt_input: Box::new(psbt_input), + }, + }); + + Ok(self) + } + + /// Only spend utxos added by [`add_utxo`]. + /// + /// The wallet will **not** add additional utxos to the transaction even if they are needed to + /// make the transaction valid. + /// + /// [`add_utxo`]: Self::add_utxo + pub fn manually_selected_only(&mut self) -> &mut Self { + self.params.manually_selected_only = true; + self + } + + /// Replace the internal list of unspendable utxos with a new list + /// + /// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::add_utxo`] + /// have priority over these. See the docs of the two linked methods for more details. + pub fn unspendable(&mut self, unspendable: Vec) -> &mut Self { + self.params.unspendable = unspendable.into_iter().collect(); + self + } + + /// Add a utxo to the internal list of unspendable utxos + /// + /// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::add_utxo`] + /// have priority over this. See the docs of the two linked methods for more details. + pub fn add_unspendable(&mut self, unspendable: OutPoint) -> &mut Self { + self.params.unspendable.insert(unspendable); + self + } + + /// Sign with a specific sig hash + /// + /// **Use this option very carefully** + pub fn sighash(&mut self, sighash: psbt::PsbtSighashType) -> &mut Self { + self.params.sighash = Some(sighash); + self + } + + /// Choose the ordering for inputs and outputs of the transaction + pub fn ordering(&mut self, ordering: TxOrdering) -> &mut Self { + self.params.ordering = ordering; + self + } + + /// Use a specific nLockTime while creating the transaction + /// + /// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator. + pub fn nlocktime(&mut self, locktime: absolute::LockTime) -> &mut Self { + self.params.locktime = Some(locktime); + self + } + + /// Build a transaction with a specific version + /// + /// The `version` should always be greater than `0` and greater than `1` if the wallet's + /// descriptors contain an "older" (OP_CSV) operator. + pub fn version(&mut self, version: i32) -> &mut Self { + self.params.version = Some(Version(version)); + self + } + + /// Do not spend change outputs + /// + /// This effectively adds all the change outputs to the "unspendable" list. See + /// [`TxBuilder::unspendable`]. + pub fn do_not_spend_change(&mut self) -> &mut Self { + self.params.change_policy = ChangeSpendPolicy::ChangeForbidden; + self + } + + /// Only spend change outputs + /// + /// This effectively adds all the non-change outputs to the "unspendable" list. See + /// [`TxBuilder::unspendable`]. + pub fn only_spend_change(&mut self) -> &mut Self { + self.params.change_policy = ChangeSpendPolicy::OnlyChange; + self + } + + /// Set a specific [`ChangeSpendPolicy`]. See [`TxBuilder::do_not_spend_change`] and + /// [`TxBuilder::only_spend_change`] for some shortcuts. + pub fn change_policy(&mut self, change_policy: ChangeSpendPolicy) -> &mut Self { + self.params.change_policy = change_policy; + self + } + + /// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::psbt::Input::witness_utxo) field when spending from + /// SegWit descriptors. + /// + /// 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 + } + + /// Fill-in the [`psbt::Output::redeem_script`](bitcoin::psbt::Output::redeem_script) and + /// [`psbt::Output::witness_script`](bitcoin::psbt::Output::witness_script) fields. + /// + /// This is useful for signers which always require it, like ColdCard hardware wallets. + pub fn include_output_redeem_witness_script(&mut self) -> &mut Self { + self.params.include_output_redeem_witness_script = true; + self + } + + /// Fill-in the `PSBT_GLOBAL_XPUB` field with the extended keys contained in both the external + /// and internal descriptors + /// + /// This is useful for offline signers that take part to a multisig. Some hardware wallets like + /// BitBox and ColdCard are known to require this. + pub fn add_global_xpubs(&mut self) -> &mut Self { + self.params.add_global_xpubs = true; + self + } + + /// Spend all the available inputs. This respects filters like [`TxBuilder::unspendable`] and the change policy. + pub fn drain_wallet(&mut self) -> &mut Self { + self.params.drain_wallet = true; + self + } + + /// Choose the coin selection algorithm + /// + /// Overrides the [`DefaultCoinSelectionAlgorithm`]. + /// + /// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder. + pub fn coin_selection( + self, + coin_selection: P, + ) -> TxBuilder<'a, P, Ctx> { + TxBuilder { + wallet: self.wallet, + params: self.params, + coin_selection, + phantom: PhantomData, + } + } + + /// Enable signaling RBF + /// + /// This will use the default nSequence value of `0xFFFFFFFD`. + pub fn enable_rbf(&mut self) -> &mut Self { + self.params.rbf = Some(RbfValue::Default); + self + } + + /// Enable signaling RBF with a specific nSequence value + /// + /// This can cause conflicts if the wallet's descriptors contain an "older" (OP_CSV) operator + /// and the given `nsequence` is lower than the CSV value. + /// + /// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not + /// be a valid nSequence to signal RBF. + pub fn enable_rbf_with_sequence(&mut self, nsequence: Sequence) -> &mut Self { + self.params.rbf = Some(RbfValue::Value(nsequence)); + self + } + + /// Set the current blockchain height. + /// + /// This will be used to: + /// 1. Set the nLockTime for preventing fee sniping. + /// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`]. + /// 2. Decide whether coinbase outputs are mature or not. If the coinbase outputs are not + /// mature at `current_height`, we ignore them in the coin selection. + /// If you want to create a transaction that spends immature coinbase inputs, manually + /// add them using [`TxBuilder::add_utxos`]. + /// + /// In both cases, if you don't provide a current height, we use the last sync height. + pub fn current_height(&mut self, height: u32) -> &mut Self { + self.params.current_height = + Some(absolute::LockTime::from_height(height).expect("Invalid height")); + self + } + + /// Set whether or not the dust limit is checked. + /// + /// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard. + pub fn allow_dust(&mut self, allow_dust: bool) -> &mut Self { + self.params.allow_dust = allow_dust; + self + } +} + +impl<'a, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, Cs, Ctx> { + /// Finish building the transaction. + /// + /// Returns a new [`Psbt`] per [`BIP174`]. + /// + /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki + pub fn finish(self) -> Result { + self.wallet + .borrow_mut() + .create_tx(self.coin_selection, self.params) + } +} + +#[derive(Debug)] +/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`] +pub enum AddUtxoError { + /// Happens when trying to spend an UTXO that is not in the internal database + UnknownUtxo(OutPoint), +} + +impl fmt::Display for AddUtxoError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnknownUtxo(outpoint) => write!( + f, + "UTXO not found in the internal database for txid: {} with vout: {}", + outpoint.txid, outpoint.vout + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for AddUtxoError {} + +#[derive(Debug)] +/// Error returned from [`TxBuilder::add_foreign_utxo`]. +pub enum AddForeignUtxoError { + /// Foreign utxo outpoint txid does not match PSBT input txid + InvalidTxid { + /// PSBT input txid + input_txid: Txid, + /// Foreign UTXO outpoint + foreign_utxo: OutPoint, + }, + /// Requested outpoint doesn't exist in the tx (vout greater than available outputs) + InvalidOutpoint(OutPoint), + /// Foreign utxo missing witness_utxo or non_witness_utxo + MissingUtxo, +} + +impl fmt::Display for AddForeignUtxoError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidTxid { + input_txid, + foreign_utxo, + } => write!( + f, + "Foreign UTXO outpoint txid: {} does not match PSBT input txid: {}", + foreign_utxo.txid, input_txid, + ), + Self::InvalidOutpoint(outpoint) => write!( + f, + "Requested outpoint doesn't exist for txid: {} with vout: {}", + outpoint.txid, outpoint.vout, + ), + Self::MissingUtxo => write!(f, "Foreign utxo missing witness_utxo or non_witness_utxo"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for AddForeignUtxoError {} + +#[derive(Debug)] +/// Error returned from [`TxBuilder::allow_shrinking`] +pub enum AllowShrinkingError { + /// Script/PubKey was not in the original transaction + MissingScriptPubKey(ScriptBuf), +} + +impl fmt::Display for AllowShrinkingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingScriptPubKey(script_buf) => write!( + f, + "Script/PubKey was not in the original transaction: {}", + script_buf, + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for AllowShrinkingError {} + +impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs, CreateTx> { + /// Replace the recipients already added with a new list + pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, Amount)>) -> &mut Self { + self.params.recipients = recipients + .into_iter() + .map(|(script, amount)| (script, amount.to_sat())) + .collect(); + self + } + + /// Add a recipient to the internal list + pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: Amount) -> &mut Self { + self.params + .recipients + .push((script_pubkey, amount.to_sat())); + self + } + + /// Add data as an output, using OP_RETURN + pub fn add_data>(&mut self, data: &T) -> &mut Self { + let script = ScriptBuf::new_op_return(data); + self.add_recipient(script, Amount::ZERO); + self + } + + /// Sets the address to *drain* excess coins to. + /// + /// Usually, when there are excess coins they are sent to a change address generated by the + /// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of + /// your choosing. Just as with a change output, if the drain output is not needed (the excess + /// coins are too small) it will not be included in the resulting transaction. The only + /// difference is that it is valid to use `drain_to` without setting any ordinary recipients + /// with [`add_recipient`] (but it is perfectly fine to add recipients as well). + /// + /// If you choose not to set any recipients, you should either provide the utxos that the + /// transaction should spend via [`add_utxos`], or set [`drain_wallet`] to spend all of them. + /// + /// When bumping the fees of a transaction made with this option, you probably want to + /// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees. + /// + /// # Example + /// + /// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a + /// single address. + /// + /// ``` + /// # use std::str::FromStr; + /// # use bitcoin::*; + /// # use bdk_wallet::*; + /// # use bdk_wallet::wallet::ChangeSet; + /// # use bdk_wallet::wallet::error::CreateTxError; + /// # use bdk_wallet::wallet::tx_builder::CreateTx; + /// # use bdk_persist::PersistBackend; + /// # use anyhow::Error; + /// # let to_address = + /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") + /// .unwrap() + /// .assume_checked(); + /// # let mut wallet = doctest_wallet!(); + /// let mut tx_builder = wallet.build_tx(); + /// + /// tx_builder + /// // Spend all outputs in this wallet. + /// .drain_wallet() + /// // Send the excess (which is all the coins minus the fee) to this address. + /// .drain_to(to_address.script_pubkey()) + /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")) + /// .enable_rbf(); + /// let psbt = tx_builder.finish()?; + /// # Ok::<(), anyhow::Error>(()) + /// ``` + /// + /// [`allow_shrinking`]: Self::allow_shrinking + /// [`add_recipient`]: Self::add_recipient + /// [`add_utxos`]: Self::add_utxos + /// [`drain_wallet`]: Self::drain_wallet + pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self { + self.params.drain_to = Some(script_pubkey); + self + } +} + +// methods supported only by bump_fee +impl<'a> TxBuilder<'a, DefaultCoinSelectionAlgorithm, BumpFee> { + /// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this + /// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet + /// will attempt to find a change output to shrink instead. + /// + /// **Note** that the output may shrink to below the dust limit and therefore be removed. If it is + /// preserved then it is currently not guaranteed to be in the same position as it was + /// originally. + /// + /// Returns an `Err` if `script_pubkey` can't be found among the recipients of the + /// transaction we are bumping. + pub fn allow_shrinking( + &mut self, + script_pubkey: ScriptBuf, + ) -> Result<&mut Self, AllowShrinkingError> { + match self + .params + .recipients + .iter() + .position(|(recipient_script, _)| *recipient_script == script_pubkey) + { + Some(position) => { + self.params.recipients.remove(position); + self.params.drain_to = Some(script_pubkey); + Ok(self) + } + None => Err(AllowShrinkingError::MissingScriptPubKey(script_pubkey)), + } + } +} + +/// Ordering of the transaction's inputs and outputs +#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] +pub enum TxOrdering { + /// Randomized (default) + #[default] + Shuffle, + /// Unchanged + Untouched, + /// BIP69 / Lexicographic + Bip69Lexicographic, +} + +impl TxOrdering { + /// Sort transaction inputs and outputs by [`TxOrdering`] variant + pub fn sort_tx(&self, tx: &mut Transaction) { + match self { + TxOrdering::Untouched => {} + TxOrdering::Shuffle => { + use rand::seq::SliceRandom; + let mut rng = rand::thread_rng(); + tx.input.shuffle(&mut rng); + tx.output.shuffle(&mut rng); + } + TxOrdering::Bip69Lexicographic => { + tx.input.sort_unstable_by_key(|txin| { + (txin.previous_output.txid, txin.previous_output.vout) + }); + tx.output + .sort_unstable_by_key(|txout| (txout.value, txout.script_pubkey.clone())); + } + } + } +} + +/// Transaction version +/// +/// Has a default value of `1` +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] +pub(crate) struct Version(pub(crate) i32); + +impl Default for Version { + fn default() -> Self { + Version(1) + } +} + +/// RBF nSequence value +/// +/// Has a default value of `0xFFFFFFFD` +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] +pub(crate) enum RbfValue { + Default, + Value(Sequence), +} + +impl RbfValue { + pub(crate) fn get_value(&self) -> Sequence { + match self { + RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME, + RbfValue::Value(v) => *v, + } + } +} + +/// Policy regarding the use of change outputs when creating a transaction +#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] +pub enum ChangeSpendPolicy { + /// Use both change and non-change outputs (default) + #[default] + ChangeAllowed, + /// Only use change outputs (see [`TxBuilder::only_spend_change`]) + OnlyChange, + /// Only use non-change outputs (see [`TxBuilder::do_not_spend_change`]) + ChangeForbidden, +} + +impl ChangeSpendPolicy { + pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool { + match self { + ChangeSpendPolicy::ChangeAllowed => true, + ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal, + ChangeSpendPolicy::ChangeForbidden => utxo.keychain == KeychainKind::External, + } + } +} + +#[cfg(test)] +mod test { + const ORDERING_TEST_TX: &str = "0200000003c26f3eb7932f7acddc5ddd26602b77e7516079b03090a16e2c2f54\ + 85d1fd600f0100000000ffffffffc26f3eb7932f7acddc5ddd26602b77e75160\ + 79b03090a16e2c2f5485d1fd600f0000000000ffffffff571fb3e02278217852\ + dd5d299947e2b7354a639adc32ec1fa7b82cfb5dec530e0500000000ffffffff\ + 03e80300000000000002aaeee80300000000000001aa200300000000000001ff\ + 00000000"; + macro_rules! ordering_test_tx { + () => { + deserialize::(&Vec::::from_hex(ORDERING_TEST_TX).unwrap()) + .unwrap() + }; + } + + use bdk_chain::ConfirmationTime; + use bitcoin::consensus::deserialize; + use bitcoin::hex::FromHex; + use bitcoin::TxOut; + + use super::*; + + #[test] + fn test_output_ordering_default_shuffle() { + assert_eq!(TxOrdering::default(), TxOrdering::Shuffle); + } + + #[test] + fn test_output_ordering_untouched() { + let original_tx = ordering_test_tx!(); + let mut tx = original_tx.clone(); + + TxOrdering::Untouched.sort_tx(&mut tx); + + assert_eq!(original_tx, tx); + } + + #[test] + fn test_output_ordering_shuffle() { + let original_tx = ordering_test_tx!(); + let mut tx = original_tx.clone(); + + (0..40) + .find(|_| { + TxOrdering::Shuffle.sort_tx(&mut tx); + original_tx.input != tx.input + }) + .expect("it should have moved the inputs at least once"); + + let mut tx = original_tx.clone(); + (0..40) + .find(|_| { + TxOrdering::Shuffle.sort_tx(&mut tx); + original_tx.output != tx.output + }) + .expect("it should have moved the outputs at least once"); + } + + #[test] + fn test_output_ordering_bip69() { + use core::str::FromStr; + + let original_tx = ordering_test_tx!(); + let mut tx = original_tx; + + TxOrdering::Bip69Lexicographic.sort_tx(&mut tx); + + assert_eq!( + tx.input[0].previous_output, + bitcoin::OutPoint::from_str( + "0e53ec5dfb2cb8a71fec32dc9a634a35b7e24799295ddd5278217822e0b31f57:5" + ) + .unwrap() + ); + assert_eq!( + tx.input[1].previous_output, + bitcoin::OutPoint::from_str( + "0f60fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2:0" + ) + .unwrap() + ); + assert_eq!( + tx.input[2].previous_output, + bitcoin::OutPoint::from_str( + "0f60fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2:1" + ) + .unwrap() + ); + + assert_eq!(tx.output[0].value.to_sat(), 800); + assert_eq!(tx.output[1].script_pubkey, ScriptBuf::from(vec![0xAA])); + assert_eq!( + tx.output[2].script_pubkey, + ScriptBuf::from(vec![0xAA, 0xEE]) + ); + } + + fn get_test_utxos() -> Vec { + use bitcoin::hashes::Hash; + + vec![ + LocalOutput { + outpoint: OutPoint { + txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(), + vout: 0, + }, + txout: TxOut::NULL, + keychain: KeychainKind::External, + is_spent: false, + confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 }, + derivation_index: 0, + }, + LocalOutput { + outpoint: OutPoint { + txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(), + vout: 1, + }, + txout: TxOut::NULL, + keychain: KeychainKind::Internal, + is_spent: false, + confirmation_time: ConfirmationTime::Confirmed { + height: 32, + time: 42, + }, + derivation_index: 1, + }, + ] + } + + #[test] + fn test_change_spend_policy_default() { + let change_spend_policy = ChangeSpendPolicy::default(); + let filtered = get_test_utxos() + .into_iter() + .filter(|u| change_spend_policy.is_satisfied_by(u)) + .count(); + + assert_eq!(filtered, 2); + } + + #[test] + fn test_change_spend_policy_no_internal() { + let change_spend_policy = ChangeSpendPolicy::ChangeForbidden; + let filtered = get_test_utxos() + .into_iter() + .filter(|u| change_spend_policy.is_satisfied_by(u)) + .collect::>(); + + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].keychain, KeychainKind::External); + } + + #[test] + fn test_change_spend_policy_only_internal() { + let change_spend_policy = ChangeSpendPolicy::OnlyChange; + let filtered = get_test_utxos() + .into_iter() + .filter(|u| change_spend_policy.is_satisfied_by(u)) + .collect::>(); + + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].keychain, KeychainKind::Internal); + } + + #[test] + fn test_default_tx_version_1() { + let version = Version::default(); + assert_eq!(version.0, 1); + } +} diff --git a/crates/wallet/src/wallet/utils.rs b/crates/wallet/src/wallet/utils.rs new file mode 100644 index 00000000..208a88df --- /dev/null +++ b/crates/wallet/src/wallet/utils.rs @@ -0,0 +1,185 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use bitcoin::secp256k1::{All, Secp256k1}; +use bitcoin::{absolute, Script, Sequence}; + +use miniscript::{MiniscriptKey, Satisfier, ToPublicKey}; + +/// Trait to check if a value is below the dust limit. +/// We are performing dust value calculation for a given script public key using rust-bitcoin to +/// keep it compatible with network dust rate +// we implement this trait to make sure we don't mess up the comparison with off-by-one like a < +// instead of a <= etc. +pub trait IsDust { + /// Check whether or not a value is below dust limit + fn is_dust(&self, script: &Script) -> bool; +} + +impl IsDust for u64 { + fn is_dust(&self, script: &Script) -> bool { + *self < script.dust_value().to_sat() + } +} + +pub struct After { + pub current_height: Option, + pub assume_height_reached: bool, +} + +impl After { + pub(crate) fn new(current_height: Option, assume_height_reached: bool) -> After { + After { + current_height, + assume_height_reached, + } + } +} + +pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool { + // The RBF value must enable relative timelocks + if !rbf.is_relative_lock_time() { + return false; + } + + // Both values should be represented in the same unit (either time-based or + // block-height based) + if rbf.is_time_locked() != csv.is_time_locked() { + return false; + } + + // The value should be at least `csv` + if rbf < csv { + return false; + } + + true +} + +impl Satisfier for After { + fn check_after(&self, n: absolute::LockTime) -> bool { + if let Some(current_height) = self.current_height { + current_height >= n.to_consensus_u32() + } else { + self.assume_height_reached + } + } +} + +pub struct Older { + pub current_height: Option, + pub create_height: Option, + pub assume_height_reached: bool, +} + +impl Older { + pub(crate) fn new( + current_height: Option, + create_height: Option, + assume_height_reached: bool, + ) -> Older { + Older { + current_height, + create_height, + assume_height_reached, + } + } +} + +impl Satisfier for Older { + fn check_older(&self, n: Sequence) -> bool { + if let Some(current_height) = self.current_height { + // TODO: test >= / > + current_height + >= self + .create_height + .unwrap_or(0) + .checked_add(n.to_consensus_u32()) + .expect("Overflowing addition") + } else { + self.assume_height_reached + } + } +} + +pub(crate) type SecpCtx = Secp256k1; + +#[cfg(test)] +mod test { + // When nSequence is lower than this flag the timelock is interpreted as block-height-based, + // otherwise it's time-based + pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22; + + use super::{check_nsequence_rbf, IsDust}; + use crate::bitcoin::{Address, Network, Sequence}; + use core::str::FromStr; + + #[test] + fn test_is_dust() { + let script_p2pkh = Address::from_str("1GNgwA8JfG7Kc8akJ8opdNWJUihqUztfPe") + .unwrap() + .require_network(Network::Bitcoin) + .unwrap() + .script_pubkey(); + assert!(script_p2pkh.is_p2pkh()); + assert!(545.is_dust(&script_p2pkh)); + assert!(!546.is_dust(&script_p2pkh)); + + let script_p2wpkh = Address::from_str("bc1qxlh2mnc0yqwas76gqq665qkggee5m98t8yskd8") + .unwrap() + .require_network(Network::Bitcoin) + .unwrap() + .script_pubkey(); + assert!(script_p2wpkh.is_p2wpkh()); + assert!(293.is_dust(&script_p2wpkh)); + assert!(!294.is_dust(&script_p2wpkh)); + } + + #[test] + fn test_check_nsequence_rbf_msb_set() { + let result = check_nsequence_rbf(Sequence(0x80000000), Sequence(5000)); + assert!(!result); + } + + #[test] + fn test_check_nsequence_rbf_lt_csv() { + let result = check_nsequence_rbf(Sequence(4000), Sequence(5000)); + assert!(!result); + } + + #[test] + fn test_check_nsequence_rbf_different_unit() { + let result = + check_nsequence_rbf(Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000), Sequence(5000)); + assert!(!result); + } + + #[test] + fn test_check_nsequence_rbf_mask() { + let result = check_nsequence_rbf(Sequence(0x3f + 10_000), Sequence(5000)); + assert!(result); + } + + #[test] + fn test_check_nsequence_rbf_same_unit_blocks() { + let result = check_nsequence_rbf(Sequence(10_000), Sequence(5000)); + assert!(result); + } + + #[test] + fn test_check_nsequence_rbf_same_unit_time() { + let result = check_nsequence_rbf( + Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000), + Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000), + ); + assert!(result); + } +} diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs new file mode 100644 index 00000000..a51dcafb --- /dev/null +++ b/crates/wallet/tests/common.rs @@ -0,0 +1,172 @@ +#![allow(unused)] + +use bdk_chain::indexed_tx_graph::Indexer; +use bdk_chain::{BlockId, ConfirmationTime}; +use bdk_wallet::{KeychainKind, LocalOutput, Wallet}; +use bitcoin::hashes::Hash; +use bitcoin::{ + transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut, + Txid, +}; +use std::str::FromStr; + +// Return a fake wallet that appears to be funded for testing. +// +// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000 +// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 +// sats are the transaction fee. +pub fn get_funded_wallet_with_change( + descriptor: &str, + change: Option<&str>, +) -> (Wallet, bitcoin::Txid) { + let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest).unwrap(); + let change_address = wallet.peek_address(KeychainKind::External, 0).address; + let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5") + .expect("address") + .require_network(Network::Regtest) + .unwrap(); + + let tx0 = Transaction { + version: transaction::Version::ONE, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: Txid::all_zeros(), + vout: 0, + }, + script_sig: Default::default(), + sequence: Default::default(), + witness: Default::default(), + }], + output: vec![TxOut { + value: Amount::from_sat(76_000), + script_pubkey: change_address.script_pubkey(), + }], + }; + + let tx1 = Transaction { + version: transaction::Version::ONE, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: tx0.txid(), + vout: 0, + }, + script_sig: Default::default(), + sequence: Default::default(), + witness: Default::default(), + }], + output: vec![ + TxOut { + value: Amount::from_sat(50_000), + script_pubkey: change_address.script_pubkey(), + }, + TxOut { + value: Amount::from_sat(25_000), + script_pubkey: sendto_address.script_pubkey(), + }, + ], + }; + + wallet + .insert_checkpoint(BlockId { + height: 1_000, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_checkpoint(BlockId { + height: 2_000, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_tx( + tx0, + ConfirmationTime::Confirmed { + height: 1_000, + time: 100, + }, + ) + .unwrap(); + wallet + .insert_tx( + tx1.clone(), + ConfirmationTime::Confirmed { + height: 2_000, + time: 200, + }, + ) + .unwrap(); + + (wallet, tx1.txid()) +} + +// Return a fake wallet that appears to be funded for testing. +// +// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000 +// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 +// sats are the transaction fee. +pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) { + get_funded_wallet_with_change(descriptor, None) +} + +pub fn get_test_wpkh() -> &'static str { + "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)" +} + +pub fn get_test_single_sig_csv() -> &'static str { + // and(pk(Alice),older(6)) + "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))" +} + +pub fn get_test_a_or_b_plus_csv() -> &'static str { + // or(pk(Alice),and(pk(Bob),older(144))) + "wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))" +} + +pub fn get_test_single_sig_cltv() -> &'static str { + // and(pk(Alice),after(100000)) + "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))" +} + +pub fn get_test_tr_single_sig() -> &'static str { + "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)" +} + +pub fn get_test_tr_with_taptree() -> &'static str { + "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" +} + +pub fn get_test_tr_with_taptree_both_priv() -> &'static str { + "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(cNaQCDwmmh4dS9LzCgVtyy1e1xjCJ21GUDHe9K98nzb689JvinGV)})" +} + +pub fn get_test_tr_repeated_key() -> &'static str { + "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})" +} + +pub fn get_test_tr_single_sig_xprv() -> &'static str { + "tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)" +} + +pub fn get_test_tr_with_taptree_xprv() -> &'static str { + "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" +} + +pub fn get_test_tr_dup_keys() -> &'static str { + "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" +} + +/// Construct a new [`FeeRate`] from the given raw `sat_vb` feerate. This is +/// useful in cases where we want to create a feerate from a `f64`, as the +/// traditional [`FeeRate::from_sat_per_vb`] method will only accept an integer. +/// +/// **Note** this 'quick and dirty' conversion should only be used when the input +/// parameter has units of `satoshis/vbyte` **AND** is not expected to overflow, +/// or else the resulting value will be inaccurate. +pub fn feerate_unchecked(sat_vb: f64) -> FeeRate { + // 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu + let sat_kwu = (sat_vb * 250.0).ceil() as u64; + FeeRate::from_sat_per_kwu(sat_kwu) +} diff --git a/crates/wallet/tests/psbt.rs b/crates/wallet/tests/psbt.rs new file mode 100644 index 00000000..3652f109 --- /dev/null +++ b/crates/wallet/tests/psbt.rs @@ -0,0 +1,221 @@ +use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, TxIn}; +use bdk_wallet::{psbt, KeychainKind, SignOptions}; +use core::str::FromStr; +mod common; +use common::*; + +// 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::from_str(PSBT_STR).unwrap(); + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let send_to = wallet.peek_address(KeychainKind::External, 0); + let mut builder = wallet.build_tx(); + builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000)); + let mut psbt = builder.finish().unwrap(); + psbt.inputs.push(psbt_bip.inputs[0].clone()); + let options = SignOptions { + trust_witness_utxo: true, + ..Default::default() + }; + let _ = wallet.sign(&mut psbt, options).unwrap(); +} + +#[test] +#[should_panic(expected = "InputIndexOutOfRange")] +fn test_psbt_malformed_psbt_input_segwit() { + let psbt_bip = Psbt::from_str(PSBT_STR).unwrap(); + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let send_to = wallet.peek_address(KeychainKind::External, 0); + let mut builder = wallet.build_tx(); + builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000)); + let mut psbt = builder.finish().unwrap(); + psbt.inputs.push(psbt_bip.inputs[1].clone()); + let options = SignOptions { + trust_witness_utxo: true, + ..Default::default() + }; + let _ = wallet.sign(&mut psbt, options).unwrap(); +} + +#[test] +#[should_panic(expected = "InputIndexOutOfRange")] +fn test_psbt_malformed_tx_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let send_to = wallet.peek_address(KeychainKind::External, 0); + let mut builder = wallet.build_tx(); + builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000)); + let mut psbt = builder.finish().unwrap(); + psbt.unsigned_tx.input.push(TxIn::default()); + let options = SignOptions { + trust_witness_utxo: true, + ..Default::default() + }; + let _ = wallet.sign(&mut psbt, options).unwrap(); +} + +#[test] +fn test_psbt_sign_with_finalized() { + let psbt_bip = Psbt::from_str(PSBT_STR).unwrap(); + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let send_to = wallet.peek_address(KeychainKind::External, 0); + let mut builder = wallet.build_tx(); + builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000)); + let mut psbt = builder.finish().unwrap(); + + // add a finalized input + psbt.inputs.push(psbt_bip.inputs[0].clone()); + psbt.unsigned_tx + .input + .push(psbt_bip.unsigned_tx.input[0].clone()); + + let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); +} + +#[test] +fn test_psbt_fee_rate_with_witness_utxo() { + use psbt::PsbtUtils; + + let expected_fee_rate = FeeRate::from_sat_per_kwu(310); + + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.peek_address(KeychainKind::External, 0); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + builder.fee_rate(expected_fee_rate); + let mut psbt = builder.finish().unwrap(); + let fee_amount = psbt.fee_amount(); + assert!(fee_amount.is_some()); + + let unfinalized_fee_rate = psbt.fee_rate().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let finalized_fee_rate = psbt.fee_rate().unwrap(); + assert!(finalized_fee_rate >= expected_fee_rate); + assert!(finalized_fee_rate < unfinalized_fee_rate); +} + +#[test] +fn test_psbt_fee_rate_with_nonwitness_utxo() { + use psbt::PsbtUtils; + + let expected_fee_rate = FeeRate::from_sat_per_kwu(310); + + let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.peek_address(KeychainKind::External, 0); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + builder.fee_rate(expected_fee_rate); + let mut psbt = builder.finish().unwrap(); + let fee_amount = psbt.fee_amount(); + assert!(fee_amount.is_some()); + let unfinalized_fee_rate = psbt.fee_rate().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let finalized_fee_rate = psbt.fee_rate().unwrap(); + assert!(finalized_fee_rate >= expected_fee_rate); + assert!(finalized_fee_rate < unfinalized_fee_rate); +} + +#[test] +fn test_psbt_fee_rate_with_missing_txout() { + use psbt::PsbtUtils; + + let expected_fee_rate = FeeRate::from_sat_per_kwu(310); + + let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wpkh_wallet.peek_address(KeychainKind::External, 0); + let mut builder = wpkh_wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + builder.fee_rate(expected_fee_rate); + let mut wpkh_psbt = builder.finish().unwrap(); + + wpkh_psbt.inputs[0].witness_utxo = None; + wpkh_psbt.inputs[0].non_witness_utxo = None; + assert!(wpkh_psbt.fee_amount().is_none()); + assert!(wpkh_psbt.fee_rate().is_none()); + + let (mut pkh_wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = pkh_wallet.peek_address(KeychainKind::External, 0); + let mut builder = pkh_wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + builder.fee_rate(expected_fee_rate); + let mut pkh_psbt = builder.finish().unwrap(); + + pkh_psbt.inputs[0].non_witness_utxo = None; + assert!(pkh_psbt.fee_amount().is_none()); + assert!(pkh_psbt.fee_rate().is_none()); +} + +#[test] +fn test_psbt_multiple_internalkey_signers() { + use bdk_wallet::signer::{SignerContext, SignerOrdering, SignerWrapper}; + use bdk_wallet::KeychainKind; + use bitcoin::key::TapTweak; + use bitcoin::secp256k1::{schnorr, Keypair, Message, Secp256k1, XOnlyPublicKey}; + use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType}; + use bitcoin::{PrivateKey, TxOut}; + use std::sync::Arc; + + let secp = Secp256k1::new(); + let wif = "cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG"; + let desc = format!("tr({})", wif); + let prv = PrivateKey::from_wif(wif).unwrap(); + let keypair = Keypair::from_secret_key(&secp, &prv.inner); + + let (mut wallet, _) = get_funded_wallet(&desc); + let to_spend = wallet.get_balance().total(); + let send_to = wallet.peek_address(KeychainKind::External, 0); + let mut builder = wallet.build_tx(); + builder.drain_to(send_to.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + let unsigned_tx = psbt.unsigned_tx.clone(); + + // Adds a signer for the wrong internal key, bdk should not use this key to sign + wallet.add_signer( + KeychainKind::External, + // A signerordering lower than 100, bdk will use this signer first + SignerOrdering(0), + Arc::new(SignerWrapper::new( + PrivateKey::from_wif("5J5PZqvCe1uThJ3FZeUUFLCh2FuK9pZhtEK4MzhNmugqTmxCdwE").unwrap(), + SignerContext::Tap { + is_internal_key: true, + }, + )), + ); + let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); + assert!(finalized); + + // To verify, we need the signature, message, and pubkey + let witness = psbt.inputs[0].final_script_witness.as_ref().unwrap(); + assert!(!witness.is_empty()); + let signature = schnorr::Signature::from_slice(witness.iter().next().unwrap()).unwrap(); + + // the prevout we're spending + let prevouts = &[TxOut { + script_pubkey: send_to.script_pubkey(), + value: to_spend, + }]; + let prevouts = Prevouts::All(prevouts); + let input_index = 0; + let mut sighash_cache = SighashCache::new(unsigned_tx); + let sighash = sighash_cache + .taproot_key_spend_signature_hash(input_index, &prevouts, TapSighashType::Default) + .unwrap(); + let message = Message::from(sighash); + + // add tweak. this was taken from `signer::sign_psbt_schnorr` + let keypair = keypair.tap_tweak(&secp, None).to_inner(); + let (xonlykey, _parity) = XOnlyPublicKey::from_keypair(&keypair); + + // Must verify if we used the correct key to sign + let verify_res = secp.verify_schnorr(&signature, &message, &xonlykey); + assert!(verify_res.is_ok(), "The wrong internal key was used"); +} diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs new file mode 100644 index 00000000..58b25061 --- /dev/null +++ b/crates/wallet/tests/wallet.rs @@ -0,0 +1,3947 @@ +use std::str::FromStr; + +use assert_matches::assert_matches; +use bdk_chain::collections::BTreeMap; +use bdk_chain::COINBASE_MATURITY; +use bdk_chain::{BlockId, ConfirmationTime}; +use bdk_wallet::descriptor::{calc_checksum, IntoWalletDescriptor}; +use bdk_wallet::psbt::PsbtUtils; +use bdk_wallet::signer::{SignOptions, SignerError}; +use bdk_wallet::wallet::coin_selection::{self, LargestFirstCoinSelection}; +use bdk_wallet::wallet::error::CreateTxError; +use bdk_wallet::wallet::tx_builder::AddForeignUtxoError; +use bdk_wallet::wallet::NewError; +use bdk_wallet::wallet::{AddressInfo, Balance, Wallet}; +use bdk_wallet::KeychainKind; +use bitcoin::hashes::Hash; +use bitcoin::key::Secp256k1; +use bitcoin::psbt; +use bitcoin::script::PushBytesBuf; +use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; +use bitcoin::taproot::TapNodeHash; +use bitcoin::{ + absolute, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, + Sequence, Transaction, TxIn, TxOut, Txid, Weight, +}; + +mod common; +use common::*; + +fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) -> OutPoint { + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + script_pubkey: addr.script_pubkey(), + value: Amount::from_sat(value), + }], + }; + + wallet.insert_tx(tx.clone(), height).unwrap(); + + OutPoint { + txid: tx.txid(), + vout: 0, + } +} + +fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { + let latest_cp = wallet.latest_checkpoint(); + let height = latest_cp.height(); + let anchor = if height == 0 { + ConfirmationTime::Unconfirmed { last_seen: 0 } + } else { + ConfirmationTime::Confirmed { height, time: 0 } + }; + receive_output(wallet, value, anchor) +} + +// The satisfaction size of a P2WPKH is 112 WU = +// 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len) +// On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for +// a total of 105 WU. +// Here, we push just once for simplicity, so we have to add an extra byte for the missing +// OP_PUSH. +const P2WPKH_FAKE_WITNESS_SIZE: usize = 106; + +const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48]; + +#[test] +fn load_recovers_wallet() { + let temp_dir = tempfile::tempdir().expect("must create tempdir"); + let file_path = temp_dir.path().join("store.db"); + + // create new wallet + let wallet_spk_index = { + let db = bdk_file_store::Store::create_new(DB_MAGIC, &file_path).expect("must create db"); + let mut wallet = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet) + .expect("must init wallet"); + + wallet.reveal_next_address(KeychainKind::External).unwrap(); + wallet.spk_index().clone() + }; + + // recover wallet + { + let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db"); + let wallet = Wallet::load(db).expect("must recover wallet"); + assert_eq!(wallet.network(), Network::Testnet); + assert_eq!( + wallet.spk_index().keychains().collect::>(), + wallet_spk_index.keychains().collect::>() + ); + assert_eq!( + wallet.spk_index().last_revealed_indices(), + wallet_spk_index.last_revealed_indices() + ); + let secp = Secp256k1::new(); + assert_eq!( + *wallet.get_descriptor_for_keychain(KeychainKind::External), + get_test_tr_single_sig_xprv() + .into_wallet_descriptor(&secp, wallet.network()) + .unwrap() + .0 + ); + } + + // `new` can only be called on empty db + { + let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db"); + let result = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet); + assert!(matches!(result, Err(NewError::NonEmptyDatabase))); + } +} + +#[test] +fn new_or_load() { + let temp_dir = tempfile::tempdir().expect("must create tempdir"); + let file_path = temp_dir.path().join("store.db"); + + // init wallet when non-existent + let wallet_keychains: BTreeMap<_, _> = { + let db = bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path) + .expect("must create db"); + let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet) + .expect("must init wallet"); + wallet.keychains().map(|(k, v)| (*k, v.clone())).collect() + }; + + // wrong network + { + let db = + bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db"); + let err = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Bitcoin) + .expect_err("wrong network"); + assert!( + matches!( + err, + bdk_wallet::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch { + got: Some(Network::Testnet), + expected: Network::Bitcoin + } + ), + "err: {}", + err, + ); + } + + // wrong genesis hash + { + let exp_blockhash = BlockHash::all_zeros(); + let got_blockhash = + bitcoin::blockdata::constants::genesis_block(Network::Testnet).block_hash(); + + let db = + bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db"); + let err = Wallet::new_or_load_with_genesis_hash( + get_test_wpkh(), + None, + db, + Network::Testnet, + exp_blockhash, + ) + .expect_err("wrong genesis hash"); + assert!( + matches!( + err, + bdk_wallet::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected } + if got == Some(got_blockhash) && expected == exp_blockhash + ), + "err: {}", + err, + ); + } + + // wrong external descriptor + { + let exp_descriptor = get_test_tr_single_sig(); + let got_descriptor = get_test_wpkh() + .into_wallet_descriptor(&Secp256k1::new(), Network::Testnet) + .unwrap() + .0; + + let db = + bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db"); + let err = Wallet::new_or_load(exp_descriptor, None, db, Network::Testnet) + .expect_err("wrong external descriptor"); + assert!( + matches!( + err, + bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain } + if got == &Some(got_descriptor) && keychain == KeychainKind::External + ), + "err: {}", + err, + ); + } + + // wrong internal descriptor + { + let exp_descriptor = Some(get_test_tr_single_sig()); + let got_descriptor = None; + + let db = + bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db"); + let err = Wallet::new_or_load(get_test_wpkh(), exp_descriptor, db, Network::Testnet) + .expect_err("wrong internal descriptor"); + assert!( + matches!( + err, + bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain } + if got == &got_descriptor && keychain == KeychainKind::Internal + ), + "err: {}", + err, + ); + } + + // all parameters match + { + let db = + bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db"); + let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet) + .expect("must recover wallet"); + assert_eq!(wallet.network(), Network::Testnet); + assert!(wallet + .keychains() + .map(|(k, v)| (*k, v.clone())) + .eq(wallet_keychains)); + } +} + +#[test] +fn test_descriptor_checksum() { + let (wallet, _) = get_funded_wallet(get_test_wpkh()); + let checksum = wallet.descriptor_checksum(KeychainKind::External); + assert_eq!(checksum.len(), 8); + + let raw_descriptor = wallet + .keychains() + .next() + .unwrap() + .1 + .to_string() + .split_once('#') + .unwrap() + .0 + .to_string(); + assert_eq!(calc_checksum(&raw_descriptor).unwrap(), checksum); +} + +#[test] +fn test_get_funded_wallet_balance() { + let (wallet, _) = get_funded_wallet(get_test_wpkh()); + + // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 + // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 + // sats are the transaction fee. + assert_eq!(wallet.get_balance().confirmed, Amount::from_sat(50_000)); +} + +#[test] +fn test_get_funded_wallet_sent_and_received() { + let (wallet, txid) = get_funded_wallet(get_test_wpkh()); + + let mut tx_amounts: Vec<(Txid, (Amount, Amount))> = wallet + .transactions() + .map(|ct| (ct.tx_node.txid, wallet.sent_and_received(&ct.tx_node))) + .collect(); + tx_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0)); + + let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let (sent, received) = wallet.sent_and_received(&tx); + + // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 + // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 + // sats are the transaction fee. + assert_eq!(sent.to_sat(), 76_000); + assert_eq!(received.to_sat(), 50_000); +} + +#[test] +fn test_get_funded_wallet_tx_fees() { + let (wallet, txid) = get_funded_wallet(get_test_wpkh()); + + let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx_fee = wallet.calculate_fee(&tx).expect("transaction fee"); + + // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 + // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 + // sats are the transaction fee. + assert_eq!(tx_fee, 1000) +} + +#[test] +fn test_get_funded_wallet_tx_fee_rate() { + let (wallet, txid) = get_funded_wallet(get_test_wpkh()); + + let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx_fee_rate = wallet + .calculate_fee_rate(&tx) + .expect("transaction fee rate"); + + // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 + // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 + // sats are the transaction fee. + + // tx weight = 452 wu, as vbytes = (452 + 3) / 4 = 113 + // fee_rate (sats per kwu) = fee / weight = 1000sat / 0.452kwu = 2212 + // fee_rate (sats per vbyte ceil) = fee / vsize = 1000sat / 113vb = 9 + assert_eq!(tx_fee_rate.to_sat_per_kwu(), 2212); + assert_eq!(tx_fee_rate.to_sat_per_vb_ceil(), 9); +} + +#[test] +fn test_list_output() { + let (wallet, txid) = get_funded_wallet(get_test_wpkh()); + let txos = wallet + .list_output() + .map(|op| (op.outpoint, op)) + .collect::>(); + assert_eq!(txos.len(), 2); + for (op, txo) in txos { + if op.txid == txid { + assert_eq!(txo.txout.value.to_sat(), 50_000); + assert!(!txo.is_spent); + } else { + assert_eq!(txo.txout.value.to_sat(), 76_000); + assert!(txo.is_spent); + } + } +} + +macro_rules! assert_fee_rate { + ($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({ + let psbt = $psbt.clone(); + #[allow(unused_mut)] + let mut tx = $psbt.clone().extract_tx().expect("failed to extract tx"); + $( + $( $add_signature )* + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + )* + + #[allow(unused_mut)] + #[allow(unused_assignments)] + let mut dust_change = false; + $( + $( $dust_change )* + dust_change = true; + )* + + let fee_amount = psbt + .inputs + .iter() + .fold(0, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value.to_sat()) + - psbt + .unsigned_tx + .output + .iter() + .fold(0, |acc, o| acc + o.value.to_sat()); + + assert_eq!(fee_amount, $fees); + + let tx_fee_rate = (Amount::from_sat(fee_amount) / tx.weight()) + .to_sat_per_kwu(); + let fee_rate = $fee_rate.to_sat_per_kwu(); + let half_default = FeeRate::BROADCAST_MIN.checked_div(2) + .unwrap() + .to_sat_per_kwu(); + + if !dust_change { + assert!(tx_fee_rate >= fee_rate && tx_fee_rate - fee_rate < half_default, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate); + } else { + assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate); + } + }); +} + +macro_rules! from_str { + ($e:expr, $t:ty) => {{ + use core::str::FromStr; + <$t>::from_str($e).unwrap() + }}; + + ($e:expr) => { + from_str!($e, _) + }; +} + +#[test] +#[should_panic(expected = "NoRecipients")] +fn test_create_tx_empty_recipients() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + wallet.build_tx().finish().unwrap(); +} + +#[test] +#[should_panic(expected = "NoUtxosSelected")] +fn test_create_tx_manually_selected_empty_utxos() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .manually_selected_only(); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_version_0() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .version(0); + assert!(matches!(builder.finish(), Err(CreateTxError::Version0))); +} + +#[test] +fn test_create_tx_version_1_csv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .version(1); + assert!(matches!(builder.finish(), Err(CreateTxError::Version1Csv))); +} + +#[test] +fn test_create_tx_custom_version() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .version(42); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.version.0, 42); +} + +#[test] +fn test_create_tx_default_locktime_is_last_sync_height() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + + // Since we never synced the wallet we don't have a last_sync_height + // we could use to try to prevent fee sniping. We default to 0. + assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 2_000); +} + +#[test] +fn test_create_tx_fee_sniping_locktime_last_sync() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + + let psbt = builder.finish().unwrap(); + + // If there's no current_height we're left with using the last sync height + assert_eq!( + psbt.unsigned_tx.lock_time.to_consensus_u32(), + wallet.latest_checkpoint().height() + ); +} + +#[test] +fn test_create_tx_default_locktime_cltv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 100_000); +} + +#[test] +fn test_create_tx_custom_locktime() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .current_height(630_001) + .nlocktime(absolute::LockTime::from_height(630_000).unwrap()); + let psbt = builder.finish().unwrap(); + + // When we explicitly specify a nlocktime + // we don't try any fee sniping prevention trick + // (we ignore the current_height) + assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 630_000); +} + +#[test] +fn test_create_tx_custom_locktime_compatible_with_cltv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .nlocktime(absolute::LockTime::from_height(630_000).unwrap()); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 630_000); +} + +#[test] +fn test_create_tx_custom_locktime_incompatible_with_cltv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .nlocktime(absolute::LockTime::from_height(50000).unwrap()); + assert!(matches!(builder.finish(), + Err(CreateTxError::LockTime { requested, required }) + if requested.to_consensus_u32() == 50_000 && required.to_consensus_u32() == 100_000)); +} + +#[test] +fn test_create_tx_no_rbf_csv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); +} + +#[test] +fn test_create_tx_with_default_rbf_csv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + // When CSV is enabled it takes precedence over the rbf value (unless forced by the user). + // It will be set to the OP_CSV value, in this case 6 + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); +} + +#[test] +fn test_create_tx_with_custom_rbf_csv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .enable_rbf_with_sequence(Sequence(3)); + assert!(matches!(builder.finish(), + Err(CreateTxError::RbfSequenceCsv { rbf, csv }) + if rbf.to_consensus_u32() == 3 && csv.to_consensus_u32() == 6)); +} + +#[test] +fn test_create_tx_no_rbf_cltv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); +} + +#[test] +fn test_create_tx_invalid_rbf_sequence() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .enable_rbf_with_sequence(Sequence(0xFFFFFFFE)); + assert!(matches!(builder.finish(), Err(CreateTxError::RbfSequence))); +} + +#[test] +fn test_create_tx_custom_rbf_sequence() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .enable_rbf_with_sequence(Sequence(0xDEADBEEF)); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF)); +} + +#[test] +fn test_create_tx_default_sequence() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); +} + +#[test] +fn test_create_tx_change_policy_no_internal() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .do_not_spend_change(); + assert!(matches!( + builder.finish(), + Err(CreateTxError::ChangePolicyDescriptor) + )); +} + +macro_rules! check_fee { + ($wallet:expr, $psbt: expr) => {{ + let tx = $psbt.clone().extract_tx().expect("failed to extract tx"); + let tx_fee = $wallet.calculate_fee(&tx).ok(); + assert_eq!(tx_fee, $psbt.fee_amount()); + tx_fee + }}; +} + +#[test] +fn test_create_tx_drain_wallet_and_drain_to() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!( + psbt.unsigned_tx.output[0].value.to_sat(), + 50_000 - fee.unwrap_or(0) + ); +} + +#[test] +fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") + .unwrap() + .assume_checked(); + let drain_addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(20_000)) + .drain_to(drain_addr.script_pubkey()) + .drain_wallet(); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + let outputs = psbt.unsigned_tx.output; + + assert_eq!(outputs.len(), 2); + let main_output = outputs + .iter() + .find(|x| x.script_pubkey == addr.script_pubkey()) + .unwrap(); + let drain_output = outputs + .iter() + .find(|x| x.script_pubkey == drain_addr.script_pubkey()) + .unwrap(); + assert_eq!(main_output.value.to_sat(), 20_000,); + assert_eq!(drain_output.value.to_sat(), 30_000 - fee.unwrap_or(0)); +} + +#[test] +fn test_create_tx_drain_to_and_utxos() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let utxos: Vec<_> = wallet.list_unspent().map(|u| u.outpoint).collect(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxos(&utxos) + .unwrap(); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!( + psbt.unsigned_tx.output[0].value.to_sat(), + 50_000 - fee.unwrap_or(0) + ); +} + +#[test] +#[should_panic(expected = "NoRecipients")] +fn test_create_tx_drain_to_no_drain_wallet_no_utxos() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let drain_addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(drain_addr.script_pubkey()); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_default_fee_rate() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::BROADCAST_MIN, @add_signature); +} + +#[test] +fn test_create_tx_custom_fee_rate() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature); +} + +#[test] +fn test_create_tx_absolute_fee() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(100); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_eq!(fee.unwrap_or(0), 100); + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!( + psbt.unsigned_tx.output[0].value.to_sat(), + 50_000 - fee.unwrap_or(0) + ); +} + +#[test] +fn test_create_tx_absolute_zero_fee() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(0); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_eq!(fee.unwrap_or(0), 0); + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!( + psbt.unsigned_tx.output[0].value.to_sat(), + 50_000 - fee.unwrap_or(0) + ); +} + +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_create_tx_absolute_high_fee() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(60_000); + let _ = builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_add_change() { + use bdk_wallet::wallet::tx_builder::TxOrdering; + + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .ordering(TxOrdering::Untouched); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_eq!(psbt.unsigned_tx.output.len(), 2); + assert_eq!(psbt.unsigned_tx.output[0].value.to_sat(), 25_000); + assert_eq!( + psbt.unsigned_tx.output[1].value.to_sat(), + 25_000 - fee.unwrap_or(0) + ); +} + +#[test] +fn test_create_tx_skip_change_dust() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(49_800)); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_eq!(psbt.unsigned_tx.output.len(), 1); + assert_eq!(psbt.unsigned_tx.output[0].value.to_sat(), 49_800); + assert_eq!(fee.unwrap_or(0), 200); +} + +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_create_tx_drain_to_dust_amount() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + // very high fee rate, so that the only output would be below dust + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_rate(FeeRate::from_sat_per_vb_unchecked(454)); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_ordering_respected() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) + .add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)) + .ordering(bdk_wallet::wallet::tx_builder::TxOrdering::Bip69Lexicographic); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_eq!(psbt.unsigned_tx.output.len(), 3); + assert_eq!( + psbt.unsigned_tx.output[0].value.to_sat(), + 10_000 - fee.unwrap_or(0) + ); + assert_eq!(psbt.unsigned_tx.output[1].value.to_sat(), 10_000); + assert_eq!(psbt.unsigned_tx.output[2].value.to_sat(), 30_000); +} + +#[test] +fn test_create_tx_default_sighash() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.inputs[0].sighash_type, None); +} + +#[test] +fn test_create_tx_custom_sighash() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) + .sighash(EcdsaSighashType::Single.into()); + let psbt = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0].sighash_type, + Some(EcdsaSighashType::Single.into()) + ); +} + +#[test] +fn test_create_tx_input_hd_keypaths() { + use bitcoin::bip32::{DerivationPath, Fingerprint}; + use core::str::FromStr; + + let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1); + assert_eq!( + psbt.inputs[0].bip32_derivation.values().next().unwrap(), + &( + Fingerprint::from_str("d34db33f").unwrap(), + DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap() + ) + ); +} + +#[test] +fn test_create_tx_output_hd_keypaths() { + use bitcoin::bip32::{DerivationPath, Fingerprint}; + use core::str::FromStr; + + let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); + + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1); + let expected_derivation_path = format!("m/44'/0'/0'/0/{}", addr.index); + assert_eq!( + psbt.outputs[0].bip32_derivation.values().next().unwrap(), + &( + Fingerprint::from_str("d34db33f").unwrap(), + DerivationPath::from_str(&expected_derivation_path).unwrap() + ) + ); +} + +#[test] +fn test_create_tx_set_redeem_script_p2sh() { + use bitcoin::hex::FromHex; + + let (mut wallet, _) = + get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0].redeem_script, + Some(ScriptBuf::from( + Vec::::from_hex( + "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" + ) + .unwrap() + )) + ); + assert_eq!(psbt.inputs[0].witness_script, None); +} + +#[test] +fn test_create_tx_set_witness_script_p2wsh() { + use bitcoin::hex::FromHex; + + let (mut wallet, _) = + get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.inputs[0].redeem_script, None); + assert_eq!( + psbt.inputs[0].witness_script, + Some(ScriptBuf::from( + Vec::::from_hex( + "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" + ) + .unwrap() + )) + ); +} + +#[test] +fn test_create_tx_set_redeem_witness_script_p2wsh_p2sh() { + let (mut wallet, _) = + get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + + let script = ScriptBuf::from_hex( + "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac", + ) + .unwrap(); + + assert_eq!(psbt.inputs[0].redeem_script, Some(script.to_p2wsh())); + assert_eq!(psbt.inputs[0].witness_script, Some(script)); +} + +#[test] +fn test_create_tx_non_witness_utxo() { + let (mut wallet, _) = + get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + + assert!(psbt.inputs[0].non_witness_utxo.is_some()); + assert!(psbt.inputs[0].witness_utxo.is_none()); +} + +#[test] +fn test_create_tx_only_witness_utxo() { + let (mut wallet, _) = + get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .only_witness_utxo() + .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_shwpkh_has_witness_utxo() { + let (mut wallet, _) = + get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + + assert!(psbt.inputs[0].witness_utxo.is_some()); +} + +#[test] +fn test_create_tx_both_non_witness_utxo_and_witness_utxo_default() { + let (mut wallet, _) = + get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + + assert!(psbt.inputs[0].non_witness_utxo.is_some()); + assert!(psbt.inputs[0].witness_utxo.is_some()); +} + +#[test] +fn test_create_tx_add_utxo() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let small_output_tx = Transaction { + input: vec![], + output: vec![TxOut { + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .unwrap() + .script_pubkey(), + value: Amount::from_sat(25_000), + }], + version: transaction::Version::non_standard(0), + lock_time: absolute::LockTime::ZERO, + }; + wallet + .insert_tx( + small_output_tx.clone(), + ConfirmationTime::Unconfirmed { last_seen: 0 }, + ) + .unwrap(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) + .add_utxo(OutPoint { + txid: small_output_tx.txid(), + vout: 0, + }) + .unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + + assert_eq!( + psbt.unsigned_tx.input.len(), + 2, + "should add an additional input since 25_000 < 30_000" + ); + assert_eq!( + sent_received.0, + Amount::from_sat(75_000), + "total should be sum of both inputs" + ); +} + +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_create_tx_manually_selected_insufficient() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let small_output_tx = Transaction { + input: vec![], + output: vec![TxOut { + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .unwrap() + .script_pubkey(), + value: Amount::from_sat(25_000), + }], + version: transaction::Version::non_standard(0), + lock_time: absolute::LockTime::ZERO, + }; + + wallet + .insert_tx( + small_output_tx.clone(), + ConfirmationTime::Unconfirmed { last_seen: 0 }, + ) + .unwrap(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) + .add_utxo(OutPoint { + txid: small_output_tx.txid(), + vout: 0, + }) + .unwrap() + .manually_selected_only(); + builder.finish().unwrap(); +} + +#[test] +#[should_panic(expected = "SpendingPolicyRequired(External)")] +fn test_create_tx_policy_path_required() { + let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_policy_path_no_csv() { + let descriptors = get_test_wpkh(); + let mut wallet = Wallet::new_no_persist(descriptors, None, Network::Regtest).unwrap(); + + let tx = Transaction { + version: transaction::Version::non_standard(0), + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .unwrap() + .script_pubkey(), + value: Amount::from_sat(50_000), + }], + }; + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); + let root_id = external_policy.id; + // child #0 is just the key "A" + let path = vec![(root_id, vec![0])].into_iter().collect(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) + .policy_path(path, KeychainKind::External); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF)); +} + +#[test] +fn test_create_tx_policy_path_use_csv() { + let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); + + let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); + let root_id = external_policy.id; + // child #1 is or(pk(B),older(144)) + let path = vec![(root_id, vec![1])].into_iter().collect(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) + .policy_path(path, KeychainKind::External); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144)); +} + +#[test] +fn test_create_tx_policy_path_ignored_subtree_with_csv() { + let (mut wallet, _) = get_funded_wallet("wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),or_i(and_v(v:pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(30)),and_v(v:pkh(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(90)))))"); + + let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); + let root_id = external_policy.id; + // child #0 is pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu) + let path = vec![(root_id, vec![0])].into_iter().collect(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) + .policy_path(path, KeychainKind::External); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); +} + +#[test] +fn test_create_tx_global_xpubs_with_origin() { + use bitcoin::bip32; + use bitcoin::hex::FromHex; + + let (mut wallet, _) = get_funded_wallet("wpkh([73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .add_global_xpubs(); + let psbt = builder.finish().unwrap(); + + let key = bip32::Xpub::from_str("tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3").unwrap(); + let fingerprint = bip32::Fingerprint::from_hex("73756c7f").unwrap(); + let path = bip32::DerivationPath::from_str("m/48'/0'/0'/2'").unwrap(); + + assert_eq!(psbt.xpub.len(), 1); + assert_eq!(psbt.xpub.get(&key), Some(&(fingerprint, path))); +} + +#[test] +fn test_add_foreign_utxo() { + let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); + let (wallet2, _) = + get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let utxo = wallet2.list_unspent().next().expect("must take!"); + let foreign_utxo_satisfaction = wallet2 + .get_descriptor_for_keychain(KeychainKind::External) + .max_weight_to_satisfy() + .unwrap(); + + let psbt_input = psbt::Input { + witness_utxo: Some(utxo.txout.clone()), + ..Default::default() + }; + + let mut builder = wallet1.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)) + .only_witness_utxo() + .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) + .unwrap(); + let mut psbt = builder.finish().unwrap(); + wallet1.insert_txout(utxo.outpoint, utxo.txout); + let fee = check_fee!(wallet1, psbt); + let sent_received = + wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + + assert_eq!( + (sent_received.0 - sent_received.1).to_sat(), + 10_000 + fee.unwrap_or(0), + "we should have only net spent ~10_000" + ); + + assert!( + psbt.unsigned_tx + .input + .iter() + .any(|input| input.previous_output == utxo.outpoint), + "foreign_utxo should be in there" + ); + + 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( + &mut psbt, + SignOptions { + trust_witness_utxo: true, + ..Default::default() + }, + ) + .unwrap(); + assert!(finished, "all the inputs should have been signed now"); +} + +#[test] +#[should_panic( + expected = "MissingTxOut([OutPoint { txid: 0x21d7fb1bceda00ab4069fc52d06baa13470803e9050edd16f5736e5d8c4925fd, vout: 0 }])" +)] +fn test_calculate_fee_with_missing_foreign_utxo() { + let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); + let (wallet2, _) = + get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let utxo = wallet2.list_unspent().next().expect("must take!"); + let foreign_utxo_satisfaction = wallet2 + .get_descriptor_for_keychain(KeychainKind::External) + .max_weight_to_satisfy() + .unwrap(); + + let psbt_input = psbt::Input { + witness_utxo: Some(utxo.txout.clone()), + ..Default::default() + }; + + let mut builder = wallet1.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)) + .only_witness_utxo() + .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) + .unwrap(); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx().expect("failed to extract tx"); + wallet1.calculate_fee(&tx).unwrap(); +} + +#[test] +fn test_add_foreign_utxo_invalid_psbt_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let outpoint = wallet.list_unspent().next().expect("must exist").outpoint; + let foreign_utxo_satisfaction = wallet + .get_descriptor_for_keychain(KeychainKind::External) + .max_weight_to_satisfy() + .unwrap(); + + let mut builder = wallet.build_tx(); + let result = + builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction); + assert!(matches!(result, Err(AddForeignUtxoError::MissingUtxo))); +} + +#[test] +fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { + let (mut wallet1, txid1) = get_funded_wallet(get_test_wpkh()); + let (wallet2, txid2) = + get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + + let utxo2 = wallet2.list_unspent().next().unwrap(); + let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone(); + let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx.clone(); + + let satisfaction_weight = wallet2 + .get_descriptor_for_keychain(KeychainKind::External) + .max_weight_to_satisfy() + .unwrap(); + + let mut builder = wallet1.build_tx(); + assert!( + builder + .add_foreign_utxo( + utxo2.outpoint, + psbt::Input { + non_witness_utxo: Some(tx1.as_ref().clone()), + ..Default::default() + }, + satisfaction_weight + ) + .is_err(), + "should fail when outpoint doesn't match psbt_input" + ); + assert!( + builder + .add_foreign_utxo( + utxo2.outpoint, + psbt::Input { + non_witness_utxo: Some(tx2.as_ref().clone()), + ..Default::default() + }, + satisfaction_weight + ) + .is_ok(), + "should be ok when outpoint does match psbt_input" + ); +} + +#[test] +fn test_add_foreign_utxo_only_witness_utxo() { + let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); + let (wallet2, txid2) = + get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let utxo2 = wallet2.list_unspent().next().unwrap(); + + let satisfaction_weight = wallet2 + .get_descriptor_for_keychain(KeychainKind::External) + .max_weight_to_satisfy() + .unwrap(); + + let mut builder = wallet1.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)); + + { + let mut builder = builder.clone(); + let psbt_input = psbt::Input { + witness_utxo: Some(utxo2.txout.clone()), + ..Default::default() + }; + builder + .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) + .unwrap(); + assert!( + builder.finish().is_err(), + "psbt_input with witness_utxo should fail with only witness_utxo" + ); + } + + { + 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.get_tx(txid2).unwrap().tx_node.tx; + let psbt_input = psbt::Input { + non_witness_utxo: Some(tx2.as_ref().clone()), + ..Default::default() + }; + builder + .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) + .unwrap(); + assert!( + builder.finish().is_ok(), + "psbt_input with non_witness_utxo should succeed by default" + ); + } +} + +#[test] +fn test_get_psbt_input() { + // this should grab a known good utxo and set the input + let (wallet, _) = get_funded_wallet(get_test_wpkh()); + for utxo in wallet.list_unspent() { + let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap(); + assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some()); + } +} + +#[test] +#[should_panic( + expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")" +)] +fn test_create_tx_global_xpubs_origin_missing() { + let (mut wallet, _) = get_funded_wallet("wpkh(tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .add_global_xpubs(); + builder.finish().unwrap(); +} + +#[test] +fn test_create_tx_global_xpubs_master_without_origin() { + use bitcoin::bip32; + use bitcoin::hex::FromHex; + + let (mut wallet, _) = get_funded_wallet("wpkh(tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL/0/*)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .add_global_xpubs(); + let psbt = builder.finish().unwrap(); + + let key = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL").unwrap(); + let fingerprint = bip32::Fingerprint::from_hex("997a323b").unwrap(); + + assert_eq!(psbt.xpub.len(), 1); + assert_eq!( + psbt.xpub.get(&key), + Some(&(fingerprint, bip32::DerivationPath::default())) + ); +} + +#[test] +#[should_panic(expected = "IrreplaceableTransaction")] +fn test_bump_fee_irreplaceable_tx() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + wallet.build_fee_bump(txid).unwrap().finish().unwrap(); +} + +#[test] +#[should_panic(expected = "TransactionConfirmed")] +fn test_bump_fee_confirmed_tx() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.txid(); + + wallet + .insert_tx( + tx, + ConfirmationTime::Confirmed { + height: 42, + time: 42_000, + }, + ) + .unwrap(); + + wallet.build_fee_bump(txid).unwrap().finish().unwrap(); +} + +#[test] +fn test_bump_fee_low_fee_rate() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let feerate = psbt.fee_rate().unwrap(); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.txid(); + + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::BROADCAST_MIN); + let res = builder.finish(); + assert_matches!( + res, + Err(CreateTxError::FeeRateTooLow { .. }), + "expected FeeRateTooLow error" + ); + + let required = feerate.to_sat_per_kwu() + 250; // +1 sat/vb + let sat_vb = required as f64 / 250.0; + let expect = format!("Fee rate too low: required {} sat/vb", sat_vb); + assert_eq!(res.unwrap_err().to_string(), expect); +} + +#[test] +#[should_panic(expected = "FeeTooLow")] +fn test_bump_fee_low_abs() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.txid(); + + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(10); + builder.finish().unwrap(); +} + +#[test] +#[should_panic(expected = "FeeTooLow")] +fn test_bump_fee_zero_abs() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(0); + builder.finish().unwrap(); +} + +#[test] +fn test_bump_fee_reduce_change() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let original_sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let original_fee = check_fee!(wallet, psbt); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(feerate).enable_rbf(); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!(sent_received.0, original_sent_received.0); + assert_eq!( + sent_received.1 + Amount::from_sat(fee.unwrap_or(0)), + original_sent_received.1 + Amount::from_sat(original_fee.unwrap_or(0)) + ); + assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0)); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + Amount::from_sat(25_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(200); + builder.enable_rbf(); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!(sent_received.0, original_sent_received.0); + assert_eq!( + sent_received.1 + Amount::from_sat(fee.unwrap_or(0)), + original_sent_received.1 + Amount::from_sat(original_fee.unwrap_or(0)) + ); + assert!( + fee.unwrap_or(0) > original_fee.unwrap_or(0), + "{} > {}", + fee.unwrap_or(0), + original_fee.unwrap_or(0) + ); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + Amount::from_sat(25_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_eq!(fee.unwrap_or(0), 200); +} + +#[test] +fn test_bump_fee_reduce_single_recipient() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let tx = psbt.clone().extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let original_fee = check_fee!(wallet, psbt); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .fee_rate(feerate) + .allow_shrinking(addr.script_pubkey()) + .unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!(sent_received.0, original_sent_received.0); + assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0)); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.output.len(), 1); + assert_eq!( + tx.output[0].value + Amount::from_sat(fee.unwrap_or(0)), + sent_received.0 + ); + + assert_fee_rate!(psbt, fee.unwrap_or(0), feerate, @add_signature); +} + +#[test] +fn test_bump_fee_absolute_reduce_single_recipient() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let original_fee = check_fee!(wallet, psbt); + let tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .allow_shrinking(addr.script_pubkey()) + .unwrap() + .fee_absolute(300); + let psbt = builder.finish().unwrap(); + let tx = &psbt.unsigned_tx; + let sent_received = wallet.sent_and_received(tx); + let fee = check_fee!(wallet, psbt); + + assert_eq!(sent_received.0, original_sent_received.0); + assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0)); + + assert_eq!(tx.output.len(), 1); + assert_eq!( + tx.output[0].value + Amount::from_sat(fee.unwrap_or(0)), + sent_received.0 + ); + + assert_eq!(fee.unwrap_or(0), 300); +} + +#[test] +fn test_bump_fee_drain_wallet() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + // receive an extra tx so that our wallet has two utxos. + let tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .unwrap() + .script_pubkey(), + value: Amount::from_sat(25_000), + }], + }; + wallet + .insert_tx( + tx.clone(), + ConfirmationTime::Confirmed { + height: wallet.latest_checkpoint().height(), + time: 42_000, + }, + ) + .unwrap(); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxo(OutPoint { + txid: tx.txid(), + vout: 0, + }) + .unwrap() + .manually_selected_only() + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + assert_eq!(original_sent_received.0, Amount::from_sat(25_000)); + + // for the new feerate, it should be enough to reduce the output, but since we specify + // `drain_wallet` we expect to spend everything + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .drain_wallet() + .allow_shrinking(addr.script_pubkey()) + .unwrap() + .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.extract_tx().expect("failed to extract tx")); + + assert_eq!(sent_received.0, Amount::from_sat(75_000)); +} + +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_bump_fee_remove_output_manually_selected_only() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + // receive an extra tx so that our wallet has two utxos. then we manually pick only one of + // them, and make sure that `bump_fee` doesn't try to add more. This fails because we've + // told the wallet it's not allowed to add more inputs AND it can't reduce the value of the + // existing output. In other words, bump_fee + manually_selected_only is always an error + // unless you've also set "allow_shrinking" OR there is a change output. + let init_tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .unwrap() + .script_pubkey(), + value: Amount::from_sat(25_000), + }], + }; + wallet + .insert_tx( + init_tx.clone(), + wallet + .transactions() + .last() + .unwrap() + .chain_position + .cloned() + .into(), + ) + .unwrap(); + let outpoint = OutPoint { + txid: init_tx.txid(), + vout: 0, + }; + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxo(outpoint) + .unwrap() + .manually_selected_only() + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + assert_eq!(original_sent_received.0, Amount::from_sat(25_000)); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .manually_selected_only() + .fee_rate(FeeRate::from_sat_per_vb_unchecked(255)); + builder.finish().unwrap(); +} + +#[test] +fn test_bump_fee_add_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let init_tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .unwrap() + .script_pubkey(), + value: Amount::from_sat(25_000), + }], + }; + let pos = wallet + .transactions() + .last() + .unwrap() + .chain_position + .cloned() + .into(); + wallet.insert_tx(init_tx, pos).unwrap(); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx().expect("failed to extract tx"); + let original_details = wallet.sent_and_received(&tx); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50)); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + assert_eq!( + sent_received.0, + original_details.0 + Amount::from_sat(25_000) + ); + assert_eq!( + Amount::from_sat(fee.unwrap_or(0)) + sent_received.1, + Amount::from_sat(30_000) + ); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + Amount::from_sat(45_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); +} + +#[test] +fn test_bump_fee_absolute_add_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + receive_output_in_latest_block(&mut wallet, 25_000); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(6_000); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!( + sent_received.0, + original_sent_received.0 + Amount::from_sat(25_000) + ); + assert_eq!( + Amount::from_sat(fee.unwrap_or(0)) + sent_received.1, + Amount::from_sat(30_000) + ); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + Amount::from_sat(45_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_eq!(fee.unwrap_or(0), 6_000); +} + +#[test] +fn test_bump_fee_no_change_add_input_and_change() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let op = receive_output_in_latest_block(&mut wallet, 25_000); + + // initially make a tx without change by using `drain_to` + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .add_utxo(op) + .unwrap() + .manually_selected_only() + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let original_sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let original_fee = check_fee!(wallet, psbt); + + let tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + // now bump the fees without using `allow_shrinking`. the wallet should add an + // extra input and a change output, and leave the original output untouched + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50)); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + let original_send_all_amount = + original_sent_received.0 - Amount::from_sat(original_fee.unwrap_or(0)); + assert_eq!( + sent_received.0, + original_sent_received.0 + Amount::from_sat(50_000) + ); + assert_eq!( + sent_received.1, + Amount::from_sat(75_000) - original_send_all_amount - Amount::from_sat(fee.unwrap_or(0)) + ); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + original_send_all_amount + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + Amount::from_sat(75_000) - original_send_all_amount - Amount::from_sat(fee.unwrap_or(0)) + ); + + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); +} + +#[test] +fn test_bump_fee_add_input_change_dust() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + receive_output_in_latest_block(&mut wallet, 25_000); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let original_sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let original_fee = check_fee!(wallet, psbt); + + let mut tx = psbt.extract_tx().expect("failed to extract tx"); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realistic weight + } + let original_tx_weight = tx.weight(); + assert_eq!(tx.input.len(), 1); + assert_eq!(tx.output.len(), 2); + let txid = tx.txid(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + // We set a fee high enough that during rbf we are forced to add + // a new input and also that we have to remove the change + // that we had previously + + // We calculate the new weight as: + // original weight + // + extra input weight: 160 WU = (32 (prevout) + 4 (vout) + 4 (nsequence)) * 4 + // + input satisfaction weight: 112 WU = 106 (witness) + 2 (witness len) + (1 (script len)) * 4 + // - change output weight: 124 WU = (8 (value) + 1 (script len) + 22 (script)) * 4 + let new_tx_weight = + original_tx_weight + Weight::from_wu(160) + Weight::from_wu(112) - Weight::from_wu(124); + // two inputs (50k, 25k) and one output (45k) - epsilon + // We use epsilon here to avoid asking for a slightly too high feerate + let fee_abs = 50_000 + 25_000 - 45_000 - 10; + builder.fee_rate(Amount::from_sat(fee_abs) / new_tx_weight); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!( + original_sent_received.1, + Amount::from_sat(5_000 - original_fee.unwrap_or(0)) + ); + + assert_eq!( + sent_received.0, + original_sent_received.0 + Amount::from_sat(25_000) + ); + assert_eq!(fee.unwrap_or(0), 30_000); + assert_eq!(sent_received.1, Amount::ZERO); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 1); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + Amount::from_sat(45_000) + ); + + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(140), @dust_change, @add_signature); +} + +#[test] +fn test_bump_fee_force_add_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let mut tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.txid(); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + wallet + .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + // the new fee_rate is low enough that just reducing the change would be fine, but we force + // the addition of an extra input with `add_utxo()` + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .add_utxo(incoming_op) + .unwrap() + .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!( + sent_received.0, + original_sent_received.0 + Amount::from_sat(25_000) + ); + assert_eq!( + Amount::from_sat(fee.unwrap_or(0)) + sent_received.1, + Amount::from_sat(30_000) + ); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + Amount::from_sat(45_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb_unchecked(5), @add_signature); +} + +#[test] +fn test_bump_fee_absolute_force_add_input() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let mut tx = psbt.extract_tx().expect("failed to extract tx"); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.txid(); + // skip saving the new utxos, we know they can't be used anyways + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + wallet + .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + // the new fee_rate is low enough that just reducing the change would be fine, but we force + // the addition of an extra input with `add_utxo()` + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.add_utxo(incoming_op).unwrap().fee_absolute(250); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + let fee = check_fee!(wallet, psbt); + + assert_eq!( + sent_received.0, + original_sent_received.0 + Amount::from_sat(25_000) + ); + assert_eq!( + Amount::from_sat(fee.unwrap_or(0)) + sent_received.1, + Amount::from_sat(30_000) + ); + + let tx = &psbt.unsigned_tx; + assert_eq!(tx.input.len(), 2); + assert_eq!(tx.output.len(), 2); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey == addr.script_pubkey()) + .unwrap() + .value, + Amount::from_sat(45_000) + ); + assert_eq!( + tx.output + .iter() + .find(|txout| txout.script_pubkey != addr.script_pubkey()) + .unwrap() + .value, + sent_received.1 + ); + + assert_eq!(fee.unwrap_or(0), 250); +} + +#[test] +#[should_panic(expected = "InsufficientFunds")] +fn test_bump_fee_unconfirmed_inputs_only() { + // We try to bump the fee, but: + // - We can't reduce the change, as we have no change + // - All our UTXOs are unconfirmed + // So, we fail with "InsufficientFunds", as per RBF rule 2: + // The replacement transaction may only include an unconfirmed input + // if that input was included in one of the original transactions. + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .drain_wallet() + .drain_to(addr.script_pubkey()) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + // Now we receive one transaction with 0 confirmations. We won't be able to use that for + // fee bumping, as it's still unconfirmed! + receive_output( + &mut wallet, + 25_000, + ConfirmationTime::Unconfirmed { last_seen: 0 }, + ); + let mut tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.txid(); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25)); + builder.finish().unwrap(); +} + +#[test] +fn test_bump_fee_unconfirmed_input() { + // We create a tx draining the wallet and spending one confirmed + // and one unconfirmed UTXO. We check that we can fee bump normally + // (BIP125 rule 2 only apply to newly added unconfirmed input, you can + // always fee bump with an unconfirmed input if it was included in the + // original transaction) + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + // We receive a tx with 0 confirmations, which will be used as an input + // in the drain tx. + receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0)); + let mut builder = wallet.build_tx(); + builder + .drain_wallet() + .drain_to(addr.script_pubkey()) + .enable_rbf(); + let psbt = builder.finish().unwrap(); + let mut tx = psbt.extract_tx().expect("failed to extract tx"); + let txid = tx.txid(); + for txin in &mut tx.input { + txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature + } + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .unwrap(); + + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .fee_rate(FeeRate::from_sat_per_vb_unchecked(15)) + .allow_shrinking(addr.script_pubkey()) + .unwrap(); + builder.finish().unwrap(); +} + +#[test] +fn test_fee_amount_negative_drain_val() { + // While building the transaction, bdk would calculate the drain_value + // as + // current_delta - fee_amount - drain_fee + // using saturating_sub, meaning that if the result would end up negative, + // it'll remain to zero instead. + // This caused a bug in master where we would calculate the wrong fee + // for a transaction. + // See https://github.com/bitcoindevkit/bdk/issues/660 + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt") + .unwrap() + .assume_checked(); + let fee_rate = FeeRate::from_sat_per_kwu(500); + let incoming_op = receive_output_in_latest_block(&mut wallet, 8859); + + let mut builder = wallet.build_tx(); + builder + .add_recipient(send_to.script_pubkey(), Amount::from_sat(8630)) + .add_utxo(incoming_op) + .unwrap() + .enable_rbf() + .fee_rate(fee_rate); + let psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + assert_eq!(psbt.inputs.len(), 1); + assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate, @add_signature); +} + +#[test] +fn test_sign_single_xprv() { + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_sign_single_xprv_with_master_fingerprint_and_path() { + let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_sign_single_xprv_bip44_path() { + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_sign_single_xprv_sh_wpkh() { + let (mut wallet, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_sign_single_wif() { + let (mut wallet, _) = + get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_sign_single_xprv_no_hd_keypaths() { + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + psbt.inputs[0].bip32_derivation.clear(); + assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!(extracted.input[0].witness.len(), 2); +} + +#[test] +fn test_include_output_redeem_witness_script() { + let (mut wallet, _) = get_funded_wallet("sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))"); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) + .include_output_redeem_witness_script(); + let psbt = builder.finish().unwrap(); + + // p2sh-p2wsh transaction should contain both witness and redeem scripts + assert!(psbt + .outputs + .iter() + .any(|output| output.redeem_script.is_some() && output.witness_script.is_some())); +} + +#[test] +fn test_signing_only_one_of_multiple_inputs() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) + .include_output_redeem_witness_script(); + let mut psbt = builder.finish().unwrap(); + + // add another input to the psbt that is at least passable. + let dud_input = bitcoin::psbt::Input { + witness_utxo: Some(TxOut { + value: Amount::from_sat(100_000), + script_pubkey: miniscript::Descriptor::::from_str( + "wpkh(025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357)", + ) + .unwrap() + .script_pubkey(), + }), + ..Default::default() + }; + + psbt.inputs.push(dud_input); + psbt.unsigned_tx.input.push(bitcoin::TxIn::default()); + 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" + ); + assert!( + psbt.inputs[0].final_script_witness.is_some(), + "should finalized input it signed" + ) +} + +#[test] +fn test_remove_partial_sigs_after_finalize_sign_option() { + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + + for remove_partial_sigs in &[true, false] { + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + assert!(wallet + .sign( + &mut psbt, + SignOptions { + remove_partial_sigs: *remove_partial_sigs, + ..Default::default() + }, + ) + .unwrap()); + + psbt.inputs.iter().for_each(|input| { + if *remove_partial_sigs { + assert!(input.partial_sigs.is_empty()) + } else { + assert!(!input.partial_sigs.is_empty()) + } + }); + } +} + +#[test] +fn test_try_finalize_sign_option() { + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + + for try_finalize in &[true, false] { + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + let finalized = wallet + .sign( + &mut psbt, + SignOptions { + try_finalize: *try_finalize, + ..Default::default() + }, + ) + .unwrap(); + + psbt.inputs.iter().for_each(|input| { + if *try_finalize { + assert!(finalized); + assert!(input.final_script_sig.is_some()); + assert!(input.final_script_witness.is_some()); + } else { + assert!(!finalized); + assert!(input.final_script_sig.is_none()); + assert!(input.final_script_witness.is_none()); + } + }); + } +} + +#[test] +fn test_sign_nonstandard_sighash() { + let sighash = EcdsaSighashType::NonePlusAnyoneCanPay; + + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .sighash(sighash.into()) + .drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + let result = wallet.sign(&mut psbt, Default::default()); + assert!( + result.is_err(), + "Signing should have failed because the TX uses non-standard sighashes" + ); + assert_matches!( + result, + Err(SignerError::NonStandardSighash), + "Signing failed with the wrong error type" + ); + + // try again after opting-in + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + assert!(result.is_ok(), "Signing should have worked"); + assert!( + result.unwrap(), + "Should finalize the input since we can produce signatures" + ); + + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!( + *extracted.input[0].witness.to_vec()[0].last().unwrap(), + sighash.to_u32() as u8, + "The signature should have been made with the right sighash" + ); +} + +#[test] +fn test_unused_address() { + let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", + None, Network::Testnet).unwrap(); + + // `list_unused_addresses` should be empty if we haven't revealed any + assert!(wallet + .list_unused_addresses(KeychainKind::External) + .next() + .is_none()); + + assert_eq!( + wallet + .next_unused_address(KeychainKind::External) + .unwrap() + .to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); + assert_eq!( + wallet + .list_unused_addresses(KeychainKind::External) + .next() + .unwrap() + .to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); +} + +#[test] +fn test_next_unused_address() { + let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; + let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet).unwrap(); + assert_eq!(wallet.derivation_index(KeychainKind::External), None); + + assert_eq!( + wallet + .next_unused_address(KeychainKind::External) + .unwrap() + .to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); + assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); + // calling next_unused again gives same address + assert_eq!( + wallet + .next_unused_address(KeychainKind::External) + .unwrap() + .to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); + assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); + + // test mark used / unused + assert!(wallet.mark_used(KeychainKind::External, 0)); + let next_unused_addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + assert_eq!(next_unused_addr.index, 1); + + assert!(wallet.unmark_used(KeychainKind::External, 0)); + let next_unused_addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + assert_eq!(next_unused_addr.index, 0); + + // use the above address + receive_output_in_latest_block(&mut wallet, 25_000); + + assert_eq!( + wallet + .next_unused_address(KeychainKind::External) + .unwrap() + .to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); + assert_eq!(wallet.derivation_index(KeychainKind::External), Some(1)); + + // trying to mark index 0 unused should return false + assert!(!wallet.unmark_used(KeychainKind::External, 0)); +} + +#[test] +fn test_peek_address_at_index() { + let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", + None, Network::Testnet).unwrap(); + + assert_eq!( + wallet.peek_address(KeychainKind::External, 1).to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); + + assert_eq!( + wallet.peek_address(KeychainKind::External, 0).to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); + + assert_eq!( + wallet.peek_address(KeychainKind::External, 2).to_string(), + "tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2" + ); + + // current new address is not affected + assert_eq!( + wallet + .reveal_next_address(KeychainKind::External) + .unwrap() + .to_string(), + "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" + ); + + assert_eq!( + wallet + .reveal_next_address(KeychainKind::External) + .unwrap() + .to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); +} + +#[test] +fn test_peek_address_at_index_not_derivable() { + let wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)", + None, Network::Testnet).unwrap(); + + assert_eq!( + wallet.peek_address(KeychainKind::External, 1).to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); + + assert_eq!( + wallet.peek_address(KeychainKind::External, 0).to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); + + assert_eq!( + wallet.peek_address(KeychainKind::External, 2).to_string(), + "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" + ); +} + +#[test] +fn test_returns_index_and_address() { + let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", + None, Network::Testnet).unwrap(); + + // new index 0 + assert_eq!( + wallet.reveal_next_address(KeychainKind::External).unwrap(), + AddressInfo { + index: 0, + address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a") + .unwrap() + .assume_checked(), + keychain: KeychainKind::External, + } + ); + + // new index 1 + assert_eq!( + wallet.reveal_next_address(KeychainKind::External).unwrap(), + AddressInfo { + index: 1, + address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7") + .unwrap() + .assume_checked(), + keychain: KeychainKind::External, + } + ); + + // peek index 25 + assert_eq!( + wallet.peek_address(KeychainKind::External, 25), + AddressInfo { + index: 25, + address: Address::from_str("tb1qsp7qu0knx3sl6536dzs0703u2w2ag6ppl9d0c2") + .unwrap() + .assume_checked(), + keychain: KeychainKind::External, + } + ); + + // new index 2 + assert_eq!( + wallet.reveal_next_address(KeychainKind::External).unwrap(), + AddressInfo { + index: 2, + address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2") + .unwrap() + .assume_checked(), + keychain: KeychainKind::External, + } + ); +} + +#[test] +fn test_sending_to_bip350_bech32m_address() { + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = Address::from_str("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); + builder.finish().unwrap(); +} + +#[test] +fn test_get_address() { + use bdk_wallet::descriptor::template::Bip84; + let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let wallet = Wallet::new_no_persist( + Bip84(key, KeychainKind::External), + Some(Bip84(key, KeychainKind::Internal)), + Network::Regtest, + ) + .unwrap(); + + assert_eq!( + wallet.peek_address(KeychainKind::External, 0), + AddressInfo { + index: 0, + address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w") + .unwrap() + .assume_checked(), + keychain: KeychainKind::External, + } + ); + + assert_eq!( + wallet.peek_address(KeychainKind::Internal, 0), + AddressInfo { + index: 0, + address: Address::from_str("bcrt1q0ue3s5y935tw7v3gmnh36c5zzsaw4n9c9smq79") + .unwrap() + .assume_checked(), + keychain: KeychainKind::Internal, + } + ); + + let wallet = + Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); + + assert_eq!( + wallet.peek_address(KeychainKind::Internal, 0), + AddressInfo { + index: 0, + address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w") + .unwrap() + .assume_checked(), + keychain: KeychainKind::External, + }, + "when there's no internal descriptor it should just use external" + ); +} + +#[test] +fn test_reveal_addresses() { + let desc = get_test_tr_single_sig_xprv(); + let mut wallet = Wallet::new_no_persist(desc, None, Network::Signet).unwrap(); + let keychain = KeychainKind::External; + + let last_revealed_addr = wallet + .reveal_addresses_to(keychain, 9) + .unwrap() + .last() + .unwrap(); + assert_eq!(wallet.derivation_index(keychain), Some(9)); + + let unused_addrs = wallet.list_unused_addresses(keychain).collect::>(); + assert_eq!(unused_addrs.len(), 10); + assert_eq!(unused_addrs.last().unwrap(), &last_revealed_addr); + + // revealing to an already revealed index returns nothing + let mut already_revealed = wallet.reveal_addresses_to(keychain, 9).unwrap(); + assert!(already_revealed.next().is_none()); +} + +#[test] +fn test_get_address_no_reuse_single_descriptor() { + use bdk_wallet::descriptor::template::Bip84; + use std::collections::HashSet; + + let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let mut wallet = + Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); + + let mut used_set = HashSet::new(); + + (0..3).for_each(|_| { + let external_addr = wallet + .reveal_next_address(KeychainKind::External) + .unwrap() + .address; + assert!(used_set.insert(external_addr)); + + let internal_addr = wallet + .reveal_next_address(KeychainKind::Internal) + .unwrap() + .address; + assert!(used_set.insert(internal_addr)); + }); +} + +#[test] +fn test_taproot_remove_tapfields_after_finalize_sign_option() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); + + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); + assert!(finalized); + + // removes tap_* from inputs + for input in &psbt.inputs { + assert!(input.tap_key_sig.is_none()); + assert!(input.tap_script_sigs.is_empty()); + assert!(input.tap_scripts.is_empty()); + assert!(input.tap_key_origins.is_empty()); + assert!(input.tap_internal_key.is_none()); + assert!(input.tap_merkle_root.is_none()); + } + // removes key origins from outputs + for output in &psbt.outputs { + assert!(output.tap_key_origins.is_empty()); + } +} + +#[test] +fn test_taproot_psbt_populate_tap_key_origins() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); + let addr = wallet.reveal_next_address(KeychainKind::External).unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(), + vec![( + from_str!("b96d3a3dc76a4fc74e976511b23aecb78e0754c23c0ed7a6513e18cbbc7178e9"), + (vec![], (from_str!("f6a5cb8b"), from_str!("m/0"))) + )], + "Wrong input tap_key_origins" + ); + assert_eq!( + psbt.outputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(), + vec![( + from_str!("e9b03068cf4a2621d4f81e68f6c4216e6bd260fe6edf6acc55c8d8ae5aeff0a8"), + (vec![], (from_str!("f6a5cb8b"), from_str!("m/1"))) + )], + "Wrong output tap_key_origins" + ); +} + +#[test] +fn test_taproot_psbt_populate_tap_key_origins_repeated_key() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_repeated_key()); + let addr = wallet.reveal_next_address(KeychainKind::External).unwrap(); + + let path = vec![("rn4nre9c".to_string(), vec![0])] + .into_iter() + .collect(); + + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) + .policy_path(path, KeychainKind::External); + let psbt = builder.finish().unwrap(); + + let mut input_key_origins = psbt.inputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(); + input_key_origins.sort(); + + assert_eq!( + input_key_origins, + vec![ + ( + from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"), + ( + vec![ + from_str!( + "858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e" + ), + from_str!( + "f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903" + ), + ], + (FromStr::from_str("ece52657").unwrap(), vec![].into()) + ) + ), + ( + from_str!("b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"), + ( + vec![], + (FromStr::from_str("871fd295").unwrap(), vec![].into()) + ) + ) + ], + "Wrong input tap_key_origins" + ); + + let mut output_key_origins = psbt.outputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(); + output_key_origins.sort(); + + assert_eq!( + input_key_origins, output_key_origins, + "Wrong output tap_key_origins" + ); +} + +#[test] +fn test_taproot_psbt_input_tap_tree() { + use bitcoin::hex::FromHex; + use bitcoin::taproot; + + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0].tap_merkle_root, + Some( + TapNodeHash::from_str( + "61f81509635053e52d9d1217545916167394490da2287aca4693606e43851986" + ) + .unwrap() + ), + ); + assert_eq!( + psbt.inputs[0].tap_scripts.clone().into_iter().collect::>(), + vec![ + (taproot::ControlBlock::decode(&Vec::::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b7ef769a745e625ed4b9a4982a4dc08274c59187e73e6f07171108f455081cb2").unwrap()).unwrap(), (ScriptBuf::from_hex("208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac").unwrap(), taproot::LeafVersion::TapScript)), + (taproot::ControlBlock::decode(&Vec::::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b9a515f7be31a70186e3c5937ee4a70cc4b4e1efe876c1d38e408222ffc64834").unwrap()).unwrap(), (ScriptBuf::from_hex("2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac").unwrap(), taproot::LeafVersion::TapScript)), + ], + ); + assert_eq!( + psbt.inputs[0].tap_internal_key, + Some(from_str!( + "b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55" + )) + ); + + // Since we are creating an output to the same address as the input, assert that the + // internal_key is the same + assert_eq!( + psbt.inputs[0].tap_internal_key, + psbt.outputs[0].tap_internal_key + ); + + let tap_tree: bitcoin::taproot::TapTree = serde_json::from_str(r#"[1,{"Script":["2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac",192]},1,{"Script":["208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac",192]}]"#).unwrap(); + assert_eq!(psbt.outputs[0].tap_tree, Some(tap_tree)); +} + +#[test] +fn test_taproot_sign_missing_witness_utxo() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + let witness_utxo = psbt.inputs[0].witness_utxo.take(); + + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + assert_matches!( + result, + Err(SignerError::MissingWitnessUtxo), + "Signing should have failed with the correct error because the witness_utxo is missing" + ); + + // restore the witness_utxo + psbt.inputs[0].witness_utxo = witness_utxo; + + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + + assert_matches!( + result, + Ok(true), + "Should finalize the input since we can produce signatures" + ); +} + +#[test] +fn test_taproot_sign_using_non_witness_utxo() { + let (mut wallet, prev_txid) = get_funded_wallet(get_test_tr_single_sig()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + psbt.inputs[0].witness_utxo = None; + psbt.inputs[0].non_witness_utxo = + Some(wallet.get_tx(prev_txid).unwrap().tx_node.as_ref().clone()); + assert!( + psbt.inputs[0].non_witness_utxo.is_some(), + "Previous tx should be present in the database" + ); + + let result = wallet.sign(&mut psbt, Default::default()); + assert!(result.is_ok(), "Signing should have worked"); + assert!( + result.unwrap(), + "Should finalize the input since we can produce signatures" + ); +} + +#[test] +fn test_taproot_foreign_utxo() { + let (mut wallet1, _) = get_funded_wallet(get_test_wpkh()); + let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig()); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let utxo = wallet2.list_unspent().next().unwrap(); + let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); + let foreign_utxo_satisfaction = wallet2 + .get_descriptor_for_keychain(KeychainKind::External) + .max_weight_to_satisfy() + .unwrap(); + + assert!( + psbt_input.non_witness_utxo.is_none(), + "`non_witness_utxo` should never be populated for taproot" + ); + + let mut builder = wallet1.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)) + .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) + .unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = + wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); + wallet1.insert_txout(utxo.outpoint, utxo.txout); + let fee = check_fee!(wallet1, psbt); + + assert_eq!( + sent_received.0 - sent_received.1, + Amount::from_sat(10_000 + fee.unwrap_or(0)), + "we should have only net spent ~10_000" + ); + + assert!( + psbt.unsigned_tx + .input + .iter() + .any(|input| input.previous_output == utxo.outpoint), + "foreign_utxo should be in there" + ); +} + +fn test_spend_from_wallet(mut wallet: Wallet) { + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let mut psbt = builder.finish().unwrap(); + + assert!( + wallet.sign(&mut psbt, Default::default()).unwrap(), + "Unable to finalize tx" + ); +} + +// #[test] +// fn test_taproot_key_spend() { +// let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); +// test_spend_from_wallet(wallet); + +// let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); +// test_spend_from_wallet(wallet); +// } + +#[test] +fn test_taproot_no_key_spend() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let mut psbt = builder.finish().unwrap(); + + assert!( + wallet + .sign( + &mut psbt, + SignOptions { + sign_with_tap_internal_key: false, + ..Default::default() + }, + ) + .unwrap(), + "Unable to finalize tx" + ); + + assert!(psbt.inputs.iter().all(|i| i.tap_key_sig.is_none())); +} + +#[test] +fn test_taproot_script_spend() { + let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); + test_spend_from_wallet(wallet); + + let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree_xprv()); + test_spend_from_wallet(wallet); +} + +#[test] +fn test_taproot_script_spend_sign_all_leaves() { + use bdk_wallet::signer::TapLeavesOptions; + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let mut psbt = builder.finish().unwrap(); + + assert!( + wallet + .sign( + &mut psbt, + SignOptions { + tap_leaves_options: TapLeavesOptions::All, + ..Default::default() + }, + ) + .unwrap(), + "Unable to finalize tx" + ); + + assert!(psbt + .inputs + .iter() + .all(|i| i.tap_script_sigs.len() == i.tap_scripts.len())); +} + +#[test] +fn test_taproot_script_spend_sign_include_some_leaves() { + use bdk_wallet::signer::TapLeavesOptions; + use bitcoin::taproot::TapLeafHash; + + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let mut psbt = builder.finish().unwrap(); + let mut script_leaves: Vec<_> = psbt.inputs[0] + .tap_scripts + .clone() + .values() + .map(|(script, version)| TapLeafHash::from_script(script, *version)) + .collect(); + let included_script_leaves = vec![script_leaves.pop().unwrap()]; + let excluded_script_leaves = script_leaves; + + assert!( + wallet + .sign( + &mut psbt, + SignOptions { + tap_leaves_options: TapLeavesOptions::Include(included_script_leaves.clone()), + ..Default::default() + }, + ) + .unwrap(), + "Unable to finalize tx" + ); + + assert!(psbt.inputs[0] + .tap_script_sigs + .iter() + .all(|s| included_script_leaves.contains(&s.0 .1) + && !excluded_script_leaves.contains(&s.0 .1))); +} + +#[test] +fn test_taproot_script_spend_sign_exclude_some_leaves() { + use bdk_wallet::signer::TapLeavesOptions; + use bitcoin::taproot::TapLeafHash; + + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let mut psbt = builder.finish().unwrap(); + let mut script_leaves: Vec<_> = psbt.inputs[0] + .tap_scripts + .clone() + .values() + .map(|(script, version)| TapLeafHash::from_script(script, *version)) + .collect(); + let included_script_leaves = [script_leaves.pop().unwrap()]; + let excluded_script_leaves = script_leaves; + + assert!( + wallet + .sign( + &mut psbt, + SignOptions { + tap_leaves_options: TapLeavesOptions::Exclude(excluded_script_leaves.clone()), + ..Default::default() + }, + ) + .unwrap(), + "Unable to finalize tx" + ); + + assert!(psbt.inputs[0] + .tap_script_sigs + .iter() + .all(|s| included_script_leaves.contains(&s.0 .1) + && !excluded_script_leaves.contains(&s.0 .1))); +} + +#[test] +fn test_taproot_script_spend_sign_no_leaves() { + use bdk_wallet::signer::TapLeavesOptions; + let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let mut psbt = builder.finish().unwrap(); + + wallet + .sign( + &mut psbt, + SignOptions { + tap_leaves_options: TapLeavesOptions::None, + ..Default::default() + }, + ) + .unwrap(); + + assert!(psbt.inputs.iter().all(|i| i.tap_script_sigs.is_empty())); +} + +#[test] +fn test_taproot_sign_derive_index_from_psbt() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); + + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let mut psbt = builder.finish().unwrap(); + + // re-create the wallet with an empty db + let wallet_empty = + Wallet::new_no_persist(get_test_tr_single_sig_xprv(), None, Network::Regtest).unwrap(); + + // signing with an empty db means that we will only look at the psbt to infer the + // derivation index + assert!( + wallet_empty.sign(&mut psbt, Default::default()).unwrap(), + "Unable to finalize tx" + ); +} + +#[test] +fn test_taproot_sign_explicit_sighash_all() { + let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .sighash(TapSighashType::All.into()) + .drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + let result = wallet.sign(&mut psbt, Default::default()); + assert!( + result.is_ok(), + "Signing should work because SIGHASH_ALL is safe" + ) +} + +#[test] +fn test_taproot_sign_non_default_sighash() { + let sighash = TapSighashType::NonePlusAnyoneCanPay; + + let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .sighash(sighash.into()) + .drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + let witness_utxo = psbt.inputs[0].witness_utxo.take(); + + let result = wallet.sign(&mut psbt, Default::default()); + assert!( + result.is_err(), + "Signing should have failed because the TX uses non-standard sighashes" + ); + assert_matches!( + result, + Err(SignerError::NonStandardSighash), + "Signing failed with the wrong error type" + ); + + // try again after opting-in + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + assert!( + result.is_err(), + "Signing should have failed because the witness_utxo is missing" + ); + assert_matches!( + result, + Err(SignerError::MissingWitnessUtxo), + "Signing failed with the wrong error type" + ); + + // restore the witness_utxo + psbt.inputs[0].witness_utxo = witness_utxo; + + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + + assert!(result.is_ok(), "Signing should have worked"); + assert!( + result.unwrap(), + "Should finalize the input since we can produce signatures" + ); + + let extracted = psbt.extract_tx().expect("failed to extract tx"); + assert_eq!( + *extracted.input[0].witness.to_vec()[0].last().unwrap(), + sighash as u8, + "The signature should have been made with the right sighash" + ); +} + +#[test] +fn test_spend_coinbase() { + let descriptor = get_test_wpkh(); + let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Regtest).unwrap(); + + let confirmation_height = 5; + wallet + .insert_checkpoint(BlockId { + height: confirmation_height, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + let coinbase_tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + ..Default::default() + }], + output: vec![TxOut { + script_pubkey: wallet + .next_unused_address(KeychainKind::External) + .unwrap() + .script_pubkey(), + value: Amount::from_sat(25_000), + }], + }; + wallet + .insert_tx( + coinbase_tx, + ConfirmationTime::Confirmed { + height: confirmation_height, + time: 30_000, + }, + ) + .unwrap(); + + let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1; + let maturity_time = confirmation_height + COINBASE_MATURITY; + + let balance = wallet.get_balance(); + assert_eq!( + balance, + Balance { + immature: Amount::from_sat(25_000), + trusted_pending: Amount::ZERO, + untrusted_pending: Amount::ZERO, + confirmed: Amount::ZERO + } + ); + + // We try to create a transaction, only to notice that all + // our funds are unspendable + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), balance.immature / 2) + .current_height(confirmation_height); + assert!(matches!( + builder.finish(), + Err(CreateTxError::CoinSelection( + coin_selection::Error::InsufficientFunds { + needed: _, + available: 0 + } + )) + )); + + // Still unspendable... + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), balance.immature / 2) + .current_height(not_yet_mature_time); + assert_matches!( + builder.finish(), + Err(CreateTxError::CoinSelection( + coin_selection::Error::InsufficientFunds { + needed: _, + available: 0 + } + )) + ); + + wallet + .insert_checkpoint(BlockId { + height: maturity_time, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + let balance = wallet.get_balance(); + assert_eq!( + balance, + Balance { + immature: Amount::ZERO, + trusted_pending: Amount::ZERO, + untrusted_pending: Amount::ZERO, + confirmed: Amount::from_sat(25_000) + } + ); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), balance.confirmed / 2) + .current_height(maturity_time); + builder.finish().unwrap(); +} + +#[test] +fn test_allow_dust_limit() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); + + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + + let mut builder = wallet.build_tx(); + + builder.add_recipient(addr.script_pubkey(), Amount::ZERO); + + assert_matches!( + builder.finish(), + Err(CreateTxError::OutputBelowDustLimit(0)) + ); + + let mut builder = wallet.build_tx(); + + builder + .allow_dust(true) + .add_recipient(addr.script_pubkey(), Amount::ZERO); + + assert!(builder.finish().is_ok()); +} + +#[test] +fn test_fee_rate_sign_no_grinding_high_r() { + // Our goal is to obtain a transaction with a signature with high-R (71 bytes + // instead of 70). We then check that our fee rate and fee calculation is + // alright. + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); + let mut builder = wallet.build_tx(); + let mut data = PushBytesBuf::try_from(vec![0]).unwrap(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_rate(fee_rate) + .add_data(&data); + let mut psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + let (op_return_vout, _) = psbt + .unsigned_tx + .output + .iter() + .enumerate() + .find(|(_n, i)| i.script_pubkey.is_op_return()) + .unwrap(); + + let mut sig_len: usize = 0; + // We try to sign many different times until we find a longer signature (71 bytes) + while sig_len < 71 { + // Changing the OP_RETURN data will make the signature change (but not the fee, until + // data[0] is small enough) + data.as_mut_bytes()[0] += 1; + psbt.unsigned_tx.output[op_return_vout].script_pubkey = ScriptBuf::new_op_return(&data); + // Clearing the previous signature + psbt.inputs[0].partial_sigs.clear(); + // Signing + wallet + .sign( + &mut psbt, + SignOptions { + remove_partial_sigs: false, + try_finalize: false, + allow_grinding: false, + ..Default::default() + }, + ) + .unwrap(); + // We only have one key in the partial_sigs map, this is a trick to retrieve it + let key = psbt.inputs[0].partial_sigs.keys().next().unwrap(); + sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len(); + } + // Actually finalizing the transaction... + wallet + .sign( + &mut psbt, + SignOptions { + remove_partial_sigs: false, + allow_grinding: false, + ..Default::default() + }, + ) + .unwrap(); + // ...and checking that everything is fine + assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate); +} + +#[test] +fn test_fee_rate_sign_grinding_low_r() { + // Our goal is to obtain a transaction with a signature with low-R (70 bytes) + // by setting the `allow_grinding` signing option as true. + // We then check that our fee rate and fee calculation is alright and that our + // signature is 70 bytes. + let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); + let addr = wallet.next_unused_address(KeychainKind::External).unwrap(); + let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .drain_wallet() + .fee_rate(fee_rate); + let mut psbt = builder.finish().unwrap(); + let fee = check_fee!(wallet, psbt); + + wallet + .sign( + &mut psbt, + SignOptions { + remove_partial_sigs: false, + allow_grinding: true, + ..Default::default() + }, + ) + .unwrap(); + + let key = psbt.inputs[0].partial_sigs.keys().next().unwrap(); + let sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len(); + assert_eq!(sig_len, 70); + assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate); +} + +#[test] +fn test_taproot_load_descriptor_duplicated_keys() { + // Added after issue https://github.com/bitcoindevkit/bdk/issues/760 + // + // Having the same key in multiple taproot leaves is safe and should be accepted by BDK + + let (wallet, _) = get_funded_wallet(get_test_tr_dup_keys()); + let addr = wallet.peek_address(KeychainKind::External, 0); + + assert_eq!( + addr.to_string(), + "bcrt1pvysh4nmh85ysrkpwtrr8q8gdadhgdejpy6f9v424a8v9htjxjhyqw9c5s5" + ); +} + +#[test] +/// The wallet should re-use previously allocated change addresses when the tx using them is cancelled +fn test_tx_cancellation() { + macro_rules! new_tx { + ($wallet:expr) => {{ + let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") + .unwrap() + .assume_checked(); + let mut builder = $wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)); + + let psbt = builder.finish().unwrap(); + + psbt + }}; + } + + let (mut wallet, _) = + get_funded_wallet_with_change(get_test_wpkh(), Some(get_test_tr_single_sig_xprv())); + + let psbt1 = new_tx!(wallet); + let change_derivation_1 = psbt1 + .unsigned_tx + .output + .iter() + .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey)) + .unwrap(); + assert_eq!(change_derivation_1, (KeychainKind::Internal, 0)); + + let psbt2 = new_tx!(wallet); + + let change_derivation_2 = psbt2 + .unsigned_tx + .output + .iter() + .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey)) + .unwrap(); + assert_eq!(change_derivation_2, (KeychainKind::Internal, 1)); + + wallet.cancel_tx(&psbt1.extract_tx().expect("failed to extract tx")); + + let psbt3 = new_tx!(wallet); + let change_derivation_3 = psbt3 + .unsigned_tx + .output + .iter() + .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey)) + .unwrap(); + assert_eq!(change_derivation_3, (KeychainKind::Internal, 0)); + + let psbt3 = new_tx!(wallet); + let change_derivation_3 = psbt3 + .unsigned_tx + .output + .iter() + .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey)) + .unwrap(); + assert_eq!(change_derivation_3, (KeychainKind::Internal, 2)); + + wallet.cancel_tx(&psbt3.extract_tx().expect("failed to extract tx")); + + let psbt3 = new_tx!(wallet); + let change_derivation_4 = psbt3 + .unsigned_tx + .output + .iter() + .find_map(|txout| wallet.derivation_of_spk(&txout.script_pubkey)) + .unwrap(); + assert_eq!(change_derivation_4, (KeychainKind::Internal, 2)); +} + +#[test] +fn test_thread_safety() { + fn thread_safe() {} + thread_safe::(); // compiles only if true +} diff --git a/example-crates/wallet_electrum/Cargo.toml b/example-crates/wallet_electrum/Cargo.toml index 847cd90d..2f562837 100644 --- a/example-crates/wallet_electrum/Cargo.toml +++ b/example-crates/wallet_electrum/Cargo.toml @@ -4,7 +4,7 @@ version = "0.2.0" edition = "2021" [dependencies] -bdk = { path = "../../crates/bdk" } +bdk_wallet = { path = "../../crates/wallet" } bdk_electrum = { path = "../../crates/electrum" } bdk_file_store = { path = "../../crates/file_store" } anyhow = "1" diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index eca96f32..c411713f 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -6,19 +6,20 @@ const BATCH_SIZE: usize = 5; use std::io::Write; use std::str::FromStr; -use bdk::bitcoin::{Address, Amount}; -use bdk::chain::collections::HashSet; -use bdk::{bitcoin::Network, Wallet}; -use bdk::{KeychainKind, SignOptions}; use bdk_electrum::{ electrum_client::{self, ElectrumApi}, ElectrumExt, }; use bdk_file_store::Store; +use bdk_wallet::bitcoin::{Address, Amount}; +use bdk_wallet::chain::collections::HashSet; +use bdk_wallet::{bitcoin::Network, Wallet}; +use bdk_wallet::{KeychainKind, SignOptions}; fn main() -> Result<(), anyhow::Error> { let db_path = std::env::temp_dir().join("bdk-electrum-example"); - let db = Store::::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; + let db = + Store::::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; diff --git a/example-crates/wallet_esplora_async/Cargo.toml b/example-crates/wallet_esplora_async/Cargo.toml index c588a87a..803c0fd3 100644 --- a/example-crates/wallet_esplora_async/Cargo.toml +++ b/example-crates/wallet_esplora_async/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bdk = { path = "../../crates/bdk" } +bdk_wallet = { path = "../../crates/wallet" } bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] } bdk_file_store = { path = "../../crates/file_store" } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index 7664ec32..22fb8b2d 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -1,11 +1,11 @@ use std::{collections::BTreeSet, io::Write, str::FromStr}; -use bdk::{ +use bdk_esplora::{esplora_client, EsploraAsyncExt}; +use bdk_file_store::Store; +use bdk_wallet::{ bitcoin::{Address, Amount, Network, Script}, KeychainKind, SignOptions, Wallet, }; -use bdk_esplora::{esplora_client, EsploraAsyncExt}; -use bdk_file_store::Store; const DB_MAGIC: &str = "bdk_wallet_esplora_async_example"; const SEND_AMOUNT: Amount = Amount::from_sat(5000); @@ -15,7 +15,8 @@ const PARALLEL_REQUESTS: usize = 5; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { let db_path = std::env::temp_dir().join("bdk-esplora-async-example"); - let db = Store::::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; + let db = + Store::::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; diff --git a/example-crates/wallet_esplora_blocking/Cargo.toml b/example-crates/wallet_esplora_blocking/Cargo.toml index 0679bd8f..857660ac 100644 --- a/example-crates/wallet_esplora_blocking/Cargo.toml +++ b/example-crates/wallet_esplora_blocking/Cargo.toml @@ -7,7 +7,7 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bdk = { path = "../../crates/bdk" } +bdk_wallet = { path = "../../crates/wallet" } bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] } bdk_file_store = { path = "../../crates/file_store" } anyhow = "1" diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 4d713156..f42cb9d8 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -5,16 +5,17 @@ const PARALLEL_REQUESTS: usize = 1; use std::{collections::BTreeSet, io::Write, str::FromStr}; -use bdk::{ +use bdk_esplora::{esplora_client, EsploraExt}; +use bdk_file_store::Store; +use bdk_wallet::{ bitcoin::{Address, Amount, Network}, KeychainKind, SignOptions, Wallet, }; -use bdk_esplora::{esplora_client, EsploraExt}; -use bdk_file_store::Store; fn main() -> Result<(), anyhow::Error> { let db_path = std::env::temp_dir().join("bdk-esplora-example"); - let db = Store::::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; + let db = + Store::::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; diff --git a/example-crates/wallet_rpc/Cargo.toml b/example-crates/wallet_rpc/Cargo.toml index 174144e9..b7a9a9e4 100644 --- a/example-crates/wallet_rpc/Cargo.toml +++ b/example-crates/wallet_rpc/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bdk = { path = "../../crates/bdk" } +bdk_wallet = { path = "../../crates/wallet" } bdk_file_store = { path = "../../crates/file_store" } bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" } diff --git a/example-crates/wallet_rpc/README.md b/example-crates/wallet_rpc/README.md index 03c1b0f5..28eb07b1 100644 --- a/example-crates/wallet_rpc/README.md +++ b/example-crates/wallet_rpc/README.md @@ -4,7 +4,7 @@ $ cargo run --bin wallet_rpc -- --help wallet_rpc 0.1.0 -Bitcoind RPC example using `bdk::Wallet` +Bitcoind RPC example using `bdk_wallet::Wallet` USAGE: wallet_rpc [OPTIONS] [CHANGE_DESCRIPTOR] diff --git a/example-crates/wallet_rpc/src/main.rs b/example-crates/wallet_rpc/src/main.rs index 264204a7..3bdd515c 100644 --- a/example-crates/wallet_rpc/src/main.rs +++ b/example-crates/wallet_rpc/src/main.rs @@ -1,18 +1,18 @@ -use bdk::{ - bitcoin::{Block, Network, Transaction}, - wallet::Wallet, -}; use bdk_bitcoind_rpc::{ bitcoincore_rpc::{Auth, Client, RpcApi}, Emitter, }; use bdk_file_store::Store; +use bdk_wallet::{ + bitcoin::{Block, Network, Transaction}, + wallet::Wallet, +}; use clap::{self, Parser}; use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant}; const DB_MAGIC: &str = "bdk-rpc-wallet-example"; -/// Bitcoind RPC example using `bdk::Wallet`. +/// Bitcoind RPC example using `bdk_wallet::Wallet`. /// /// This syncs the chain block-by-block and prints the current balance, transaction count and UTXO /// count. @@ -89,7 +89,10 @@ fn main() -> anyhow::Result<()> { let mut wallet = Wallet::new_or_load( &args.descriptor, args.change_descriptor.as_ref(), - Store::::open_or_create_new(DB_MAGIC.as_bytes(), args.db_path)?, + Store::::open_or_create_new( + DB_MAGIC.as_bytes(), + args.db_path, + )?, args.network, )?; println!(