From 0fcf029742cbe279f0f10db75c6a81a583280c24 Mon Sep 17 00:00:00 2001 From: Rishit Modi Date: Fri, 10 Apr 2026 22:58:38 +0530 Subject: [PATCH 1/3] feat(wallet): add Wallet::sign_psbt wrapper for Psbt::sign Add a thin wrapper method `sign_psbt` that delegates to `bitcoin::Psbt::sign`, passing the wallet's internal secp256k1 context. This allows users to sign PSBTs with their own `GetKey` implementor (e.g., `Xpriv` or hardware wallet adapters) without managing a separate secp256k1 context. docs(wallet): document Wallet::sign_psbt usage and requirement --- src/wallet/mod.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 60d1ccef..5b699be3 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1853,6 +1853,38 @@ impl Wallet { } } + /// Sign a PSBT using an external key provider via [`bitcoin::Psbt::sign`]. + /// + /// This is a thin wrapper around [`bitcoin::Psbt::sign`] that supplies the wallet's + /// internal [`secp256k1`] context. It lets callers sign with any type that implements + /// [`psbt::GetKey`], such as [`bitcoin::bip32::Xpriv`], without having to manage their + /// own secp256k1 context. + /// + /// # BIP32 derivation metadata + /// + /// [`bitcoin::Psbt::sign`] uses the `bip32_derivation` fields (for legacy and segwit inputs) + /// or the `tap_key_origins` fields (for taproot inputs) in each PSBT input to locate + /// the correct child keys. PSBTs received from external coordinator tools or + /// hardware wallet flows typically carry this metadata already. + /// + /// # Returns + /// + /// On success, a [`psbt::SigningKeysMap`] mapping each signed input index to the keys + /// that were used. On failure, a tuple of the partial success map and a + /// [`psbt::SigningErrors`] map of per-input errors. + /// + /// [`secp256k1`]: bitcoin::secp256k1 + pub fn sign_psbt( + &self, + psbt: &mut Psbt, + key: &K, + ) -> Result + where + K: psbt::GetKey, + { + psbt.sign(key, &self.secp) + } + /// Return the spending policies for the wallet's descriptor. pub fn policies(&self, keychain: KeychainKind) -> Result, DescriptorError> { let signers = match keychain { From d266142fce5052737b0586ed5f1d0354246ceb98 Mon Sep 17 00:00:00 2001 From: Rishit Modi Date: Fri, 10 Apr 2026 23:22:14 +0530 Subject: [PATCH 2/3] test(wallet): add tests for Wallet::sign_psbt Add tests covering signing with a matching and non-matching `Xpriv`. - `test_sign_psbt_with_xpriv`: verifies that a matching key signs at least one input - `test_sign_psbt_with_wrong_key_signs_nothing`: verifies that an unrelated key returns Ok but produces no signatures --- tests/psbt.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/psbt.rs b/tests/psbt.rs index 08c4acc9..15c39fbf 100644 --- a/tests/psbt.rs +++ b/tests/psbt.rs @@ -1,3 +1,5 @@ +use bdk_wallet::bitcoin::bip32::Xpriv; +use bdk_wallet::bitcoin::psbt::SigningKeys; use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, TxIn}; use bdk_wallet::test_utils::*; use bdk_wallet::{psbt, KeychainKind, SignOptions}; @@ -221,3 +223,62 @@ 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_sign_psbt_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 = wallet + .sign_psbt(&mut psbt, &xpriv) + .expect("sign_psbt should succeed with the correct xpriv"); + + assert!( + !signing_keys.is_empty(), + "expected at least one input to be signed" + ); +} + +#[test] +fn test_sign_psbt_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 = wallet + .sign_psbt(&mut psbt, &wrong_xpriv) + .expect("sign_psbt 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" + ); + } +} From 43702e76bb832a5516363dedf90f11b3964c4d7c Mon Sep 17 00:00:00 2001 From: Rishit Modi Date: Sun, 21 Jun 2026 02:43:25 +0530 Subject: [PATCH 3/3] feat(wallet): add get_keymap() helper, sign_psbt doc example and test --- src/wallet/mod.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++ tests/psbt.rs | 20 +++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 5b699be3..8992d1f6 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1853,6 +1853,17 @@ impl Wallet { } } + /// Returns a combined [`KeyMap`] from all signing keys loaded in the wallet. + /// + /// The map merges keys from both the external (receive) and internal (change) keychains. + /// It can be used with [`miniscript::descriptor::KeyMapWrapper`] to sign a PSBT via + /// [`Wallet::sign_psbt`] using the wallet's own keys. + pub fn get_keymap(&self) -> KeyMap { + let mut keymap = self.signers.as_key_map(&self.secp); + keymap.extend(self.change_signers.as_key_map(&self.secp)); + keymap + } + /// Sign a PSBT using an external key provider via [`bitcoin::Psbt::sign`]. /// /// This is a thin wrapper around [`bitcoin::Psbt::sign`] that supplies the wallet's @@ -1867,6 +1878,23 @@ impl Wallet { /// the correct child keys. PSBTs received from external coordinator tools or /// hardware wallet flows typically carry this metadata already. /// + /// # Signing with the wallet's own keys + /// + /// Use [`Wallet::get_keymap`] together with [`miniscript::descriptor::KeyMapWrapper`] to sign + /// with the keys already loaded in the wallet: + /// + /// ```rust,no_run + /// # use bdk_wallet::Wallet; + /// # use bdk_wallet::bitcoin::Psbt; + /// # use miniscript::descriptor::KeyMapWrapper; + /// # fn example(wallet: &Wallet, psbt: &mut Psbt) -> Result<(), Box> { + /// let keymap = wallet.get_keymap(); + /// let wrapper = KeyMapWrapper::from(keymap); + /// wallet.sign_psbt(psbt, &wrapper).map_err(|(_, e)| format!("{e:?}"))?; + /// # Ok(()) + /// # } + /// ``` + /// /// # Returns /// /// On success, a [`psbt::SigningKeysMap`] mapping each signed input index to the keys @@ -3185,4 +3213,33 @@ mod test { let wallet_name = result_with_change.unwrap(); assert_eq!(wallet_name, "vn4aqs37jgrerlc3"); } + + #[test] + fn test_get_keymap() { + use crate::test_utils::get_test_wpkh_and_change_desc; + let (external_desc, internal_desc) = get_test_wpkh_and_change_desc(); + let wallet = Wallet::create(external_desc, internal_desc) + .network(Network::Testnet) + .create_wallet_no_persist() + .unwrap(); + + let keymap = wallet.get_keymap(); + assert!(!keymap.is_empty()); + + let external_keymap = wallet + .get_signers(KeychainKind::External) + .as_key_map(wallet.secp_ctx()); + let internal_keymap = wallet + .get_signers(KeychainKind::Internal) + .as_key_map(wallet.secp_ctx()); + + assert_eq!(keymap.len(), external_keymap.len() + internal_keymap.len()); + + for (pubkey, _) in external_keymap.iter() { + assert!(keymap.contains_key(pubkey)); + } + for (pubkey, _) in internal_keymap.iter() { + assert!(keymap.contains_key(pubkey)); + } + } } diff --git a/tests/psbt.rs b/tests/psbt.rs index 15c39fbf..2eaee6f4 100644 --- a/tests/psbt.rs +++ b/tests/psbt.rs @@ -282,3 +282,23 @@ fn test_sign_psbt_with_wrong_key_signs_nothing() { ); } } + +#[test] +fn test_sign_psbt_with_wallet_keymap() { + use miniscript::descriptor::KeyMapWrapper; + 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 keymap = wallet.get_keymap(); + let wrapper = KeyMapWrapper::from(keymap); + + let signing_keys = wallet + .sign_psbt(&mut psbt, &wrapper) + .expect("sign_psbt should succeed with wallet keymap"); + + assert!( + !signing_keys.is_empty(), + "expected at least one input to be signed using wallet keymap" + ); +}