Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error>> {
/// 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
Comment on lines +1867 to +1904

@noahjoeris noahjoeris Jun 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: What do you think of those docs changes? I added that it's used for software signing and pointed to the correct approach for external signing.

Suggested change
/// 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
/// 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. Use it for software signing.
///
/// # 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 constructed from within the wallet, or 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
/// [`TransactionSigner`]: crate::signer::TransactionSigner

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should refer to TransactionSigner, as it'll be later removed with signers module.

The hardware signers should handle the PSBT and signing keys on their own, considering that we had archived https://github.com/bitcoindevkit/rust-hwi and it's considered unmaintained.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should refer to TransactionSigner, as it'll be later removed with signers module.

Anyway, we can add it now and remove it when removing the module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep thanks, was thinking about it too.
From my understanding we added sign_with_signers as a transition step to keep supporting the bdk signer for some more time while moving to psbt.sign.
Which means yeah agree we shouldn't refer to it.

pub fn sign_psbt<K>(
&self,
psbt: &mut Psbt,
key: &K,
) -> Result<psbt::SigningKeysMap, (psbt::SigningKeysMap, psbt::SigningErrors)>
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<Option<Policy>, DescriptorError> {
let signers = match keychain {
Expand Down Expand Up @@ -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));
}
}
}
81 changes: 81 additions & 0 deletions tests/psbt.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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"
);
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you could also add a test that uses the get_keymap and sign with Wallet keys here.


#[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"
);
}