-
Notifications
You must be signed in to change notification settings - Fork 7
feat(akita-field): add serde to akita-field #400
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
Changes from all commits
8b021f7
226fbe0
ad582d3
f464251
2368c7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| //! `serde` for the extension fields, as base-coefficient arrays. | ||
| //! | ||
| //! An element encodes as its `[F; K]` coefficients in the same basis order as | ||
| //! [`AkitaSerialize`](akita_serialization::AkitaSerialize), so canonicality | ||
| //! follows from the base field's decode. As with the prime fields, | ||
| //! verifier-reachable decoding of Akita containers stays on | ||
| //! [`AkitaDeserialize`](akita_serialization::AkitaDeserialize), which is the | ||
| //! only path that bounds container lengths. | ||
|
|
||
| use serde::{Deserialize, Deserializer, Serialize, Serializer}; | ||
|
|
||
| use super::{FpExt2, FpExt2Config, FpExt4, FpExt8}; | ||
| use crate::FieldCore; | ||
|
|
||
| macro_rules! impl_ext_serde { | ||
| ($ty:ident $(, $cfg:ident: $bound:path)?; $k:literal; |$coeffs:ident| $new:expr) => { | ||
| impl<F: FieldCore + Serialize $(, $cfg: $bound)?> Serialize for $ty<F $(, $cfg)?> { | ||
| fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { | ||
| self.coeffs.serialize(serializer) | ||
| } | ||
| } | ||
|
|
||
| impl<'de, F: FieldCore + Deserialize<'de> $(, $cfg: $bound)?> Deserialize<'de> | ||
| for $ty<F $(, $cfg)?> | ||
| { | ||
| fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { | ||
| let $coeffs = <[F; $k]>::deserialize(deserializer)?; | ||
| Ok($new) | ||
| } | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| impl_ext_serde!(FpExt2, C: FpExt2Config<F>; 2; |coeffs| Self::new(coeffs[0], coeffs[1])); | ||
| impl_ext_serde!(FpExt4; 4; |coeffs| Self::new(coeffs)); | ||
|
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth aligning the definitions/API or are there reasons to specify C and pass the coeffs explicitly for FpExt2? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The difference follows the underlying public types. I therefore would not add a forwarding constructor or reshape the field APIs in this Serde PR just to make these three macro calls look identical. We can make the |
||
| impl_ext_serde!(FpExt8; 8; |coeffs| Self::new(coeffs)); | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use serde::de::DeserializeOwned; | ||
|
|
||
| use super::*; | ||
| use crate::ext::Ext2; | ||
| use crate::prime::Fp128; | ||
| use crate::CanonicalField; | ||
|
|
||
| type F = Fp128<0xffff_ffff_ffff_ffff_ffff_ffff_ffff_feed>; | ||
| type E2 = Ext2<F>; | ||
| type E4 = FpExt4<F>; | ||
| type E8 = FpExt8<F>; | ||
|
|
||
| const BASE_WIDTH: usize = 16; | ||
|
|
||
| fn encode<T: Serialize>(value: T) -> Vec<u8> { | ||
| postcard::to_stdvec(&value).expect("encoding cannot fail") | ||
| } | ||
|
|
||
| fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, postcard::Error> { | ||
| postcard::from_bytes(bytes) | ||
| } | ||
|
|
||
| fn base(value: u128) -> F { | ||
| F::from_canonical_u128_checked(value).expect("value is canonical") | ||
| } | ||
|
|
||
| fn assert_layout(bytes: &[u8], coeffs: &[u128]) { | ||
| assert_eq!(bytes.len(), coeffs.len() * BASE_WIDTH); | ||
| for (index, coeff) in coeffs.iter().enumerate() { | ||
| let start = index * BASE_WIDTH; | ||
| assert_eq!(&bytes[start..start + BASE_WIDTH], coeff.to_le_bytes()); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn coefficients_encode_in_basis_order_at_fixed_width() { | ||
| assert_layout(&encode(E2::new(base(1), base(2))), &[1, 2]); | ||
| assert_layout( | ||
| &encode(E4::new([base(1), base(2), base(3), base(4)])), | ||
| &[1, 2, 3, 4], | ||
| ); | ||
| assert_layout( | ||
| &encode(E8::new([ | ||
| base(1), | ||
| base(2), | ||
| base(3), | ||
| base(4), | ||
| base(5), | ||
| base(6), | ||
| base(7), | ||
| base(8), | ||
| ])), | ||
| &[1, 2, 3, 4, 5, 6, 7, 8], | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn extension_fields_round_trip() { | ||
| let e2 = E2::new(base(3), base(0xffff_ffff_ffff_ffff_ffff_ffff_ffff_feec)); | ||
| assert_eq!(decode::<E2>(&encode(e2)).unwrap(), e2); | ||
|
|
||
| let e4 = E4::new([base(0), base(1), base(1 << 100), base(7)]); | ||
| assert_eq!(decode::<E4>(&encode(e4)).unwrap(), e4); | ||
|
|
||
| let e8 = E8::new([ | ||
| base(0), | ||
| base(1), | ||
| base(2), | ||
| base(1 << 64), | ||
| base(4), | ||
| base(5), | ||
| base(6), | ||
| base(0xffff_ffff_ffff_ffff_ffff_ffff_ffff_feec), | ||
| ]); | ||
| assert_eq!(decode::<E8>(&encode(e8)).unwrap(), e8); | ||
| } | ||
|
|
||
| #[test] | ||
| fn non_canonical_and_short_coefficient_arrays_are_rejected() { | ||
| let mut bytes = encode(E2::new(base(1), base(2))); | ||
| assert!(decode::<E2>(&bytes[..BASE_WIDTH]).is_err()); | ||
|
|
||
| bytes[BASE_WIDTH..].copy_from_slice(&u128::MAX.to_le_bytes()); | ||
| assert!(decode::<E2>(&bytes).is_err()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| //! `serde` for the prime fields, as fixed-width canonical little-endian bytes. | ||
| //! | ||
| //! An element encodes as the little-endian bytes of its canonical representative | ||
| //! (`Fp32` → `[u8; 4]`, `Fp64` → `[u8; 8]`, `Fp128` → `[u8; 16]`); decode rejects | ||
| //! non-canonical values (`val >= P`) rather than reducing. Encoding the byte | ||
| //! array rather than the storage integer keeps the length value-independent | ||
| //! under formats that varint-encode integers, matches Jolt's `JoltProof` | ||
| //! convention, and agrees with | ||
| //! [`AkitaSerialize`](akita_serialization::AkitaSerialize) for these types. | ||
| //! | ||
| //! This is a host and tooling surface. Verifier-reachable decoding of Akita | ||
| //! containers stays on | ||
| //! [`AkitaDeserialize`](akita_serialization::AkitaDeserialize), which is the | ||
| //! only path that bounds container lengths. | ||
|
|
||
| use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; | ||
|
|
||
| use super::{Fp128, Fp32, Fp64}; | ||
| use crate::CanonicalField; | ||
|
|
||
| macro_rules! impl_prime_serde { | ||
| ($ty:ident<$p:ident: $p_ty:ty>) => { | ||
| impl<const $p: $p_ty> Serialize for $ty<$p> { | ||
| fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { | ||
| (self.to_canonical_u128() as $p_ty) | ||
| .to_le_bytes() | ||
| .serialize(serializer) | ||
| } | ||
| } | ||
|
|
||
| impl<'de, const $p: $p_ty> Deserialize<'de> for $ty<$p> { | ||
| fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { | ||
| let bytes = <[u8; size_of::<$p_ty>()]>::deserialize(deserializer)?; | ||
| let raw = <$p_ty>::from_le_bytes(bytes); | ||
| Self::from_canonical_u128_checked(u128::from(raw)).ok_or_else(|| { | ||
| de::Error::custom(format_args!( | ||
| concat!(stringify!($ty), " value {} is not a canonical residue"), | ||
| raw | ||
| )) | ||
| }) | ||
| } | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| impl_prime_serde!(Fp32<P: u32>); | ||
| impl_prime_serde!(Fp64<P: u64>); | ||
| impl_prime_serde!(Fp128<P: u128>); | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use akita_serialization::AkitaSerialize; | ||
| use serde::de::DeserializeOwned; | ||
|
|
||
| use super::*; | ||
|
|
||
| type F32 = Fp32<4_294_967_291>; | ||
| type F64 = Fp64<18_446_744_073_709_551_557>; | ||
| type F128 = Fp128<0xffff_ffff_ffff_ffff_ffff_ffff_ffff_feed>; | ||
|
|
||
| fn encode<T: Serialize>(value: T) -> Vec<u8> { | ||
| postcard::to_stdvec(&value).expect("encoding cannot fail") | ||
| } | ||
|
|
||
| fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, postcard::Error> { | ||
| postcard::from_bytes(bytes) | ||
| } | ||
|
|
||
| fn fp32(value: u128) -> F32 { | ||
| F32::from_canonical_u128_checked(value).expect("value is canonical") | ||
| } | ||
|
|
||
| fn fp64(value: u128) -> F64 { | ||
| F64::from_canonical_u128_checked(value).expect("value is canonical") | ||
| } | ||
|
|
||
| fn fp128(value: u128) -> F128 { | ||
| F128::from_canonical_u128_checked(value).expect("value is canonical") | ||
| } | ||
|
|
||
| #[test] | ||
| fn fp32_encodes_as_four_canonical_le_bytes() { | ||
| assert_eq!(encode(fp32(200)), [0xc8, 0x00, 0x00, 0x00]); | ||
| assert_eq!(encode(fp32(4_294_967_290)), [0xfa, 0xff, 0xff, 0xff]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn fp64_encodes_as_eight_canonical_le_bytes() { | ||
| assert_eq!( | ||
| encode(fp64(200)), | ||
| [0xc8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] | ||
| ); | ||
| assert_eq!( | ||
| encode(fp64(18_446_744_073_709_551_556)), | ||
| [0xc4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn fp128_encodes_as_sixteen_canonical_le_bytes() { | ||
| let mut small = [0x00; 16]; | ||
| small[0] = 0xc8; | ||
| assert_eq!(encode(fp128(200)), small); | ||
|
|
||
| let mut large = [0xff; 16]; | ||
| large[0] = 0xec; | ||
| large[1] = 0xfe; | ||
| assert_eq!( | ||
| encode(fp128(0xffff_ffff_ffff_ffff_ffff_ffff_ffff_feec)), | ||
| large | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn prime_fields_round_trip() { | ||
| for value in [0, 1, 200, 4_294_967_290] { | ||
| assert_eq!(decode::<F32>(&encode(fp32(value))).unwrap(), fp32(value)); | ||
| } | ||
| for value in [0, 1, 200, 4_294_967_296, 18_446_744_073_709_551_556] { | ||
| assert_eq!(decode::<F64>(&encode(fp64(value))).unwrap(), fp64(value)); | ||
| } | ||
| for value in [ | ||
| 0, | ||
| 1, | ||
| 200, | ||
| 18_446_744_073_709_551_616, | ||
| 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_feec, | ||
| ] { | ||
| assert_eq!(decode::<F128>(&encode(fp128(value))).unwrap(), fp128(value)); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn non_canonical_and_truncated_encodings_are_rejected() { | ||
| assert!(decode::<F32>(&encode(4_294_967_291u32.to_le_bytes())).is_err()); | ||
| assert!(decode::<F64>(&encode(18_446_744_073_709_551_557u64.to_le_bytes())).is_err()); | ||
| assert!(decode::<F128>(&encode( | ||
| 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_feedu128.to_le_bytes() | ||
| )) | ||
| .is_err()); | ||
| assert!(decode::<F128>(&encode(u128::MAX.to_le_bytes())).is_err()); | ||
| assert!(decode::<F128>(&encode(fp128(200))[..15]).is_err()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn serde_bytes_match_akita_serialize_bytes() { | ||
| fn assert_agrees<T: Serialize + AkitaSerialize>(value: T) { | ||
| let mut akita = Vec::new(); | ||
| value | ||
| .serialize_uncompressed(&mut akita) | ||
| .expect("encoding cannot fail"); | ||
| assert_eq!(encode(&value), akita); | ||
| } | ||
|
|
||
| assert_agrees(fp32(4_294_967_290)); | ||
| assert_agrees(fp64(18_446_744_073_709_551_556)); | ||
| assert_agrees(fp128(0xffff_ffff_ffff_ffff_ffff_ffff_ffff_feec)); | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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.
any reason to not use a more recent version? say
serde = "1.0.229"or is just to be aligned with jolt?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.
serde = "1"already accepts every compatible Serde 1.x release. It is equivalent to Jolt'sversion = "1.0"requirement. The PR lock currently resolves 1.0.228, while Jolt's latestmainresolves 1.0.229. Writing"1.0.229"would raise the minimum accepted version to 1.0.229, but it would not pin that exact release. An exact pin would require"=1.0.229".We do not use an API introduced in 1.0.229, so I think
"1"is the truthful manifest requirement and is aligned with Jolt. If we want the current resolved release in this PR, we can refresh the lock without raising the crate's minimum Serde version.