Skip to content

Commit 069d2fd

Browse files
committed
Add XOnlyPublicKey support for PSBT key retrieval and improve Taproot signing
This commit enhances PSBT signing functionality by: 1. Added new KeyRequest::XOnlyPubkey variant to support direct retrieval using XOnly public keys 2. Implemented GetKey for HashMap<XOnlyPublicKey, PrivateKey> for more efficient Taproot key management 3. Modified HashMap<PublicKey, PrivateKey> implementation to handle XOnlyPublicKey requests by checking both even and odd parity variants These changes allow for more flexible key management in Taproot transactions. Specifically, wallet implementations can now store keys indexed by either PublicKey or XOnlyPublicKey and successfully sign PSBTs with Taproot inputs. Added tests for both implementations to verify correct behavior. Added test for odd parity key retrieval. Closes rust-bitcoin#4150
1 parent 6620a29 commit 069d2fd

File tree

2 files changed

+186
-9
lines changed

2 files changed

+186
-9
lines changed

bitcoin/src/crypto/key.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,20 @@ impl PrivateKey {
533533
inner: secp256k1::SecretKey::from_byte_array(key)?,
534534
})
535535
}
536+
537+
/// Returns a new private key with the negated secret value.
538+
///
539+
/// The resulting key corresponds to the same x-only public key (identical x-coordinate)
540+
/// but with the opposite y-coordinate parity. This is useful for ensuring compatibility
541+
/// with specific public key formats and BIP-340 requirements.
542+
#[inline]
543+
pub fn negate(&self) -> Self {
544+
PrivateKey {
545+
compressed: self.compressed,
546+
network: self.network,
547+
inner: self.inner.negate(),
548+
}
549+
}
536550
}
537551

538552
impl fmt::Display for PrivateKey {

bitcoin/src/psbt/mod.rs

Lines changed: 172 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,8 @@ impl Psbt {
427427
k.get_key(&KeyRequest::Bip32(key_source.clone()), secp)
428428
{
429429
secret_key
430+
} else if let Ok(Some(sk)) = k.get_key(&KeyRequest::XOnlyPubkey(xonly), secp) {
431+
sk
430432
} else {
431433
continue;
432434
};
@@ -737,6 +739,8 @@ pub enum KeyRequest {
737739
Pubkey(PublicKey),
738740
/// Request a private key using BIP-32 fingerprint and derivation path.
739741
Bip32(KeySource),
742+
/// Request a private key using the associated x-only public key.
743+
XOnlyPubkey(XOnlyPublicKey),
740744
}
741745

742746
/// Trait to get a private key from a key request, key is then used to sign an input.
@@ -768,6 +772,7 @@ impl GetKey for Xpriv {
768772
) -> Result<Option<PrivateKey>, Self::Error> {
769773
match key_request {
770774
KeyRequest::Pubkey(_) => Err(GetKeyError::NotSupported),
775+
KeyRequest::XOnlyPubkey(_) => Err(GetKeyError::NotSupported),
771776
KeyRequest::Bip32((fingerprint, path)) => {
772777
let key = if self.fingerprint(secp) == *fingerprint {
773778
let k = self.derive_xpriv(secp, &path);
@@ -831,7 +836,7 @@ impl_get_key_for_set!(BTreeSet);
831836
impl_get_key_for_set!(HashSet);
832837

833838
#[rustfmt::skip]
834-
macro_rules! impl_get_key_for_map {
839+
macro_rules! impl_get_key_for_pubkey_map {
835840
($map:ident) => {
836841

837842
impl GetKey for $map<PublicKey, PrivateKey> {
@@ -844,13 +849,67 @@ impl GetKey for $map<PublicKey, PrivateKey> {
844849
) -> Result<Option<PrivateKey>, Self::Error> {
845850
match key_request {
846851
KeyRequest::Pubkey(pk) => Ok(self.get(&pk).cloned()),
852+
KeyRequest::XOnlyPubkey(xonly) => {
853+
let pubkey_even = PublicKey::new(xonly.public_key(secp256k1::Parity::Even));
854+
let key = self.get(&pubkey_even).cloned();
855+
856+
if key.is_some() {
857+
return Ok(key);
858+
}
859+
860+
let pubkey_odd = PublicKey::new(xonly.public_key(secp256k1::Parity::Odd));
861+
if let Some(priv_key) = self.get(&pubkey_odd).copied() {
862+
let negated_priv_key = priv_key.negate();
863+
return Ok(Some(negated_priv_key));
864+
}
865+
866+
Ok(None)
867+
},
868+
KeyRequest::Bip32(_) => Err(GetKeyError::NotSupported),
869+
}
870+
}
871+
}}}
872+
impl_get_key_for_pubkey_map!(BTreeMap);
873+
#[cfg(feature = "std")]
874+
impl_get_key_for_pubkey_map!(HashMap);
875+
876+
#[rustfmt::skip]
877+
macro_rules! impl_get_key_for_xonly_map {
878+
($map:ident) => {
879+
880+
impl GetKey for $map<XOnlyPublicKey, PrivateKey> {
881+
type Error = GetKeyError;
882+
883+
fn get_key<C: Signing>(
884+
&self,
885+
key_request: &KeyRequest,
886+
secp: &Secp256k1<C>,
887+
) -> Result<Option<PrivateKey>, Self::Error> {
888+
match key_request {
889+
KeyRequest::XOnlyPubkey(xonly) => Ok(self.get(xonly).cloned()),
890+
KeyRequest::Pubkey(pk) => {
891+
let (xonly, parity) = pk.inner.x_only_public_key();
892+
893+
if let Some(mut priv_key) = self.get(&XOnlyPublicKey::from(xonly)).cloned() {
894+
let computed_pk = priv_key.public_key(&secp);
895+
let (_, computed_parity) = computed_pk.inner.x_only_public_key();
896+
897+
if computed_parity != parity {
898+
priv_key = priv_key.negate();
899+
}
900+
901+
return Ok(Some(priv_key));
902+
}
903+
904+
Ok(None)
905+
},
847906
KeyRequest::Bip32(_) => Err(GetKeyError::NotSupported),
848907
}
849908
}
850909
}}}
851-
impl_get_key_for_map!(BTreeMap);
910+
impl_get_key_for_xonly_map!(BTreeMap);
852911
#[cfg(feature = "std")]
853-
impl_get_key_for_map!(HashMap);
912+
impl_get_key_for_xonly_map!(HashMap);
854913

855914
/// Errors when getting a key.
856915
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -1228,7 +1287,14 @@ mod tests {
12281287
use hashes::{hash160, ripemd160, sha256};
12291288
use hex::{test_hex_unwrap as hex, FromHex};
12301289
#[cfg(feature = "rand-std")]
1231-
use secp256k1::{All, SecretKey};
1290+
use {
1291+
crate::address::script_pubkey::ScriptBufExt as _,
1292+
crate::bip32::{DerivationPath, Fingerprint},
1293+
crate::locktime,
1294+
crate::witness_version::WitnessVersion,
1295+
crate::WitnessProgram,
1296+
secp256k1::{All, SecretKey},
1297+
};
12321298

12331299
use super::*;
12341300
use crate::address::script_pubkey::ScriptExt as _;
@@ -2169,6 +2235,42 @@ mod tests {
21692235
assert_eq!(got.unwrap(), priv_key)
21702236
}
21712237

2238+
#[test]
2239+
#[cfg(feature = "rand-std")]
2240+
fn pubkey_map_get_key_negates_odd_parity_keys() {
2241+
use crate::psbt::{GetKey, KeyRequest};
2242+
2243+
let (mut priv_key, mut pk, secp) = gen_keys();
2244+
let (xonly, parity) = pk.inner.x_only_public_key();
2245+
2246+
let mut pubkey_map: HashMap<PublicKey, PrivateKey> = HashMap::new();
2247+
2248+
if parity == secp256k1::Parity::Even {
2249+
priv_key = PrivateKey {
2250+
compressed: priv_key.compressed,
2251+
network: priv_key.network,
2252+
inner: priv_key.inner.negate(),
2253+
};
2254+
pk = priv_key.public_key(&secp);
2255+
}
2256+
2257+
pubkey_map.insert(pk, priv_key);
2258+
2259+
let req_result = pubkey_map.get_key(&KeyRequest::XOnlyPubkey(xonly), &secp).unwrap();
2260+
2261+
let retrieved_key = req_result.unwrap();
2262+
2263+
let retrieved_pub_key = retrieved_key.public_key(&secp);
2264+
let (retrieved_xonly, retrieved_parity) = retrieved_pub_key.inner.x_only_public_key();
2265+
2266+
assert_eq!(xonly, retrieved_xonly);
2267+
assert_eq!(
2268+
retrieved_parity,
2269+
secp256k1::Parity::Even,
2270+
"Key should be normalized to have even parity, even when original had odd parity"
2271+
);
2272+
}
2273+
21722274
#[test]
21732275
fn fee() {
21742276
let output_0_val = Amount::from_sat_u32(99_999_699);
@@ -2273,12 +2375,73 @@ mod tests {
22732375

22742376
#[test]
22752377
#[cfg(feature = "rand-std")]
2276-
fn sign_psbt() {
2277-
use crate::address::script_pubkey::ScriptBufExt as _;
2278-
use crate::bip32::{DerivationPath, Fingerprint};
2279-
use crate::witness_version::WitnessVersion;
2280-
use crate::WitnessProgram;
2378+
fn hashmap_can_sign_taproot() {
2379+
let (priv_key, pk, secp) = gen_keys();
2380+
let internal_key: XOnlyPublicKey = pk.inner.into();
2381+
2382+
let tx = Transaction {
2383+
version: transaction::Version::TWO,
2384+
lock_time: locktime::absolute::LockTime::ZERO,
2385+
input: vec![TxIn::EMPTY_COINBASE],
2386+
output: vec![TxOut { value: Amount::ZERO, script_pubkey: ScriptBuf::new() }],
2387+
};
22812388

2389+
let mut psbt = Psbt::from_unsigned_tx(tx).unwrap();
2390+
psbt.inputs[0].tap_internal_key = Some(internal_key);
2391+
psbt.inputs[0].witness_utxo = Some(transaction::TxOut {
2392+
value: Amount::from_sat_u32(10),
2393+
script_pubkey: ScriptBuf::new_p2tr(&secp, internal_key, None),
2394+
});
2395+
2396+
let mut key_map: HashMap<PublicKey, PrivateKey> = HashMap::new();
2397+
key_map.insert(pk, priv_key);
2398+
2399+
let key_source = (Fingerprint::default(), DerivationPath::default());
2400+
let mut tap_key_origins = std::collections::BTreeMap::new();
2401+
tap_key_origins.insert(internal_key, (vec![], key_source));
2402+
psbt.inputs[0].tap_key_origins = tap_key_origins;
2403+
2404+
let signing_keys = psbt.sign(&key_map, &secp).unwrap();
2405+
assert_eq!(signing_keys.len(), 1);
2406+
assert_eq!(signing_keys[&0], SigningKeys::Schnorr(vec![internal_key]));
2407+
}
2408+
2409+
#[test]
2410+
#[cfg(feature = "rand-std")]
2411+
fn xonly_hashmap_can_sign_taproot() {
2412+
let (priv_key, pk, secp) = gen_keys();
2413+
let internal_key: XOnlyPublicKey = pk.inner.into();
2414+
2415+
let tx = Transaction {
2416+
version: transaction::Version::TWO,
2417+
lock_time: locktime::absolute::LockTime::ZERO,
2418+
input: vec![TxIn::EMPTY_COINBASE],
2419+
output: vec![TxOut { value: Amount::ZERO, script_pubkey: ScriptBuf::new() }],
2420+
};
2421+
2422+
let mut psbt = Psbt::from_unsigned_tx(tx).unwrap();
2423+
psbt.inputs[0].tap_internal_key = Some(internal_key);
2424+
psbt.inputs[0].witness_utxo = Some(transaction::TxOut {
2425+
value: Amount::from_sat_u32(10),
2426+
script_pubkey: ScriptBuf::new_p2tr(&secp, internal_key, None),
2427+
});
2428+
2429+
let mut xonly_key_map: HashMap<XOnlyPublicKey, PrivateKey> = HashMap::new();
2430+
xonly_key_map.insert(internal_key, priv_key);
2431+
2432+
let key_source = (Fingerprint::default(), DerivationPath::default());
2433+
let mut tap_key_origins = std::collections::BTreeMap::new();
2434+
tap_key_origins.insert(internal_key, (vec![], key_source));
2435+
psbt.inputs[0].tap_key_origins = tap_key_origins;
2436+
2437+
let signing_keys = psbt.sign(&xonly_key_map, &secp).unwrap();
2438+
assert_eq!(signing_keys.len(), 1);
2439+
assert_eq!(signing_keys[&0], SigningKeys::Schnorr(vec![internal_key]));
2440+
}
2441+
2442+
#[test]
2443+
#[cfg(feature = "rand-std")]
2444+
fn sign_psbt() {
22822445
let unsigned_tx = Transaction {
22832446
version: transaction::Version::TWO,
22842447
lock_time: absolute::LockTime::ZERO,

0 commit comments

Comments
 (0)