Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/akita-field/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jolt-field = { git = "https://github.com/a16z/jolt", rev = "2509bdcea9bb3e8978bb
num-traits = "0.2"
rand_core = { version = "0.6", features = ["getrandom"] }
rayon = { version = "1.10", optional = true }
serde = "1"

Copy link
Copy Markdown

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?

Copy link
Copy Markdown

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's version = "1.0" requirement. The PR lock currently resolves 1.0.228, while Jolt's latest main resolves 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.

thiserror = "2.0"

[dev-dependencies]
Expand Down
1 change: 1 addition & 0 deletions crates/akita-field/src/ext/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod fp_ext4;
mod fp_ext8;
pub(crate) mod lift;
mod native_algebra;
mod serde_support;
#[cfg(test)]
mod tests;

Expand Down
38 changes: 38 additions & 0 deletions crates/akita-field/src/ext/serde_support.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! `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 is
//! enforced by the base field's own decode. As with the prime fields, this is a
//! host and tooling surface: verifier-reachable decoding 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;

/// Implements serde for one extension arity. `$cfg` is the extension-config
/// parameter, which only `FpExt2` carries.
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference follows the underlying public types. FpExt2<F, C> carries C: FpExt2Config<F> because the quadratic nonresidue configuration is part of its type, and its constructor is new(c0, c1). FpExt4<F> and FpExt8<F> use fixed Akita bases, carry no configuration type, and their constructors take coefficient arrays. Jolt's pending Solinas implementation has the same type and constructor split.

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 FpExt2 line clearer by destructuring the decoded array as let [c0, c1] = ... before calling Self::new(c0, c1), but C must remain in the implementation because it is an actual parameter of FpExt2.

impl_ext_serde!(FpExt8; 8; |coeffs| Self::new(coeffs));
1 change: 1 addition & 0 deletions crates/akita-field/src/prime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub(crate) mod fp64;
mod native_algebra;
mod native_capability;
pub(crate) mod pseudo_mersenne;
mod serde_support;
pub(crate) mod util;

pub use fp128::{
Expand Down
42 changes: 42 additions & 0 deletions crates/akita-field/src/prime/serde_support.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! `serde` for the prime fields, over the canonical residue.
//!
//! An element encodes as its canonical representative in the field's storage
//! width; decode rejects non-canonical values (`val >= P`) rather than reducing.
//!
//! This is a host and tooling surface. Akita's protocol wire format is
//! [`AkitaSerialize`](akita_serialization::AkitaSerialize) /
//! [`AkitaDeserialize`](akita_serialization::AkitaDeserialize), and
//! verifier-reachable decoding stays there: a serde format bounds sequence
//! lengths only if its consumer configured a limit, so these impls cannot make
//! the container guarantee that `AkitaDeserialize` does.

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).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 raw = <$p_ty as Deserialize>::deserialize(deserializer)?;
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>);
1 change: 1 addition & 0 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions profile/akita-recursion/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading