diff --git a/README.md b/README.md index 2176a473..71b0265d 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 c63f2b0d..c6e20cda 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/compiler.rs b/examples/compiler.rs index 2a28519f..19eea3d2 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 @@ -68,7 +71,12 @@ fn main() -> Result<(), Box> { // BDK also has it's own `Policy` structure to represent the spending condition in a more // human readable json format. - let spending_policy = wallet.policies(KeychainKind::External)?; + // Signers are caller-owned. This descriptor is compiled from a policy and holds no + // secrets, so an empty container is enough to extract the spending policy. + let signers = SignersContainer::default(); + let spending_policy = wallet + .public_descriptor(KeychainKind::External) + .extract_policy(&signers, BuildSatisfaction::None, wallet.secp_ctx())?; println!( "The BDK spending policy: \n{}", serde_json::to_string_pretty(&spending_policy)? diff --git a/examples/electrum.rs b/examples/electrum.rs index e89c1175..2a667ecb 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,24 @@ const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7 const ELECTRUM_URL: &str = "ssl://mempool.space:40002"; fn main() -> Result<(), anyhow::Error> { + // Keys are caller-owned: parse the descriptors ourselves and keep the KeyMap. + let secp = Secp256k1::new(); + let (external_descriptor, mut keymap) = + EXTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + let (internal_descriptor, internal_keymap) = + INTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + keymap.extend(internal_keymap); + let signer = KeyMapWrapper::from(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 +100,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!("failed to sign PSBT: {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 +137,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!("failed to sign PSBT: {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 1780b287..b1c37fdd 100644 --- a/examples/esplora_async.rs +++ b/examples/esplora_async.rs @@ -1,4 +1,7 @@ use bdk_esplora::{EsploraAsyncExt, esplora_client}; +use bdk_wallet::bitcoin::secp256k1::Secp256k1; +use bdk_wallet::descriptor::IntoWalletDescriptor; +use bdk_wallet::miniscript::descriptor::KeyMapWrapper; use bdk_wallet::{ KeychainKind, SignOptions, Wallet, bitcoin::{Amount, FeeRate, Network}, @@ -20,16 +23,24 @@ const ESPLORA_URL: &str = "https://mempool.space/testnet4/api"; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { + // Keys are caller-owned: parse the descriptors ourselves and keep the KeyMap. + let secp = Secp256k1::new(); + let (external_descriptor, mut keymap) = + EXTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + let (internal_descriptor, internal_keymap) = + INTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + keymap.extend(internal_keymap); + let signer = KeyMapWrapper::from(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 +94,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!("failed to sign PSBT: {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 +130,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!("failed to sign PSBT: {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 179a7d79..bbd593aa 100644 --- a/examples/esplora_blocking.rs +++ b/examples/esplora_blocking.rs @@ -1,4 +1,7 @@ use bdk_esplora::{EsploraExt, esplora_client}; +use bdk_wallet::bitcoin::secp256k1::Secp256k1; +use bdk_wallet::descriptor::IntoWalletDescriptor; +use bdk_wallet::miniscript::descriptor::KeyMapWrapper; use bdk_wallet::rusqlite::Connection; use bdk_wallet::{ KeychainKind, SignOptions, Wallet, @@ -20,16 +23,24 @@ const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7 const ESPLORA_URL: &str = "https://mempool.space/testnet4/api"; fn main() -> Result<(), anyhow::Error> { + // Keys are caller-owned: parse the descriptors ourselves and keep the KeyMap. + let secp = Secp256k1::new(); + let (external_descriptor, mut keymap) = + EXTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + let (internal_descriptor, internal_keymap) = + INTERNAL_DESC.into_wallet_descriptor(&secp, NETWORK.into())?; + keymap.extend(internal_keymap); + let signer = KeyMapWrapper::from(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 +89,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!("failed to sign PSBT: {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 +126,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!("failed to sign PSBT: {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/descriptor/mod.rs b/src/descriptor/mod.rs index f30ea0f3..921cda23 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 a293f808..7b5486db 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -484,7 +484,16 @@ impl Condition { } } - pub(crate) fn merge(mut self, other: &Condition) -> Result { + /// Merge two conditions, returning the strictest requirement satisfying both. + /// + /// Use this to combine the conditions of every keychain from which inputs may be selected, + /// before handing the result to + /// [`TxBuilder::set_condition`](crate::wallet::tx_builder::TxBuilder::set_condition). + /// + /// # Errors + /// + /// If the two conditions are incompatible, e.g. a height-based and a time-based timelock. + 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/export.rs b/src/wallet/export.rs index 6a6cc967..d34ddc7c 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/*)", +//! const EXTERNAL: &str = "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)"; +//! const INTERNAL: &str = "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"; +//! let wallet = Wallet::create(EXTERNAL, INTERNAL) +//! .network(Network::Testnet) +//! .create_wallet_no_persist()?; +//! // Keys are caller-owned: supply the keymaps explicitly. +//! let secp = wallet.secp_ctx(); +//! let (_, external_keymap) = miniscript::Descriptor::parse_descriptor(secp, EXTERNAL)?; +//! let (_, internal_keymap) = miniscript::Descriptor::parse_descriptor(secp, INTERNAL)?; +//! let export = FullyNodedExport::export_wallet_with_keymaps( +//! &wallet, +//! &external_keymap, +//! &internal_keymap, +//! "exported wallet", +//! true, //! ) -//! .network(Network::Testnet) -//! .create_wallet_no_persist()?; -//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap(); +//! .unwrap(); //! //! println!("Exported: {}", export.to_string()); //! # Ok::<_, Box>(()) @@ -147,6 +157,7 @@ use miniscript::{Descriptor, ScriptContext, Terminal}; use crate::types::KeychainKind; use crate::wallet::Wallet; +use miniscript::descriptor::KeyMap; /// Alias for [`FullyNodedExport`] #[deprecated(since = "0.18.0", note = "Please use [`FullyNodedExport`] instead")] @@ -194,19 +205,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 +236,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"); @@ -737,13 +742,36 @@ mod test { wallet } + /// Export using keymaps parsed from the descriptors, mirroring what a caller does now that + /// the wallet no longer owns key material. + fn export_with_desc_keymaps( + wallet: &Wallet, + descriptor: &str, + change_descriptor: &str, + label: &str, + include_blockheight: bool, + ) -> Result { + let secp = wallet.secp_ctx(); + let (_, external_keymap) = Descriptor::parse_descriptor(secp, descriptor).unwrap(); + let (_, internal_keymap) = Descriptor::parse_descriptor(secp, change_descriptor).unwrap(); + FullyNodedExport::export_wallet_with_keymaps( + wallet, + &external_keymap, + &internal_keymap, + label, + include_blockheight, + ) + } + #[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 export = + export_with_desc_keymaps(&wallet, descriptor, change_descriptor, "Test Label", true) + .unwrap(); assert_eq!(export.descriptor(), descriptor); assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); @@ -762,7 +790,8 @@ 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(); + export_with_desc_keymaps(&wallet, descriptor, change_descriptor, "Test Label", true) + .unwrap(); } #[test] @@ -775,7 +804,8 @@ 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(); + export_with_desc_keymaps(&wallet, descriptor, change_descriptor, "Test Label", true) + .unwrap(); } #[test] @@ -792,7 +822,9 @@ mod test { ))"; let wallet = get_test_wallet(descriptor, change_descriptor, Network::Testnet); - let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap(); + let export = + export_with_desc_keymaps(&wallet, descriptor, change_descriptor, "Test Label", true) + .unwrap(); assert_eq!(export.descriptor(), descriptor); assert_eq!(export.change_descriptor(), Some(change_descriptor.into())); @@ -805,7 +837,9 @@ 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 export = + export_with_desc_keymaps(&wallet, descriptor, change_descriptor, "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 +852,9 @@ 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 export = + export_with_desc_keymaps(&wallet, descriptor, change_descriptor, "Test Label", true) + .unwrap(); assert_eq!( export.to_string(), diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 3a5e7fde..5090d0b2 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -43,10 +43,7 @@ use bitcoin::{ sighash::{EcdsaSighashType, TapSighashType}, transaction::{self, Version}, }; -use miniscript::{ - descriptor::KeyMap, - psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}, -}; +use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}; use rand_core::RngCore; mod changeset; @@ -65,16 +62,16 @@ pub(crate) mod utils; use crate::collections::{BTreeMap, HashMap, HashSet}; use crate::descriptor::{ - DerivedDescriptor, DescriptorMeta, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, - Policy, XKeyUtils, calc_checksum, check_wallet_descriptor, error::Error as DescriptorError, - policy::BuildSatisfaction, + Condition, DerivedDescriptor, DescriptorMeta, ExtendedDescriptor, ExtractPolicy, + IntoWalletDescriptor, XKeyUtils, calc_checksum, check_wallet_descriptor, + error::Error as DescriptorError, policy::BuildSatisfaction, }; use crate::psbt::PsbtUtils; use crate::types::*; use crate::wallet::{ coin_selection::{DefaultCoinSelectionAlgorithm, Excess, InsufficientFunds}, error::{BuildFeeBumpError, CreateTxError, MiniscriptPsbtError}, - signer::{SignOptions, SignerError, SignerOrdering, SignersContainer, TransactionSigner}, + signer::{SignOptions, SignerError, SignersContainer}, tx_builder::{FeePolicy, TxBuilder, TxParams}, utils::{After, Older, SecpCtx, check_nsequence_rbf}, }; @@ -133,8 +130,6 @@ type IndexedTxOut = ((KeychainKind, u32), FullTxOut); /// [`take_staged`]: Wallet::take_staged #[derive(Debug)] pub struct Wallet { - signers: Arc, - change_signers: Arc, chain: LocalChain, tx_graph: IndexedTxGraph>, stage: ChangeSet, @@ -344,29 +339,16 @@ impl Wallet { .unwrap_or(genesis_block(network).block_hash()); let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); - let (descriptor, mut descriptor_keymap) = (params.descriptor)(&secp, network_kind)?; + let (descriptor, _) = (params.descriptor)(&secp, network_kind)?; check_wallet_descriptor(&descriptor)?; - descriptor_keymap.extend(params.descriptor_keymap); - - let signers = Arc::new(SignersContainer::build( - descriptor_keymap, - &descriptor, - &secp, - )); - let (change_descriptor, change_signers) = match params.change_descriptor { + let change_descriptor = match params.change_descriptor { Some(make_desc) => { - let (change_descriptor, mut internal_keymap) = make_desc(&secp, network_kind)?; + let (change_descriptor, _) = make_desc(&secp, network_kind)?; check_wallet_descriptor(&change_descriptor)?; - internal_keymap.extend(params.change_descriptor_keymap); - let change_signers = Arc::new(SignersContainer::build( - internal_keymap, - &change_descriptor, - &secp, - )); - (Some(change_descriptor), change_signers) + Some(change_descriptor) } - None => (None, Arc::new(SignersContainer::new())), + None => None, }; let locked_outpoints = HashSet::new(); @@ -390,8 +372,6 @@ impl Wallet { )?; Ok(Wallet { - signers, - change_signers, network, chain, tx_graph, @@ -403,11 +383,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. The wallet does not hold key + /// material: keep your own [`KeyMap`](miniscript::descriptor::KeyMap) and sign with + /// [`bitcoin::Psbt::sign`], or build a + /// [`SignersContainer`](crate::signer::SignersContainer) and pass it to + /// [`Wallet::sign_with_signers`]. You can check the wallet's descriptors are what you expect + /// with [`LoadParams::descriptor`]. /// /// # Synopsis /// @@ -426,18 +407,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 match 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 @@ -488,11 +463,10 @@ impl Wallet { .descriptor .ok_or(LoadError::MissingDescriptor(KeychainKind::External))?; check_wallet_descriptor(&descriptor).map_err(LoadError::Descriptor)?; - let mut external_keymap = params.descriptor_keymap; if let Some(expected) = params.check_descriptor { if let Some(make_desc) = expected { - let (exp_desc, keymap) = + let (exp_desc, _) = make_desc(&secp, network_kind).map_err(LoadError::Descriptor)?; if descriptor.descriptor_id() != exp_desc.descriptor_id() { return Err(LoadError::Mismatch(LoadMismatch::Descriptor { @@ -501,9 +475,6 @@ impl Wallet { expected: Some(Box::new(exp_desc)), })); } - if params.extract_keys { - external_keymap.extend(keymap); - } } else { return Err(LoadError::Mismatch(LoadMismatch::Descriptor { keychain: KeychainKind::External, @@ -512,10 +483,8 @@ impl Wallet { })); } } - let signers = Arc::new(SignersContainer::build(external_keymap, &descriptor, &secp)); let mut change_descriptor = None; - let mut internal_keymap = params.change_descriptor_keymap; match (changeset.change_descriptor, params.check_change_descriptor) { // Empty signer. @@ -549,7 +518,7 @@ impl Wallet { // Parameters must match. Some(make_desc) => { check_wallet_descriptor(&desc).map_err(LoadError::Descriptor)?; - let (exp_desc, keymap) = + let (exp_desc, _) = make_desc(&secp, network_kind).map_err(LoadError::Descriptor)?; if desc.descriptor_id() != exp_desc.descriptor_id() { return Err(LoadError::Mismatch(LoadMismatch::Descriptor { @@ -558,23 +527,11 @@ impl Wallet { expected: Some(Box::new(exp_desc)), })); } - if params.extract_keys { - internal_keymap.extend(keymap); - } change_descriptor = Some(desc); } }, } - let change_signers = match change_descriptor { - Some(ref change_descriptor) => Arc::new(SignersContainer::build( - internal_keymap, - change_descriptor, - &secp, - )), - None => Arc::new(SignersContainer::new()), - }; - // Apply locked outpoints let locked_outpoints = changeset.locked_outpoints.outpoints; let locked_outpoints = locked_outpoints @@ -597,8 +554,6 @@ impl Wallet { .map_err(LoadError::Descriptor)?; Ok(Some(Wallet { - signers, - change_signers, chain, tx_graph, stage, @@ -1162,70 +1117,6 @@ impl Wallet { ) } - /// Add an external signer - /// - /// See [the `signer` module](signer) for an example. - pub fn add_signer( - &mut self, - keychain: KeychainKind, - ordering: SignerOrdering, - signer: Arc, - ) { - let signers = match keychain { - KeychainKind::External => Arc::make_mut(&mut self.signers), - KeychainKind::Internal => Arc::make_mut(&mut self.change_signers), - }; - - signers.add_external(signer.id(&self.secp), ordering, signer); - } - - /// Set the keymap for a given keychain. - /// - /// 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. - pub fn set_keymap(&mut self, keychain: KeychainKind, keymap: KeyMap) { - let wallet_signers = match keychain { - KeychainKind::External => Arc::make_mut(&mut self.signers), - KeychainKind::Internal => Arc::make_mut(&mut self.change_signers), - }; - if let Some(descriptor) = self.tx_graph.index.get_descriptor(keychain) { - *wallet_signers = SignersContainer::build(keymap, descriptor, &self.secp) - } - } - - /// Set the keymap for each keychain. - pub fn set_keymaps(&mut self, keymaps: impl IntoIterator) { - for (keychain, keymap) in keymaps { - self.set_keymap(keychain, keymap); - } - } - - /// Get the signers - /// - /// ## Example - /// - /// ``` - /// # use bdk_wallet::{Wallet, KeychainKind}; - /// # use bdk_wallet::bitcoin::Network; - /// let descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/0/*)"; - /// let change_descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/1/*)"; - /// let wallet = Wallet::create(descriptor, change_descriptor) - /// .network(Network::Testnet) - /// .create_wallet_no_persist()?; - /// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) { - /// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/* - /// println!("secret_key: {}", secret_key); - /// } - /// - /// Ok::<(), Box>(()) - /// ``` - pub fn get_signers(&self, keychain: KeychainKind) -> Arc { - match keychain { - KeychainKind::External => Arc::clone(&self.signers), - KeychainKind::Internal => Arc::clone(&self.change_signers), - } - } - /// Start building a transaction. /// /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the @@ -1239,6 +1130,8 @@ impl Wallet { /// # use bdk_wallet::*; /// # use bdk_wallet::ChangeSet; /// # use bdk_wallet::error::CreateTxError; + /// # use bdk_wallet::descriptor::IntoWalletDescriptor; + /// # use bdk_wallet::signer::SignersContainer; /// # use anyhow::Error; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let mut wallet = doctest_wallet!(); @@ -1269,65 +1162,54 @@ 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()?; - - // 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, - )); + // The spending condition may be supplied by the caller via `TxBuilder::set_condition`. + // Otherwise we derive it from the descriptors themselves. Deriving needs no key material: + // signers only influence a policy's `contribution`/`satisfaction`, which `get_condition` + // ignores, so an empty container is sufficient. If a descriptor offers several ways to be + // satisfied, `get_condition` cannot choose between them and the caller must say which one + // it intends via `set_condition`. + let requirements = match params.condition { + Some(condition) => condition, + None => { + let keychains: BTreeMap<_, _> = self.tx_graph.index.keychains().collect(); + let no_signers = SignersContainer::default(); + let no_path = BTreeMap::new(); + + let mut requirements = Condition::default(); + for (keychain, skip) in [ + ( + KeychainKind::External, + tx_builder::ChangeSpendPolicy::OnlyChange, + ), + ( + KeychainKind::Internal, + tx_builder::ChangeSpendPolicy::ChangeForbidden, + ), + ] { + if params.change_policy == skip { + continue; + } + let Some(descriptor) = keychains.get(&keychain) else { + continue; + }; + let Some(policy) = descriptor.extract_policy( + &no_signers, + BuildSatisfaction::None, + &self.secp, + )? + else { + continue; + }; + // A policy that cannot be resolved without an explicit path needs the caller + // to pick one. + let condition = policy + .get_condition(&no_path) + .map_err(|_| CreateTxError::SpendingPolicyRequired(keychain))?; + requirements = requirements.merge(&condition)?; + } + requirements + } }; - // 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 requirements = - external_requirements.merge(&internal_requirements.unwrap_or_default())?; let version = match params.version { Some(transaction::Version(0)) => return Err(CreateTxError::Version0), @@ -1602,6 +1484,8 @@ impl Wallet { /// # use bdk_wallet::*; /// # use bdk_wallet::ChangeSet; /// # use bdk_wallet::error::CreateTxError; + /// # use bdk_wallet::descriptor::IntoWalletDescriptor; + /// # use bdk_wallet::signer::SignersContainer; /// # use anyhow::Error; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let mut wallet = doctest_wallet!(); @@ -1612,7 +1496,11 @@ impl Wallet { /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); /// builder.finish()? /// }; - /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; + /// // Keys are caller-owned: build a signer container from the signing descriptor. + /// let (signing_desc, keymap) = + /// descriptor.into_wallet_descriptor(wallet.secp_ctx(), wallet.network().into())?; + /// let signers = SignersContainer::build(keymap, &signing_desc, wallet.secp_ctx()); + /// let _ = wallet.sign_with_signers(&mut psbt, &[&signers], 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 +1510,7 @@ impl Wallet { /// builder.finish()? /// }; /// - /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; + /// let _ = wallet.sign_with_signers(&mut psbt, &[&signers], SignOptions::default())?; /// let fee_bumped_tx = psbt.extract_tx(); /// // broadcast fee_bumped_tx to replace original /// # Ok::<(), anyhow::Error>(()) @@ -1763,42 +1651,10 @@ impl Wallet { }) } - /// Sign a transaction with all the wallet's signers, in the order specified by every signer's - /// [`SignerOrdering`]. This function returns the `Result` type with an encapsulated `bool` that - /// has the value true if the PSBT was finalized, or false otherwise. - /// - /// ## Example - /// - /// ``` - /// # use std::str::FromStr; - /// # use bitcoin::*; - /// # use bdk_wallet::*; - /// # use bdk_wallet::ChangeSet; - /// # use bdk_wallet::error::CreateTxError; - /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; - /// # let mut wallet = doctest_wallet!(); - /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); - /// let mut psbt = { - /// let mut builder = wallet.build_tx(); - /// builder.add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); - /// builder.finish()? - /// }; - /// let finalized = wallet.sign(&mut psbt, SignOptions::default())?; - /// assert!(finalized, "we should have signed all the inputs"); - /// # Ok::<(),anyhow::Error>(()) - /// ``` - pub fn sign(&self, psbt: &mut Psbt, sign_options: SignOptions) -> Result { - self.sign_with_signers( - psbt, - &[self.signers.as_ref(), self.change_signers.as_ref()], - sign_options, - ) - } - /// Sign a transaction with the provided signer containers. /// /// Signer containers are processed in the order provided. Signers inside each container are - /// processed according to their [`SignerOrdering`]. + /// processed according to their [`SignerOrdering`](crate::signer::SignerOrdering). /// /// The [`SignOptions`] can be used to tweak the behavior of the software signers, and the way /// the transaction is finalized at the end. Note that it can't be guaranteed that *every* @@ -1891,20 +1747,6 @@ impl Wallet { } } - /// Return the spending policies for the wallet's descriptor. - pub fn policies(&self, keychain: KeychainKind) -> Result, DescriptorError> { - let signers = match keychain { - KeychainKind::External => &self.signers, - KeychainKind::Internal => &self.change_signers, - }; - - self.public_descriptor(keychain).extract_policy( - signers, - BuildSatisfaction::None, - &self.secp, - ) - } - /// Returns the descriptor used to create addresses for a particular `keychain`. /// /// It's the "public" version of the wallet's descriptor, meaning a new descriptor that has diff --git a/src/wallet/params.rs b/src/wallet/params.rs index 5e6385de..e2f1c9a7 100644 --- a/src/wallet/params.rs +++ b/src/wallet/params.rs @@ -60,9 +60,7 @@ where #[must_use] pub struct CreateParams { pub(crate) descriptor: DescriptorToExtract, - pub(crate) descriptor_keymap: KeyMap, pub(crate) change_descriptor: Option, - pub(crate) change_descriptor_keymap: KeyMap, pub(crate) network: Network, pub(crate) genesis_hash: Option, pub(crate) lookahead: u32, @@ -83,9 +81,7 @@ impl CreateParams { pub fn new_single(descriptor: D) -> Self { Self { descriptor: make_descriptor_to_extract(descriptor), - descriptor_keymap: KeyMap::default(), change_descriptor: None, - change_descriptor_keymap: KeyMap::default(), network: Network::Bitcoin, genesis_hash: None, lookahead: DEFAULT_LOOKAHEAD, @@ -105,9 +101,7 @@ impl CreateParams { ) -> Self { Self { descriptor: make_descriptor_to_extract(descriptor), - descriptor_keymap: KeyMap::default(), change_descriptor: Some(make_descriptor_to_extract(change_descriptor)), - change_descriptor_keymap: KeyMap::default(), network: Network::Bitcoin, genesis_hash: None, lookahead: DEFAULT_LOOKAHEAD, @@ -130,9 +124,7 @@ impl CreateParams { ) -> Self { Self { descriptor: make_two_path_descriptor_to_extract(two_path_descriptor.clone(), 0), - descriptor_keymap: KeyMap::default(), change_descriptor: Some(make_two_path_descriptor_to_extract(two_path_descriptor, 1)), - change_descriptor_keymap: KeyMap::default(), network: Network::Bitcoin, genesis_hash: None, lookahead: DEFAULT_LOOKAHEAD, @@ -140,16 +132,6 @@ impl CreateParams { } } - /// Extend the given `keychain`'s `keymap`. - pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self { - match keychain { - KeychainKind::External => &mut self.descriptor_keymap, - KeychainKind::Internal => &mut self.change_descriptor_keymap, - } - .extend(keymap); - self - } - /// Set [`Self::network`]. pub fn network(mut self, network: Network) -> Self { self.network = network; @@ -213,14 +195,11 @@ impl CreateParams { /// Parameters for [`Wallet::load`] or [`PersistedWallet::load`]. #[must_use] pub struct LoadParams { - pub(crate) descriptor_keymap: KeyMap, - pub(crate) change_descriptor_keymap: KeyMap, pub(crate) lookahead: u32, pub(crate) check_network: Option, pub(crate) check_genesis_hash: Option, pub(crate) check_descriptor: Option>, pub(crate) check_change_descriptor: Option>, - pub(crate) extract_keys: bool, pub(crate) use_spk_cache: bool, } @@ -230,34 +209,16 @@ impl LoadParams { /// Default values: `lookahead` = [`DEFAULT_LOOKAHEAD`] pub fn new() -> Self { Self { - descriptor_keymap: KeyMap::default(), - change_descriptor_keymap: KeyMap::default(), lookahead: DEFAULT_LOOKAHEAD, check_network: None, check_genesis_hash: None, check_descriptor: None, check_change_descriptor: None, - extract_keys: false, use_spk_cache: false, } } - /// Extend the given `keychain`'s `keymap`. - pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self { - match keychain { - KeychainKind::External => &mut self.descriptor_keymap, - KeychainKind::Internal => &mut self.change_descriptor_keymap, - } - .extend(keymap); - self - } - /// 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, @@ -315,13 +276,6 @@ impl LoadParams { self } - /// Whether to try extracting private keys from the *provided descriptors* upon loading. - /// See also [`LoadParams::descriptor`]. - pub fn extract_keys(mut self) -> Self { - self.extract_keys = true; - self - } - /// Use a persistent cache of indexed script pubkeys (SPKs). /// /// NOTE: This should only be used if you have previously persisted a cache of script diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index db944718..71b4554a 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). +//! +//! The `Wallet` no longer holds key material. Prefer signing PSBTs with +//! [`bitcoin::Psbt::sign`] directly; reach for the containers here when you need the extra +//! control that [`SignOptions`] provides, or when plugging in your own [`TransactionSigner`]. //! //! ``` //! # use alloc::sync::Arc; @@ -65,17 +69,18 @@ //! } //! } //! -//! 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/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 6a5e1eea..9ed110e1 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -36,7 +36,7 @@ //! # Ok::<(), anyhow::Error>(()) //! ``` -use alloc::{boxed::Box, string::String, vec::Vec}; +use alloc::{boxed::Box, vec::Vec}; use core::fmt; use alloc::sync::Arc; @@ -52,7 +52,8 @@ use rand_core::RngCore; use super::coin_selection::CoinSelectionAlgorithm; use super::utils::shuffle_slice; use super::{CreateTxError, Wallet}; -use crate::collections::{BTreeMap, HashMap, HashSet}; +use crate::collections::{HashMap, HashSet}; +use crate::descriptor::Condition; use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; /// A transaction builder @@ -124,8 +125,7 @@ pub(crate) struct TxParams { pub(crate) drain_wallet: bool, pub(crate) drain_to: Option, pub(crate) fee_policy: Option, - pub(crate) internal_policy_path: Option>>, - pub(crate) external_policy_path: Option>>, + pub(crate) condition: Option, pub(crate) utxos: Vec, pub(crate) unspendable: HashSet, pub(crate) manually_selected_only: bool, @@ -191,80 +191,64 @@ impl<'a, Cs> TxBuilder<'a, Cs> { self } - /// Set the policy path to use while creating the transaction for a given keychain. + /// Set the CSV / CLTV requirements for the intended spending 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 - /// [`SatisfiableItem::Thresh::items`](crate::descriptor::policy::SatisfiableItem::Thresh::items)). + /// This is needed when different ways of satisfying the descriptor imply different `nSequence` + /// or `nLockTime` values. Derive a [`Condition`] before building the transaction from + /// caller-owned policy information: extract the spending policy with + /// [`ExtractPolicy::extract_policy`](crate::descriptor::ExtractPolicy::extract_policy), pick a + /// path, and call [`Policy::get_condition`](crate::descriptor::Policy::get_condition). You can + /// also construct one directly when the CSV / CLTV values are already known. /// - /// ## Example - /// - /// An example of when the policy path is needed is the following descriptor: - /// `wsh(thresh(2,pk(A),sj:and_v(v:pk(B),n:older(6)),snj:and_v(v:pk(C),after(630000))))`, - /// derived from the miniscript policy - /// `thresh(2,pk(A),and(pk(B),older(6)),and(pk(C),after(630000)))`. It declares three - /// descriptor fragments, and at the top level it uses `thresh()` to ensure that at least - /// two of them are satisfied. The individual fragments are: - /// - /// 1. `pk(A)` - /// 2. `and(pk(B),older(6))` - /// 3. `and(pk(C),after(630000))` - /// - /// When those conditions are combined in pairs, it's clear that the transaction needs to be - /// created differently depending on how the user intends to satisfy the policy afterwards: - /// - /// * If fragments `1` and `2` are used, the transaction will need to use a specific - /// `n_sequence` in order to spend an `OP_CSV` branch. - /// * If fragments `1` and `3` are used, the transaction will need to use a specific `locktime` - /// in order to spend an `OP_CLTV` branch. - /// * If fragments `2` and `3` are used, the transaction will need both. + /// For wallets with separate external and internal descriptors, merge the conditions for every + /// keychain from which inputs may be selected using [`Condition::merge`]. /// - /// When the spending policy is represented as a tree (see - /// [`Wallet::policies`](super::Wallet::policies)), every node - /// is assigned a unique identifier that can be used in the policy path to specify which of - /// the node's children the user intends to satisfy: for instance, assuming the `thresh()` - /// root node of this example has an id of `aabbccdd`, the policy path map would look like: + /// Without a condition, building a transaction for a descriptor that requires an explicit + /// spending path fails with [`CreateTxError::SpendingPolicyRequired`]. /// - /// `{ "aabbccdd" => [0, 1] }` - /// - /// where the key is the node's id, and the value is a list of the children that should be - /// used, in no particular order. - /// - /// If a particularly complex descriptor has multiple ambiguous thresholds in its structure, - /// multiple entries can be added to the map, one for each node that requires an explicit path. + /// ## Example /// /// ``` - /// # use std::str::FromStr; /// # use std::collections::BTreeMap; - /// # use bitcoin::*; /// # use bdk_wallet::*; - /// # let to_address = - /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") - /// .unwrap() - /// .assume_checked(); - /// # let mut wallet = doctest_wallet!(); + /// # 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("aabbccdd".to_string(), vec![0, 1]); - /// - /// let builder = wallet - /// .build_tx() - /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)) - /// .policy_path(path, KeychainKind::External); + /// path.insert(policy.id.clone(), vec![0, 1]); + /// let condition = policy.get_condition(&path)?; + /// assert_eq!(condition.csv, Some(Sequence(6))); /// + /// // Inputs of the resulting transaction will carry nSequence = 6. + /// let mut builder = wallet.build_tx(); + /// builder.set_condition(condition); /// # Ok::<(), anyhow::Error>(()) /// ``` - pub fn policy_path( - &mut self, - policy_path: BTreeMap>, - keychain: KeychainKind, - ) -> &mut Self { - let to_update = match keychain { - KeychainKind::Internal => &mut self.params.internal_policy_path, - KeychainKind::External => &mut self.params.external_policy_path, - }; - - *to_update = Some(policy_path); + pub fn set_condition(&mut self, condition: Condition) -> &mut Self { + self.params.condition = Some(condition); self } diff --git a/tests/add_foreign_utxo.rs b/tests/add_foreign_utxo.rs index dd4cf771..d6876646 100644 --- a/tests/add_foreign_utxo.rs +++ b/tests/add_foreign_utxo.rs @@ -8,12 +8,18 @@ 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 signers1 = signers_from_descriptor(&wallet1, get_test_wpkh_and_change_desc().0); + let signers2 = signers_from_descriptor( + &wallet2, + "wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)", + ); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") .unwrap() @@ -56,8 +62,9 @@ fn test_add_foreign_utxo() { ); let finished = wallet1 - .sign( + .sign_with_signers( &mut psbt, + &[&signers1], SignOptions { trust_witness_utxo: true, ..Default::default() @@ -71,8 +78,9 @@ fn test_add_foreign_utxo() { ); let finished = wallet2 - .sign( + .sign_with_signers( &mut psbt, + &[&signers2], SignOptions { trust_witness_utxo: true, ..Default::default() diff --git a/tests/common.rs b/tests/common.rs index 37795f05..4b16dfbd 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,8 +1,33 @@ #![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}; +/// Build a caller-owned [`SignersContainer`] from a signing descriptor. +/// +/// The `Wallet` no longer holds key material, so tests that need to sign construct their own +/// container from the descriptor that carries the secrets. +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()) +} + +/// Extract just the [`KeyMap`] from a signing descriptor, for use with [`bitcoin::Psbt::sign`]. +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 +} + /// The satisfaction size of P2WPKH is 108 WU = /// 1 (elements in witness) + 1 (size) + 72 (signature + sighash) + 1 (size) + 33 (pubkey). pub const P2WPKH_FAKE_PK_SIZE: usize = 33; diff --git a/tests/persisted_wallet.rs b/tests/persisted_wallet.rs index aa0e8ad8..c736404f 100644 --- a/tests/persisted_wallet.rs +++ b/tests/persisted_wallet.rs @@ -28,7 +28,6 @@ use bdk_wallet::persist_test_utils::{ }; mod common; -use common::*; const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48]; @@ -292,22 +291,6 @@ fn wallet_load_checks() -> anyhow::Result<()> { ))), "unexpected descriptors check result", ); - // check setting keymaps - let (_, external_keymap) = parse_descriptor(external_desc); - let (_, internal_keymap) = parse_descriptor(internal_desc); - let wallet = Wallet::load() - .keymap(KeychainKind::External, external_keymap) - .keymap(KeychainKind::Internal, internal_keymap) - .load_wallet(&mut open_db(&file_path)?) - .expect("db should not fail") - .expect("wallet was persisted"); - for keychain in [KeychainKind::External, KeychainKind::Internal] { - let keymap = wallet.get_signers(keychain).as_key_map(wallet.secp_ctx()); - assert!( - !keymap.is_empty(), - "load should populate keymap for keychain {keychain:?}" - ); - } Ok(()) } @@ -363,7 +346,6 @@ fn wallet_should_persist_anchors_and_recover() { 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"); @@ -405,23 +387,16 @@ fn single_descriptor_wallet_persist_and_recover() { 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 7ccd7272..a736af2f 100644 --- a/tests/psbt.rs +++ b/tests/psbt.rs @@ -4,6 +4,9 @@ use bdk_wallet::{KeychainKind, SignOptions, psbt}; use bitcoin::{Amount, FeeRate, Psbt, TxIn}; use core::str::FromStr; +mod common; +use common::signers_from_descriptor; + // from bip 174 const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA"; @@ -12,6 +15,7 @@ const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6 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 signers = signers_from_descriptor(&wallet, get_test_wpkh()); let send_to = wallet.peek_address(KeychainKind::External, 0); let mut builder = wallet.build_tx(); builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000)); @@ -21,7 +25,9 @@ fn test_psbt_malformed_psbt_input_legacy() { trust_witness_utxo: true, ..Default::default() }; - let _ = wallet.sign(&mut psbt, options).unwrap(); + let _ = wallet + .sign_with_signers(&mut psbt, &[&signers], options) + .unwrap(); } #[test] @@ -29,6 +35,7 @@ fn test_psbt_malformed_psbt_input_legacy() { 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 signers = signers_from_descriptor(&wallet, get_test_wpkh()); let send_to = wallet.peek_address(KeychainKind::External, 0); let mut builder = wallet.build_tx(); builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000)); @@ -38,13 +45,16 @@ fn test_psbt_malformed_psbt_input_segwit() { trust_witness_utxo: true, ..Default::default() }; - let _ = wallet.sign(&mut psbt, options).unwrap(); + 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 signers = signers_from_descriptor(&wallet, get_test_wpkh()); let send_to = wallet.peek_address(KeychainKind::External, 0); let mut builder = wallet.build_tx(); builder.add_recipient(send_to.script_pubkey(), Amount::from_sat(10_000)); @@ -54,7 +64,9 @@ fn test_psbt_malformed_tx_input() { trust_witness_utxo: true, ..Default::default() }; - let _ = wallet.sign(&mut psbt, options).unwrap(); + let _ = wallet + .sign_with_signers(&mut psbt, &[&signers], options) + .unwrap(); } #[test] @@ -72,7 +84,10 @@ fn test_psbt_sign_with_finalized() { .input .push(psbt_bip.unsigned_tx.input[0].clone()); - let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap(); + let signers = signers_from_descriptor(&wallet, get_test_wpkh()); + let _ = wallet + .sign_with_signers(&mut psbt, &[&signers], SignOptions::default()) + .unwrap(); } #[test] @@ -84,6 +99,10 @@ fn test_psbt_fee_rate_with_witness_utxo() { let (mut wallet, _) = get_funded_wallet_single( "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", ); + let signers = signers_from_descriptor( + &wallet, + "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", + ); let addr = wallet.peek_address(KeychainKind::External, 0); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); @@ -94,7 +113,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 finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(); assert!(finalized); let finalized_fee_rate = psbt.fee_rate().unwrap(); @@ -111,6 +132,10 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() { let (mut wallet, _) = get_funded_wallet_single( "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", ); + let signers = signers_from_descriptor( + &wallet, + "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", + ); let addr = wallet.peek_address(KeychainKind::External, 0); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); @@ -120,7 +145,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 finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(); assert!(finalized); let finalized_fee_rate = psbt.fee_rate().unwrap(); @@ -165,7 +192,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::{SignerCommon, SignerContext, SignerOrdering, SignerWrapper}; use bitcoin::key::TapTweak; use bitcoin::secp256k1::{Keypair, Message, Secp256k1, XOnlyPublicKey, schnorr}; use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType}; @@ -180,6 +207,7 @@ fn test_psbt_multiple_internalkey_signers() { let change_desc = "tr(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; let (mut wallet, _) = get_funded_wallet(&desc, change_desc); + let mut signers = signers_from_descriptor(&wallet, &desc); let to_spend = wallet.balance().total(); let send_to = wallet.peek_address(KeychainKind::External, 0); let mut builder = wallet.build_tx(); @@ -188,18 +216,21 @@ fn test_psbt_multiple_internalkey_signers() { let unsigned_tx = psbt.unsigned_tx.clone(); // Adds a signer for the wrong internal key, bdk should not use this key to sign - wallet.add_signer( - KeychainKind::External, - // A signerordering lower than 100, bdk will use this signer first + let wrong_signer = 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 diff --git a/tests/wallet.rs b/tests/wallet.rs index a27d21d8..40eb4352 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -6,7 +6,8 @@ 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::policy::BuildSatisfaction; +use bdk_wallet::descriptor::{DescriptorError, ExtractPolicy, IntoWalletDescriptor, calc_checksum}; use bdk_wallet::error::CreateTxError; use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::signer::{SignOptions, SignerError, SignersContainer}; @@ -28,6 +29,7 @@ use rand::SeedableRng; use rand::rngs::StdRng; mod common; +use common::signers_from_descriptor; #[test] fn test_error_external_and_internal_are_the_same() { @@ -284,7 +286,10 @@ 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 signers = signers_from_descriptor(&wallet, get_test_single_sig_cltv_timestamp()); + let finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], SignOptions::default()) + .unwrap(); assert!(finalized); } @@ -1087,8 +1092,16 @@ fn test_create_tx_policy_path_no_csv() { }; insert_tx(&mut wallet, tx); - let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); - let root_id = external_policy.id; + let external_policy = wallet + .public_descriptor(KeychainKind::External) + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::None, + wallet.secp_ctx(), + ) + .unwrap() + .unwrap(); + let root_id = external_policy.id.clone(); // child #0 is just the key "A" let path = vec![(root_id, vec![0])].into_iter().collect(); @@ -1098,7 +1111,7 @@ fn test_create_tx_policy_path_no_csv() { let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) - .policy_path(path, KeychainKind::External); + .set_condition(external_policy.get_condition(&path).unwrap()); let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFD)); @@ -1108,8 +1121,16 @@ fn test_create_tx_policy_path_no_csv() { fn test_create_tx_policy_path_use_csv() { let (mut wallet, _) = get_funded_wallet_single(get_test_a_or_b_plus_csv()); - let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); - let root_id = external_policy.id; + let external_policy = wallet + .public_descriptor(KeychainKind::External) + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::None, + wallet.secp_ctx(), + ) + .unwrap() + .unwrap(); + let root_id = external_policy.id.clone(); // child #1 is or(pk(B),older(144)) let path = vec![(root_id, vec![1])].into_iter().collect(); @@ -1119,7 +1140,7 @@ fn test_create_tx_policy_path_use_csv() { let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) - .policy_path(path, KeychainKind::External); + .set_condition(external_policy.get_condition(&path).unwrap()); let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144)); @@ -1131,8 +1152,16 @@ fn test_create_tx_policy_path_ignored_subtree_with_csv() { "wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),or_i(and_v(v:pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(30)),and_v(v:pkh(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(90)))))", ); - let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); - let root_id = external_policy.id; + let external_policy = wallet + .public_descriptor(KeychainKind::External) + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::None, + wallet.secp_ctx(), + ) + .unwrap() + .unwrap(); + let root_id = external_policy.id.clone(); // child #0 is pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu) let path = vec![(root_id, vec![0])].into_iter().collect(); @@ -1142,7 +1171,7 @@ fn test_create_tx_policy_path_ignored_subtree_with_csv() { let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) - .policy_path(path, KeychainKind::External); + .set_condition(external_policy.get_condition(&path).unwrap()); let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFD)); @@ -1379,7 +1408,13 @@ fn test_sign_single_xprv() { 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, + "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", + ); + let finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1448,7 +1483,13 @@ fn test_sign_single_xprv_with_master_fingerprint_and_path() { 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, + "wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", + ); + let finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1465,7 +1506,13 @@ fn test_sign_single_xprv_bip44_path() { 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, + "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)", + ); + let finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1482,7 +1529,13 @@ fn test_sign_single_xprv_sh_wpkh() { 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, + "sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))", + ); + let finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1498,7 +1551,13 @@ fn test_sign_single_wif() { 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, + "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)", + ); + let finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1518,7 +1577,13 @@ 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, + "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", + ); + let finalized = wallet + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(); assert!(finalized); let extracted = psbt.extract_tx().expect("failed to extract tx"); @@ -1579,9 +1644,11 @@ fn test_signing_only_one_of_multiple_inputs() { psbt.inputs.push(dud_input); psbt.unsigned_tx.input.push(bitcoin::TxIn::default()); + let signers = signers_from_descriptor(&wallet, get_test_wpkh_and_change_desc().0); let is_final = wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { trust_witness_utxo: true, ..Default::default() @@ -1609,10 +1676,15 @@ fn test_try_finalize_sign_option() { let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); let mut psbt = builder.finish().unwrap(); + let signers = signers_from_descriptor( + &wallet, + "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", + ); let finalized = wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: *try_finalize, ..Default::default() @@ -1642,10 +1714,12 @@ fn test_try_finalize_psbt_outcomes() { let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); let mut psbt = builder.finish().unwrap(); + let signers = signers_from_descriptor(&wallet, get_test_wpkh()); let is_final = wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, ..Default::default() @@ -1738,10 +1812,12 @@ fn test_try_finalize_psbt_preserves_opaque_input_fields() { let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); let mut psbt = builder.finish().unwrap(); + let signers = signers_from_descriptor(&wallet, get_test_wpkh()); wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, ..Default::default() @@ -1803,10 +1879,12 @@ fn test_try_finalize_psbt_returns_index_out_of_bounds_for_malformed_psbt() { let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); let mut psbt = builder.finish().unwrap(); + let signers = signers_from_descriptor(&wallet, get_test_wpkh()); wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, ..Default::default() @@ -1836,10 +1914,12 @@ fn test_try_finalize_psbt_uses_psbt_timelocks() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); let mut psbt = builder.finish().unwrap(); + let signers = signers_from_descriptor(&wallet, get_test_single_sig_cltv()); wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, ..Default::default() @@ -1870,14 +1950,16 @@ fn test_try_finalize_psbt_uses_psbt_timelocks() { { let (mut wallet, _) = get_funded_wallet_single(get_test_single_sig_csv()); + let signers = signers_from_descriptor(&wallet, get_test_single_sig_csv()); 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() @@ -1916,10 +1998,12 @@ fn test_taproot_try_finalize_sign_option() { let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); let mut psbt = builder.finish().unwrap(); + let signers = signers_from_descriptor(&wallet, get_test_tr_with_taptree()); let finalized = wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: *try_finalize, ..Default::default() @@ -1971,7 +2055,11 @@ fn test_sign_nonstandard_sighash() { .drain_wallet(); let mut psbt = builder.finish().unwrap(); - let result = wallet.sign(&mut psbt, Default::default()); + let signers = signers_from_descriptor( + &wallet, + "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", + ); + 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 +2071,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() @@ -2341,7 +2430,16 @@ fn test_taproot_psbt_populate_tap_key_origins_repeated_key() { let (mut wallet, _) = get_funded_wallet(get_test_tr_repeated_key(), get_test_tr_single_sig()); let addr = wallet.reveal_next_address(KeychainKind::External); - let path = vec![("rn4nre9c".to_string(), vec![0])] + let external_policy = wallet + .public_descriptor(KeychainKind::External) + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::None, + wallet.secp_ctx(), + ) + .unwrap() + .unwrap(); + let path = vec![(external_policy.id.clone(), vec![0])] .into_iter() .collect(); @@ -2349,7 +2447,7 @@ fn test_taproot_psbt_populate_tap_key_origins_repeated_key() { builder .drain_to(addr.script_pubkey()) .drain_wallet() - .policy_path(path, KeychainKind::External); + .set_condition(external_policy.get_condition(&path).unwrap()); let psbt = builder.finish().unwrap(); let mut input_key_origins = psbt.inputs[0] @@ -2455,8 +2553,10 @@ fn test_taproot_sign_missing_witness_utxo() { let mut psbt = builder.finish().unwrap(); let witness_utxo = psbt.inputs[0].witness_utxo.take(); - let result = wallet.sign( + let signers = signers_from_descriptor(&wallet, get_test_tr_single_sig()); + let result = wallet.sign_with_signers( &mut psbt, + &[&signers], SignOptions { allow_all_sighashes: true, ..Default::default() @@ -2471,8 +2571,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() @@ -2502,7 +2603,8 @@ fn test_taproot_sign_using_non_witness_utxo() { "Previous tx should be present in the database" ); - let result = wallet.sign(&mut psbt, Default::default()); + let signers = signers_from_descriptor(&wallet, get_test_tr_single_sig()); + let result = wallet.sign_with_signers(&mut psbt, &[&signers], Default::default()); assert!(result.is_ok(), "Signing should have worked"); assert!( result.unwrap(), @@ -2510,7 +2612,8 @@ fn test_taproot_sign_using_non_witness_utxo() { ); } -fn test_spend_from_wallet(mut wallet: Wallet) { +fn test_spend_from_wallet(mut wallet: Wallet, descriptor: impl IntoWalletDescriptor) { + let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); @@ -2519,7 +2622,9 @@ fn test_spend_from_wallet(mut wallet: Wallet) { assert_eq!(psbt.unsigned_tx.version.0, 2); assert!( - wallet.sign(&mut psbt, Default::default()).unwrap(), + wallet + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(), "Unable to finalize tx" ); } @@ -2541,11 +2646,13 @@ fn test_taproot_no_key_spend() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); let mut psbt = builder.finish().unwrap(); + let signers = signers_from_descriptor(&wallet, get_test_tr_with_taptree_both_priv()); assert!( wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { sign_with_tap_internal_key: false, ..Default::default() @@ -2561,10 +2668,10 @@ 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); + test_spend_from_wallet(wallet, get_test_tr_with_taptree()); let (wallet, _) = get_funded_wallet_single(get_test_tr_with_taptree_xprv()); - test_spend_from_wallet(wallet); + test_spend_from_wallet(wallet, get_test_tr_with_taptree_xprv()); } #[test] @@ -2576,11 +2683,13 @@ fn test_taproot_script_spend_sign_all_leaves() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); let mut psbt = builder.finish().unwrap(); + let signers = signers_from_descriptor(&wallet, get_test_tr_with_taptree_both_priv()); assert!( wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { tap_leaves_options: TapLeavesOptions::All, ..Default::default() @@ -2616,11 +2725,13 @@ fn test_taproot_script_spend_sign_include_some_leaves() { .collect(); let included_script_leaves = vec![script_leaves.pop().unwrap()]; let excluded_script_leaves = script_leaves; + let signers = signers_from_descriptor(&wallet, get_test_tr_with_taptree_both_priv()); assert!( wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { tap_leaves_options: TapLeavesOptions::Include(included_script_leaves.clone()), ..Default::default() @@ -2654,11 +2765,13 @@ fn test_taproot_script_spend_sign_exclude_some_leaves() { .collect(); let included_script_leaves = [script_leaves.pop().unwrap()]; let excluded_script_leaves = script_leaves; + let signers = signers_from_descriptor(&wallet, get_test_tr_with_taptree_both_priv()); assert!( wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { tap_leaves_options: TapLeavesOptions::Exclude(excluded_script_leaves.clone()), ..Default::default() @@ -2682,10 +2795,12 @@ fn test_taproot_script_spend_sign_no_leaves() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); let mut psbt = builder.finish().unwrap(); + let signers = signers_from_descriptor(&wallet, get_test_tr_with_taptree_both_priv()); wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { tap_leaves_options: TapLeavesOptions::None, ..Default::default() @@ -2714,8 +2829,11 @@ fn test_taproot_sign_derive_index_from_psbt() { // signing with an empty db means that we will only look at the psbt to infer the // derivation index + let signers = signers_from_descriptor(&wallet_empty, get_test_tr_single_sig_xprv()); assert!( - wallet_empty.sign(&mut psbt, Default::default()).unwrap(), + wallet_empty + .sign_with_signers(&mut psbt, &[&signers], Default::default()) + .unwrap(), "Unable to finalize tx" ); } @@ -2731,7 +2849,8 @@ fn test_taproot_sign_explicit_sighash_all() { .drain_wallet(); let mut psbt = builder.finish().unwrap(); - let result = wallet.sign(&mut psbt, Default::default()); + let signers = signers_from_descriptor(&wallet, get_test_tr_single_sig()); + let result = wallet.sign_with_signers(&mut psbt, &[&signers], Default::default()); assert!( result.is_ok(), "Signing should work because SIGHASH_ALL is safe" @@ -2753,7 +2872,8 @@ 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 signers = signers_from_descriptor(&wallet, get_test_tr_single_sig()); + 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 +2885,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 +2906,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() @@ -2966,6 +3088,10 @@ fn test_fee_rate_sign_no_grinding_high_r() { let (mut wallet, _) = get_funded_wallet_single( "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", ); + let signers = signers_from_descriptor( + &wallet, + "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", + ); let addr = wallet.next_unused_address(KeychainKind::External); let fee_rate = FeeRate::from_sat_per_vb_u32(1); let mut builder = wallet.build_tx(); @@ -2994,10 +3120,15 @@ fn test_fee_rate_sign_no_grinding_high_r() { psbt.unsigned_tx.output[op_return_vout].script_pubkey = ScriptBuf::new_op_return(&data); // Clearing the previous signature psbt.inputs[0].partial_sigs.clear(); + let signers = signers_from_descriptor( + &wallet, + "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", + ); // Signing wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, allow_grinding: false, @@ -3014,8 +3145,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() @@ -3044,10 +3176,15 @@ fn test_fee_rate_sign_grinding_low_r() { .fee_rate(fee_rate); let mut psbt = builder.finish().unwrap(); let fee = check_fee!(wallet, psbt); + let signers = signers_from_descriptor( + &wallet, + "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)", + ); wallet - .sign( + .sign_with_signers( &mut psbt, + &[&signers], SignOptions { try_finalize: false, allow_grinding: true, @@ -3143,7 +3280,12 @@ 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 signers = signers_from_descriptor(&wallet, get_test_tr_single_sig_xprv()); + assert!( + wallet + .sign_with_signers(&mut psbt, &[&signers], SignOptions::default()) + .unwrap() + ); let tx = psbt.extract_tx().unwrap(); let _txid = tx.compute_txid(); insert_tx(&mut wallet, tx); @@ -3412,7 +3554,13 @@ 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 signers = signers_from_descriptor(&wallet, descriptor); + let change_signers = signers_from_descriptor(&wallet, change_descriptor); + let _ = wallet.sign_with_signers( + &mut psbt, + &[&signers, &change_signers], + SignOptions::default(), + )?; let tx_a = psbt.extract_tx()?; let txid_a = tx_a.compute_txid(); @@ -3436,7 +3584,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, + &[&signers, &change_signers], + SignOptions::default(), + ); let tx_b = psbt.extract_tx()?; // txB MUST NOT use the available unconfirmed TRUC UTXO. @@ -3467,7 +3619,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, + &[&signers, &change_signers], + SignOptions::default(), + )?; let tx_c = psbt.extract_tx()?; // txC MUST ONLY use the available confirmed UTXOs AND/OR unconfirmed TRUC UTXOs.