From 6118da97b78a12a9c41699be7b30ee967811b1c8 Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Wed, 1 Jul 2026 10:52:25 +0300 Subject: [PATCH 1/3] refactor(wallet): migrate signing to caller-owned keys Prefer bitcoin::Psbt::sign or Wallet::sign_with_signers in docs, examples, and tests instead of relying on wallet-owned signer state. Add FullyNodedExport::export_wallet_with_keymaps so private descriptor material can be supplied explicitly during export. --- README.md | 1 - examples/bitcoind_rpc.rs | 1 - examples/electrum.rs | 26 ++- examples/esplora_async.rs | 27 ++- examples/esplora_blocking.rs | 27 ++- src/wallet/export.rs | 152 ++++++++++++---- src/wallet/mod.rs | 37 ++-- src/wallet/params.rs | 5 - src/wallet/signer.rs | 20 ++- tests/add_foreign_utxo.rs | 17 +- tests/common.rs | 20 +++ tests/persisted_wallet.rs | 23 +-- tests/psbt.rs | 167 +++++++++++++++--- tests/wallet.rs | 324 ++++++++++++++++++++++------------- 14 files changed, 590 insertions(+), 257 deletions(-) diff --git a/README.md b/README.md index 2176a4734..71b0265d8 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,6 @@ let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfS let mut wallet = match Wallet::load() .descriptor(KeychainKind::External, Some(descriptor)) .descriptor(KeychainKind::Internal, Some(change_descriptor)) - .extract_keys() .check_network(network) .load_wallet(&mut conn)? { diff --git a/examples/bitcoind_rpc.rs b/examples/bitcoind_rpc.rs index c63f2b0db..c6e20cda0 100644 --- a/examples/bitcoind_rpc.rs +++ b/examples/bitcoind_rpc.rs @@ -97,7 +97,6 @@ fn main() -> anyhow::Result<()> { let wallet_opt = Wallet::load() .descriptor(KeychainKind::External, Some(args.descriptor.clone())) .descriptor(KeychainKind::Internal, args.change_descriptor.clone()) - .extract_keys() .check_network(args.network) .load_wallet(&mut db)?; let mut wallet = match wallet_opt { diff --git a/examples/electrum.rs b/examples/electrum.rs index e89c11752..911eb8f7a 100644 --- a/examples/electrum.rs +++ b/examples/electrum.rs @@ -4,7 +4,10 @@ use bdk_wallet::Wallet; use bdk_wallet::bitcoin::Amount; use bdk_wallet::bitcoin::FeeRate; use bdk_wallet::bitcoin::Network; +use bdk_wallet::bitcoin::secp256k1::Secp256k1; use bdk_wallet::chain::collections::HashSet; +use bdk_wallet::descriptor::IntoWalletDescriptor; +use bdk_wallet::miniscript::descriptor::KeyMapWrapper; use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::rusqlite::Connection; use bdk_wallet::{KeychainKind, SignOptions}; @@ -23,16 +26,22 @@ const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7 const ELECTRUM_URL: &str = "ssl://mempool.space:40002"; fn main() -> Result<(), anyhow::Error> { + let secp = Secp256k1::new(); + let (external_descriptor, mut external_keymap) = + EXTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + let (internal_descriptor, internal_keymap) = + INTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + external_keymap.extend(internal_keymap); + let signer = KeyMapWrapper::from(external_keymap); let mut db = Connection::open(DB_PATH)?; let wallet_opt = Wallet::load() - .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) - .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) - .extract_keys() + .descriptor(KeychainKind::External, Some(external_descriptor.clone())) + .descriptor(KeychainKind::Internal, Some(internal_descriptor.clone())) .check_network(NETWORK) .load_wallet(&mut db)?; let mut wallet = match wallet_opt { Some(wallet) => wallet, - None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC) + None => Wallet::create(external_descriptor, internal_descriptor) .network(NETWORK) .create_wallet(&mut db)?, }; @@ -89,7 +98,9 @@ fn main() -> Result<(), anyhow::Error> { tx_builder.fee_rate(target_fee_rate); let mut psbt = tx_builder.finish()?; - let finalized = wallet.sign(&mut psbt, SignOptions::default())?; + psbt.sign(&signer, wallet.secp_ctx()) + .map_err(|(_, e)| anyhow::anyhow!("{e:?}"))?; + let finalized = wallet.finalize_psbt(&mut psbt, SignOptions::default())?; assert!(finalized); let original_fee = psbt.fee_amount().unwrap(); let tx_feerate = psbt.fee_rate().unwrap(); @@ -124,7 +135,10 @@ fn main() -> Result<(), anyhow::Error> { let mut builder = wallet.build_fee_bump(txid).expect("failed to bump tx"); builder.fee_rate(feerate); let mut bumped_psbt = builder.finish().unwrap(); - let finalize_btx = wallet.sign(&mut bumped_psbt, SignOptions::default())?; + bumped_psbt + .sign(&signer, wallet.secp_ctx()) + .map_err(|(_, e)| anyhow::anyhow!("{e:?}"))?; + let finalize_btx = wallet.finalize_psbt(&mut bumped_psbt, SignOptions::default())?; assert!(finalize_btx); let new_fee = bumped_psbt.fee_amount().unwrap(); let bumped_tx = bumped_psbt.extract_tx()?; diff --git a/examples/esplora_async.rs b/examples/esplora_async.rs index 1780b287e..adfb3927a 100644 --- a/examples/esplora_async.rs +++ b/examples/esplora_async.rs @@ -1,7 +1,9 @@ use bdk_esplora::{EsploraAsyncExt, esplora_client}; use bdk_wallet::{ KeychainKind, SignOptions, Wallet, - bitcoin::{Amount, FeeRate, Network}, + bitcoin::{Amount, FeeRate, Network, secp256k1::Secp256k1}, + descriptor::IntoWalletDescriptor, + miniscript::descriptor::KeyMapWrapper, psbt::PsbtUtils, rusqlite::Connection, }; @@ -20,16 +22,22 @@ const ESPLORA_URL: &str = "https://mempool.space/testnet4/api"; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { + let secp = Secp256k1::new(); + let (external_descriptor, mut external_keymap) = + EXTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + let (internal_descriptor, internal_keymap) = + INTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + external_keymap.extend(internal_keymap); + let signer = KeyMapWrapper::from(external_keymap); let mut db = Connection::open(DB_PATH)?; let wallet_opt = Wallet::load() - .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) - .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) - .extract_keys() + .descriptor(KeychainKind::External, Some(external_descriptor.clone())) + .descriptor(KeychainKind::Internal, Some(internal_descriptor.clone())) .check_network(NETWORK) .load_wallet(&mut db)?; let mut wallet = match wallet_opt { Some(wallet) => wallet, - None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC) + None => Wallet::create(external_descriptor, internal_descriptor) .network(NETWORK) .create_wallet(&mut db)?, }; @@ -83,7 +91,9 @@ async fn main() -> Result<(), anyhow::Error> { tx_builder.fee_rate(target_fee_rate); let mut psbt = tx_builder.finish()?; - let finalized = wallet.sign(&mut psbt, SignOptions::default())?; + psbt.sign(&signer, wallet.secp_ctx()) + .map_err(|(_, e)| anyhow::anyhow!("{e:?}"))?; + let finalized = wallet.finalize_psbt(&mut psbt, SignOptions::default())?; assert!(finalized); let original_fee = psbt.fee_amount().unwrap(); let tx_feerate = psbt.fee_rate().unwrap(); @@ -117,7 +127,10 @@ async fn main() -> Result<(), anyhow::Error> { let mut builder = wallet.build_fee_bump(txid).expect("failed to bump tx"); builder.fee_rate(feerate); let mut bumped_psbt = builder.finish().unwrap(); - let finalize_btx = wallet.sign(&mut bumped_psbt, SignOptions::default())?; + bumped_psbt + .sign(&signer, wallet.secp_ctx()) + .map_err(|(_, e)| anyhow::anyhow!("{e:?}"))?; + let finalize_btx = wallet.finalize_psbt(&mut bumped_psbt, SignOptions::default())?; assert!(finalize_btx); let new_fee = bumped_psbt.fee_amount().unwrap(); let bumped_tx = bumped_psbt.extract_tx()?; diff --git a/examples/esplora_blocking.rs b/examples/esplora_blocking.rs index 179a7d796..e954e8445 100644 --- a/examples/esplora_blocking.rs +++ b/examples/esplora_blocking.rs @@ -2,7 +2,9 @@ use bdk_esplora::{EsploraExt, esplora_client}; use bdk_wallet::rusqlite::Connection; use bdk_wallet::{ KeychainKind, SignOptions, Wallet, - bitcoin::{Amount, FeeRate, Network}, + bitcoin::{Amount, FeeRate, Network, secp256k1::Secp256k1}, + descriptor::IntoWalletDescriptor, + miniscript::descriptor::KeyMapWrapper, psbt::PsbtUtils, }; use std::thread::sleep; @@ -20,16 +22,22 @@ const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7 const ESPLORA_URL: &str = "https://mempool.space/testnet4/api"; fn main() -> Result<(), anyhow::Error> { + let secp = Secp256k1::new(); + let (external_descriptor, mut external_keymap) = + EXTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + let (internal_descriptor, internal_keymap) = + INTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + external_keymap.extend(internal_keymap); + let signer = KeyMapWrapper::from(external_keymap); let mut db = Connection::open(DB_PATH)?; let wallet_opt = Wallet::load() - .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) - .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) - .extract_keys() + .descriptor(KeychainKind::External, Some(external_descriptor.clone())) + .descriptor(KeychainKind::Internal, Some(internal_descriptor.clone())) .check_network(NETWORK) .load_wallet(&mut db)?; let mut wallet = match wallet_opt { Some(wallet) => wallet, - None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC) + None => Wallet::create(external_descriptor, internal_descriptor) .network(NETWORK) .create_wallet(&mut db)?, }; @@ -78,7 +86,9 @@ fn main() -> Result<(), anyhow::Error> { tx_builder.fee_rate(target_fee_rate); let mut psbt = tx_builder.finish()?; - let finalized = wallet.sign(&mut psbt, SignOptions::default())?; + psbt.sign(&signer, wallet.secp_ctx()) + .map_err(|(_, e)| anyhow::anyhow!("{e:?}"))?; + let finalized = wallet.finalize_psbt(&mut psbt, SignOptions::default())?; assert!(finalized); let original_fee = psbt.fee_amount().unwrap(); let tx_feerate = psbt.fee_rate().unwrap(); @@ -113,7 +123,10 @@ fn main() -> Result<(), anyhow::Error> { let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_rate(feerate); let mut new_psbt = builder.finish().unwrap(); - let finalize_tx = wallet.sign(&mut new_psbt, SignOptions::default())?; + new_psbt + .sign(&signer, wallet.secp_ctx()) + .map_err(|(_, e)| anyhow::anyhow!("{e:?}"))?; + let finalize_tx = wallet.finalize_psbt(&mut new_psbt, SignOptions::default())?; assert!(finalize_tx); let new_fee = new_psbt.fee_amount().unwrap(); let bumped_tx = new_psbt.extract_tx()?; diff --git a/src/wallet/export.rs b/src/wallet/export.rs index 6a6cc967c..396ce8cc6 100644 --- a/src/wallet/export.rs +++ b/src/wallet/export.rs @@ -46,13 +46,23 @@ //! # use bitcoin::*; //! # use bdk_wallet::export::*; //! # use bdk_wallet::*; -//! let wallet = Wallet::create( -//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)", -//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)", -//! ) -//! .network(Network::Testnet) -//! .create_wallet_no_persist()?; -//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap(); +//! # use miniscript::{Descriptor, descriptor::DescriptorPublicKey}; +//! # use bitcoin::secp256k1::Secp256k1; +//! let external = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; +//! let internal = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; +//! let secp = Secp256k1::new(); +//! let (external_descriptor, external_keymap) = Descriptor::::parse_descriptor(&secp, external)?; +//! let (internal_descriptor, internal_keymap) = Descriptor::::parse_descriptor(&secp, internal)?; +//! let wallet = Wallet::create(external_descriptor, internal_descriptor) +//! .network(Network::Testnet) +//! .create_wallet_no_persist()?; +//! let export = FullyNodedExport::export_wallet_with_keymaps( +//! &wallet, +//! &external_keymap, +//! &internal_keymap, +//! "exported wallet", +//! true, +//! )?; //! //! println!("Exported: {}", export.to_string()); //! # Ok::<_, Box>(()) @@ -142,7 +152,7 @@ use core::fmt; use core::str::FromStr; use serde::{Deserialize, Serialize}; -use miniscript::descriptor::{DescriptorPublicKey, ShInner, WshInner}; +use miniscript::descriptor::{DescriptorPublicKey, KeyMap, ShInner, WshInner}; use miniscript::{Descriptor, ScriptContext, Terminal}; use crate::types::KeychainKind; @@ -194,19 +204,18 @@ impl FullyNodedExport { /// /// If the database is empty or `include_blockheight` is false, the `blockheight` field /// returned will be `0`. - pub fn export_wallet( + pub fn export_wallet_with_keymaps( wallet: &Wallet, + external_keymap: &KeyMap, + internal_keymap: &KeyMap, label: &str, include_blockheight: bool, ) -> Result { - let descriptor = wallet - .public_descriptor(KeychainKind::External) - .to_string_with_secret( - &wallet - .get_signers(KeychainKind::External) - .as_key_map(wallet.secp_ctx()), - ); - let descriptor = remove_checksum(descriptor); + let descriptor = remove_checksum( + wallet + .public_descriptor(KeychainKind::External) + .to_string_with_secret(external_keymap), + ); Self::is_compatible_with_core(&descriptor)?; let blockheight = if include_blockheight { @@ -226,16 +235,11 @@ impl FullyNodedExport { blockheight, }; - let change_descriptor = { - let descriptor = wallet + let change_descriptor = Some(remove_checksum( + wallet .public_descriptor(KeychainKind::Internal) - .to_string_with_secret( - &wallet - .get_signers(KeychainKind::Internal) - .as_key_map(wallet.secp_ctx()), - ); - Some(remove_checksum(descriptor)) - }; + .to_string_with_secret(internal_keymap), + )); if export.change_descriptor() != change_descriptor { return Err("Incompatible change descriptor"); @@ -244,6 +248,25 @@ impl FullyNodedExport { Ok(export) } + /// Export a wallet to FullyNoded format using keys stored in the wallet. + pub fn export_wallet( + wallet: &Wallet, + label: &str, + include_blockheight: bool, + ) -> Result { + Self::export_wallet_with_keymaps( + wallet, + &wallet + .get_signers(KeychainKind::External) + .as_key_map(wallet.secp_ctx()), + &wallet + .get_signers(KeychainKind::Internal) + .as_key_map(wallet.secp_ctx()), + label, + include_blockheight, + ) + } + fn is_compatible_with_core(descriptor: &str) -> Result<(), &'static str> { fn check_ms( terminal: &Terminal, @@ -716,7 +739,7 @@ mod test { use core::str::FromStr; use bdk_chain::BlockId; - use bitcoin::{BlockHash, Network, hashes::Hash}; + use bitcoin::{BlockHash, Network, hashes::Hash, secp256k1::Secp256k1}; use super::*; use crate::Wallet; @@ -737,13 +760,31 @@ mod test { wallet } + fn keymaps_from_descriptors(external: &str, internal: &str) -> (KeyMap, KeyMap) { + let secp = Secp256k1::new(); + let (_, external_keymap) = + Descriptor::::parse_descriptor(&secp, external).unwrap(); + let (_, internal_keymap) = + Descriptor::::parse_descriptor(&secp, internal).unwrap(); + (external_keymap, internal_keymap) + } + #[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, change_descriptor, Network::Bitcoin); - let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + let (external_keymap, internal_keymap) = + keymaps_from_descriptors(descriptor, change_descriptor); + let export = FullyNodedExport::export_wallet_with_keymaps( + &wallet, + &external_keymap, + &internal_keymap, + "Test Label", + true, + ) + .unwrap(); assert_eq!(export.descriptor(), descriptor); assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); @@ -762,7 +803,16 @@ mod test { let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/0)"; let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin); - FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + let (external_keymap, internal_keymap) = + keymaps_from_descriptors(descriptor, change_descriptor); + FullyNodedExport::export_wallet_with_keymaps( + &wallet, + &external_keymap, + &internal_keymap, + "Test Label", + true, + ) + .unwrap(); } #[test] @@ -775,7 +825,16 @@ mod test { let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)"; let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin); - FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + let (external_keymap, internal_keymap) = + keymaps_from_descriptors(descriptor, change_descriptor); + FullyNodedExport::export_wallet_with_keymaps( + &wallet, + &external_keymap, + &internal_keymap, + "Test Label", + true, + ) + .unwrap(); } #[test] @@ -792,7 +851,16 @@ mod test { ))"; let wallet = get_test_wallet(descriptor, change_descriptor, Network::Testnet); - let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + let (external_keymap, internal_keymap) = + keymaps_from_descriptors(descriptor, change_descriptor); + let export = FullyNodedExport::export_wallet_with_keymaps( + &wallet, + &external_keymap, + &internal_keymap, + "Test Label", + true, + ) + .unwrap(); assert_eq!(export.descriptor(), descriptor); assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); @@ -805,7 +873,16 @@ mod test { let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)"; let wallet = get_test_wallet(descriptor, change_descriptor, Network::Testnet); - let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + let (external_keymap, internal_keymap) = + keymaps_from_descriptors(descriptor, change_descriptor); + let export = FullyNodedExport::export_wallet_with_keymaps( + &wallet, + &external_keymap, + &internal_keymap, + "Test Label", + true, + ) + .unwrap(); assert_eq!(export.descriptor(), descriptor); assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); assert_eq!(export.blockheight, 5000); @@ -818,7 +895,16 @@ mod test { let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin); - let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + let (external_keymap, internal_keymap) = + keymaps_from_descriptors(descriptor, change_descriptor); + let export = FullyNodedExport::export_wallet_with_keymaps( + &wallet, + &external_keymap, + &internal_keymap, + "Test Label", + true, + ) + .unwrap(); assert_eq!( export.to_string(), diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 3a5e7fde0..bca18b592 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -119,8 +119,9 @@ type IndexedTxOut = ((KeychainKind, u32), FullTxOut); /// 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. +/// 1. output *descriptors* from which it derives addresses. +/// 2. local chain state. +/// 3. a transaction graph indexed by those descriptors. /// /// The user is responsible for loading and writing wallet changes which are represented as /// [`ChangeSet`]s (see [`take_staged`]). Also see individual functions and example for instructions @@ -403,11 +404,12 @@ impl Wallet { /// Build [`Wallet`] by loading from persistence or [`ChangeSet`]. /// - /// Note that the descriptor secret keys are not persisted to the db. You can add - /// signers after-the-fact with [`Wallet::add_signer`] or [`Wallet::set_keymap`]. You - /// can also add keys when building the wallet by using [`LoadParams::keymap`]. Finally - /// you can check the wallet's descriptors are what you expect with [`LoadParams::descriptor`] - /// which will try to populate signers if [`LoadParams::extract_keys`] is enabled. + /// Note that descriptor secret keys are not persisted to the db. The recommended signing + /// flow is to keep signing keys outside the wallet and pass them to [`bitcoin::Psbt::sign`] + /// with [`Wallet::secp_ctx`]. + /// [`Wallet::sign_with_signers`] supports caller-owned [`SignersContainer`]s while the + /// [`signer`] module is phased out. + /// You can check the wallet's descriptors are what you expect with [`LoadParams::descriptor`]. /// /// # Synopsis /// @@ -426,18 +428,12 @@ impl Wallet { /// // Load a wallet that is persisted to SQLite database. /// # let temp_dir = tempfile::tempdir().expect("must create tempdir"); /// # let file_path = temp_dir.path().join("store.db"); - /// # let external_keymap = Default::default(); - /// # let internal_keymap = Default::default(); /// # let genesis_hash = BlockHash::all_zeros(); /// let mut conn = bdk_wallet::rusqlite::Connection::open(file_path)?; /// let mut wallet = Wallet::load() - /// // check loaded descriptors matches these values and extract private keys + /// // check loaded descriptors matches these values /// .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) /// .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) - /// .extract_keys() - /// // you can also manually add private keys - /// .keymap(KeychainKind::External, external_keymap) - /// .keymap(KeychainKind::Internal, internal_keymap) /// // ensure loaded wallet's genesis hash matches this value /// .check_genesis_hash(genesis_hash) /// // set a lookahead for our indexer @@ -1601,18 +1597,24 @@ impl Wallet { /// # use bitcoin::*; /// # use bdk_wallet::*; /// # use bdk_wallet::ChangeSet; + /// # use bdk_wallet::descriptor::IntoWalletDescriptor; /// # use bdk_wallet::error::CreateTxError; /// # use anyhow::Error; - /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// # use miniscript::descriptor::KeyMapWrapper; + /// # let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; /// # let mut wallet = doctest_wallet!(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); + /// let (_, keymap) = descriptor + /// .into_wallet_descriptor(wallet.secp_ctx(), wallet.network().into())?; + /// let signer = KeyMapWrapper::from(keymap); /// let mut psbt = { /// let mut builder = wallet.build_tx(); /// builder /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); /// builder.finish()? /// }; - /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; + /// psbt.sign(&signer, wallet.secp_ctx()).map_err(|(_, e)| Error::msg(format!("{e:?}")))?; + /// wallet.finalize_psbt(&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 = { @@ -1622,7 +1624,8 @@ impl Wallet { /// builder.finish()? /// }; /// - /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; + /// psbt.sign(&signer, wallet.secp_ctx()).map_err(|(_, e)| Error::msg(format!("{e:?}")))?; + /// wallet.finalize_psbt(&mut psbt, SignOptions::default())?; /// let fee_bumped_tx = psbt.extract_tx(); /// // broadcast fee_bumped_tx to replace original /// # Ok::<(), anyhow::Error>(()) diff --git a/src/wallet/params.rs b/src/wallet/params.rs index 5e6385de9..056c14728 100644 --- a/src/wallet/params.rs +++ b/src/wallet/params.rs @@ -253,11 +253,6 @@ impl LoadParams { } /// Checks the `expected_descriptor` matches exactly what is loaded for `keychain`. - /// - /// # Note - /// - /// You must also specify [`extract_keys`](Self::extract_keys) if you wish to add a signer - /// for an expected descriptor containing secrets. pub fn descriptor(mut self, keychain: KeychainKind, expected_descriptor: Option) -> Self where D: IntoWalletDescriptor + Send + 'static, diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index db9447182..90949de60 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -11,8 +11,12 @@ //! 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. +//! This module provides the ability to build caller-owned signer containers and use them with +//! [`Wallet::sign_with_signers`](super::Wallet::sign_with_signers). +//! +//! This is an interim bridge for callers still relying on bdk's signer infrastructure. Ideally +//! you should sign PSBTs with [`bitcoin::Psbt::sign`] (software signing), or have your own signer +//! (hardware signing). //! //! ``` //! # use alloc::sync::Arc; @@ -65,17 +69,19 @@ //! } //! } //! -//! let custom_signer = CustomSigner::connect(); +//! let custom_signer = Arc::new(CustomSigner::connect()); //! //! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/0/*)"; //! let change_descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/1/*)"; -//! let mut wallet = Wallet::create(descriptor, change_descriptor) +//! let wallet = Wallet::create(descriptor, change_descriptor) //! .network(Network::Testnet) //! .create_wallet_no_persist()?; -//! wallet.add_signer( -//! KeychainKind::External, +//! +//! let mut external_signers = SignersContainer::new(); +//! external_signers.add_external( +//! custom_signer.id(wallet.secp_ctx()), //! SignerOrdering(200), -//! Arc::new(custom_signer) +//! custom_signer, //! ); //! //! # Ok::<_, anyhow::Error>(()) diff --git a/tests/add_foreign_utxo.rs b/tests/add_foreign_utxo.rs index dd4cf7711..25c0b5121 100644 --- a/tests/add_foreign_utxo.rs +++ b/tests/add_foreign_utxo.rs @@ -8,12 +8,14 @@ use bdk_wallet::tx_builder::AddForeignUtxoError; use bitcoin::{Address, Amount, psbt}; mod common; +use common::signers_from_descriptor; #[test] fn test_add_foreign_utxo() { - let (mut wallet1, _) = get_funded_wallet_wpkh(); - let (wallet2, _) = - get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); + let (wallet1_descriptor, wallet1_change_descriptor) = get_test_wpkh_and_change_desc(); + let (mut wallet1, _) = get_funded_wallet(wallet1_descriptor, wallet1_change_descriptor); + let wallet2_descriptor = "wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"; + let (wallet2, _) = get_funded_wallet_single(wallet2_descriptor); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") .unwrap() @@ -55,9 +57,12 @@ fn test_add_foreign_utxo() { "foreign_utxo should be in there" ); + let wallet1_external_signers = signers_from_descriptor(&wallet1, wallet1_descriptor); + let wallet1_internal_signers = signers_from_descriptor(&wallet1, wallet1_change_descriptor); let finished = wallet1 - .sign( + .sign_with_signers( &mut psbt, + &[&wallet1_external_signers, &wallet1_internal_signers], SignOptions { trust_witness_utxo: true, ..Default::default() @@ -70,9 +75,11 @@ fn test_add_foreign_utxo() { "only one of the inputs should have been signed so far" ); + let wallet2_signers = signers_from_descriptor(&wallet2, wallet2_descriptor); let finished = wallet2 - .sign( + .sign_with_signers( &mut psbt, + &[&wallet2_signers], SignOptions { trust_witness_utxo: true, ..Default::default() diff --git a/tests/common.rs b/tests/common.rs index 37795f058..37ad6d356 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,5 +1,8 @@ #![allow(unused)] +use bdk_wallet::Wallet; +use bdk_wallet::descriptor::IntoWalletDescriptor; +use bdk_wallet::signer::SignersContainer; use bitcoin::secp256k1::Secp256k1; use miniscript::{Descriptor, DescriptorPublicKey, descriptor::KeyMap}; @@ -17,6 +20,23 @@ pub fn parse_descriptor(s: &str) -> (Descriptor, KeyMap) { .expect("failed to parse descriptor") } +pub fn signers_from_descriptor( + wallet: &Wallet, + descriptor: impl IntoWalletDescriptor, +) -> SignersContainer { + let (descriptor, keymap) = descriptor + .into_wallet_descriptor(wallet.secp_ctx(), wallet.network().into()) + .expect("failed to parse signing descriptor"); + SignersContainer::build(keymap, &descriptor, wallet.secp_ctx()) +} + +pub fn keymap_from_descriptor(wallet: &Wallet, descriptor: impl IntoWalletDescriptor) -> KeyMap { + let (_, keymap) = descriptor + .into_wallet_descriptor(wallet.secp_ctx(), wallet.network().into()) + .expect("failed to parse signing descriptor"); + keymap +} + /// Validate and return the transaction fee from a PSBT. /// Panics if extraction fails, fee calculation fails, or if calculated fee doesn't match PSBT's /// fee. diff --git a/tests/persisted_wallet.rs b/tests/persisted_wallet.rs index 10f21308c..c654e7a9b 100644 --- a/tests/persisted_wallet.rs +++ b/tests/persisted_wallet.rs @@ -10,6 +10,10 @@ use bdk_chain::{ use bdk_wallet::coin_selection::InsufficientFunds; use bdk_wallet::descriptor::IntoWalletDescriptor; use bdk_wallet::error::CreateTxError; + +use bdk_wallet::persist_test_utils::{ + persist_keychains, persist_network, persist_single_keychain, persist_wallet_changeset, +}; use bdk_wallet::test_utils::*; use bdk_wallet::{ ChangeSet, KeychainKind, LoadError, LoadMismatch, LoadWithPersistError, Wallet, WalletPersister, @@ -23,10 +27,6 @@ use bitcoin::{ }; use miniscript::{Descriptor, DescriptorPublicKey}; -use bdk_wallet::persist_test_utils::{ - persist_keychains, persist_network, persist_single_keychain, persist_wallet_changeset, -}; - mod common; use common::*; @@ -358,12 +358,8 @@ fn wallet_should_persist_anchors_and_recover() { assert!(wallet.persist(&mut db).unwrap()); // should recover persisted wallet - let secp = wallet.secp_ctx(); - let (_, keymap) = >::parse_descriptor(secp, desc).unwrap(); - assert!(!keymap.is_empty()); let wallet = Wallet::load() .descriptor(KeychainKind::External, Some(desc)) - .extract_keys() .load_wallet(&mut db) .unwrap() .expect("must have loaded changeset"); @@ -384,8 +380,6 @@ fn wallet_should_persist_anchors_and_recover() { #[test] fn single_descriptor_wallet_persist_and_recover() { - use bdk_chain::miniscript::Descriptor; - use bdk_chain::miniscript::DescriptorPublicKey; use bdk_chain::rusqlite; let temp_dir = tempfile::tempdir().unwrap(); let db_path = temp_dir.path().join("wallet.db"); @@ -401,27 +395,18 @@ fn single_descriptor_wallet_persist_and_recover() { // should recover persisted wallet let secp = wallet.secp_ctx(); - let (_, keymap) = >::parse_descriptor(secp, desc).unwrap(); - assert!(!keymap.is_empty()); let wallet = Wallet::load() .descriptor(KeychainKind::External, Some(desc)) - .extract_keys() .load_wallet(&mut db) .unwrap() .expect("must have loaded changeset"); assert_eq!(wallet.derivation_index(KeychainKind::External), Some(2)); - // should have private key - assert_eq!( - wallet.get_signers(KeychainKind::External).as_key_map(secp), - keymap, - ); // should error on wrong internal params let desc = get_test_wpkh(); let (exp_desc, _) = >::parse_descriptor(secp, desc).unwrap(); let err = Wallet::load() .descriptor(KeychainKind::Internal, Some(desc)) - .extract_keys() .load_wallet(&mut db); assert_matches!( err, diff --git a/tests/psbt.rs b/tests/psbt.rs index 7ccd7272a..def2ff191 100644 --- a/tests/psbt.rs +++ b/tests/psbt.rs @@ -1,8 +1,13 @@ -use bdk_wallet::bitcoin; +use bdk_wallet::bitcoin::bip32::Xpriv; +use bdk_wallet::bitcoin::psbt::SigningKeys; use bdk_wallet::test_utils::*; use bdk_wallet::{KeychainKind, SignOptions, psbt}; use bitcoin::{Amount, FeeRate, Psbt, TxIn}; use core::str::FromStr; +use miniscript::descriptor::KeyMapWrapper; + +mod common; +use common::{keymap_from_descriptor, signers_from_descriptor}; // from bip 174 const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA"; @@ -11,7 +16,8 @@ const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6 #[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_single(get_test_wpkh()); + let desc = get_test_wpkh(); + let (mut wallet, _) = get_funded_wallet_single(desc); 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)); @@ -21,14 +27,18 @@ fn test_psbt_malformed_psbt_input_legacy() { trust_witness_utxo: true, ..Default::default() }; - let _ = wallet.sign(&mut psbt, options).unwrap(); + let signers = signers_from_descriptor(&wallet, desc); + let _ = wallet + .sign_with_signers(&mut psbt, &[&signers], 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_single(get_test_wpkh()); + let descriptor = get_test_wpkh(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); 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)); @@ -38,13 +48,17 @@ fn test_psbt_malformed_psbt_input_segwit() { trust_witness_utxo: true, ..Default::default() }; - let _ = wallet.sign(&mut psbt, options).unwrap(); + let signers = signers_from_descriptor(&wallet, descriptor); + let _ = wallet + .sign_with_signers(&mut psbt, &[&signers], options) + .unwrap(); } #[test] #[should_panic(expected = "InputIndexOutOfRange")] fn test_psbt_malformed_tx_input() { - let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh()); + let descriptor = get_test_wpkh(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); 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)); @@ -54,13 +68,17 @@ fn test_psbt_malformed_tx_input() { trust_witness_utxo: true, ..Default::default() }; - let _ = wallet.sign(&mut psbt, options).unwrap(); + let signers = signers_from_descriptor(&wallet, descriptor); + let _ = wallet + .sign_with_signers(&mut psbt, &[&signers], options) + .unwrap(); } #[test] fn test_psbt_sign_with_finalized() { let psbt_bip = Psbt::from_str(PSBT_STR).unwrap(); - let (mut wallet, _) = get_funded_wallet_wpkh(); + let (descriptor, change_descriptor) = get_test_wpkh_and_change_desc(); + let (mut wallet, _) = get_funded_wallet(descriptor, change_descriptor); 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)); @@ -72,7 +90,15 @@ fn test_psbt_sign_with_finalized() { .input .push(psbt_bip.unsigned_tx.input[0].clone()); - let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); + let external_signers = signers_from_descriptor(&wallet, descriptor); + let internal_signers = signers_from_descriptor(&wallet, change_descriptor); + let _ = wallet + .sign_with_signers( + &mut psbt, + &[&external_signers, &internal_signers], + SignOptions::default(), + ) + .unwrap(); } #[test] @@ -81,9 +107,8 @@ fn test_psbt_fee_rate_with_witness_utxo() { let expected_fee_rate = FeeRate::from_sat_per_kwu(310); - let (mut wallet, _) = get_funded_wallet_single( - "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", - ); + let descriptor = "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); let addr = wallet.peek_address(KeychainKind::External, 0); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); @@ -94,7 +119,9 @@ fn test_psbt_fee_rate_with_witness_utxo() { let unfinalized_fee_rate = psbt.fee_rate().unwrap(); - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet, descriptor)); + psbt.sign(&signer, wallet.secp_ctx()).unwrap(); + let finalized = wallet.finalize_psbt(&mut psbt, Default::default()).unwrap(); assert!(finalized); let finalized_fee_rate = psbt.fee_rate().unwrap(); @@ -108,9 +135,8 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() { let expected_fee_rate = FeeRate::from_sat_per_kwu(310); - let (mut wallet, _) = get_funded_wallet_single( - "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", - ); + let descriptor = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); let addr = wallet.peek_address(KeychainKind::External, 0); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); @@ -120,7 +146,9 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() { assert!(fee_amount.is_some()); let unfinalized_fee_rate = psbt.fee_rate().unwrap(); - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet, descriptor)); + psbt.sign(&signer, wallet.secp_ctx()).unwrap(); + let finalized = wallet.finalize_psbt(&mut psbt, Default::default()).unwrap(); assert!(finalized); let finalized_fee_rate = psbt.fee_rate().unwrap(); @@ -165,7 +193,7 @@ fn test_psbt_fee_rate_with_missing_txout() { #[test] fn test_psbt_multiple_internalkey_signers() { use bdk_wallet::KeychainKind; - use bdk_wallet::signer::{SignerContext, SignerOrdering, SignerWrapper}; + use bdk_wallet::signer::{SignerContext, SignerOrdering, SignerWrapper, TransactionSigner}; use bitcoin::key::TapTweak; use bitcoin::secp256k1::{Keypair, Message, Secp256k1, XOnlyPublicKey, schnorr}; use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType}; @@ -186,20 +214,24 @@ fn test_psbt_multiple_internalkey_signers() { builder.drain_to(send_to.script_pubkey()).drain_wallet(); let mut psbt = builder.finish().unwrap(); let unsigned_tx = psbt.unsigned_tx.clone(); + let mut signers = signers_from_descriptor(&wallet, desc.as_str()); // 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 + let wrong_signer: Arc = Arc::new(SignerWrapper::new( + PrivateKey::from_wif("5J5PZqvCe1uThJ3FZeUUFLCh2FuK9pZhtEK4MzhNmugqTmxCdwE").unwrap(), + SignerContext::Tap { + is_internal_key: true, + }, + )); + signers.add_external( + wrong_signer.id(wallet.secp_ctx()), + // A signer ordering 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, - }, - )), + wrong_signer, ); - let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); + let finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], SignOptions::default()) + .unwrap(); assert!(finalized); // To verify, we need the signature, message, and pubkey @@ -228,3 +260,82 @@ fn test_psbt_multiple_internalkey_signers() { let verify_res = secp.verify_schnorr(&signature, &message, &xonlykey); assert!(verify_res.is_ok(), "The wrong internal key was used"); } + +// wpkh PSBT with bip32_derivation populated, derived from +// tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L +const WPKH_PSBT_WITH_DERIVATION: &str = "cHNidP8BAHECAAAAAbOlV/kRKdNVk6Wn2cay5JUvpFw4tEsKWylqu+HfPKDyAAAAAAD9////ArObAAAAAAAAFgAU2+Ijq+8PDcPUGgHQqOPg9+6n9h8QJwAAAAAAABYAFKENkldInmhd2gMGYjkNwXeFL68T0AcAAAABAHEBAAAAATCzgdcz18YZK+8oNpJzqjM8ErFYW3hJLi+bO4bjmQrRAAAAAAD/////AlDDAAAAAAAAFgAUoQ2SV0ieaF3aAwZiOQ3Bd4UvrxOoYQAAAAAAABYAFIgWLNSQrRaGsRyWtCHaqeauCPYsAAAAAAEBH1DDAAAAAAAAFgAUoQ2SV0ieaF3aAwZiOQ3Bd4UvrxMiBgLOtp4iMz+DVWxdHvunWgM0a/PVLPvTn8XSTe0DTvfZ9Bjic/5CVAAAgAEAAIAAAACAAAAAAAAAAAAAIgIDxWYngmqPgL3saGZ4NTgcy5W/XINU8lkqnqjKC+oJuRwY4nP+QlQAAIABAACAAAAAgAEAAAAAAAAAACICAs62niIzP4NVbF0e+6daAzRr89Us+9OfxdJN7QNO99n0GOJz/kJUAACAAQAAgAAAAIAAAAAAAAAAAAA="; + +#[test] +fn test_psbt_sign_with_xpriv() { + let mut psbt = Psbt::from_str(WPKH_PSBT_WITH_DERIVATION).unwrap(); + let (desc, change_desc) = get_test_wpkh_and_change_desc(); + let (wallet, _) = get_funded_wallet(desc, change_desc); + + let xpriv = Xpriv::from_str( + "tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L", + ) + .unwrap(); + + let signing_keys = psbt + .sign(&xpriv, wallet.secp_ctx()) + .expect("PSBT signing should succeed with the correct xpriv"); + + assert!( + !signing_keys.is_empty(), + "expected at least one input to be signed" + ); +} + +#[test] +fn test_psbt_sign_with_wrong_key_signs_nothing() { + let mut psbt = Psbt::from_str(WPKH_PSBT_WITH_DERIVATION).unwrap(); + let (desc, change_desc) = get_test_wpkh_and_change_desc(); + let (wallet, _) = get_funded_wallet(desc, change_desc); + + let wrong_xpriv = Xpriv::from_str( + "tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS", + ) + .unwrap(); + + let signing_keys = psbt + .sign(&wrong_xpriv, wallet.secp_ctx()) + .expect("PSBT signing returns Ok even when no keys matched"); + + let keys_used: usize = signing_keys + .values() + .map(|keys| match keys { + SigningKeys::Ecdsa(v) => v.len(), + SigningKeys::Schnorr(v) => v.len(), + }) + .sum(); + assert_eq!( + keys_used, 0, + "expected zero keys used when signing with an unrelated xpriv" + ); + for input in &psbt.inputs { + assert!( + input.partial_sigs.is_empty(), + "expected no partial signatures when signing with an unrelated key" + ); + } +} + +#[test] +fn test_psbt_sign_with_descriptor_keymap() { + let mut psbt = Psbt::from_str(WPKH_PSBT_WITH_DERIVATION).unwrap(); + let (desc, change_desc) = get_test_wpkh_and_change_desc(); + let (wallet, _) = get_funded_wallet(desc, change_desc); + + let mut keymap = keymap_from_descriptor(&wallet, desc); + keymap.extend(keymap_from_descriptor(&wallet, change_desc)); + let wrapper = KeyMapWrapper::from(keymap); + + let signing_keys = psbt + .sign(&wrapper, wallet.secp_ctx()) + .expect("PSBT signing should succeed with descriptor keymap"); + + assert!( + !signing_keys.is_empty(), + "expected at least one input to be signed using descriptor keymap" + ); +} diff --git a/tests/wallet.rs b/tests/wallet.rs index a27d21d8b..6a577109d 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -6,10 +6,10 @@ use bdk_chain::{BlockId, CanonicalizationParams, ConfirmationBlockTime}; use bdk_wallet::KeychainKind; use bdk_wallet::coin_selection; use bdk_wallet::coin_selection::InsufficientFunds; -use bdk_wallet::descriptor::{DescriptorError, IntoWalletDescriptor, calc_checksum}; +use bdk_wallet::descriptor::{DescriptorError, calc_checksum}; use bdk_wallet::error::CreateTxError; use bdk_wallet::psbt::PsbtUtils; -use bdk_wallet::signer::{SignOptions, SignerError, SignersContainer}; +use bdk_wallet::signer::{SignOptions, SignerError}; use bdk_wallet::test_utils::*; use bdk_wallet::{ AddressInfo, Balance, FinalizeInputOutcome, IndexOutOfBoundsError, PersistedWallet, Update, @@ -21,13 +21,15 @@ use bitcoin::script::PushBytesBuf; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; use bitcoin::taproot::TapNodeHash; use bitcoin::{ - Address, Amount, BlockHash, FeeRate, Network, NetworkKind, OutPoint, ScriptBuf, Sequence, - SignedAmount, Transaction, TxIn, TxOut, Txid, absolute, transaction, + Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, Sequence, SignedAmount, + Transaction, TxIn, TxOut, Txid, absolute, transaction, }; +use miniscript::descriptor::KeyMapWrapper; use rand::SeedableRng; use rand::rngs::StdRng; mod common; +use common::{keymap_from_descriptor, signers_from_descriptor}; #[test] fn test_error_external_and_internal_are_the_same() { @@ -276,7 +278,8 @@ fn test_create_tx_default_locktime_cltv() { #[test] fn test_create_tx_locktime_cltv_timestamp() { - let (mut wallet, _) = get_funded_wallet_single(get_test_single_sig_cltv_timestamp()); + let descriptor = get_test_single_sig_cltv_timestamp(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); @@ -284,7 +287,11 @@ fn test_create_tx_locktime_cltv_timestamp() { assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 1_734_230_218); - let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet, descriptor)); + psbt.sign(&signer, wallet.secp_ctx()).unwrap(); + let finalized = wallet + .finalize_psbt(&mut psbt, SignOptions::default()) + .unwrap(); assert!(finalized); } @@ -1371,15 +1378,16 @@ fn test_fee_amount_negative_drain_val() { #[test] fn test_sign_single_xprv() { - let (mut wallet, _) = get_funded_wallet_single( - "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", - ); + let descriptor = "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); let addr = wallet.next_unused_address(KeychainKind::External); 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(); + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet, descriptor)); + psbt.sign(&signer, wallet.secp_ctx()).unwrap(); + let finalized = wallet.finalize_psbt(&mut psbt, Default::default()).unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1390,22 +1398,8 @@ fn test_sign_single_xprv() { fn test_sign_with_signers() { let (descriptor, change_descriptor) = get_test_wpkh_and_change_desc(); let (mut wallet, _) = get_funded_wallet(descriptor, change_descriptor); - let (_, external_keymap) = descriptor - .into_wallet_descriptor(wallet.secp_ctx(), NetworkKind::Test) - .unwrap(); - let external_signers = SignersContainer::build( - external_keymap, - wallet.public_descriptor(KeychainKind::External), - wallet.secp_ctx(), - ); - let (_, internal_keymap) = change_descriptor - .into_wallet_descriptor(wallet.secp_ctx(), NetworkKind::Test) - .unwrap(); - let internal_signers = SignersContainer::build( - internal_keymap, - wallet.public_descriptor(KeychainKind::Internal), - wallet.secp_ctx(), - ); + let external_signers = signers_from_descriptor(&wallet, descriptor); + let internal_signers = signers_from_descriptor(&wallet, change_descriptor); let latest_block = wallet.latest_checkpoint().block_id(); let internal_addr = wallet.next_unused_address(KeychainKind::Internal); @@ -1440,15 +1434,17 @@ fn test_sign_with_signers() { #[test] fn test_sign_single_xprv_with_master_fingerprint_and_path() { - let (mut wallet, _) = get_funded_wallet_single( - "wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", - ); + let descriptor = "wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); let addr = wallet.next_unused_address(KeychainKind::External); 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(); + let signers = signers_from_descriptor(&wallet, descriptor); + let finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1457,15 +1453,16 @@ fn test_sign_single_xprv_with_master_fingerprint_and_path() { #[test] fn test_sign_single_xprv_bip44_path() { - let (mut wallet, _) = get_funded_wallet_single( - "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)", - ); + let descriptor = "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); let addr = wallet.next_unused_address(KeychainKind::External); 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(); + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet, descriptor)); + psbt.sign(&signer, wallet.secp_ctx()).unwrap(); + let finalized = wallet.finalize_psbt(&mut psbt, Default::default()).unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1474,15 +1471,16 @@ fn test_sign_single_xprv_bip44_path() { #[test] fn test_sign_single_xprv_sh_wpkh() { - let (mut wallet, _) = get_funded_wallet_single( - "sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))", - ); + let descriptor = "sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); let addr = wallet.next_unused_address(KeychainKind::External); 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(); + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet, descriptor)); + psbt.sign(&signer, wallet.secp_ctx()).unwrap(); + let finalized = wallet.finalize_psbt(&mut psbt, Default::default()).unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1491,14 +1489,16 @@ fn test_sign_single_xprv_sh_wpkh() { #[test] fn test_sign_single_wif() { - let (mut wallet, _) = - get_funded_wallet_single("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"); + let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); let addr = wallet.next_unused_address(KeychainKind::External); 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(); + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet, descriptor)); + psbt.sign(&signer, wallet.secp_ctx()).unwrap(); + let finalized = wallet.finalize_psbt(&mut psbt, Default::default()).unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1507,9 +1507,8 @@ fn test_sign_single_wif() { #[test] fn test_sign_single_xprv_no_hd_keypaths() { - let (mut wallet, _) = get_funded_wallet_single( - "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", - ); + let descriptor = "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); @@ -1518,7 +1517,10 @@ fn test_sign_single_xprv_no_hd_keypaths() { psbt.inputs[0].bip32_derivation.clear(); assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0); - let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + let signers = signers_from_descriptor(&wallet, descriptor); + let finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1556,7 +1558,8 @@ fn test_output_redeem_witness_script_populated_automatically() { #[test] fn test_signing_only_one_of_multiple_inputs() { - let (mut wallet, _) = get_funded_wallet_wpkh(); + let (descriptor, change_descriptor) = get_test_wpkh_and_change_desc(); + let (mut wallet, _) = get_funded_wallet(descriptor, change_descriptor); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") .unwrap() .assume_checked(); @@ -1579,14 +1582,12 @@ fn test_signing_only_one_of_multiple_inputs() { psbt.inputs.push(dud_input); psbt.unsigned_tx.input.push(bitcoin::TxIn::default()); + let mut keymap = keymap_from_descriptor(&wallet, descriptor); + keymap.extend(keymap_from_descriptor(&wallet, change_descriptor)); + let signer = KeyMapWrapper::from(keymap); + psbt.sign(&signer, wallet.secp_ctx()).unwrap(); let is_final = wallet - .sign( - &mut psbt, - SignOptions { - trust_witness_utxo: true, - ..Default::default() - }, - ) + .finalize_psbt(&mut psbt, SignOptions::default()) .unwrap(); assert!( !is_final, @@ -1600,9 +1601,9 @@ fn test_signing_only_one_of_multiple_inputs() { #[test] fn test_try_finalize_sign_option() { - let (mut wallet, _) = get_funded_wallet_single( - "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", - ); + let descriptor = "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); for try_finalize in &[true, false] { let addr = wallet.next_unused_address(KeychainKind::External); @@ -1611,8 +1612,9 @@ fn test_try_finalize_sign_option() { let mut psbt = builder.finish().unwrap(); let finalized = wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: *try_finalize, ..Default::default() @@ -1637,15 +1639,18 @@ fn test_try_finalize_sign_option() { #[test] fn test_try_finalize_psbt_outcomes() { { - let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh()); + let descriptor = get_test_wpkh(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); let mut psbt = builder.finish().unwrap(); let is_final = wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, ..Default::default() @@ -1733,15 +1738,18 @@ fn test_try_finalize_psbt_outcomes() { #[test] fn test_try_finalize_psbt_preserves_opaque_input_fields() { - let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh()); + let descriptor = get_test_wpkh(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); let mut psbt = builder.finish().unwrap(); wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, ..Default::default() @@ -1798,15 +1806,18 @@ fn test_try_finalize_psbt_returns_index_out_of_bounds_for_malformed_psbt() { } { - let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh()); + let descriptor = get_test_wpkh(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); let mut psbt = builder.finish().unwrap(); wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, ..Default::default() @@ -1831,15 +1842,18 @@ fn test_try_finalize_psbt_returns_index_out_of_bounds_for_malformed_psbt() { #[test] fn test_try_finalize_psbt_uses_psbt_timelocks() { { - let (mut wallet, _) = get_funded_wallet_single(get_test_single_sig_cltv()); + let descriptor = get_test_single_sig_cltv(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); 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( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, ..Default::default() @@ -1869,15 +1883,18 @@ fn test_try_finalize_psbt_uses_psbt_timelocks() { } { - let (mut wallet, _) = get_funded_wallet_single(get_test_single_sig_csv()); + let descriptor = get_test_single_sig_csv(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); 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( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, ..Default::default() @@ -1909,7 +1926,9 @@ fn test_try_finalize_psbt_uses_psbt_timelocks() { #[test] fn test_taproot_try_finalize_sign_option() { - let (mut wallet, _) = get_funded_wallet_single(get_test_tr_with_taptree()); + let descriptor = get_test_tr_with_taptree(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); for try_finalize in &[true, false] { let addr = wallet.next_unused_address(KeychainKind::External); @@ -1918,8 +1937,9 @@ fn test_taproot_try_finalize_sign_option() { let mut psbt = builder.finish().unwrap(); let finalized = wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: *try_finalize, ..Default::default() @@ -1960,9 +1980,9 @@ fn test_taproot_try_finalize_sign_option() { fn test_sign_nonstandard_sighash() { let sighash = EcdsaSighashType::NonePlusAnyoneCanPay; - let (mut wallet, _) = get_funded_wallet_single( - "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", - ); + let descriptor = "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder @@ -1971,7 +1991,7 @@ fn test_sign_nonstandard_sighash() { .drain_wallet(); let mut psbt = builder.finish().unwrap(); - let result = wallet.sign(&mut psbt, Default::default()); + let result = wallet.sign_with_signers(&mut psbt, &[&signers], Default::default()); assert!( result.is_err(), "Signing should have failed because the TX uses non-standard sighashes" @@ -1983,8 +2003,9 @@ fn test_sign_nonstandard_sighash() { ); // try again after opting-in - let result = wallet.sign( + let result = wallet.sign_with_signers( &mut psbt, + &[&signers], SignOptions { allow_all_sighashes: true, ..Default::default() @@ -2448,15 +2469,18 @@ fn test_taproot_psbt_input_tap_tree() { #[test] fn test_taproot_sign_missing_witness_utxo() { - let (mut wallet, _) = get_funded_wallet_single(get_test_tr_single_sig()); + let descriptor = get_test_tr_single_sig(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); 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( + let result = wallet.sign_with_signers( &mut psbt, + &[&signers], SignOptions { allow_all_sighashes: true, ..Default::default() @@ -2471,8 +2495,9 @@ fn test_taproot_sign_missing_witness_utxo() { // restore the witness_utxo psbt.inputs[0].witness_utxo = witness_utxo; - let result = wallet.sign( + let result = wallet.sign_with_signers( &mut psbt, + &[&signers], SignOptions { allow_all_sighashes: true, ..Default::default() @@ -2488,7 +2513,8 @@ fn test_taproot_sign_missing_witness_utxo() { #[test] fn test_taproot_sign_using_non_witness_utxo() { - let (mut wallet, prev_txid) = get_funded_wallet_single(get_test_tr_single_sig()); + let descriptor = get_test_tr_single_sig(); + let (mut wallet, prev_txid) = get_funded_wallet_single(descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); @@ -2502,15 +2528,16 @@ fn test_taproot_sign_using_non_witness_utxo() { "Previous tx should be present in the database" ); - let result = wallet.sign(&mut psbt, Default::default()); - assert!(result.is_ok(), "Signing should have worked"); + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet, descriptor)); + psbt.sign(&signer, wallet.secp_ctx()) + .expect("signing should have worked"); assert!( - result.unwrap(), + wallet.finalize_psbt(&mut psbt, Default::default()).unwrap(), "Should finalize the input since we can produce signatures" ); } -fn test_spend_from_wallet(mut wallet: Wallet) { +fn test_spend_from_wallet(mut wallet: Wallet, descriptor: &str) { let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); @@ -2518,8 +2545,10 @@ fn test_spend_from_wallet(mut wallet: Wallet) { let mut psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.version.0, 2); + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet, descriptor)); + psbt.sign(&signer, wallet.secp_ctx()).unwrap(); assert!( - wallet.sign(&mut psbt, Default::default()).unwrap(), + wallet.finalize_psbt(&mut psbt, Default::default()).unwrap(), "Unable to finalize tx" ); } @@ -2535,7 +2564,9 @@ fn test_spend_from_wallet(mut wallet: Wallet) { #[test] fn test_taproot_no_key_spend() { - let (mut wallet, _) = get_funded_wallet_single(get_test_tr_with_taptree_both_priv()); + let descriptor = get_test_tr_with_taptree_both_priv(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); @@ -2544,8 +2575,9 @@ fn test_taproot_no_key_spend() { assert!( wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { sign_with_tap_internal_key: false, ..Default::default() @@ -2560,17 +2592,21 @@ fn test_taproot_no_key_spend() { #[test] fn test_taproot_script_spend() { - let (wallet, _) = get_funded_wallet_single(get_test_tr_with_taptree()); - test_spend_from_wallet(wallet); + let descriptor = get_test_tr_with_taptree(); + let (wallet, _) = get_funded_wallet_single(descriptor); + test_spend_from_wallet(wallet, descriptor); - let (wallet, _) = get_funded_wallet_single(get_test_tr_with_taptree_xprv()); - test_spend_from_wallet(wallet); + let descriptor = get_test_tr_with_taptree_xprv(); + let (wallet, _) = get_funded_wallet_single(descriptor); + test_spend_from_wallet(wallet, descriptor); } #[test] fn test_taproot_script_spend_sign_all_leaves() { use bdk_wallet::signer::TapLeavesOptions; - let (mut wallet, _) = get_funded_wallet_single(get_test_tr_with_taptree_both_priv()); + let descriptor = get_test_tr_with_taptree_both_priv(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); @@ -2579,8 +2615,9 @@ fn test_taproot_script_spend_sign_all_leaves() { assert!( wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { tap_leaves_options: TapLeavesOptions::All, ..Default::default() @@ -2602,7 +2639,9 @@ fn test_taproot_script_spend_sign_include_some_leaves() { use bdk_wallet::signer::TapLeavesOptions; use bitcoin::taproot::TapLeafHash; - let (mut wallet, _) = get_funded_wallet_single(get_test_tr_with_taptree_both_priv()); + let descriptor = get_test_tr_with_taptree_both_priv(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); @@ -2619,8 +2658,9 @@ fn test_taproot_script_spend_sign_include_some_leaves() { assert!( wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { tap_leaves_options: TapLeavesOptions::Include(included_script_leaves.clone()), ..Default::default() @@ -2640,7 +2680,9 @@ fn test_taproot_script_spend_sign_exclude_some_leaves() { use bdk_wallet::signer::TapLeavesOptions; use bitcoin::taproot::TapLeafHash; - let (mut wallet, _) = get_funded_wallet_single(get_test_tr_with_taptree_both_priv()); + let descriptor = get_test_tr_with_taptree_both_priv(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); @@ -2657,8 +2699,9 @@ fn test_taproot_script_spend_sign_exclude_some_leaves() { assert!( wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { tap_leaves_options: TapLeavesOptions::Exclude(excluded_script_leaves.clone()), ..Default::default() @@ -2676,7 +2719,9 @@ fn test_taproot_script_spend_sign_exclude_some_leaves() { #[test] fn test_taproot_script_spend_sign_no_leaves() { use bdk_wallet::signer::TapLeavesOptions; - let (mut wallet, _) = get_funded_wallet_single(get_test_tr_with_taptree_both_priv()); + let descriptor = get_test_tr_with_taptree_both_priv(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); @@ -2684,8 +2729,9 @@ fn test_taproot_script_spend_sign_no_leaves() { let mut psbt = builder.finish().unwrap(); wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { tap_leaves_options: TapLeavesOptions::None, ..Default::default() @@ -2698,7 +2744,9 @@ fn test_taproot_script_spend_sign_no_leaves() { #[test] fn test_taproot_sign_derive_index_from_psbt() { - let (mut wallet, _) = get_funded_wallet_single(get_test_tr_single_sig_xprv()); + let descriptor = get_test_tr_single_sig_xprv(); + let change_descriptor = get_test_tr_single_sig(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); let addr = wallet.next_unused_address(KeychainKind::External); @@ -2707,22 +2755,27 @@ fn test_taproot_sign_derive_index_from_psbt() { let mut psbt = builder.finish().unwrap(); // re-create the wallet with an empty db - let wallet_empty = Wallet::create(get_test_tr_single_sig_xprv(), get_test_tr_single_sig()) + let wallet_empty = Wallet::create(descriptor, change_descriptor) .network(Network::Regtest) .create_wallet_no_persist() .unwrap(); // signing with an empty db means that we will only look at the psbt to infer the // derivation index + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet_empty, descriptor)); + psbt.sign(&signer, wallet_empty.secp_ctx()).unwrap(); assert!( - wallet_empty.sign(&mut psbt, Default::default()).unwrap(), + wallet_empty + .finalize_psbt(&mut psbt, Default::default()) + .unwrap(), "Unable to finalize tx" ); } #[test] fn test_taproot_sign_explicit_sighash_all() { - let (mut wallet, _) = get_funded_wallet_single(get_test_tr_single_sig()); + let descriptor = get_test_tr_single_sig(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder @@ -2731,18 +2784,21 @@ fn test_taproot_sign_explicit_sighash_all() { .drain_wallet(); let mut psbt = builder.finish().unwrap(); - let result = wallet.sign(&mut psbt, Default::default()); + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet, descriptor)); + let result = psbt.sign(&signer, wallet.secp_ctx()); 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_single(get_test_tr_single_sig()); + let descriptor = get_test_tr_single_sig(); + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder @@ -2753,7 +2809,7 @@ fn test_taproot_sign_non_default_sighash() { let witness_utxo = psbt.inputs[0].witness_utxo.take(); - let result = wallet.sign(&mut psbt, Default::default()); + let result = wallet.sign_with_signers(&mut psbt, &[&signers], Default::default()); assert!( result.is_err(), "Signing should have failed because the TX uses non-standard sighashes" @@ -2765,8 +2821,9 @@ fn test_taproot_sign_non_default_sighash() { ); // try again after opting-in - let result = wallet.sign( + let result = wallet.sign_with_signers( &mut psbt, + &[&signers], SignOptions { allow_all_sighashes: true, ..Default::default() @@ -2785,8 +2842,9 @@ fn test_taproot_sign_non_default_sighash() { // restore the witness_utxo psbt.inputs[0].witness_utxo = witness_utxo; - let result = wallet.sign( + let result = wallet.sign_with_signers( &mut psbt, + &[&signers], SignOptions { allow_all_sighashes: true, ..Default::default() @@ -2963,9 +3021,9 @@ 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_single( - "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", - ); + let descriptor = "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let fee_rate = FeeRate::from_sat_per_vb_u32(1); let mut builder = wallet.build_tx(); @@ -2996,8 +3054,9 @@ fn test_fee_rate_sign_no_grinding_high_r() { psbt.inputs[0].partial_sigs.clear(); // Signing wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, allow_grinding: false, @@ -3014,8 +3073,9 @@ fn test_fee_rate_sign_no_grinding_high_r() { } // Actually finalizing the transaction... wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { allow_grinding: false, ..Default::default() @@ -3032,9 +3092,9 @@ fn test_fee_rate_sign_grinding_low_r() { // 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_single( - "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", - ); + let descriptor = "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let fee_rate = FeeRate::from_sat_per_vb_u32(1); let mut builder = wallet.build_tx(); @@ -3046,8 +3106,9 @@ fn test_fee_rate_sign_grinding_low_r() { let fee = check_fee!(wallet, psbt); wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, allow_grinding: true, @@ -3129,7 +3190,8 @@ fn test_thread_safety() { #[test] fn single_descriptor_wallet_can_create_tx_and_receive_change() { // create single descriptor wallet and fund it - let mut wallet = Wallet::create_single(get_test_tr_single_sig_xprv()) + let descriptor = get_test_tr_single_sig_xprv(); + let mut wallet = Wallet::create_single(descriptor) .network(Network::Testnet) .create_wallet_no_persist() .unwrap(); @@ -3143,7 +3205,13 @@ fn single_descriptor_wallet_can_create_tx_and_receive_change() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), amount); let mut psbt = builder.finish().unwrap(); - assert!(wallet.sign(&mut psbt, SignOptions::default()).unwrap()); + let signer = KeyMapWrapper::from(keymap_from_descriptor(&wallet, descriptor)); + psbt.sign(&signer, wallet.secp_ctx()).unwrap(); + assert!( + wallet + .finalize_psbt(&mut psbt, SignOptions::default()) + .unwrap() + ); let tx = psbt.extract_tx().unwrap(); let _txid = tx.compute_txid(); insert_tx(&mut wallet, tx); @@ -3376,6 +3444,8 @@ fn test_create_and_spend_from_truc_tx() -> anyhow::Result<()> { .network(Network::Regtest) .create_wallet_no_persist() .expect("should create wallet successfully!"); + let external_signers = signers_from_descriptor(&wallet, descriptor); + let internal_signers = signers_from_descriptor(&wallet, change_descriptor); // establish a chain tip so confirmed funds can be anchored to a block in the active chain. let block = BlockId { @@ -3412,7 +3482,11 @@ fn test_create_and_spend_from_truc_tx() -> anyhow::Result<()> { let mut psbt = builder.finish().expect("should create txA (TRUC) successfully! as per BIP-431 it can spend confirmed outputs from non-TRUC txs."); - let _ = wallet.sign(&mut psbt, SignOptions::default())?; + let _ = wallet.sign_with_signers( + &mut psbt, + &[&external_signers, &internal_signers], + SignOptions::default(), + )?; let tx_a = psbt.extract_tx()?; let txid_a = tx_a.compute_txid(); @@ -3436,7 +3510,11 @@ fn test_create_and_spend_from_truc_tx() -> anyhow::Result<()> { .finish() .expect("SHOULD create txB (non-TRUC) successfully! However, a non-TRUC transaction can only spend confirmed outputs from TRUC transactions"); - let _ = wallet.sign(&mut psbt, SignOptions::default()); + let _ = wallet.sign_with_signers( + &mut psbt, + &[&external_signers, &internal_signers], + SignOptions::default(), + ); let tx_b = psbt.extract_tx()?; // txB MUST NOT use the available unconfirmed TRUC UTXO. @@ -3467,7 +3545,11 @@ fn test_create_and_spend_from_truc_tx() -> anyhow::Result<()> { let mut psbt = builder.finish().expect("should create txC (TRUC) successfully! as per BIP-431 it can spend unconfirmed outputs from TRUC txs."); - let _ = wallet.sign(&mut psbt, SignOptions::default())?; + let _ = wallet.sign_with_signers( + &mut psbt, + &[&external_signers, &internal_signers], + SignOptions::default(), + )?; let tx_c = psbt.extract_tx()?; // txC MUST ONLY use the available confirmed UTXOs AND/OR unconfirmed TRUC UTXOs. From 12851003d5bffdc01f7a619c6254b7a4c30ed928 Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Mon, 27 Jul 2026 18:37:28 +0300 Subject: [PATCH 2/3] feat(wallet): add TxBuilder::set_condition Add TxBuilder::set_condition and wire create_tx to prefer an explicit Condition, falling back to policy-path derivation. Export Condition / Condition::merge. Migrate the compiler example off Wallet::policies. --- examples/compiler.rs | 13 +++- src/descriptor/mod.rs | 2 +- src/descriptor/policy.rs | 6 +- src/wallet/mod.rs | 125 +++++++++++++++++++++------------------ src/wallet/tx_builder.rs | 86 +++++++++++++++++++++++++++ tests/wallet.rs | 115 ++++++++++++++++++++++++++++++++++- 6 files changed, 285 insertions(+), 62 deletions(-) diff --git a/examples/compiler.rs b/examples/compiler.rs index 2a28519fc..7c90c2848 100644 --- a/examples/compiler.rs +++ b/examples/compiler.rs @@ -21,6 +21,9 @@ use bitcoin::Network; use miniscript::Descriptor; use miniscript::policy::Concrete; +use bdk_wallet::descriptor::ExtractPolicy; +use bdk_wallet::descriptor::policy::BuildSatisfaction; +use bdk_wallet::signer::SignersContainer; use bdk_wallet::{KeychainKind, Wallet}; /// Miniscript policy is a high level abstraction of spending conditions. Defined in the @@ -66,9 +69,15 @@ fn main() -> Result<(), Box> { wallet.next_unused_address(KeychainKind::External), ); - // BDK also has it's own `Policy` structure to represent the spending condition in a more + // BDK also has its own `Policy` structure to represent the spending condition in a more // human readable json format. - let spending_policy = wallet.policies(KeychainKind::External)?; + let spending_policy = wallet + .public_descriptor(KeychainKind::External) + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::None, + wallet.secp_ctx(), + )?; println!( "The BDK spending policy: \n{}", serde_json::to_string_pretty(&spending_policy)? diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index f30ea0f34..921cda23a 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -46,7 +46,7 @@ pub mod template; pub use self::checksum::calc_checksum; pub use self::error::Error as DescriptorError; -pub use self::policy::Policy; +pub use self::policy::{Condition, Policy}; use self::template::DescriptorTemplateOut; use crate::keys::{IntoDescriptorKey, KeyError}; use crate::wallet::{signer::SignersContainer, utils::SecpCtx}; diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index a293f8086..abf5024b9 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -484,7 +484,11 @@ impl Condition { } } - pub(crate) fn merge(mut self, other: &Condition) -> Result { + /// Merge two [`Condition`]s into the strictest combined CSV / CLTV requirements. + /// + /// Useful when combining external and internal keychain requirements into one `Condition` + /// before passing it to the transaction builder. + pub 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, diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index bca18b592..353d4cc5a 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1265,65 +1265,78 @@ impl Wallet { params: TxParams, rng: &mut impl RngCore, ) -> Result { - let keychains: BTreeMap<_, _> = self.tx_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 - .map(|desc| { - Ok::<_, CreateTxError>( - desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? - .unwrap(), - ) - }) - .transpose()?; + let requirements = match params.condition { + Some(condition) => condition, + // Fall back to deriving the condition from the deprecated policy paths, to be removed + // along with `TxBuilder::policy_path`. The wallet-owned signers are required here: + // policy node ids depend on them, so the paths only match policies extracted with the + // very same signers as `Wallet::policies` uses. + None => { + let keychains: BTreeMap<_, _> = self.tx_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 + .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 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, - )); - }; - } + // 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 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 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())?; + external_requirements.merge(&internal_requirements.unwrap_or_default())? + } + }; let version = match params.version { Some(transaction::Version(0)) => return Err(CreateTxError::Version0), diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 6a5e1eea1..4687e94dd 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -53,6 +53,7 @@ use super::coin_selection::CoinSelectionAlgorithm; use super::utils::shuffle_slice; use super::{CreateTxError, Wallet}; use crate::collections::{BTreeMap, HashMap, HashSet}; +use crate::descriptor::Condition; use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; /// A transaction builder @@ -126,6 +127,7 @@ pub(crate) struct TxParams { pub(crate) fee_policy: Option, pub(crate) internal_policy_path: Option>>, pub(crate) external_policy_path: Option>>, + pub(crate) condition: Option, pub(crate) utxos: Vec, pub(crate) unspendable: HashSet, pub(crate) manually_selected_only: bool, @@ -191,6 +193,90 @@ impl<'a, Cs> TxBuilder<'a, Cs> { self } + /// Set the CSV / CLTV requirements for the intended spending path. + /// + /// This replaces [`Self::policy_path`] as the transaction builder's policy interface. Derive + /// the [`Condition`] before building the transaction, using caller-owned policy information. + /// + /// It is needed when different ways of satisfying the descriptor imply different `nSequence` / + /// `nLockTime` values. An example 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, the transaction must be built differently + /// depending on which pair the user intends to satisfy afterwards: + /// + /// * If fragments `1` and `2` are used, the transaction needs a specific `nSequence` for the + /// `OP_CSV` branch. + /// * If fragments `1` and `3` are used, the transaction needs a specific `nLockTime` for the + /// `OP_CLTV` branch. + /// * If fragments `2` and `3` are used, the transaction needs both. + /// + /// Without an explicit condition (or the deprecated [`Self::policy_path`]), building a + /// transaction for such a descriptor fails with [`CreateTxError::SpendingPolicyRequired`]. + /// + /// Obtain a [`Condition`] by picking a path on the spending policy tree and calling + /// [`Policy::get_condition`](crate::descriptor::Policy::get_condition), or construct one + /// directly when you already know the CSV / CLTV values (e.g. a single fixed `older(N)`). + /// + /// For wallets with separate external and internal descriptors, merge the conditions for every + /// keychain from which inputs may be selected using [`Condition::merge`]. + /// + /// ## Example + /// + /// ``` + /// # use std::collections::BTreeMap; + /// # use bdk_wallet::*; + /// # use bdk_wallet::descriptor::{ExtractPolicy, policy::BuildSatisfaction}; + /// # use bdk_wallet::signer::SignersContainer; + /// # use bitcoin::{Network, Sequence}; + /// # let descriptor = concat!( + /// # "wsh(thresh(2,", + /// # "pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),", + /// # "sj:and_v(v:pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),", + /// # "n:older(6)),", + /// # "snj:and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),", + /// # "after(630000))))", + /// # ); + /// # let mut wallet = Wallet::create_single(descriptor) + /// # .network(Network::Regtest) + /// # .create_wallet_no_persist()?; + /// let policy = wallet + /// .public_descriptor(KeychainKind::External) + /// .extract_policy( + /// // Signers only affect the `contribution` and `satisfaction` fields, which + /// // `get_condition` ignores, so an empty container is enough here. + /// &SignersContainer::default(), + /// BuildSatisfaction::None, + /// wallet.secp_ctx(), + /// )? + /// .expect("descriptor has a spending policy"); + /// + /// // Choose pk(A) + and(pk(B), older(6)) from the thresh() root. + /// let mut path = BTreeMap::new(); + /// path.insert(policy.id.clone(), vec![0, 1]); + /// let condition = policy.get_condition(&path)?; + /// assert_eq!(condition.csv, Some(Sequence(6))); + /// assert_eq!(condition.timelock, None); + /// + /// // Inputs of the resulting transaction will carry nSequence = 6. + /// let mut builder = wallet.build_tx(); + /// builder.set_condition(condition); + /// + /// # Ok::<(), anyhow::Error>(()) + /// ``` + pub fn set_condition(&mut self, condition: Condition) -> &mut Self { + self.params.condition = Some(condition); + 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 diff --git a/tests/wallet.rs b/tests/wallet.rs index 6a577109d..2720bec20 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -6,10 +6,12 @@ use bdk_chain::{BlockId, CanonicalizationParams, ConfirmationBlockTime}; use bdk_wallet::KeychainKind; use bdk_wallet::coin_selection; use bdk_wallet::coin_selection::InsufficientFunds; -use bdk_wallet::descriptor::{DescriptorError, calc_checksum}; +use bdk_wallet::descriptor::{ + DescriptorError, ExtractPolicy, calc_checksum, policy::BuildSatisfaction, +}; use bdk_wallet::error::CreateTxError; use bdk_wallet::psbt::PsbtUtils; -use bdk_wallet::signer::{SignOptions, SignerError}; +use bdk_wallet::signer::{SignOptions, SignerError, SignersContainer}; use bdk_wallet::test_utils::*; use bdk_wallet::{ AddressInfo, Balance, FinalizeInputOutcome, IndexOutOfBoundsError, PersistedWallet, Update, @@ -1155,6 +1157,115 @@ fn test_create_tx_policy_path_ignored_subtree_with_csv() { assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFD)); } +#[test] +fn test_create_tx_condition_no_csv() { + let (descriptor, change_descriptor) = get_test_wpkh_and_change_desc(); + let mut wallet = Wallet::create(descriptor, change_descriptor) + .network(Network::Regtest) + .create_wallet_no_persist() + .expect("wallet"); + + 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) + .script_pubkey(), + value: Amount::from_sat(50_000), + }], + }; + insert_tx(&mut wallet, tx); + + // `get_condition` ignores the signer-dependent parts of the policy, so no signers are needed. + let policy = wallet + .public_descriptor(KeychainKind::External) + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::None, + wallet.secp_ctx(), + ) + .unwrap() + .expect("descriptor has a spending policy"); + // child #0 is just the key "A" + let path = vec![(policy.id.clone(), vec![0])].into_iter().collect(); + let condition = policy.get_condition(&path).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)) + .set_condition(condition); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFD)); +} + +#[test] +fn test_create_tx_condition_use_csv() { + let (mut wallet, _) = get_funded_wallet_single(get_test_a_or_b_plus_csv()); + + // `get_condition` ignores the signer-dependent parts of the policy, so no signers are needed. + let policy = wallet + .public_descriptor(KeychainKind::External) + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::None, + wallet.secp_ctx(), + ) + .unwrap() + .expect("descriptor has a spending policy"); + // child #1 is and(pk(B),older(144)) + let path = vec![(policy.id.clone(), vec![1])].into_iter().collect(); + let condition = policy.get_condition(&path).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)) + .set_condition(condition); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144)); +} + +#[test] +fn test_create_tx_condition_ignored_subtree_with_csv() { + let (mut wallet, _) = get_funded_wallet_single( + "wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),or_i(and_v(v:pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(30)),and_v(v:pkh(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(90)))))", + ); + + // `get_condition` ignores the signer-dependent parts of the policy, so no signers are needed. + let policy = wallet + .public_descriptor(KeychainKind::External) + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::None, + wallet.secp_ctx(), + ) + .unwrap() + .expect("descriptor has a spending policy"); + // child #0 is pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu) + let path = vec![(policy.id.clone(), vec![0])].into_iter().collect(); + let condition = policy.get_condition(&path).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)) + .set_condition(condition); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFD)); +} + #[test] fn test_create_tx_global_xpubs_with_origin() { use bitcoin::bip32; From 9a9b2ca94446bf1c3e7e011394a5e799d69c477a Mon Sep 17 00:00:00 2001 From: Noah Joeris Date: Mon, 27 Jul 2026 18:37:28 +0300 Subject: [PATCH 3/3] deprecate(wallet): wallet-owned signer and policy APIs Mark Wallet::sign, signer/keymap accessors, keymap load/create helpers, FullyNodedExport::export_wallet, Wallet::policies, and TxBuilder::policy_path as deprecated. Replacement: keep KeyMaps/Xprivs outside Wallet; bitcoin::Psbt::sign or Wallet::sign_with_signers; extract_policy + Policy::get_condition + TxBuilder::set_condition for spending paths. --- src/wallet/export.rs | 40 ++++++++++++++ src/wallet/mod.rs | 112 ++++++++++++++++++++++++++++++++++++++ src/wallet/params.rs | 12 ++++ src/wallet/tx_builder.rs | 5 ++ tests/persisted_wallet.rs | 1 + tests/wallet.rs | 4 ++ 6 files changed, 174 insertions(+) diff --git a/src/wallet/export.rs b/src/wallet/export.rs index 396ce8cc6..3fbf4f4ad 100644 --- a/src/wallet/export.rs +++ b/src/wallet/export.rs @@ -249,6 +249,11 @@ impl FullyNodedExport { } /// Export a wallet to FullyNoded format using keys stored in the wallet. + #[deprecated( + since = "3.2.0", + note = "pass external and internal KeyMaps explicitly; use FullyNodedExport::export_wallet_with_keymaps instead." + )] + #[allow(deprecated)] pub fn export_wallet( wallet: &Wallet, label: &str, @@ -792,6 +797,41 @@ mod test { assert_eq!(export.label, "Test Label"); } + #[test] + #[allow(deprecated)] + fn test_deprecated_export_wallet_matches_export_with_keymaps() { + let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; + let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)"; + let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin); + let (external_keymap, internal_keymap) = + keymaps_from_descriptors(descriptor, change_descriptor); + + let deprecated_export = + FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + let export_with_keymaps = FullyNodedExport::export_wallet_with_keymaps( + &wallet, + &external_keymap, + &internal_keymap, + "Test Label", + true, + ) + .unwrap(); + + assert_eq!( + deprecated_export.descriptor(), + export_with_keymaps.descriptor() + ); + assert_eq!( + deprecated_export.change_descriptor(), + export_with_keymaps.change_descriptor() + ); + assert_eq!( + deprecated_export.blockheight, + export_with_keymaps.blockheight + ); + assert_eq!(deprecated_export.label, export_with_keymaps.label); + } + #[test] #[should_panic(expected = "Incompatible change descriptor")] fn test_export_no_change() { diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 353d4cc5a..ab7af6338 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1161,6 +1161,10 @@ impl Wallet { /// Add an external signer /// /// See [the `signer` module](signer) for an example. + #[deprecated( + since = "3.2.0", + note = "use your KeyMap or Xpriv with bitcoin::Psbt::sign and wallet.secp_ctx() for software signing, or use your own signer for hardware signing. If you rely on SignOptions, use Wallet::sign_with_signers with caller-owned SignersContainers." + )] pub fn add_signer( &mut self, keychain: KeychainKind, @@ -1179,6 +1183,10 @@ impl Wallet { /// /// Note this does nothing if the given keychain has no descriptor because we won't /// know the context (segwit, taproot, etc) in which to create signatures. + #[deprecated( + since = "3.2.0", + note = "use your KeyMap or Xpriv with bitcoin::Psbt::sign and wallet.secp_ctx() for software signing, or use your own signer for hardware signing. If you rely on SignOptions, use Wallet::sign_with_signers with caller-owned SignersContainers." + )] pub fn set_keymap(&mut self, keychain: KeychainKind, keymap: KeyMap) { let wallet_signers = match keychain { KeychainKind::External => Arc::make_mut(&mut self.signers), @@ -1190,6 +1198,11 @@ impl Wallet { } /// Set the keymap for each keychain. + #[deprecated( + since = "3.2.0", + note = "use your KeyMap or Xpriv with bitcoin::Psbt::sign and wallet.secp_ctx() for software signing, or use your own signer for hardware signing. If you rely on SignOptions, use Wallet::sign_with_signers with caller-owned SignersContainers." + )] + #[allow(deprecated)] pub fn set_keymaps(&mut self, keymaps: impl IntoIterator) { for (keychain, keymap) in keymaps { self.set_keymap(keychain, keymap); @@ -1208,6 +1221,7 @@ impl Wallet { /// let wallet = Wallet::create(descriptor, change_descriptor) /// .network(Network::Testnet) /// .create_wallet_no_persist()?; + /// #[allow(deprecated)] /// 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); @@ -1215,6 +1229,10 @@ impl Wallet { /// /// Ok::<(), Box>(()) /// ``` + #[deprecated( + since = "3.2.0", + note = "the Wallet is being phased out as a key store; keep your own KeyMap or Xpriv instead. If you rely on SignOptions, use Wallet::sign_with_signers with caller-owned SignersContainers." + )] pub fn get_signers(&self, keychain: KeychainKind) -> Arc { match keychain { KeychainKind::External => Arc::clone(&self.signers), @@ -1799,10 +1817,15 @@ impl Wallet { /// builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); /// builder.finish()? /// }; + /// #[allow(deprecated)] /// let finalized = wallet.sign(&mut psbt, SignOptions::default())?; /// assert!(finalized, "we should have signed all the inputs"); /// # Ok::<(),anyhow::Error>(()) /// ``` + #[deprecated( + since = "3.2.0", + note = "use your KeyMap or Xpriv with bitcoin::Psbt::sign and wallet.secp_ctx() for software signing, or use your own signer for hardware signing. If you rely on SignOptions, use Wallet::sign_with_signers with caller-owned SignersContainers." + )] pub fn sign(&self, psbt: &mut Psbt, sign_options: SignOptions) -> Result { self.sign_with_signers( psbt, @@ -1908,6 +1931,15 @@ impl Wallet { } /// Return the spending policies for the wallet's descriptor. + /// + /// Use `wallet.public_descriptor(keychain).extract_policy(signers, BuildSatisfaction::None, + /// wallet.secp_ctx())` with caller-owned signers instead. For transaction building, derive a + /// [`Condition`](crate::descriptor::Condition) via [`Policy::get_condition`] and pass it to + /// [`TxBuilder::set_condition`](tx_builder::TxBuilder::set_condition). + #[deprecated( + since = "3.2.0", + note = "use wallet.public_descriptor(keychain).extract_policy(signers, BuildSatisfaction::None, wallet.secp_ctx()) with caller-owned signers; for tx building, Policy::get_condition then TxBuilder::set_condition" + )] pub fn policies(&self, keychain: KeychainKind) -> Result, DescriptorError> { let signers = match keychain { KeychainKind::External => &self.signers, @@ -4073,4 +4105,84 @@ mod test { let wallet_name = result_with_change.unwrap(); assert_eq!(wallet_name, "vn4aqs37jgrerlc3"); } + + #[test] + #[allow(deprecated)] + fn test_deprecated_keymap_methods() { + use crate::test_utils::get_test_wpkh_and_change_desc; + + let (external_desc, internal_desc) = get_test_wpkh_and_change_desc(); + let secp = SecpCtx::new(); + let (external_descriptor, external_keymap) = external_desc + .into_wallet_descriptor(&secp, NetworkKind::Test) + .unwrap(); + let (internal_descriptor, internal_keymap) = internal_desc + .into_wallet_descriptor(&secp, NetworkKind::Test) + .unwrap(); + + let assert_keymaps = |wallet: &Wallet| { + assert_eq!( + wallet + .get_signers(KeychainKind::External) + .as_key_map(wallet.secp_ctx()), + external_keymap + ); + assert_eq!( + wallet + .get_signers(KeychainKind::Internal) + .as_key_map(wallet.secp_ctx()), + internal_keymap + ); + }; + + let wallet = Wallet::create(external_descriptor.clone(), internal_descriptor.clone()) + .network(Network::Testnet) + .keymap(KeychainKind::External, external_keymap.clone()) + .keymap(KeychainKind::Internal, internal_keymap.clone()) + .create_wallet_no_persist() + .unwrap(); + assert_keymaps(&wallet); + + let mut wallet = Wallet::create(external_descriptor, internal_descriptor) + .network(Network::Testnet) + .create_wallet_no_persist() + .unwrap(); + wallet.set_keymaps([ + (KeychainKind::External, external_keymap.clone()), + (KeychainKind::Internal, internal_keymap.clone()), + ]); + assert_keymaps(&wallet); + } + + #[test] + #[allow(deprecated)] + fn test_deprecated_sign_matches_sign_with_signers() { + use crate::test_utils::get_funded_wallet_wpkh; + + let (mut wallet, _) = get_funded_wallet_wpkh(); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let psbt = builder.finish().unwrap(); + + let mut deprecated_psbt = psbt.clone(); + let mut signers_psbt = psbt; + + let deprecated_finalized = wallet + .sign(&mut deprecated_psbt, SignOptions::default()) + .unwrap(); + let external_signers = wallet.get_signers(KeychainKind::External); + let internal_signers = wallet.get_signers(KeychainKind::Internal); + let signers_finalized = wallet + .sign_with_signers( + &mut signers_psbt, + &[external_signers.as_ref(), internal_signers.as_ref()], + SignOptions::default(), + ) + .unwrap(); + + assert!(deprecated_finalized); + assert_eq!(deprecated_finalized, signers_finalized); + assert_eq!(deprecated_psbt, signers_psbt); + } } diff --git a/src/wallet/params.rs b/src/wallet/params.rs index 056c14728..7d116a621 100644 --- a/src/wallet/params.rs +++ b/src/wallet/params.rs @@ -141,6 +141,10 @@ impl CreateParams { } /// Extend the given `keychain`'s `keymap`. + #[deprecated( + since = "3.2.0", + note = "use your KeyMap or Xpriv with bitcoin::Psbt::sign and wallet.secp_ctx() for software signing, or use your own signer for hardware signing. If you rely on SignOptions, use Wallet::sign_with_signers with caller-owned SignersContainers." + )] pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self { match keychain { KeychainKind::External => &mut self.descriptor_keymap, @@ -243,6 +247,10 @@ impl LoadParams { } /// Extend the given `keychain`'s `keymap`. + #[deprecated( + since = "3.2.0", + note = "use your KeyMap or Xpriv with bitcoin::Psbt::sign and wallet.secp_ctx() for software signing, or use your own signer for hardware signing. If you rely on SignOptions, use Wallet::sign_with_signers with caller-owned SignersContainers." + )] pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self { match keychain { KeychainKind::External => &mut self.descriptor_keymap, @@ -312,6 +320,10 @@ impl LoadParams { /// Whether to try extracting private keys from the *provided descriptors* upon loading. /// See also [`LoadParams::descriptor`]. + #[deprecated( + since = "3.2.0", + note = "use your KeyMap or Xpriv with bitcoin::Psbt::sign and wallet.secp_ctx() for software signing, or use your own signer for hardware signing. If you rely on SignOptions, use Wallet::sign_with_signers with caller-owned SignersContainers." + )] pub fn extract_keys(mut self) -> Self { self.extract_keys = true; self diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 4687e94dd..c3b73079a 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -279,6 +279,9 @@ impl<'a, Cs> TxBuilder<'a, Cs> { /// Set the policy path to use while creating the transaction for a given keychain. /// + /// Use [`Self::set_condition`] instead. If both this and [`Self::set_condition`] are set, the + /// condition overrides the path. + /// /// 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 @@ -321,6 +324,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> { /// multiple entries can be added to the map, one for each node that requires an explicit path. /// /// ``` + /// # #![allow(deprecated)] /// # use std::str::FromStr; /// # use std::collections::BTreeMap; /// # use bitcoin::*; @@ -340,6 +344,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> { /// /// # Ok::<(), anyhow::Error>(()) /// ``` + #[deprecated(since = "3.2.0", note = "use set_condition instead")] pub fn policy_path( &mut self, policy_path: BTreeMap>, diff --git a/tests/persisted_wallet.rs b/tests/persisted_wallet.rs index c654e7a9b..b66d93722 100644 --- a/tests/persisted_wallet.rs +++ b/tests/persisted_wallet.rs @@ -230,6 +230,7 @@ fn wallet_is_persisted() -> anyhow::Result<()> { } #[test] +#[allow(deprecated)] fn wallet_load_checks() -> anyhow::Result<()> { fn run( filename: &str, diff --git a/tests/wallet.rs b/tests/wallet.rs index 2720bec20..927300498 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -1077,6 +1077,7 @@ fn test_create_tx_policy_path_required() { #[test] fn test_create_tx_policy_path_no_csv() { + #![allow(deprecated)] let (descriptor, change_descriptor) = get_test_wpkh_and_change_desc(); let mut wallet = Wallet::create(descriptor, change_descriptor) .network(Network::Regtest) @@ -1115,6 +1116,7 @@ fn test_create_tx_policy_path_no_csv() { #[test] fn test_create_tx_policy_path_use_csv() { + #![allow(deprecated)] let (mut wallet, _) = get_funded_wallet_single(get_test_a_or_b_plus_csv()); let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); @@ -1136,6 +1138,7 @@ fn test_create_tx_policy_path_use_csv() { #[test] fn test_create_tx_policy_path_ignored_subtree_with_csv() { + #![allow(deprecated)] let (mut wallet, _) = get_funded_wallet_single( "wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),or_i(and_v(v:pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(30)),and_v(v:pkh(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(90)))))", ); @@ -2470,6 +2473,7 @@ fn test_taproot_psbt_populate_tap_key_origins() { #[test] fn test_taproot_psbt_populate_tap_key_origins_repeated_key() { + #![allow(deprecated)] let (mut wallet, _) = get_funded_wallet(get_test_tr_repeated_key(), get_test_tr_single_sig()); let addr = wallet.reveal_next_address(KeychainKind::External);