Skip to content

Commit b6d9709

Browse files
committed
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.
1 parent 471feb1 commit b6d9709

4 files changed

Lines changed: 63 additions & 17 deletions

File tree

src/key/mod.rs

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -845,12 +845,25 @@ impl str::FromStr for XOnlyPublicKey {
845845
fn from_str(s: &str) -> Result<XOnlyPublicKey, Error> {
846846
let mut res = [0u8; constants::SCHNORR_PUBLIC_KEY_SIZE];
847847
match from_hex(s, &mut res) {
848-
Ok(constants::SCHNORR_PUBLIC_KEY_SIZE) => XOnlyPublicKey::from_byte_array(res),
848+
Ok(constants::SCHNORR_PUBLIC_KEY_SIZE) => XOnlyPublicKey::parse_byte_array(res),
849849
_ => Err(Error::InvalidPublicKey),
850850
}
851851
}
852852
}
853853

854+
impl TryFrom<[u8; constants::SCHNORR_PUBLIC_KEY_SIZE]> for XOnlyPublicKey {
855+
type Error = Error;
856+
857+
/// Attempts to create an x-only public key from a 32-byte array.
858+
///
859+
/// # Errors
860+
/// Returns [`Error::InvalidPublicKey`] if the bytes do not represent a valid
861+
/// point on the secp256k1 curve.
862+
fn try_from(data: [u8; constants::SCHNORR_PUBLIC_KEY_SIZE]) -> Result<Self, Self::Error> {
863+
Self::parse_byte_array(data)
864+
}
865+
}
866+
854867
impl XOnlyPublicKey {
855868
/// Returns the [`XOnlyPublicKey`] (and its [`Parity`]) for `keypair`.
856869
#[inline]
@@ -882,19 +895,33 @@ impl XOnlyPublicKey {
882895
#[inline]
883896
pub fn from_slice(data: &[u8]) -> Result<XOnlyPublicKey, Error> {
884897
match <[u8; constants::SCHNORR_PUBLIC_KEY_SIZE]>::try_from(data) {
885-
Ok(data) => Self::from_byte_array(data),
898+
Ok(data) => Self::parse_byte_array(data),
886899
Err(_) => Err(InvalidPublicKey),
887900
}
888901
}
889902

890-
/// Creates a schnorr public key directly from a byte array.
903+
/// Creates an x-only public key from a byte array.
891904
///
892-
/// # Errors
905+
/// # Panics
893906
///
894-
/// Returns [`Error::InvalidPublicKey`] if the array does not represent a valid Secp256k1 point
895-
/// x coordinate.
896-
#[inline]
897-
pub fn from_byte_array(
907+
/// In debug builds, this function will panic if the input does not represent
908+
/// a valid point on the curve. In release builds, it assumes the input is
909+
/// valid. For untrusted input, use [`TryFrom`].
910+
pub fn from_byte_array(data: [u8; constants::SCHNORR_PUBLIC_KEY_SIZE]) -> XOnlyPublicKey {
911+
unsafe {
912+
let mut pk = ffi::XOnlyPublicKey::new();
913+
let ret = ffi::secp256k1_xonly_pubkey_parse(
914+
ffi::secp256k1_context_no_precomp,
915+
&mut pk,
916+
data.as_c_ptr(),
917+
);
918+
debug_assert_eq!(ret, 1);
919+
XOnlyPublicKey(pk)
920+
}
921+
}
922+
923+
/// Fallible version of `from_byte_array` for parsing untrusted input.
924+
fn parse_byte_array(
898925
data: [u8; constants::SCHNORR_PUBLIC_KEY_SIZE],
899926
) -> Result<XOnlyPublicKey, Error> {
900927
unsafe {
@@ -912,7 +939,26 @@ impl XOnlyPublicKey {
912939
}
913940
}
914941

942+
/// Serializes the `XOnlyPublicKey` into a 32-byte array.
943+
///
944+
/// This represents the x-coordinate of the point on the curve.
945+
#[inline]
946+
pub fn to_byte_array(&self) -> [u8; constants::SCHNORR_PUBLIC_KEY_SIZE] {
947+
let mut ret = [0u8; constants::SCHNORR_PUBLIC_KEY_SIZE];
948+
949+
unsafe {
950+
let err = ffi::secp256k1_xonly_pubkey_serialize(
951+
ffi::secp256k1_context_no_precomp,
952+
ret.as_mut_c_ptr(),
953+
self.as_c_ptr(),
954+
);
955+
debug_assert_eq!(err, 1);
956+
}
957+
ret
958+
}
959+
915960
#[inline]
961+
#[deprecated(since = "TBD", note = "use XOnlyPublicKey::to_byte_array instead")]
916962
/// Serializes the key as a byte-encoded x coordinate value (32 bytes).
917963
pub fn serialize(&self) -> [u8; constants::SCHNORR_PUBLIC_KEY_SIZE] {
918964
let mut ret = [0u8; constants::SCHNORR_PUBLIC_KEY_SIZE];
@@ -1255,7 +1301,7 @@ impl<'de> serde::Deserialize<'de> for XOnlyPublicKey {
12551301
} else {
12561302
let visitor = super::serde_util::Tuple32Visitor::new(
12571303
"raw 32 bytes schnorr public key",
1258-
XOnlyPublicKey::from_byte_array,
1304+
XOnlyPublicKey::parse_byte_array,
12591305
);
12601306
d.deserialize_tuple(constants::SCHNORR_PUBLIC_KEY_SIZE, visitor)
12611307
}
@@ -1348,7 +1394,7 @@ impl<'a> Arbitrary<'a> for XOnlyPublicKey {
13481394
}
13491395

13501396
u.fill_buffer(&mut bytes[..])?;
1351-
if let Ok(pk) = XOnlyPublicKey::from_byte_array(bytes) {
1397+
if let Ok(pk) = XOnlyPublicKey::parse_byte_array(bytes) {
13521398
return Ok(pk);
13531399
}
13541400
}
@@ -1998,7 +2044,7 @@ mod test {
19982044
let pk = PublicKey::from_slice(&pk_bytes).expect("failed to create pk from iterator");
19992045
let kp = Keypair::from_secret_key(&sk);
20002046
let xonly =
2001-
XOnlyPublicKey::from_byte_array(PK_BYTES).expect("failed to get xonly from slice");
2047+
XOnlyPublicKey::parse_byte_array(PK_BYTES).expect("failed to get xonly from slice");
20022048

20032049
(sk, pk, kp, xonly)
20042050
}

src/schnorr.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ mod tests {
388388
let (pk, _parity) = kp.x_only_public_key();
389389

390390
let ser = pk.serialize();
391-
let pubkey2 = XOnlyPublicKey::from_byte_array(ser).unwrap();
391+
let pubkey2 = XOnlyPublicKey::try_from(ser).unwrap();
392392
assert_eq!(pk, pubkey2);
393393
}
394394

@@ -514,7 +514,7 @@ mod tests {
514514
170, 12, 208, 84, 74, 200, 135, 254, 145, 221, 209, 102,
515515
];
516516
static PK_STR: &str = "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166";
517-
let pk = XOnlyPublicKey::from_byte_array(PK_BYTES).unwrap();
517+
let pk = XOnlyPublicKey::try_from(PK_BYTES).unwrap();
518518

519519
assert_tokens(&sig.compact(), &[Token::BorrowedBytes(&SIG_BYTES[..])]);
520520
assert_tokens(&sig.compact(), &[Token::Bytes(&SIG_BYTES[..])]);
@@ -733,7 +733,7 @@ mod tests {
733733
assert_eq!(sig.to_byte_array(), signature);
734734
}
735735
let sig = Signature::from_byte_array(signature);
736-
let is_verified = if let Ok(pubkey) = XOnlyPublicKey::from_byte_array(public_key) {
736+
let is_verified = if let Ok(pubkey) = XOnlyPublicKey::try_from(public_key) {
737737
verify(&sig, &message, &pubkey).is_ok()
738738
} else {
739739
false

tests/api.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ macro_rules! bytes_rtt_test {
5454

5555
// Message is special because its to/from methods havve the name "digest" in them
5656
// PublicKey is special because it has two serialization forms with different names (but maybe I should rename them?)
57-
// FIXME XOnlyPublicKey should pass this
5857
// ecdsa::Signature and SerializedSignature and RecoverableSignature are variable-length
5958
// Scalar has to_be_bytes and to_le_bytes (and corresponding froms)
59+
bytes_rtt_test!(rtt_c, XOnlyPublicKey);
6060
bytes_rtt_test!(rtt_i, schnorr::Signature);
6161
bytes_rtt_test!(rtt_g, ellswift::ElligatorSwift);
6262

tests/serde.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ fn bincode_keypair() {
105105

106106
#[test]
107107
fn bincode_x_only_public_key() {
108-
let pk = XOnlyPublicKey::from_byte_array(XONLY_PK_BYTES)
109-
.expect("failed to create xonly pk from slice");
108+
let pk =
109+
XOnlyPublicKey::try_from(XONLY_PK_BYTES).expect("failed to create xonly pk from slice");
110110
let ser = bincode::serialize(&pk).unwrap();
111111

112112
assert_eq!(ser, XONLY_PK_BYTES);

0 commit comments

Comments
 (0)