From b6d9709576fe229864359e6d3f8bb3fb76bd3953 Mon Sep 17 00:00:00 2001 From: Abeeujah Date: Sat, 14 Feb 2026 18:40:38 +0100 Subject: [PATCH] feat: implement byte array conversions for XOnlyPublicKey Adds `to_byte_array`, `from_byte_array`, and `parse_from_byte_array` to XOnlyPublicKey. The implementation wraps `secp256k1_xonly_pubkey_parse` to ensure the provided x-coordinate represents a valid point on the curve. The `from_byte_array` method uses a `debug_assert` to satisfy the `bytes_rtt_test` macro requirements, while `parse_from_byte_array` in conjunction with `TryFrom<[u8; 32]>` provides a fallible interface for production use. --- src/key/mod.rs | 68 ++++++++++++++++++++++++++++++++++++++++++-------- src/schnorr.rs | 6 ++--- tests/api.rs | 2 +- tests/serde.rs | 4 +-- 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/key/mod.rs b/src/key/mod.rs index b80315b84..baa88789a 100644 --- a/src/key/mod.rs +++ b/src/key/mod.rs @@ -845,12 +845,25 @@ impl str::FromStr for XOnlyPublicKey { fn from_str(s: &str) -> Result { let mut res = [0u8; constants::SCHNORR_PUBLIC_KEY_SIZE]; match from_hex(s, &mut res) { - Ok(constants::SCHNORR_PUBLIC_KEY_SIZE) => XOnlyPublicKey::from_byte_array(res), + Ok(constants::SCHNORR_PUBLIC_KEY_SIZE) => XOnlyPublicKey::parse_byte_array(res), _ => Err(Error::InvalidPublicKey), } } } +impl TryFrom<[u8; constants::SCHNORR_PUBLIC_KEY_SIZE]> for XOnlyPublicKey { + type Error = Error; + + /// Attempts to create an x-only public key from a 32-byte array. + /// + /// # Errors + /// Returns [`Error::InvalidPublicKey`] if the bytes do not represent a valid + /// point on the secp256k1 curve. + fn try_from(data: [u8; constants::SCHNORR_PUBLIC_KEY_SIZE]) -> Result { + Self::parse_byte_array(data) + } +} + impl XOnlyPublicKey { /// Returns the [`XOnlyPublicKey`] (and its [`Parity`]) for `keypair`. #[inline] @@ -882,19 +895,33 @@ impl XOnlyPublicKey { #[inline] pub fn from_slice(data: &[u8]) -> Result { match <[u8; constants::SCHNORR_PUBLIC_KEY_SIZE]>::try_from(data) { - Ok(data) => Self::from_byte_array(data), + Ok(data) => Self::parse_byte_array(data), Err(_) => Err(InvalidPublicKey), } } - /// Creates a schnorr public key directly from a byte array. + /// Creates an x-only public key from a byte array. /// - /// # Errors + /// # Panics /// - /// Returns [`Error::InvalidPublicKey`] if the array does not represent a valid Secp256k1 point - /// x coordinate. - #[inline] - pub fn from_byte_array( + /// In debug builds, this function will panic if the input does not represent + /// a valid point on the curve. In release builds, it assumes the input is + /// valid. For untrusted input, use [`TryFrom`]. + pub fn from_byte_array(data: [u8; constants::SCHNORR_PUBLIC_KEY_SIZE]) -> XOnlyPublicKey { + unsafe { + let mut pk = ffi::XOnlyPublicKey::new(); + let ret = ffi::secp256k1_xonly_pubkey_parse( + ffi::secp256k1_context_no_precomp, + &mut pk, + data.as_c_ptr(), + ); + debug_assert_eq!(ret, 1); + XOnlyPublicKey(pk) + } + } + + /// Fallible version of `from_byte_array` for parsing untrusted input. + fn parse_byte_array( data: [u8; constants::SCHNORR_PUBLIC_KEY_SIZE], ) -> Result { unsafe { @@ -912,7 +939,26 @@ impl XOnlyPublicKey { } } + /// Serializes the `XOnlyPublicKey` into a 32-byte array. + /// + /// This represents the x-coordinate of the point on the curve. + #[inline] + pub fn to_byte_array(&self) -> [u8; constants::SCHNORR_PUBLIC_KEY_SIZE] { + let mut ret = [0u8; constants::SCHNORR_PUBLIC_KEY_SIZE]; + + unsafe { + let err = ffi::secp256k1_xonly_pubkey_serialize( + ffi::secp256k1_context_no_precomp, + ret.as_mut_c_ptr(), + self.as_c_ptr(), + ); + debug_assert_eq!(err, 1); + } + ret + } + #[inline] + #[deprecated(since = "TBD", note = "use XOnlyPublicKey::to_byte_array instead")] /// Serializes the key as a byte-encoded x coordinate value (32 bytes). pub fn serialize(&self) -> [u8; constants::SCHNORR_PUBLIC_KEY_SIZE] { let mut ret = [0u8; constants::SCHNORR_PUBLIC_KEY_SIZE]; @@ -1255,7 +1301,7 @@ impl<'de> serde::Deserialize<'de> for XOnlyPublicKey { } else { let visitor = super::serde_util::Tuple32Visitor::new( "raw 32 bytes schnorr public key", - XOnlyPublicKey::from_byte_array, + XOnlyPublicKey::parse_byte_array, ); d.deserialize_tuple(constants::SCHNORR_PUBLIC_KEY_SIZE, visitor) } @@ -1348,7 +1394,7 @@ impl<'a> Arbitrary<'a> for XOnlyPublicKey { } u.fill_buffer(&mut bytes[..])?; - if let Ok(pk) = XOnlyPublicKey::from_byte_array(bytes) { + if let Ok(pk) = XOnlyPublicKey::parse_byte_array(bytes) { return Ok(pk); } } @@ -1998,7 +2044,7 @@ mod test { let pk = PublicKey::from_slice(&pk_bytes).expect("failed to create pk from iterator"); let kp = Keypair::from_secret_key(&sk); let xonly = - XOnlyPublicKey::from_byte_array(PK_BYTES).expect("failed to get xonly from slice"); + XOnlyPublicKey::parse_byte_array(PK_BYTES).expect("failed to get xonly from slice"); (sk, pk, kp, xonly) } diff --git a/src/schnorr.rs b/src/schnorr.rs index 9091655a3..58215ba05 100644 --- a/src/schnorr.rs +++ b/src/schnorr.rs @@ -388,7 +388,7 @@ mod tests { let (pk, _parity) = kp.x_only_public_key(); let ser = pk.serialize(); - let pubkey2 = XOnlyPublicKey::from_byte_array(ser).unwrap(); + let pubkey2 = XOnlyPublicKey::try_from(ser).unwrap(); assert_eq!(pk, pubkey2); } @@ -514,7 +514,7 @@ mod tests { 170, 12, 208, 84, 74, 200, 135, 254, 145, 221, 209, 102, ]; static PK_STR: &str = "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166"; - let pk = XOnlyPublicKey::from_byte_array(PK_BYTES).unwrap(); + let pk = XOnlyPublicKey::try_from(PK_BYTES).unwrap(); assert_tokens(&sig.compact(), &[Token::BorrowedBytes(&SIG_BYTES[..])]); assert_tokens(&sig.compact(), &[Token::Bytes(&SIG_BYTES[..])]); @@ -733,7 +733,7 @@ mod tests { assert_eq!(sig.to_byte_array(), signature); } let sig = Signature::from_byte_array(signature); - let is_verified = if let Ok(pubkey) = XOnlyPublicKey::from_byte_array(public_key) { + let is_verified = if let Ok(pubkey) = XOnlyPublicKey::try_from(public_key) { verify(&sig, &message, &pubkey).is_ok() } else { false diff --git a/tests/api.rs b/tests/api.rs index 7a55c11e1..721976d22 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -54,9 +54,9 @@ macro_rules! bytes_rtt_test { // Message is special because its to/from methods havve the name "digest" in them // PublicKey is special because it has two serialization forms with different names (but maybe I should rename them?) -// FIXME XOnlyPublicKey should pass this // ecdsa::Signature and SerializedSignature and RecoverableSignature are variable-length // Scalar has to_be_bytes and to_le_bytes (and corresponding froms) +bytes_rtt_test!(rtt_c, XOnlyPublicKey); bytes_rtt_test!(rtt_i, schnorr::Signature); bytes_rtt_test!(rtt_g, ellswift::ElligatorSwift); diff --git a/tests/serde.rs b/tests/serde.rs index 63a9aea14..26a25dbbe 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -105,8 +105,8 @@ fn bincode_keypair() { #[test] fn bincode_x_only_public_key() { - let pk = XOnlyPublicKey::from_byte_array(XONLY_PK_BYTES) - .expect("failed to create xonly pk from slice"); + let pk = + XOnlyPublicKey::try_from(XONLY_PK_BYTES).expect("failed to create xonly pk from slice"); let ser = bincode::serialize(&pk).unwrap(); assert_eq!(ser, XONLY_PK_BYTES);