Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion ext/crates/fp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
aligned-vec = "0.6.4"
aligned-vec = { version = "0.6.4", features = ["serde"] }
build_const = "0.2.2"
byteorder = "1.5.0"
cfg-if = "1.0.1"
Expand Down
4 changes: 3 additions & 1 deletion ext/crates/fp/src/field/fp.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use serde::{Deserialize, Serialize};

use super::{
Field,
element::{FieldElement, FieldElementContainer},
Expand All @@ -8,7 +10,7 @@ pub use crate::prime::fp::*;
use crate::{constants::BITS_PER_LIMB, limb::Limb, prime::Prime};

/// A prime field. This is just a wrapper around a prime.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Fp<P> {
p: P,
}
Expand Down
68 changes: 67 additions & 1 deletion ext/crates/fp/src/matrix/matrix_inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use aligned_vec::AVec;
use either::Either;
use itertools::Itertools;
use maybe_rayon::prelude::*;
use serde::{Deserialize, Deserializer, Serialize};

use super::{QuasiInverse, Subspace};
use crate::{
Expand All @@ -19,7 +20,7 @@ use crate::{
/// The way we store matrices means it is easier to perform row operations than column operations,
/// and the way we use matrices means we want our matrices to act on the right. Hence we think of
/// vectors as row vectors.
#[derive(Clone)]
#[derive(Clone, Serialize)]
pub struct Matrix {
fp: Fp<ValidPrime>,
rows: usize,
Expand All @@ -33,6 +34,71 @@ pub struct Matrix {
pub(crate) pivots: Vec<isize>,
}

// `Deserialize` is implemented manually rather than derived so that we can validate `Matrix`'s
// internal invariants. Without these checks, malformed input could build a `Matrix` whose
// accessors (`row`, `to_bytes`, ...) later panic on bounds-checked slice indexing into `data`,
// escaping the `Deserialize` boundary instead of surfacing as a normal serde error.
impl<'de> Deserialize<'de> for Matrix {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;

#[derive(Deserialize)]
struct Raw {
fp: Fp<ValidPrime>,
rows: usize,
physical_rows: usize,
columns: usize,
data: AVec<Limb>,
stride: usize,
pivots: Vec<isize>,
}

let raw = Raw::deserialize(deserializer)?;
let expected_stride = raw.fp.number(raw.columns);
if raw.stride != expected_stride {
return Err(D::Error::custom(format!(
"Matrix stride {} does not match expected {} for columns={}",
raw.stride, expected_stride, raw.columns,
)));
}
if raw.physical_rows < raw.rows {
return Err(D::Error::custom(format!(
"Matrix physical_rows {} less than rows {}",
raw.physical_rows, raw.rows,
)));
}
if raw.data.len() != raw.physical_rows * raw.stride {
return Err(D::Error::custom(format!(
"Matrix data length {} does not match physical_rows*stride = {}*{} = {}",
raw.data.len(),
raw.physical_rows,
raw.stride,
raw.physical_rows * raw.stride,
)));
}
Comment thread
JoeyBF marked this conversation as resolved.
// `pivots` is either empty (matrix not yet row-reduced) or has one entry per column.
if !raw.pivots.is_empty() && raw.pivots.len() != raw.columns {
return Err(D::Error::custom(format!(
"Matrix pivots length {} must be 0 or columns = {}",
raw.pivots.len(),
raw.columns,
)));
}
Ok(Self {
fp: raw.fp,
rows: raw.rows,
physical_rows: raw.physical_rows,
columns: raw.columns,
data: raw.data,
stride: raw.stride,
pivots: raw.pivots,
})
}
}

impl PartialEq for Matrix {
fn eq(&self, other: &Self) -> bool {
self.data == other.data
Expand Down
3 changes: 2 additions & 1 deletion ext/crates/fp/src/matrix/quasi_inverse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::io;

use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use itertools::Itertools;
use serde::{Deserialize, Serialize};

use super::Matrix;
use crate::{
Expand All @@ -17,7 +18,7 @@ use crate::{
/// everything (with the standard basis).
/// * `preimage` - The actual quasi-inverse, where the basis of the image is that given by
/// `image`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuasiInverse {
image: Option<Vec<isize>>,
preimage: Matrix,
Expand Down
3 changes: 2 additions & 1 deletion ext/crates/fp/src/matrix/subspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{io, ops::Deref};

use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use itertools::Itertools;
use serde::{Deserialize, Serialize};

use super::Matrix;
use crate::{
Expand All @@ -18,7 +19,7 @@ use crate::{
/// # Fields
/// * `matrix` - A matrix in reduced row echelon, whose number of columns is the dimension of the
/// ambient space and each row is a basis vector of the subspace.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[repr(transparent)]
pub struct Subspace {
matrix: Matrix,
Expand Down
25 changes: 21 additions & 4 deletions ext/crates/fp/src/vector/fp_wrapper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,22 +239,39 @@ impl<'a> IntoIterator for &'a FpVector {
impl_from!();
impl_try_into!();

// `FpVector`'s serde format routes through `FqVector<Fp<ValidPrime>>`, giving a uniform
// representation across all primes: the prime is encoded in the data, so round-tripping works
// without out-of-band context. This is what the zarr save system relies on.
//
// Callers whose wire format must stay a flat `Vec<u32>` (e.g. the sseq_gui web frontend, which
// reads vectors as plain JS arrays) should declare their fields as `Vec<u32>` and convert at the
// boundary rather than relying on `FpVector`'s own serde impl.
impl Serialize for FpVector {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Vec::<u32>::from(self).serialize(serializer)
use crate::field::Fp;
let p = self.prime();
let fq = Fp::new(p);
let v = FqVector::from_raw_parts(fq, self.len(), self.limbs().to_vec());
v.serialize(serializer)
}
}

impl<'de> Deserialize<'de> for FpVector {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
panic!("Deserializing FpVector not supported");
// This is needed for ext-websocket/actions to be happy
use crate::field::{Field, Fp};
let v: FqVector<Fp<ValidPrime>> = FqVector::deserialize(deserializer)?;
let p = v.fq().characteristic();
// Reconstruct an `FpVector` by round-tripping through the binary limb format. The
// intermediate byte buffer is small and only allocated on deserialize.
let mut bytes = Vec::new();
v.to_bytes(&mut bytes).map_err(serde::de::Error::custom)?;
Self::from_bytes(p, v.len(), &mut &bytes[..]).map_err(serde::de::Error::custom)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down
42 changes: 41 additions & 1 deletion ext/crates/fp/src/vector/inner.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,59 @@
// This generates better llvm optimization
#![allow(clippy::int_plus_one)]

use serde::{Deserialize, Deserializer, Serialize};

use crate::{field::Field, limb::Limb};

/// A vector over a finite field.
///
/// Interally, it packs entries of the vectors into limbs. However, this is an abstraction that must
/// not leave the `fp` library.
#[derive(Debug, Hash, Eq, PartialEq, Clone)]
#[derive(Debug, Hash, Eq, PartialEq, Clone, Serialize)]
pub struct FqVector<F: Field> {
fq: F,
len: usize,
limbs: Vec<Limb>,
}

// `Deserialize` is implemented manually rather than derived so that we can validate the
// invariant `limbs.len() == fq.number(len)`. Without this check, malformed input that supplies
// too few limbs would build an `FqVector` whose internal accessors (`entry`, `to_bytes`, etc.)
// later panic on bounds-checked slice indexing. With it, malformed input surfaces as a normal
// serde error from the `Deserialize` impl, which is the contract callers expect.
impl<'de, F: Field + Deserialize<'de>> Deserialize<'de> for FqVector<F> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;

#[derive(Deserialize)]
#[serde(bound(deserialize = "F: Deserialize<'de>"))]
struct Raw<F> {
fq: F,
len: usize,
limbs: Vec<Limb>,
}

let raw = Raw::<F>::deserialize(deserializer)?;
let expected = raw.fq.number(raw.len);
if raw.limbs.len() != expected {
return Err(D::Error::custom(format!(
"FqVector limbs length {} does not match expected {} for len={}",
raw.limbs.len(),
expected,
raw.len,
)));
}
Ok(Self {
fq: raw.fq,
len: raw.len,
limbs: raw.limbs,
})
}
}

/// A slice of an `FqVector`.
///
/// This immutably borrows the vector and implements `Copy`.
Expand Down
Loading
Loading