Return parity from serialize in XOnlyPublicKey
What changed, and why it matters
This commit changes the public API of XOnlyPublicKey::serialize() so it now returns both the 32-byte x-only key and its parity bit, instead of just the bytes. All existing callers inside the crate are updated to take only the first element (the bytes) and ignore the parity where it isn't needed. This is a routine API design change to expose extra information for users who need to fully reconstruct a key; it does not fix a bug or close a security hole.
No security action required. Downstream users of rust-bitcoin who call XOnlyPublicKey::serialize() will need to update their code to handle the new tuple return type when upgrading. Reviewers may want to confirm that no internal caller accidentally discards parity where it is now semantically required.
Security signals we found
API signature change in public key serialization
No validation or bounds-checking changes
No secret material handling changes
No cryptographic operation changes
Evidence from the diff
The diff modifies XOnlyPublicKey::serialize() in bitcoin/src/crypto/key.rs to return a tuple ([u8; 32], Parity) rather than [u8; 32]. It then mechanically updates every internal call site to use .0 (or destructures the tuple) to retain the previous behavior. TweakedPublicKey::serialize() is kept as returning only bytes. No cryptographic logic, validation, or secret handling is changed. The change is purely an API surface expansion.
Changed components
bitcoin/src/crypto/key.rsbitcoin/src/address/mod.rsbitcoin/src/blockdata/script/builder.rsbitcoin/src/blockdata/script/witness_program.rsbitcoin/src/psbt/serialize.rsbitcoin/src/taproot/mod.rsbitcoin/tests/psbt-sign-taproot.rsInspect captured patch +14 / −14
diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs
index 844c62a5..11bf1acd 100644
--- a/bitcoin/src/address/mod.rs
+++ b/bitcoin/src/address/mod.rs
@@ -764,7 +764,7 @@ impl Address {
let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner);
(*pubkey_hash.as_byte_array() == *payload)
- || (xonly_pubkey.serialize() == *payload)
+ || (xonly_pubkey.serialize().0 == *payload)
|| (*segwit_redeem_hash(pubkey_hash).as_byte_array() == *payload)
}
@@ -773,7 +773,7 @@ impl Address {
/// This will only work for Taproot addresses. The Public Key is
/// assumed to have already been tweaked.
pub fn is_related_to_xonly_pubkey(&self, xonly_pubkey: XOnlyPublicKey) -> bool {
- xonly_pubkey.serialize() == *self.payload_as_bytes()
+ xonly_pubkey.serialize().0 == *self.payload_as_bytes()
}
/// Returns true if the address creates a particular script
diff --git a/bitcoin/src/blockdata/script/builder.rs b/bitcoin/src/blockdata/script/builder.rs
index ec778614..1cc4732d 100644
--- a/bitcoin/src/blockdata/script/builder.rs
+++ b/bitcoin/src/blockdata/script/builder.rs
@@ -146,7 +146,7 @@ impl<T> Builder<T> {
/// Adds instructions to push an XOnly public key onto the stack.
pub fn push_x_only_key(self, x_only_key: XOnlyPublicKey) -> Self {
- self.push_slice(x_only_key.serialize())
+ self.push_slice(x_only_key.serialize().0)
}
/// Adds instructions to push an absolute lock time onto the stack.
diff --git a/bitcoin/src/blockdata/script/witness_program.rs b/bitcoin/src/blockdata/script/witness_program.rs
index 4cee8811..c970edec 100644
--- a/bitcoin/src/blockdata/script/witness_program.rs
+++ b/bitcoin/src/blockdata/script/witness_program.rs
@@ -96,13 +96,13 @@ impl WitnessProgram {
) -> Self {
let internal_key = internal_key.into();
let (output_key, _parity) = internal_key.tap_tweak(merkle_root);
- let pubkey = output_key.as_x_only_public_key().serialize();
+ let (pubkey, _) = output_key.as_x_only_public_key().serialize();
Self::new_p2tr(pubkey)
}
/// Constructs a new [`WitnessProgram`] from a tweaked key for a P2TR output.
pub fn p2tr_tweaked(output_key: TweakedPublicKey) -> Self {
- let pubkey = output_key.as_x_only_public_key().serialize();
+ let (pubkey, _) = output_key.as_x_only_public_key().serialize();
Self::new_p2tr(pubkey)
}
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index 098e0bd6..194ec2b6 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -228,8 +228,8 @@ impl XOnlyPublicKey {
/// Serializes the x-only public key as a byte-encoded x coordinate value (32 bytes).
#[inline]
- pub fn serialize(&self) -> [u8; constants::SCHNORR_PUBLIC_KEY_SIZE] {
- self.as_inner().serialize()
+ pub fn serialize(&self) -> ([u8; constants::SCHNORR_PUBLIC_KEY_SIZE], Parity) {
+ (self.as_inner().serialize(), self.parity())
}
/// Converts this x-only public key to a full public key given the parity.
@@ -1203,7 +1203,7 @@ impl TweakedPublicKey {
/// Serializes the key as a byte-encoded x coordinate value (32 bytes).
#[inline]
pub fn serialize(&self) -> [u8; constants::SCHNORR_PUBLIC_KEY_SIZE] {
- self.as_x_only_public_key().serialize()
+ self.as_x_only_public_key().serialize().0
}
}
@@ -1999,7 +1999,7 @@ mod tests {
let xonly_pub_key = XOnlyPublicKey::from_byte_array(key_bytes)
.expect("Failed to create an XOnlyPublicKey from a byte array");
// Confirm that the public key from bytes serializes back to the same bytes
- assert_eq!(&xonly_pub_key.serialize(), key_bytes);
+ assert_eq!(&xonly_pub_key.serialize().0, key_bytes);
}
#[test]
diff --git a/bitcoin/src/psbt/serialize.rs b/bitcoin/src/psbt/serialize.rs
index 6f393577..1a36ea02 100644
--- a/bitcoin/src/psbt/serialize.rs
+++ b/bitcoin/src/psbt/serialize.rs
@@ -288,7 +288,7 @@ impl Deserialize for PsbtSighashType {
// Taproot related ser/deser
impl Serialize for XOnlyPublicKey {
- fn serialize(&self) -> Vec<u8> { Self::serialize(self).to_vec() }
+ fn serialize(&self) -> Vec<u8> { Self::serialize(self).0.to_vec() }
}
impl Deserialize for XOnlyPublicKey {
@@ -315,7 +315,7 @@ impl Deserialize for taproot::Signature {
impl Serialize for (XOnlyPublicKey, TapLeafHash) {
fn serialize(&self) -> Vec<u8> {
- let ser_pk = self.0.serialize();
+ let (ser_pk, _) = self.0.serialize();
let mut buf = Vec::with_capacity(ser_pk.len() + self.1.as_byte_array().len());
buf.extend(&ser_pk);
buf.extend(self.1.as_byte_array());
diff --git a/bitcoin/src/taproot/mod.rs b/bitcoin/src/taproot/mod.rs
index fa9303a0..923c8f6b 100644
--- a/bitcoin/src/taproot/mod.rs
+++ b/bitcoin/src/taproot/mod.rs
@@ -103,7 +103,7 @@ impl TapTweakHash {
let internal_key = internal_key.into();
let mut eng = sha256t::Hash::<TapTweakTag>::engine();
// always hash the key
- eng.input(&internal_key.serialize());
+ eng.input(&internal_key.serialize().0);
if let Some(h) = merkle_root {
eng.input(h.as_ref());
} else {
@@ -1245,7 +1245,7 @@ impl<Branch: AsRef<TaprootMerkleBranch> + ?Sized> ControlBlock<Branch> {
let first_byte: u8 =
i32::from(self.output_key_parity) as u8 | self.leaf_version.to_consensus();
write(&[first_byte])?;
- write(&self.internal_key.serialize())?;
+ write(&self.internal_key.serialize().0)?;
write(self.merkle_branch.as_ref().as_bytes())?;
Ok(())
}
diff --git a/bitcoin/tests/psbt-sign-taproot.rs b/bitcoin/tests/psbt-sign-taproot.rs
index b0aec650..69490627 100644
--- a/bitcoin/tests/psbt-sign-taproot.rs
+++ b/bitcoin/tests/psbt-sign-taproot.rs
@@ -164,7 +164,7 @@ fn create_basic_single_sig_script(sk: &str) -> TapScriptBuf {
let kp = sk.parse::<Keypair>().expect("failed to create keypair");
let x_only_pubkey = kp.to_x_only_public_key().0;
script::Builder::new()
- .push_slice(x_only_pubkey.serialize())
+ .push_slice(x_only_pubkey.serialize().0)
.push_opcode(OP_CHECKSIG)
.into_script()
}
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.