diff --git a/schnorr_fun/Cargo.toml b/schnorr_fun/Cargo.toml index 939f8ad3..751ca2b5 100644 --- a/schnorr_fun/Cargo.toml +++ b/schnorr_fun/Cargo.toml @@ -18,7 +18,7 @@ secp256kfun = { path = "../secp256kfun", version = "0.10", default-features = f bech32 = { version = "0.11", optional = true, default-features = false, features = ["alloc"] } [dev-dependencies] -secp256kfun = { path = "../secp256kfun", version = "0.10", features = ["proptest"] } +secp256kfun = { path = "../secp256kfun", version = "0.10", features = ["proptest", "bincode", "alloc"] } rand = { version = "0.8" } lazy_static = "1.4" bincode = "1.0" diff --git a/schnorr_fun/src/frost/shared_key.rs b/schnorr_fun/src/frost/shared_key.rs index 15d4d6dc..1c672230 100644 --- a/schnorr_fun/src/frost/shared_key.rs +++ b/schnorr_fun/src/frost/shared_key.rs @@ -159,6 +159,38 @@ impl SharedKey { let public_key = Z::cast_point(self.point_polynomial[0]).expect("invariant"); T::cast_point(public_key).expect("invariant") } + + /// Encodes a `SharedKey` as the compressed encoding of each underlying polynomial coefficient + /// + /// i.e. call [`Point::to_bytes`] on each coefficent starting with the constant term. Note that + /// even if it's a `SharedKey` the first coefficient (A.K.A the public key) will still be + /// encoded as 33 bytes. + /// + /// ⚠ Unlike other secp256kfun things this doesn't exactly match the serde/bincode + /// implemenations which will length prefix the list of points. + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(self.point_polynomial.len() * 33); + for coeff in &self.point_polynomial { + bytes.extend(coeff.to_bytes()) + } + bytes + } + + /// Decodes a `SharedKey` (for any `T` and `Z`) from a slice. + /// + /// Returns `None` if the bytes don't represent points or if the first coefficient doesn't + /// satisfy the constraints of `T` and `Z`. + pub fn from_slice(bytes: &[u8]) -> Option { + let mut poly = vec![]; + for point_bytes in bytes.chunks(33) { + poly.push(Point::from_slice(&point_bytes[..])?); + } + + // check first coefficient satisfies both type parameters + let first_coeff = Z::cast_point(poly[0])?; + let _check = T::cast_point(first_coeff)?; + + Some(Self::from_inner(poly)) } } @@ -196,19 +228,18 @@ impl SharedKey { SharedKey::from_inner(poly) } } - #[cfg(feature = "bincode")] -impl crate::fun::bincode::Decode for SharedKey { +impl crate::fun::bincode::Decode for SharedKey { fn decode( decoder: &mut D, ) -> Result { + use secp256kfun::bincode::error::DecodeError; let poly = Vec::>::decode(decoder)?; - - if poly[0].is_zero() { - return Err(secp256kfun::bincode::error::DecodeError::Other( - "first coefficient of a frost polynomial can't be zero", - )); - } + let first_coeff = Z::cast_point(poly[0]).ok_or(DecodeError::Other( + "zero public key for non-zero shared key", + ))?; + let _check = T::cast_point(first_coeff) + .ok_or(DecodeError::Other("odd-y public key for even-y shared key"))?; Ok(SharedKey { point_polynomial: poly, @@ -218,18 +249,20 @@ impl crate::fun::bincode::Decode for SharedKey { } #[cfg(feature = "serde")] -impl<'de> crate::fun::serde::Deserialize<'de> for SharedKey { +impl<'de, T: PointType, Z: ZeroChoice> crate::fun::serde::Deserialize<'de> for SharedKey { fn deserialize(deserializer: D) -> Result where D: secp256kfun::serde::Deserializer<'de>, { let poly = Vec::>::deserialize(deserializer)?; - if poly[0].is_zero() { - return Err(crate::fun::serde::de::Error::custom( - "first coefficient of a frost polynomial can't be zero", - )); - } + let first_coeff = Z::cast_point(poly[0]).ok_or(crate::fun::serde::de::Error::custom( + "zero public key for non-zero shared key", + ))?; + + let _check = T::cast_point(first_coeff).ok_or(crate::fun::serde::de::Error::custom( + "odd-y public key for even-y shared key", + ))?; Ok(Self { point_polynomial: poly, @@ -239,4 +272,139 @@ impl<'de> crate::fun::serde::Deserialize<'de> for SharedKey { } #[cfg(feature = "bincode")] -crate::fun::bincode::impl_borrow_decode!(SharedKey); +crate::fun::bincode::impl_borrow_decode!(SharedKey); +#[cfg(feature = "bincode")] +crate::fun::bincode::impl_borrow_decode!(SharedKey); +#[cfg(feature = "bincode")] +crate::fun::bincode::impl_borrow_decode!(SharedKey); + +#[cfg(test)] +mod test { + use super::*; + + #[cfg(feature = "bincode")] + #[test] + fn bincode_encoding_decoding_roundtrip() { + use crate::fun::bincode; + let poly_zero = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(0 * G), + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + ]) + .collect(), + ); + let poly_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap() + .into_xonly(); + + let poly_minus_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(-1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap(); + + let bytes_poly_zero = + bincode::encode_to_vec(&poly_zero, bincode::config::standard()).unwrap(); + let bytes_poly_one = + bincode::encode_to_vec(&poly_one, bincode::config::standard()).unwrap(); + let bytes_poly_minus_one = + bincode::encode_to_vec(&poly_minus_one, bincode::config::standard()).unwrap(); + + let (poly_zero_got, _) = bincode::decode_from_slice::, _>( + &bytes_poly_zero, + bincode::config::standard(), + ) + .unwrap(); + let (poly_one_got, _) = bincode::decode_from_slice::, _>( + &bytes_poly_one, + bincode::config::standard(), + ) + .unwrap(); + + let (poly_minus_one_got, _) = bincode::decode_from_slice::, _>( + &bytes_poly_minus_one, + bincode::config::standard(), + ) + .unwrap(); + + assert!(bincode::decode_from_slice::, _>( + &bytes_poly_zero, + bincode::config::standard(), + ) + .is_err()); + + assert!(bincode::decode_from_slice::, _>( + &bytes_poly_minus_one, + bincode::config::standard(), + ) + .is_err()); + + assert_eq!(poly_zero_got, poly_zero); + assert_eq!(poly_one_got, poly_one); + assert_eq!(poly_minus_one_got, poly_minus_one); + } + + #[test] + fn to_bytes_from_slice_roudtrip() { + let poly_zero = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(0 * G), + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + ]) + .collect(), + ); + let poly_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap() + .into_xonly(); + + let poly_minus_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(-1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap(); + + let bytes_poly_zero = poly_zero.to_bytes(); + let bytes_poly_one = poly_one.to_bytes(); + let bytes_poly_minus_one = poly_minus_one.to_bytes(); + + let poly_zero_got = SharedKey::::from_slice(&bytes_poly_zero[..]).unwrap(); + let poly_one_got = SharedKey::::from_slice(&bytes_poly_one).unwrap(); + let poly_minus_one_got = + SharedKey::::from_slice(&bytes_poly_minus_one[..]).unwrap(); + + assert!(SharedKey::::from_slice(&bytes_poly_zero[..]).is_none()); + assert!(SharedKey::::from_slice(&bytes_poly_minus_one[..]).is_none()); + + assert_eq!(poly_zero_got, poly_zero); + assert_eq!(poly_one_got, poly_one); + assert_eq!(poly_minus_one_got, poly_minus_one); + } +} diff --git a/secp256kfun/src/marker/point_type.rs b/secp256kfun/src/marker/point_type.rs index 57267627..c57d1fbe 100644 --- a/secp256kfun/src/marker/point_type.rs +++ b/secp256kfun/src/marker/point_type.rs @@ -106,6 +106,7 @@ impl PointType for EvenY { true } + /// ⚠ This will always return `None` if trying to cast from a `Zero` marked point (even if the actual point is not `Zero`) fn cast_point( point: Point, ) -> Option> {