diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 60d1ccef..8992d1f6 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1853,6 +1853,66 @@ 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 + /// 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. + /// + /// # 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 + /// 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 { @@ -3153,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 08c4acc9..2eaee6f4 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,82 @@ fn test_psbt_multiple_internalkey_signers() { let verify_res = secp.verify_schnorr(&signature, &message, &xonlykey); assert!(verify_res.is_ok(), "The wrong internal key was used"); } + +// wpkh PSBT with bip32_derivation populated, derived from +// tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L +const WPKH_PSBT_WITH_DERIVATION: &str = "cHNidP8BAHECAAAAAbOlV/kRKdNVk6Wn2cay5JUvpFw4tEsKWylqu+HfPKDyAAAAAAD9////ArObAAAAAAAAFgAU2+Ijq+8PDcPUGgHQqOPg9+6n9h8QJwAAAAAAABYAFKENkldInmhd2gMGYjkNwXeFL68T0AcAAAABAHEBAAAAATCzgdcz18YZK+8oNpJzqjM8ErFYW3hJLi+bO4bjmQrRAAAAAAD/////AlDDAAAAAAAAFgAUoQ2SV0ieaF3aAwZiOQ3Bd4UvrxOoYQAAAAAAABYAFIgWLNSQrRaGsRyWtCHaqeauCPYsAAAAAAEBH1DDAAAAAAAAFgAUoQ2SV0ieaF3aAwZiOQ3Bd4UvrxMiBgLOtp4iMz+DVWxdHvunWgM0a/PVLPvTn8XSTe0DTvfZ9Bjic/5CVAAAgAEAAIAAAACAAAAAAAAAAAAAIgIDxWYngmqPgL3saGZ4NTgcy5W/XINU8lkqnqjKC+oJuRwY4nP+QlQAAIABAACAAAAAgAEAAAAAAAAAACICAs62niIzP4NVbF0e+6daAzRr89Us+9OfxdJN7QNO99n0GOJz/kJUAACAAQAAgAAAAIAAAAAAAAAAAAA="; + +#[test] +fn test_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" + ); + } +} + +#[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" + ); +}