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
31 changes: 31 additions & 0 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1802,6 +1802,37 @@ 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 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<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
61 changes: 61 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,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"
);
}
}