-
Notifications
You must be signed in to change notification settings - Fork 877
Add XOnlyPublicKey support for PSBT key retrieval and improve Taproot signing #4238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add XOnlyPublicKey support for PSBT key retrieval and improve Taproot signing #4238
Conversation
Pull Request Test Coverage Report for Build 13994039737Details
💛 - Coveralls |
bitcoin/src/psbt/mod.rs
Outdated
sk | ||
} else if let Ok(Some(sk)) = k | ||
.get_key(&KeyRequest::Pubkey(xonly.public_key(secp256k1::Parity::Odd).into()), secp) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unfortunate. I wouldn't mind this in a backport but how about we modify GetKey
trait to also have get_by_x_only
?
I'd also prefer to provide HashMap<XOnlyPublicKey, PrivateKey>
and do the retrieval by ignoring the parity (because both keys can be computed).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unfortunate. I wouldn't mind this in a backport but how about we modify
GetKey
trait to also haveget_by_x_only
?I'd also prefer to provide
HashMap<XOnlyPublicKey, PrivateKey>
and do the retrieval by ignoring the parity (because both keys can be computed).
What about adding a new KeyRequest variant for XOnlyPubKey? I think this would better follow the established pattern for adding new key retrieval methods:
pub enum KeyRequest {
/// Request a private key using the associated public key.
Pubkey(PublicKey),
/// Request a private key using BIP-32 fingerprint and derivation path.
Bip32(KeySource),
/// Request a private key using the associated x-only public key.
XOnlyPubkey(XOnlyPublicKey),
}
This approach seems cleaner than modifying the GetKey trait to add a separate get_by_x_only method, and it maintains consistency with how other key types are handled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yes, I forgot that get_key
takes an enum (which is somewhat weird and possibly slightly slower than necessary but not really broken).
Still, I think having a map data structure that ignores parity when retrieving keys would make a lot of sense.
e36f06c
to
9193b00
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, aside from the missing negation bug it looks great!
bitcoin/src/psbt/mod.rs
Outdated
KeyRequest::XOnlyPubkey(xonly) => Ok(self.get(xonly).cloned()), | ||
KeyRequest::Pubkey(pk) => { | ||
let xonly = XOnlyPublicKey::from(pk.inner); | ||
Ok(self.get(&xonly).cloned()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's one slight issue: after retrieving the key we need to check the parity of pk
and negate the private key if it's Odd
.
6ac1002
to
4f28d1c
Compare
}; | ||
|
||
return Ok(Some(negated_priv_key)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC the parity doesn't matter for x-only keys because it's checked when signing and flipped on-demand. But I like this code conceptually because it may lead to less surprising result if anyone uses it.
bitcoin/src/psbt/mod.rs
Outdated
} | ||
|
||
let pubkey_odd = PublicKey::new(xonly.public_key(secp256k1::Parity::Odd)); | ||
if let Some(priv_key) = self.get(&pubkey_odd).cloned() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.copied
would've been a bit nicer but not a big deal.
bitcoin/src/psbt/mod.rs
Outdated
return Ok(None); | ||
}, | ||
KeyRequest::Pubkey(pk) => { | ||
let xonly = XOnlyPublicKey::from(pk.inner); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly, this is still incorrect - if pk
has odd parity this will return even parity. We need to do the check here against pk
's parity.
Also thinking about it, it'd be nicer to have a private fn make_parity(&self, parity: Parity) -> Self
on PrivateKey
since it's already used in two places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, now I think I got it. We need to account for the parity of the public key in the request. When we retrieve a private key from the map using just the x-only part, we must verify if that private key generates a public key with the same parity as the original request. If the parity doesn't match, we need to negate the private key before returning it.
My fault, for the misunderstanding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly, this is still incorrect - if
pk
has odd parity this will return even parity. We need to do the check here againstpk
's parity.Also thinking about it, it'd be nicer to have a private
fn make_parity(&self, parity: Parity) -> Self
onPrivateKey
since it's already used in two places.
Instead of a make_parity
function that requires checking the current parity again, maybe we could just add a simple negate function to PrivateKey
? Since we already know when we need to negate the key in our code, this would be more direct and avoid redundant public key generation and parity checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sorry, I forgot parity is implied. I have a horrible day today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries! Sorry to hear you're having a rough day. Hope things improve!
05c2f45
to
d2f55a6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK d2f55a6
/// but with the opposite y-coordinate parity. This is useful for ensuring compatibility | ||
/// with specific public key formats and BIP-340 requirements. | ||
#[inline] | ||
pub fn negate(&self) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to make it private but given it doesn't require secp256k1
context, I'm not too worried about making it public.
bitcoin/src/psbt/mod.rs
Outdated
pubkey_map.insert(pk, priv_key); | ||
|
||
let req_result = pubkey_map.get_key(&KeyRequest::XOnlyPubkey(xonly), &secp).unwrap(); | ||
assert!(req_result.is_some(), "Should have found a key for odd parity XOnlyPubkey request"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not needed since you're unwrapping below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
d2f55a6
to
3d87885
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 3d87885
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 3d87885; successfully ran local tests
bitcoin/src/psbt/mod.rs
Outdated
let mut psbt = Psbt::from_unsigned_tx(tx).unwrap(); | ||
psbt.inputs[0].tap_internal_key = Some(internal_key); | ||
psbt.inputs[0].witness_utxo = Some(transaction::TxOut { | ||
value: Amount::from_sat_unchecked(10), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 3d87885:
Sorry, you need to rebase because of this (the merge commit does not pass my local CI). from_sat_unchecked
was removed. You can use from_sat_u32
instead.
bitcoin/src/psbt/mod.rs
Outdated
let mut psbt = Psbt::from_unsigned_tx(tx).unwrap(); | ||
psbt.inputs[0].tap_internal_key = Some(internal_key); | ||
psbt.inputs[0].witness_utxo = Some(transaction::TxOut { | ||
value: Amount::from_sat_unchecked(10), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 3d87885:
Same here.
… 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
3d87885
to
069d2fd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 069d2fd
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 069d2fd; successfully ran local tests
Well done @erickcestari 🥂 |
…val and improve Taproot signing 95eb255 Add XOnlyPublicKey support for PSBT key retrieval and improve Taproot signing (Erick Cestari) 2858b6c Support GetKey where the Xpriv is a direct child of the looked up KeySource (Nadav Ivgi) d005ddd Refactor GetKey for sets to internally use Xpriv::get_key() (Nadav Ivgi) b75b2e3 Fix GetKey for sets to properly compare the fingerprint (Nadav Ivgi) Pull request description: Backport two PRs: - #3356 - #4238 The first includes a bug fix in a `GetKey` impl and the second is a feature we want to release. And the three refactor patches touch code that #4238 builds on so I figured we should backport them all. All 5 patches required a small amount of massaging to get in. The first 4 was just adding a `?` to the calls to `derive_priv`. The last patch needed a few calls in the unit test changing. ACKs for top commit: storopoli: ACK 95eb255 apoelstra: ACK 95eb255; successfully ran local tests Tree-SHA512: 5b73c4cd3ddfeef5d4e64a6e236c905c39c560dc704dba8d925f636d3c9b12c0706f27c3e4b29d92ab9e2d15ddbee8bbe5d0955836529d72461d6ee505d899c5
The
bip32_sign_schnorr
function was previously only attempting to retrieve private keys usingKeyRequest::Bip32
, which limited the ability to sign Taproot inputs with key maps that don't support BIP32 derivation paths.Changes
KeyRequest::XOnlyPubkey
variant to support direct retrieval using XOnly public keysGetKey
forHashMap<XOnlyPublicKey, PrivateKey>
for more efficient Taproot key managementHashMap<PublicKey, PrivateKey>
implementation to handle XOnlyPublicKey requests by checking both even and odd parity variantsThese improvements enable wallet implementations to store keys indexed by either
PublicKey
orXOnlyPublicKey
and successfully sign PSBTs.Closes #4150