From 9bf1ecf751c710d5e94b3a0262776732dcd2f235 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Tue, 29 Sep 2026 16:25:20 +0100 Subject: [PATCH 01/11] Re-add dynamic field (scaffolding) --- Cargo.lock | 1 + Cargo.toml | 1 + crates/common/src/lib.rs | 3 + crates/field/Cargo.toml | 1 + crates/field/src/codec.rs | 14 ++ crates/field/src/dynamic.rs | 363 +++++++++++++++++++++++++++++ crates/field/src/lib.rs | 1 + crates/transcript/src/challenge.rs | 53 +++++ tooling/cli/src/end_to_end.rs | 20 ++ 9 files changed, 457 insertions(+) create mode 100644 crates/field/src/dynamic.rs diff --git a/Cargo.lock b/Cargo.lock index 995b774b..3ca0cdb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -676,6 +676,7 @@ dependencies = [ "divan", "flock-core", "num-traits", + "pastey", "rand 0.10.2", "rand_core 0.10.1", "rand_pcg 0.10.2", diff --git a/Cargo.toml b/Cargo.toml index 73469b3c..831ed0ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ divan = "0.1.21" flock-core = { git = "https://github.com/succinctlabs/flock.git", rev = "879072249e52b8b9054bf0c6a034cec20f8f6fc7" } num-bigint = "0.4" num-traits = "0.2" +pastey = "0.2.3" proptest = "1.11.0" rand = "0.10" rand_chacha = "0.10" diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 7486d7d5..4e382a3e 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -89,6 +89,7 @@ define_blanket_trait! { #[cfg(test)] mod tests { use super::*; + use field::dynamic::DynField; use field::{F128, FqDefault}; #[test] @@ -105,8 +106,10 @@ mod tests { fn assert_impl_field() {} assert_impl_field::(); assert_impl_field::(); + assert_impl_field::(); fn assert_impl_claim_field() {} assert_impl_claim_field::(); + assert_impl_claim_field::(); } } diff --git a/crates/field/Cargo.toml b/crates/field/Cargo.toml index 13807f98..d053101c 100644 --- a/crates/field/Cargo.toml +++ b/crates/field/Cargo.toml @@ -12,6 +12,7 @@ crypto-primitives = { workspace = true } crypto-primitives-proc-macros = { workspace = true } rand = { workspace = true, optional = true } spongefish = { workspace = true, optional = true } +pastey = { workspace = true } [features] rand = ["dep:rand"] diff --git a/crates/field/src/codec.rs b/crates/field/src/codec.rs index ab301619..e6655e52 100644 --- a/crates/field/src/codec.rs +++ b/crates/field/src/codec.rs @@ -17,6 +17,7 @@ use spongefish::{ ByteArray, Decoding, Encoding, NargDeserialize, VerificationError, VerificationResult, }; +use crate::dynamic::DynField; use crate::{F128, Fq}; impl Encoding<[u8]> for F128 { @@ -59,6 +60,19 @@ impl NargDeserialize for Fq { } } +impl Encoding<[u8]> for DynField { + fn encode(&self) -> impl AsRef<[u8]> { + todo!(); + [0] + } +} + +impl NargDeserialize for DynField { + fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { + todo!() + } +} + #[cfg(test)] mod tests { use num_traits::{ConstOne, ConstZero}; diff --git a/crates/field/src/dynamic.rs b/crates/field/src/dynamic.rs new file mode 100644 index 00000000..e2730385 --- /dev/null +++ b/crates/field/src/dynamic.rs @@ -0,0 +1,363 @@ +//! Field with modulus not known at compile-time. +//! +//! We only expect to have one of those at any given time, so the modulus is shared globally. + +use crypto_primitives::{BaseField, LiftElement, WithAssociatedInteger}; +use crypto_primitives_proc_macros::InfallibleCheckedOp; +use num_traits::{ + Bounded, CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedSub, ConstOne, ConstZero, Inv, + One, Pow, Zero, +}; +use pastey::paste; +use std::fmt::Display; +use std::iter::{Product, Sum}; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; +use std::sync::{Arc, LazyLock, Mutex, RwLock, RwLockReadGuard}; + +static CFG: LazyLock> = LazyLock::new(|| { + RwLock::new(DynFieldConfig { + initialized: false, + modulus: 0, + }) +}); + +/// Global config for [`DynField`] shared among all instances. +#[derive(Debug, Copy, Clone)] +pub struct DynFieldConfig { + initialized: bool, + modulus: u128, +} + +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, InfallibleCheckedOp)] +#[infallible_checked_unary_op((CheckedNeg, neg))] +#[infallible_checked_binary_op((CheckedAdd, add), (CheckedSub, sub), (CheckedMul, mul))] +#[repr(transparent)] +pub struct DynField { + reduced_value: u128, +} + +impl DynField { + /// Set modulus globally. Marked `unsafe` to emphasize that there should be no [`DynField`] instances + /// remaining anywhere, or they will silently become invalid. + pub unsafe fn set_modulus(modulus: u128) { + let mut cfg = CFG + .write() + .expect("Failed to acquire write lock on DynFieldConfig"); + cfg.initialized = true; + cfg.modulus = modulus; + } +} + +// +// Core traits +// + +impl Display for DynField { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} (mod {})", self.reduced_value, DynField::modulus()) + } +} + +// +// Zero and One traits +// + +impl Zero for DynField { + #[inline(always)] + fn zero() -> Self { + Self::ZERO + } + + #[inline(always)] + fn is_zero(&self) -> bool { + self.reduced_value == 0 + } +} + +impl One for DynField { + #[inline(always)] + fn one() -> Self { + Self::ONE + } +} + +impl ConstZero for DynField { + const ZERO: Self = DynField { reduced_value: 0 }; +} + +impl ConstOne for DynField { + const ONE: Self = DynField { reduced_value: 1 }; +} + +// +// Basic arithmetic operations +// + +impl Neg for DynField { + type Output = Self; + + fn neg(self) -> Self::Output { + todo!() + } +} + +macro_rules! impl_basic_op_forward_to_assign { + ($trait:ident, $method:ident, $assign_method:ident) => { + paste! { + impl $trait for DynField { + type Output = DynField; + + #[inline(always)] + fn $method(mut self, rhs: DynField) -> Self::Output { + [<$trait Assign>]::$assign_method(&mut self, rhs); + self + } + } + + impl $trait<&Self> for DynField { + type Output = DynField; + + #[inline(always)] + fn $method(mut self, rhs: &DynField) -> Self::Output { + [<$trait Assign>]::$assign_method(&mut self, rhs); + self + } + } + + impl $trait for &DynField { + type Output = DynField; + + #[inline(always)] + fn $method(self, rhs: DynField) -> Self::Output { + (*self).$method(rhs) + } + } + + impl $trait for &DynField { + type Output = DynField; + + #[inline(always)] + fn $method(self, rhs: &DynField) -> Self::Output { + (*self).$method(*rhs) + } + } + } + }; +} + +impl_basic_op_forward_to_assign!(Add, add, add_assign); +impl_basic_op_forward_to_assign!(Sub, sub, sub_assign); +impl_basic_op_forward_to_assign!(Mul, mul, mul_assign); +impl_basic_op_forward_to_assign!(Div, div, div_assign); + +impl Pow for DynField { + type Output = Self; + + fn pow(self, exp: u32) -> Self::Output { + todo!() + } +} + +impl Pow for DynField { + type Output = Self; + + fn pow(self, exp: u128) -> Self::Output { + todo!() + } +} + +impl Pow<&u128> for DynField { + type Output = Self; + + fn pow(self, exp: &u128) -> Self::Output { + self.pow(*exp) + } +} + +impl Inv for DynField { + type Output = Option; + + #[inline(always)] + fn inv(self) -> Self::Output { + todo!() + } +} + +// +// Checked arithmetic operations +// (Note: Field operations do not overflow) +// + +impl CheckedDiv for DynField { + #[allow(clippy::arithmetic_side_effects)] // False alert + fn checked_div(&self, rhs: &Self) -> Option { + Some(self * Inv::inv(*rhs)?) + } +} + +// +// Arithmetic assign operations +// + +macro_rules! impl_op_assign_boilerplate { + ($trait:ident, $method:ident) => { + impl<'a> $trait<&'a DynField> for DynField { + #[inline(always)] + fn $method(&mut self, rhs: &'a DynField) { + self.$method(*rhs); + } + } + }; +} + +impl_op_assign_boilerplate!(AddAssign, add_assign); +impl_op_assign_boilerplate!(SubAssign, sub_assign); +impl_op_assign_boilerplate!(MulAssign, mul_assign); +impl_op_assign_boilerplate!(DivAssign, div_assign); + +impl AddAssign for DynField { + #[inline(always)] + fn add_assign(&mut self, rhs: Self) { + todo!() + } +} + +impl SubAssign for DynField { + #[inline(always)] + fn sub_assign(&mut self, rhs: Self) { + todo!() + } +} + +impl MulAssign for DynField { + #[inline(always)] + fn mul_assign(&mut self, rhs: Self) { + todo!() + } +} + +impl DivAssign for DynField { + #[inline(always)] + fn div_assign(&mut self, rhs: Self) { + todo!() + } +} + +// +// Aggregate operations +// + +impl Sum for DynField { + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |acc, x| acc + x) + } +} + +impl<'a> Sum<&'a Self> for DynField { + #[allow(clippy::arithmetic_side_effects)] // False alert + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |acc, x| acc + x) + } +} + +impl Product for DynField { + #[allow(clippy::arithmetic_side_effects)] // False alert + fn product>(iter: I) -> Self { + iter.fold(Self::ONE, |acc, x| acc * x) + } +} + +impl<'a> Product<&'a Self> for DynField { + #[allow(clippy::arithmetic_side_effects)] // False alert + fn product>(iter: I) -> Self { + iter.fold(Self::ONE, |acc, x| acc * x) + } +} + +// +// Conversions +// + +impl From for DynField { + fn from(value: bool) -> Self { + if value { Self::ONE } else { Self::ZERO } + } +} + +impl From<&u64> for DynField { + fn from(value: &u64) -> Self { + todo!() + } +} + +impl From for DynField { + fn from(value: u64) -> Self { + todo!() + } +} + +impl From<&u128> for DynField { + fn from(value: &u128) -> Self { + todo!() + } +} + +impl From for DynField { + fn from(value: u128) -> Self { + todo!() + } +} + +// TODO! + +// +// crypto-primitives +// + +impl Bounded for DynField { + #[inline(always)] + fn min_value() -> Self { + Self::ZERO + } + + #[inline(always)] + fn max_value() -> Self { + DynField { + reduced_value: DynField::modulus() - 1, + } + } +} + +impl BaseField for DynField { + fn modulus() -> Self::Integer { + let cfg = CFG + .read() + .expect("Failed to acquire read lock on DynFieldConfig"); + debug_assert!(cfg.initialized, "Field modulus has not been set yet!"); + cfg.modulus + } + + fn modulus_minus_one_div_two() -> Self::Integer { + todo!() + } +} + +impl WithAssociatedInteger for DynField { + type Integer = u128; +} + +impl LiftElement for DynField { + fn lift(&self) -> u128 { + self.reduced_value + } +} + +// +// Other +// + +#[cfg(test)] +mod tests { + use super::*; + use crypto_primitives::{BaseField, ConstField}; +} diff --git a/crates/field/src/lib.rs b/crates/field/src/lib.rs index 5db31f2f..752f8d68 100644 --- a/crates/field/src/lib.rs +++ b/crates/field/src/lib.rs @@ -2,6 +2,7 @@ #[cfg(feature = "spongefish")] mod codec; +pub mod dynamic; pub mod fq; pub mod gf128; diff --git a/crates/transcript/src/challenge.rs b/crates/transcript/src/challenge.rs index 23413838..b651f794 100644 --- a/crates/transcript/src/challenge.rs +++ b/crates/transcript/src/challenge.rs @@ -1,6 +1,7 @@ //! Typed Fiat–Shamir challenge sampling. use crate::{ProverState, VerifierState}; +use field::dynamic::DynField; use field::{F128, Fq}; /// A type that knows how to construct itself from transcript squeezes. @@ -53,6 +54,39 @@ impl TranscriptChallenge for Fq { } } +/// A `bits`-bit prime from successive `u128` squeezes, the same on both +/// sides: the fingerprint prime of 5. "An end-to-end BitZ-based SNARK over +/// any finitely generated ring", Step 2, drawn once per proof after the +/// commitment and the statement are absorbed and before the PIOP. +/// +/// TODO(random-prime): draw it. Uniform among the primes in +/// `[2^(bits-1), 2^bits)`: rejection-sample odd candidates from `next_u128` +/// and test them by Miller-Rabin with bases squeezed from the same stream, as +/// `ext_proj::sample_prime_context` does in `f2z-pcs`. Until then the fixed +/// modulus is returned and nothing is squeezed. +pub fn prime_from_squeezes(_next_u128: impl FnMut() -> u128, bits: u32) -> u128 { + assert_eq!( + bits, + field::FqDefault::BITS, + "the placeholder draw only knows the fixed modulus" + ); + field::Q100 +} + +impl ProverState { + /// Squeezes a `bits`-bit prime from this transcript. + pub fn squeeze_prime(&mut self, bits: u32) -> u128 { + prime_from_squeezes(|| self.verifier_message::(), bits) + } +} + +impl VerifierState<'_> { + /// Squeezes a `bits`-bit prime from this transcript. + pub fn squeeze_prime(&mut self, bits: u32) -> u128 { + prime_from_squeezes(|| self.verifier_message::(), bits) + } +} + impl TranscriptChallenge for F128 { fn from_squeezes(mut next_u128: impl FnMut() -> u128) -> Self { // Every 128-bit string is exactly one binary-field element. @@ -60,6 +94,12 @@ impl TranscriptChallenge for F128 { } } +impl TranscriptChallenge for DynField { + fn from_squeezes(next_u128: impl FnMut() -> u128) -> Self { + todo!() + } +} + #[cfg(test)] mod tests { use field::{F128, Q100}; @@ -107,6 +147,19 @@ mod tests { verifier.check_eof().unwrap(); } + #[test] + fn prover_and_verifier_squeeze_the_same_prime() { + let mut prover = build_prover(SESSION, INSTANCE); + let prime = prover.squeeze_prime(F::BITS); + let after = prover.squeeze::(); + let proof = prover.finish(); + + let mut verifier = build_verifier(SESSION, INSTANCE, &proof); + assert_eq!(verifier.squeeze_prime(F::BITS), prime); + assert_eq!(verifier.squeeze::(), after); + verifier.check_eof().unwrap(); + } + #[test] fn typed_f128_squeeze_matches_direct_decoding() { let mut typed = build_prover(SESSION, INSTANCE); diff --git a/tooling/cli/src/end_to_end.rs b/tooling/cli/src/end_to_end.rs index 6ddf1f9c..ce598178 100644 --- a/tooling/cli/src/end_to_end.rs +++ b/tooling/cli/src/end_to_end.rs @@ -29,6 +29,16 @@ use spartan::{ const SESSION: &[u8] = b"bitz/circuit-e2e/v1"; const WINDOW: u32 = 8; +/// Width of the fingerprint prime: that of `F`'s modulus until the draw exists. +/// +/// TODO(random-prime): derive it from the claim shape as `f2z-pcs` does, +/// `min(113, 128 - log_rows)` with the top of the interval capped by the fold +/// gate of [`BitZParams`]. +fn prime_bits() -> u32 { + todo!() + //u128::BITS - F::modulus().leading_zeros() +} + /// A trusted, deterministic circuit and its public inputs. Implementations must /// emit identical operations for symbolic and concrete backends and constrain /// every public input/output. The proved constraints are interpreted modulo @@ -261,6 +271,11 @@ where ) .map_err(Error::ConstantProve)?; } + // TODO(random-prime): bind the integer constraint digest before the + // draw, then build the parameters, matrices and witness under `prime`. + let prime = transcript.squeeze_prime(prime_bits::()); + let prime = F::Integer::from(prime); + debug_assert_eq!(prime, F::modulus()); let (spartan, terminal) = prove_spartan_piop( &mut transcript, &self.matrices, @@ -316,6 +331,11 @@ where ) .map_err(Error::ConstantVerify)?; } + // TODO(random-prime): as in `prove`, the parameters and matrices must + // be built under `prime`. + let prime = transcript.squeeze_prime(prime_bits::()); + let prime = F::Integer::from(prime); + debug_assert_eq!(prime, F::modulus()); let terminal = verify_spartan_proof(&mut transcript, &self.matrices, &proof.spartan) .map_err(Error::Spartan)?; let claim = opening_claim(&self.params, &terminal)?; From efeb8a0fb7ff0f2ac9f92a9589d4d73238b6d017 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Wed, 30 Sep 2026 06:56:02 +0100 Subject: [PATCH 02/11] Dynamic field implementation --- crates/common/src/lib.rs | 4 +- crates/field/src/codec.rs | 67 +++++- crates/field/src/dynamic.rs | 368 +++++++++++++++++++++++++---- crates/field/src/fq.rs | 250 +------------------- crates/field/src/helpers.rs | 251 ++++++++++++++++++++ crates/field/src/lib.rs | 2 + crates/transcript/src/challenge.rs | 57 +++-- 7 files changed, 685 insertions(+), 314 deletions(-) create mode 100644 crates/field/src/helpers.rs diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 4e382a3e..726f5138 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -17,7 +17,9 @@ pub use fold::{ Fold, FoldError, column_images, fold_column, fold_columns, reconstruct, row_images, }; pub use opening::OpeningQuery; -pub use params::{BitZParams, ParamsError, VirtualParams, VirtualParamsError}; +pub use params::{ + BitZParams, MIN_PRIME_BITS, ParamsError, VirtualParams, VirtualParamsError, prime_bits, +}; pub use shape::{Shape, ShapeError}; pub use table::{BitTable, TableError, TransposeError, TransposedBitTable}; pub use virtual_map::{ diff --git a/crates/field/src/codec.rs b/crates/field/src/codec.rs index e6655e52..816762e5 100644 --- a/crates/field/src/codec.rs +++ b/crates/field/src/codec.rs @@ -9,10 +9,11 @@ //! rejects anything at or above `Q`, so each element has exactly one wire //! form. No `Decoding`: `Fq` is never sampled here, and sampling it would //! need a wider squeeze to kill the modular bias. +//! - `DynField`: as `Fq`, against the installed modulus. //! //! `NargSerialize` comes from spongefish's blanket impl over `Encoding`. -use crypto_primitives::LiftElement; +use crypto_primitives::{BaseField, LiftElement}; use spongefish::{ ByteArray, Decoding, Encoding, NargDeserialize, VerificationError, VerificationResult, }; @@ -46,30 +47,34 @@ impl Encoding<[u8]> for Fq { } } +/// Deserialized `u128` from 16 little-endian bytes, checking that it's below given modulus. +fn deserialized_reduced(buf: &mut &[u8], modulus: u128) -> VerificationResult { + // Stage the cursor: the contract requires `buf` untouched on failure, + // and the range check can still fail after the read succeeds. + let mut rest = *buf; + let value = u128::from_le_bytes(<[u8; 16]>::deserialize_from_narg(&mut rest)?); + if value >= modulus { + return Err(VerificationError); + } + *buf = rest; + Ok(value) +} + impl NargDeserialize for Fq { fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { - // Stage the cursor: the contract requires `buf` untouched on failure, - // and the range check can still fail after the read succeeds. - let mut rest = *buf; - let value = u128::from_le_bytes(<[u8; 16]>::deserialize_from_narg(&mut rest)?); - if value >= Q { - return Err(VerificationError); - } - *buf = rest; - Ok(Self::from(value)) + deserialized_reduced(buf, Q).map(Self::from) } } impl Encoding<[u8]> for DynField { fn encode(&self) -> impl AsRef<[u8]> { - todo!(); - [0] + self.lift().to_le_bytes() } } impl NargDeserialize for DynField { fn deserialize_from_narg(buf: &mut &[u8]) -> VerificationResult { - todo!() + deserialized_reduced(buf, DynField::modulus()).map(Self::from) } } @@ -79,6 +84,7 @@ mod tests { use spongefish::NargSerialize; use super::*; + use crate::dynamic::test_support::with_modulus; use crate::{FqDefault, Q100}; fn f128_cases() -> [F128; 4] { @@ -156,4 +162,39 @@ mod tests { assert_eq!(buf, narg, "cursor must not move on failure"); } } + + #[test] + fn dyn_field_wire_form_is_fq_wire_form() { + with_modulus(Q100, || { + for v in [0u128, 1, 12345, Q100 - 1] { + let a = DynField::from(v); + assert_eq!(a.encode().as_ref(), FqDefault::from(v).encode().as_ref()); + assert_eq!(a.encode().as_ref(), v.to_le_bytes()); + + let mut narg = Vec::new(); + a.serialize_into_narg(&mut narg); + narg.extend_from_slice(b"tail"); + + let mut buf = narg.as_slice(); + assert_eq!(DynField::deserialize_from_narg(&mut buf).unwrap(), a); + assert_eq!(buf, b"tail"); + } + }); + } + + #[test] + fn dyn_field_narg_rejects_non_canonical() { + with_modulus(Q100, || { + for v in [Q100, Q100 + 1, u128::MAX] { + let narg = v.to_le_bytes(); + let mut buf = narg.as_slice(); + assert!(DynField::deserialize_from_narg(&mut buf).is_err()); + assert_eq!(buf, narg, "cursor must not move on failure"); + } + let narg = [0u8; 15]; + let mut buf = narg.as_slice(); + assert!(DynField::deserialize_from_narg(&mut buf).is_err()); + assert_eq!(buf, narg); + }); + } } diff --git a/crates/field/src/dynamic.rs b/crates/field/src/dynamic.rs index e2730385..5961b571 100644 --- a/crates/field/src/dynamic.rs +++ b/crates/field/src/dynamic.rs @@ -2,6 +2,7 @@ //! //! We only expect to have one of those at any given time, so the modulus is shared globally. +use crate::helpers; use crypto_primitives::{BaseField, LiftElement, WithAssociatedInteger}; use crypto_primitives_proc_macros::InfallibleCheckedOp; use num_traits::{ @@ -12,22 +13,87 @@ use pastey::paste; use std::fmt::Display; use std::iter::{Product, Sum}; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use std::sync::{Arc, LazyLock, Mutex, RwLock, RwLockReadGuard}; -static CFG: LazyLock> = LazyLock::new(|| { - RwLock::new(DynFieldConfig { - initialized: false, - modulus: 0, - }) -}); - -/// Global config for [`DynField`] shared among all instances. -#[derive(Debug, Copy, Clone)] +/// Global config for [`DynField`] shared among all instances: the modulus and +/// what its Barrett reduction precomputes from it. +#[derive(Debug, Clone, PartialEq, Eq)] pub struct DynFieldConfig { - initialized: bool, modulus: u128, + /// Bit length of the modulus. + bits: u32, + /// `floor(2^(2 * bits) / modulus)`, Barrett's reciprocal. + mu: u128, +} + +impl DynFieldConfig { + /// Whether [`DynField::set_modulus`] has run. + fn initialized(&self) -> bool { + self.modulus != 0 + } + + pub fn modulus(&self) -> u128 { + debug_assert!(self.initialized()); + self.modulus + } + + /// Bit length of the modulus. + pub fn bits(&self) -> u32 { + debug_assert!(self.initialized()); + self.bits + } +} + +/// The config, one atomic per word. +/// +/// Written only by [`DynField::set_modulus`], read by every operation. Its +/// contract rules out a writer concurrent with a reader, so the words need +/// neither a lock nor an ordering among them: `Relaxed` loads are plain +/// loads, and every thread reads the same cache line without writing it. +mod global { + use super::DynFieldConfig; + use std::sync::atomic::{AtomicU32, AtomicU64, Ordering::Relaxed}; + + static MODULUS_LO: AtomicU64 = AtomicU64::new(0); + static MODULUS_HI: AtomicU64 = AtomicU64::new(0); + static MU_LO: AtomicU64 = AtomicU64::new(0); + static MU_HI: AtomicU64 = AtomicU64::new(0); + static BITS: AtomicU32 = AtomicU32::new(0); + + #[inline(always)] + fn load_u128(lo: &AtomicU64, hi: &AtomicU64) -> u128 { + u128::from(lo.load(Relaxed)) | (u128::from(hi.load(Relaxed)) << 64) + } + + fn store_u128(lo: &AtomicU64, hi: &AtomicU64, value: u128) { + lo.store(value as u64, Relaxed); + hi.store((value >> 64) as u64, Relaxed); + } + + #[inline(always)] + pub(super) fn modulus() -> u128 { + load_u128(&MODULUS_LO, &MODULUS_HI) + } + + #[inline(always)] + pub(super) fn load() -> DynFieldConfig { + DynFieldConfig { + modulus: modulus(), + bits: BITS.load(Relaxed), + mu: load_u128(&MU_LO, &MU_HI), + } + } + + pub(super) fn store(cfg: &DynFieldConfig) { + store_u128(&MODULUS_LO, &MODULUS_HI, cfg.modulus); + store_u128(&MU_LO, &MU_HI, cfg.mu); + BITS.store(cfg.bits, Relaxed); + } } +/// An element of `Z/qZ` for the installed `q`, held reduced. +/// +/// The modulus is process-wide, so values made under different moduli are +/// the same Rust type; keeping them apart is the caller's job. #[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, InfallibleCheckedOp)] #[infallible_checked_unary_op((CheckedNeg, neg))] #[infallible_checked_binary_op((CheckedAdd, add), (CheckedSub, sub), (CheckedMul, mul))] @@ -37,14 +103,38 @@ pub struct DynField { } impl DynField { - /// Set modulus globally. Marked `unsafe` to emphasize that there should be no [`DynField`] instances - /// remaining anywhere, or they will silently become invalid. + /// Set modulus globally. + /// + /// The modulus must be an odd prime below `2^126`, the bound of the + /// Barrett reduction shared with [`Fq`](crate::Fq); anything else panics. + /// + /// # Safety + /// + /// No [`DynField`] value may be alive, or they will silently become invalid. + /// No [`DynField`] operation may be in flight on another thread: the config + /// is stored word by word, and a value keeps the representative it had under + /// the previous modulus. pub unsafe fn set_modulus(modulus: u128) { - let mut cfg = CFG - .write() - .expect("Failed to acquire write lock on DynFieldConfig"); - cfg.initialized = true; - cfg.modulus = modulus; + assert!(modulus >= 3, "modulus must be at least 3"); + assert_eq!(modulus % 2, 1, "modulus must be odd"); + assert!( + modulus < 1u128 << helpers::MAX_MODULUS_BITS, + "modulus must be below 2^126" + ); + // `DynField` claims to be a prime field, so it enforces that itself + // rather than trusting whoever drew the modulus. + assert!(helpers::is_prime(modulus), "modulus must be prime"); + let bits = u128::BITS - modulus.leading_zeros(); + let mu = helpers::barrett_mu(modulus, bits); + global::store(&DynFieldConfig { modulus, bits, mu }); + } + + /// The installed modulus and its reduction constants. + #[inline(always)] + pub fn config() -> DynFieldConfig { + let cfg = global::load(); + debug_assert!(cfg.initialized(), "Field modulus has not been set yet!"); + cfg } } @@ -96,8 +186,15 @@ impl ConstOne for DynField { impl Neg for DynField { type Output = Self; + #[inline(always)] fn neg(self) -> Self::Output { - todo!() + if self.is_zero() { + self + } else { + DynField { + reduced_value: Self::modulus() - self.reduced_value, + } + } } } @@ -150,36 +247,37 @@ impl_basic_op_forward_to_assign!(Sub, sub, sub_assign); impl_basic_op_forward_to_assign!(Mul, mul, mul_assign); impl_basic_op_forward_to_assign!(Div, div, div_assign); +// Required by `crypto_primitives::Field`, not implemented yet. + impl Pow for DynField { type Output = Self; - fn pow(self, exp: u32) -> Self::Output { - todo!() + fn pow(self, _exp: u32) -> Self::Output { + unimplemented!("exponentiation is not implemented yet") } } impl Pow for DynField { type Output = Self; - fn pow(self, exp: u128) -> Self::Output { - todo!() + fn pow(self, _exp: u128) -> Self::Output { + unimplemented!("exponentiation is not implemented yet") } } impl Pow<&u128> for DynField { type Output = Self; - fn pow(self, exp: &u128) -> Self::Output { - self.pow(*exp) + fn pow(self, _exp: &u128) -> Self::Output { + unimplemented!("exponentiation is not implemented yet") } } impl Inv for DynField { type Output = Option; - #[inline(always)] fn inv(self) -> Self::Output { - todo!() + unimplemented!("inversion is not implemented yet") } } @@ -218,28 +316,36 @@ impl_op_assign_boilerplate!(DivAssign, div_assign); impl AddAssign for DynField { #[inline(always)] fn add_assign(&mut self, rhs: Self) { - todo!() + let modulus = Self::modulus(); + // SAFETY: Both operands are below `modulus < 2^126`, so the sum cannot wrap. + let sum = unsafe { self.reduced_value.unchecked_add(rhs.reduced_value) }; + self.reduced_value = if sum >= modulus { sum - modulus } else { sum }; } } impl SubAssign for DynField { #[inline(always)] fn sub_assign(&mut self, rhs: Self) { - todo!() + self.reduced_value = if self.reduced_value >= rhs.reduced_value { + self.reduced_value - rhs.reduced_value + } else { + self.reduced_value + Self::modulus() - rhs.reduced_value + }; } } impl MulAssign for DynField { #[inline(always)] fn mul_assign(&mut self, rhs: Self) { - todo!() + let cfg = Self::config(); + let (lo, hi) = helpers::mul_wide(self.reduced_value, rhs.reduced_value); + self.reduced_value = helpers::barrett_reduce(lo, hi, cfg.modulus, cfg.mu, cfg.bits); } } impl DivAssign for DynField { - #[inline(always)] - fn div_assign(&mut self, rhs: Self) { - todo!() + fn div_assign(&mut self, _rhs: Self) { + unimplemented!("division is not implemented yet") } } @@ -286,30 +392,32 @@ impl From for DynField { impl From<&u64> for DynField { fn from(value: &u64) -> Self { - todo!() + Self::from(*value) } } +/// Reduces its input, so any `u64` is accepted. impl From for DynField { fn from(value: u64) -> Self { - todo!() + Self::from(u128::from(value)) } } impl From<&u128> for DynField { fn from(value: &u128) -> Self { - todo!() + Self::from(*value) } } +/// Reduces its input, so any `u128` is accepted. impl From for DynField { fn from(value: u128) -> Self { - todo!() + DynField { + reduced_value: value % Self::modulus(), + } } } -// TODO! - // // crypto-primitives // @@ -329,16 +437,15 @@ impl Bounded for DynField { } impl BaseField for DynField { + #[inline(always)] fn modulus() -> Self::Integer { - let cfg = CFG - .read() - .expect("Failed to acquire read lock on DynFieldConfig"); - debug_assert!(cfg.initialized, "Field modulus has not been set yet!"); - cfg.modulus + let modulus = global::modulus(); + debug_assert!(modulus != 0, "Field modulus has not been set yet!"); + modulus } fn modulus_minus_one_div_two() -> Self::Integer { - todo!() + (Self::modulus() - 1) / 2 } } @@ -356,8 +463,177 @@ impl LiftElement for DynField { // Other // +#[cfg(test)] +pub(crate) mod test_support { + use super::DynField; + use std::sync::{Mutex, PoisonError}; + + /// Runs `test` under `modulus`. The modulus is process-wide and the test + /// harness is multi-threaded, so every test that needs one holds this + /// lock for its whole run. + pub(crate) fn with_modulus(modulus: u128, test: impl FnOnce() -> T) -> T { + static LOCK: Mutex<()> = Mutex::new(()); + let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner); + // SAFETY: the lock keeps every other test's values and operations out. + unsafe { DynField::set_modulus(modulus) }; + test() + } +} + #[cfg(test)] mod tests { + use super::test_support::with_modulus; use super::*; - use crypto_primitives::{BaseField, ConstField}; + use crate::{Fq, FqDefault, Q100}; + use crypto_primitives::{ConstField, WithExtensionDegree}; + use rand_core::{Rng, SeedableRng}; + use rand_pcg::Pcg64; + + /// A prime small enough to check every pair of operands. + const SMALL: u128 = 251; + /// A 114-bit prime. + const WIDE: u128 = (1 << 114) - 11; + + fn u128_of(rng: &mut Pcg64) -> u128 { + (rng.next_u64() as u128) << 64 | rng.next_u64() as u128 + } + + /// Every pair of `extremes`, then `count` random pairs below `q`. + fn operand_pairs<'a>( + extremes: &'a [u128], + q: u128, + count: usize, + rng: &'a mut Pcg64, + ) -> impl Iterator + 'a { + extremes + .iter() + .flat_map(move |&a| extremes.iter().map(move |&b| (a, b))) + .chain((0..count).map(move |_| (u128_of(rng) % q, u128_of(rng) % q))) + } + + #[test] + fn ensure_traits() { + fn assert_impl() {} + assert_impl::(); + } + + #[test] + #[should_panic(expected = "modulus must be odd")] + fn set_modulus_rejects_even() { + // SAFETY: rejected before anything is stored. + unsafe { DynField::set_modulus(Q100 + 1) }; + } + + #[test] + #[should_panic(expected = "modulus must be prime")] + fn set_modulus_rejects_composite() { + // SAFETY: rejected before anything is stored. + unsafe { DynField::set_modulus(((1u128 << 54) - 33) * ((1u128 << 53) - 111)) }; + } + + #[test] + #[should_panic(expected = "modulus must be below 2^126")] + fn set_modulus_rejects_the_barrett_bound() { + // SAFETY: rejected before anything is stored. + unsafe { DynField::set_modulus((1 << 127) - 1) }; + } + + #[test] + fn config_reports_the_installed_modulus() { + with_modulus(Q100, || { + let cfg = DynField::config(); + assert_eq!(cfg.modulus(), Q100); + assert_eq!(cfg.bits(), FqDefault::BITS); + assert_eq!(DynField::modulus(), Q100); + assert_eq!(DynField::modulus_minus_one_div_two(), (Q100 - 1) / 2); + assert_eq!(DynField::min_value(), DynField::ZERO); + assert_eq!(DynField::max_value().lift(), Q100 - 1); + assert_eq!(DynField::extension_degree(), 1); + assert_eq!(DynField::ONE.lift(), 1); + assert!(DynField::ZERO.is_zero()); + }); + } + + /// Every ring operation against `Fq`, whose Barrett is checked against + /// long division. + fn agrees_with_fq(rng: &mut Pcg64) { + with_modulus(Q, || { + for (a, b) in operand_pairs(&[0, 1, Q - 1, Q / 2], Q, 512, rng) { + let (x, y) = (DynField::from(a), DynField::from(b)); + let (fx, fy) = (Fq::::from(a), Fq::::from(b)); + assert_eq!((x + y).lift(), (fx + fy).lift(), "{a} + {b}"); + assert_eq!((x - y).lift(), (fx - fy).lift(), "{a} - {b}"); + assert_eq!((x * y).lift(), (fx * fy).lift(), "{a} * {b}"); + assert_eq!((-x).lift(), (-fx).lift(), "-{a}"); + } + }); + } + + #[test] + fn matches_the_compile_time_field() { + let mut rng = Pcg64::seed_from_u64(501); + agrees_with_fq::(&mut rng); + agrees_with_fq::(&mut rng); + agrees_with_fq::(&mut rng); + } + + /// The largest prime below the Barrett bound, where the quotient estimate + /// has the least slack, against the add-and-double multiply, which shares + /// nothing with Barrett. + #[test] + fn multiplies_at_the_widest_modulus() { + let mut q = (1u128 << 126) - 1; + while !helpers::is_prime(q) { + q -= 2; + } + let mut rng = Pcg64::seed_from_u64(502); + with_modulus(q, || { + for (a, b) in operand_pairs(&[0, 1, q - 1, q - 2, q / 2], q, 512, &mut rng) { + let got = (DynField::from(a) * DynField::from(b)).lift(); + assert_eq!(got, helpers::mul_mod(a, b, q), "{a} * {b}"); + } + }); + } + + #[test] + fn exhaustive_over_a_small_modulus() { + with_modulus(SMALL, || { + for a in 0..SMALL { + for b in 0..SMALL { + let (x, y) = (DynField::from(a), DynField::from(b)); + assert_eq!((x * y).lift(), a * b % SMALL, "{a} * {b}"); + assert_eq!((x + y).lift(), (a + b) % SMALL, "{a} + {b}"); + assert_eq!((x - y).lift(), (a + SMALL - b) % SMALL, "{a} - {b}"); + } + } + }); + } + + #[test] + fn from_reduces() { + with_modulus(Q100, || { + assert_eq!(DynField::from(Q100).lift(), 0); + assert_eq!(DynField::from(Q100 + 1).lift(), 1); + assert_eq!(DynField::from(u128::MAX).lift(), u128::MAX % Q100); + assert_eq!(DynField::from(&u128::MAX), DynField::from(u128::MAX)); + assert_eq!(DynField::from(u64::MAX).lift(), u128::from(u64::MAX)); + assert_eq!(DynField::from(&u64::MAX), DynField::from(u64::MAX)); + assert_eq!(DynField::from(true), DynField::ONE); + assert_eq!(DynField::from(false), DynField::ZERO); + }); + } + + #[test] + fn values_follow_the_installed_modulus() { + with_modulus(SMALL, || { + let seven = DynField::from(7u64); + assert_eq!((seven * seven * seven).lift(), 343 % SMALL); + // SAFETY: nothing made under `SMALL` is used past here; the lock + // is held. + unsafe { DynField::set_modulus(Q100) }; + let seven = DynField::from(7u64); + assert_eq!((seven * seven * seven).lift(), 343); + assert_eq!(DynField::from(SMALL).lift(), SMALL); + }); + } } diff --git a/crates/field/src/fq.rs b/crates/field/src/fq.rs index ca677026..36432216 100644 --- a/crates/field/src/fq.rs +++ b/crates/field/src/fq.rs @@ -1,5 +1,6 @@ //! Arithmetic modulo a prime fixed at compile time. +use crate::helpers; use crypto_primitives::{ConstBaseField, LiftElement, WithAssociatedInteger}; use crypto_primitives_proc_macros::InfallibleCheckedOp; use num_traits::{ @@ -35,151 +36,6 @@ pub type FqDefault = Fq; #[infallible_checked_binary_op((CheckedAdd, add), (CheckedSub, sub), (CheckedMul, mul))] pub struct Fq(u128); -/// Full 128x128 -> 256-bit product as `(low, high)`. -const fn mul_wide(a: u128, b: u128) -> (u128, u128) { - let (a_lo, a_hi) = (a as u64 as u128, a >> 64); - let (b_lo, b_hi) = (b as u64 as u128, b >> 64); - - let ll = a_lo * b_lo; - let hh = a_hi * b_hi; - let (mid, mid_carry) = (a_lo * b_hi).overflowing_add(a_hi * b_lo); - - let (lo, lo_carry) = ll.overflowing_add(mid << 64); - let hi = hh + (mid >> 64) + ((mid_carry as u128) << 64) + lo_carry as u128; - (lo, hi) -} - -/// `(lo, hi) >> n` for `n < 128`, keeping the low 128 bits. Every use here -/// shifts far enough that nothing above them survives. -const fn shr_wide(lo: u128, hi: u128, n: u32) -> u128 { - if n == 0 { - lo - } else { - (lo >> n) | (hi << (128 - n)) - } -} - -/// `floor(2^(2k) / q)` by binary long division, `k` being `q`'s bit length. -/// -/// Barrett's precomputed reciprocal. The numerator has a single set bit, so -/// each step shifts the remainder up and subtracts `q` when it fits. -const fn barrett_mu(q: u128, k: u32) -> u128 { - let mut rem = 0u128; - let mut quo = 0u128; - let mut i = 2 * k; - loop { - rem = (rem << 1) | (i == 2 * k) as u128; - let fits = rem >= q; - if fits { - rem -= q; - } - quo = (quo << 1) | fits as u128; - if i == 0 { - return quo; - } - i -= 1; - } -} - -/// The first thirteen primes: the standard deterministic Miller-Rabin base set -/// below `2^81.4`. -const PRIMALITY_BASES: [u128; 13] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41]; - -/// `(a + b) mod q`, for `a, b < q < 2^126`. The sum stays below `2^127`. -const fn add_mod(a: u128, b: u128, q: u128) -> u128 { - let sum = a + b; - if sum >= q { sum - q } else { sum } -} - -/// `(a * b) mod q` by doubling, avoiding the 256-bit product a u128 cannot -/// hold. Barrett is not an option here: `MU` depends on `BITS`, which is the -/// constant this feeds. -const fn mul_mod(a: u128, b: u128, q: u128) -> u128 { - let mut result = 0u128; - let mut addend = a % q; - let mut remaining = b; - while remaining != 0 { - if remaining & 1 == 1 { - result = add_mod(result, addend, q); - } - addend = add_mod(addend, addend, q); - remaining >>= 1; - } - result -} - -/// `(base ^ exponent) mod q`, by square-and-multiply. -const fn pow_mod(base: u128, exponent: u128, q: u128) -> u128 { - let mut result = 1u128 % q; - let mut square = base % q; - let mut remaining = exponent; - while remaining != 0 { - if remaining & 1 == 1 { - result = mul_mod(result, square, q); - } - square = mul_mod(square, square, q); - remaining >>= 1; - } - result -} - -/// Whether `candidate` is prime. -/// -/// `const` because [`Fq`] asserts it on its own modulus, which makes a -/// composite `Q` a build failure rather than a type that quietly is not a -/// field. The loops are written out for the same reason: iterator combinators -/// are not available in a const context. -/// -/// # What this does not decide -/// -/// Miller-Rabin against a fixed base set is **proven** only for -/// `n < 3_317_044_064_679_887_385_961_981`, about `2^81.4`. Moduli above that -/// are not decided by any theorem here, and because the bases are public, -/// someone choosing `Q` could in principle construct a composite that passes -/// all of them. Since `Q` is a compile-time constant, doing so means editing -/// the source rather than forging a proof. -const fn is_prime(candidate: u128) -> bool { - if candidate < 2 { - return false; - } - - let mut index = 0; - while index < PRIMALITY_BASES.len() { - let base = PRIMALITY_BASES[index]; - if candidate == base { - return true; - } - if candidate.is_multiple_of(base) { - return false; - } - index += 1; - } - - // `candidate - 1 = odd * 2^shift`. - let shift = (candidate - 1).trailing_zeros(); - let odd = (candidate - 1) >> shift; - - let mut index = 0; - while index < PRIMALITY_BASES.len() { - let mut witness = pow_mod(PRIMALITY_BASES[index], odd, candidate); - if witness != 1 && witness != candidate - 1 { - let mut round = 1; - loop { - if round >= shift { - return false; - } - witness = mul_mod(witness, witness, candidate); - if witness == candidate - 1 { - break; - } - round += 1; - } - } - index += 1; - } - true -} - impl Fq { /// Bit length of the modulus, derived so it cannot disagree with `Q`. /// @@ -189,10 +45,13 @@ impl Fq { pub const BITS: u32 = { assert!(Q >= 3, "modulus must be at least 3"); assert!(Q % 2 == 1, "modulus must be odd"); - assert!(Q < 1u128 << 126, "modulus must be below 2^126"); + assert!( + Q < 1u128 << helpers::MAX_MODULUS_BITS, + "modulus must be below 2^126" + ); // `Fq` claims to be a prime field, so it enforces that itself rather // than trusting whoever names the constant. - assert!(is_prime(Q), "modulus must be prime"); + assert!(helpers::is_prime(Q), "modulus must be prime"); 128 - Q.leading_zeros() }; @@ -202,32 +61,11 @@ impl Fq { Self::from(u128::from(low) | (u128::from(high) << 64)) } - const MU: u128 = barrett_mu(Q, Self::BITS); + const MU: u128 = helpers::barrett_mu(Q, Self::BITS); - /// Barrett reduction, Handbook of Applied Cryptography Algorithm 14.42, - /// after Barrett, CRYPTO '86, LNCS 263:311-323: estimate the quotient - /// from the top bits of `x` and the precomputed reciprocal, subtract, - /// then correct. The estimate is never more than two too small, so two - /// conditional subtractions suffice. - /// - /// That algorithm is stated for a radix above 3 and takes the difference - /// modulo `b^(k+1)`, which is wide enough to hold `3Q`. Here the radix is - /// 2, where it is not, so the difference is taken modulo `2^128` instead. - /// Hence the bound on `Q`. + /// `(lo, hi) mod Q` by [`barrett_reduce`]. const fn reduce_wide(lo: u128, hi: u128) -> Self { - let k = Self::BITS; - let q1 = shr_wide(lo, hi, k - 1); - let (q2_lo, q2_hi) = mul_wide(q1, Self::MU); - let q3 = shr_wide(q2_lo, q2_hi, k + 1); - - let mut r = lo.wrapping_sub(mul_wide(q3, Q).0); - if r >= Q { - r -= Q; - } - if r >= Q { - r -= Q; - } - Self(r) + Self(helpers::barrett_reduce(lo, hi, Q, Self::MU, Self::BITS)) } } @@ -342,7 +180,7 @@ impl Sub for Fq { impl Mul for Fq { type Output = Self; fn mul(self, rhs: Self) -> Self { - let (lo, hi) = mul_wide(self.0, rhs.0); + let (lo, hi) = helpers::mul_wide(self.0, rhs.0); Self::reduce_wide(lo, hi) } } @@ -523,6 +361,7 @@ impl ConstBaseField for Fq { #[cfg(test)] mod tests { use super::*; + use crate::helpers::tests::mulmod_reference; use crypto_primitives::{BaseField, WithExtensionDegree}; use rand_core::{Rng, SeedableRng}; use rand_pcg::Pcg64; @@ -530,38 +369,6 @@ mod tests { /// A prime small enough to check every pair of operands. const SMALL: u128 = 251; - #[test] - fn primality_agrees_with_trial_division() { - for candidate in 0u128..2_000 { - let expected = candidate >= 2 - && (2..candidate) - .take_while(|d| d * d <= candidate) - .all(|d| !candidate.is_multiple_of(d)); - assert_eq!(is_prime(candidate), expected, "{candidate}"); - } - } - - #[test] - fn primality_rejects_what_a_fermat_test_would_admit() { - // Carmichael numbers pass every Fermat test; Miller-Rabin does not. - for carmichael in [561u128, 41_041, 825_265] { - assert!(!is_prime(carmichael), "{carmichael}"); - } - } - - #[test] - fn primality_holds_at_the_widths_the_moduli_use() { - assert!(is_prime((1 << 100) - 15)); - assert!(is_prime((1 << 108) - 59)); - assert!(is_prime((1 << 114) - 11)); - // A semiprime with no factor small enough for the trial-division pass. - assert!(!is_prime(((1u128 << 54) - 33) * ((1u128 << 53) - 111))); - // Every odd value between the largest prime below 2^114 and 2^114. - for offset in (1..11).step_by(2) { - assert!(!is_prime((1 << 114) - offset), "2^114 - {offset}"); - } - } - #[test] fn ensure_traits() { fn assert_impl() {} @@ -582,41 +389,6 @@ mod tests { (rng.next_u64() as u128) << 64 | rng.next_u64() as u128 } - /// `(hi:lo) mod q` by binary long division — the independent reference - /// Barrett is checked against. Structurally unlike Barrett, which - /// estimates a quotient and corrects. - fn mod_reference(lo: u128, hi: u128, q: u128) -> u128 { - let mut rem = 0u128; - for i in (0..256).rev() { - let bit = if i >= 128 { - (hi >> (i - 128)) & 1 - } else { - (lo >> i) & 1 - }; - rem = (rem << 1) | bit; - if rem >= q { - rem -= q; - } - } - rem - } - - fn mulmod_reference(a: u128, b: u128, q: u128) -> u128 { - let (lo, hi) = mul_wide(a, b); - mod_reference(lo, hi, q) - } - - #[test] - fn mul_wide_matches_u128_on_small_inputs() { - let mut rng = Pcg64::seed_from_u64(401); - for _ in 0..512 { - let (a, b) = (rng.next_u64() as u128, rng.next_u64() as u128); - assert_eq!(mul_wide(a, b), (a * b, 0)); - } - assert_eq!(mul_wide(u128::MAX, u128::MAX), (1, u128::MAX - 1)); - assert_eq!(mul_wide(1u128 << 127, 2), (0, 1)); - } - #[test] fn barrett_matches_long_division() { let mut rng = Pcg64::seed_from_u64(402); diff --git a/crates/field/src/helpers.rs b/crates/field/src/helpers.rs new file mode 100644 index 00000000..501042e7 --- /dev/null +++ b/crates/field/src/helpers.rs @@ -0,0 +1,251 @@ +//! Various helpers shared across multiple field implementations. + +/// The first thirteen primes: the standard deterministic Miller-Rabin base set +/// below `2^81.4`. +const PRIMALITY_BASES: [u128; 13] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41]; + +/// Whether `candidate` is prime. +/// +/// `const` because [`Fq`] asserts it on its own modulus, which makes a +/// composite `Q` a build failure rather than a type that quietly is not a +/// field. The loops are written out for the same reason: iterator combinators +/// are not available in a const context. +/// +/// # What this does not decide +/// +/// Miller-Rabin against a fixed base set is **proven** only for +/// `n < 3_317_044_064_679_887_385_961_981`, about `2^81.4`. Moduli above that +/// are not decided by any theorem here, and because the bases are public, +/// someone choosing `Q` could in principle construct a composite that passes +/// all of them. Since `Q` is a compile-time constant, doing so means editing +/// the source rather than forging a proof. +pub const fn is_prime(candidate: u128) -> bool { + if candidate < 2 { + return false; + } + + let mut index = 0; + while index < PRIMALITY_BASES.len() { + let base = PRIMALITY_BASES[index]; + if candidate == base { + return true; + } + if candidate.is_multiple_of(base) { + return false; + } + index += 1; + } + + // `candidate - 1 = odd * 2^shift`. + let shift = (candidate - 1).trailing_zeros(); + let odd = (candidate - 1) >> shift; + + let mut index = 0; + while index < PRIMALITY_BASES.len() { + let mut witness = pow_mod(PRIMALITY_BASES[index], odd, candidate); + if witness != 1 && witness != candidate - 1 { + let mut round = 1; + loop { + if round >= shift { + return false; + } + witness = mul_mod(witness, witness, candidate); + if witness == candidate - 1 { + break; + } + round += 1; + } + } + index += 1; + } + true +} + +/// `(a + b) mod q`, for `a, b < q < 2^126`. The sum stays below `2^127`. +pub const fn add_mod(a: u128, b: u128, q: u128) -> u128 { + let sum = a + b; + if sum >= q { sum - q } else { sum } +} + +/// `(a * b) mod q` by doubling, avoiding the 256-bit product a u128 cannot +/// hold. Barrett is not an option here: `MU` depends on `BITS`, which is the +/// constant this feeds. +pub const fn mul_mod(a: u128, b: u128, q: u128) -> u128 { + let mut result = 0u128; + let mut addend = a % q; + let mut remaining = b; + while remaining != 0 { + if remaining & 1 == 1 { + result = add_mod(result, addend, q); + } + addend = add_mod(addend, addend, q); + remaining >>= 1; + } + result +} + +/// `(base ^ exponent) mod q`, by square-and-multiply. +pub const fn pow_mod(base: u128, exponent: u128, q: u128) -> u128 { + let mut result = 1u128 % q; + let mut square = base % q; + let mut remaining = exponent; + while remaining != 0 { + if remaining & 1 == 1 { + result = mul_mod(result, square, q); + } + square = mul_mod(square, square, q); + remaining >>= 1; + } + result +} + +/// Full 128x128 -> 256-bit product as `(low, high)`. +pub const fn mul_wide(a: u128, b: u128) -> (u128, u128) { + let (a_lo, a_hi) = (a as u64 as u128, a >> 64); + let (b_lo, b_hi) = (b as u64 as u128, b >> 64); + + let ll = a_lo * b_lo; + let hh = a_hi * b_hi; + let (mid, mid_carry) = (a_lo * b_hi).overflowing_add(a_hi * b_lo); + + let (lo, lo_carry) = ll.overflowing_add(mid << 64); + let hi = hh + (mid >> 64) + ((mid_carry as u128) << 64) + lo_carry as u128; + (lo, hi) +} + +/// `(lo, hi) >> n` for `n < 128`, keeping the low 128 bits. Every use here +/// shifts far enough that nothing above them survives. +pub const fn shr_wide(lo: u128, hi: u128, n: u32) -> u128 { + if n == 0 { + lo + } else { + (lo >> n) | (hi << (128 - n)) + } +} + +/// `floor(2^(2k) / q)` by binary long division, `k` being `q`'s bit length. +/// +/// Barrett's precomputed reciprocal. The numerator has a single set bit, so +/// each step shifts the remainder up and subtracts `q` when it fits. +pub const fn barrett_mu(q: u128, k: u32) -> u128 { + let mut rem = 0u128; + let mut quo = 0u128; + let mut i = 2 * k; + loop { + rem = (rem << 1) | (i == 2 * k) as u128; + let fits = rem >= q; + if fits { + rem -= q; + } + quo = (quo << 1) | fits as u128; + if i == 0 { + return quo; + } + i -= 1; + } +} + +/// Every modulus is below `2^MAX_MODULUS_BITS`: the bound of +/// [`barrett_reduce`], which every prime field here reduces by. +pub const MAX_MODULUS_BITS: u32 = 126; + +/// `(lo, hi) mod q` for `q` of `k` bits and `mu` its reciprocal from +/// [`barrett_mu`]: Barrett reduction, Handbook of Applied Cryptography +/// Algorithm 14.42, after Barrett, CRYPTO '86, LNCS 263:311-323. Estimate the +/// quotient from the top bits of the input and the precomputed reciprocal, +/// subtract, then correct. The estimate is never more than two too small, so +/// two conditional subtractions suffice. +/// +/// That algorithm is stated for a radix above 3 and takes the difference +/// modulo `b^(k+1)`, which is wide enough to hold `3q`. Here the radix is 2, +/// where it is not, so the difference is taken modulo `2^128` instead. Hence +/// the bound `q < 2^126` on every modulus that comes through here. +#[inline] +pub const fn barrett_reduce(lo: u128, hi: u128, q: u128, mu: u128, k: u32) -> u128 { + let q1 = shr_wide(lo, hi, k - 1); + let (q2_lo, q2_hi) = mul_wide(q1, mu); + let q3 = shr_wide(q2_lo, q2_hi, k + 1); + + let mut r = lo.wrapping_sub(mul_wide(q3, q).0); + if r >= q { + r -= q; + } + if r >= q { + r -= q; + } + r +} + +#[cfg(test)] +pub mod tests { + use super::*; + use rand_core::{Rng, SeedableRng}; + use rand_pcg::Pcg64; + + #[test] + fn primality_agrees_with_trial_division() { + for candidate in 0u128..2_000 { + let expected = candidate >= 2 + && (2..candidate) + .take_while(|d| d * d <= candidate) + .all(|d| !candidate.is_multiple_of(d)); + assert_eq!(is_prime(candidate), expected, "{candidate}"); + } + } + + #[test] + fn primality_rejects_what_a_fermat_test_would_admit() { + // Carmichael numbers pass every Fermat test; Miller-Rabin does not. + for carmichael in [561u128, 41_041, 825_265] { + assert!(!is_prime(carmichael), "{carmichael}"); + } + } + + #[test] + fn primality_holds_at_the_widths_the_moduli_use() { + assert!(is_prime((1 << 100) - 15)); + assert!(is_prime((1 << 108) - 59)); + assert!(is_prime((1 << 114) - 11)); + // A semiprime with no factor small enough for the trial-division pass. + assert!(!is_prime(((1u128 << 54) - 33) * ((1u128 << 53) - 111))); + // Every odd value between the largest prime below 2^114 and 2^114. + for offset in (1..11).step_by(2) { + assert!(!is_prime((1 << 114) - offset), "2^114 - {offset}"); + } + } + + /// `(hi:lo) mod q` by binary long division — the independent reference + /// Barrett is checked against. Structurally unlike Barrett, which + /// estimates a quotient and corrects. + fn mod_reference(lo: u128, hi: u128, q: u128) -> u128 { + let mut rem = 0u128; + for i in (0..256).rev() { + let bit = if i >= 128 { + (hi >> (i - 128)) & 1 + } else { + (lo >> i) & 1 + }; + rem = (rem << 1) | bit; + if rem >= q { + rem -= q; + } + } + rem + } + + pub fn mulmod_reference(a: u128, b: u128, q: u128) -> u128 { + let (lo, hi) = mul_wide(a, b); + mod_reference(lo, hi, q) + } + + #[test] + fn mul_wide_matches_u128_on_small_inputs() { + let mut rng = Pcg64::seed_from_u64(401); + for _ in 0..512 { + let (a, b) = (rng.next_u64() as u128, rng.next_u64() as u128); + assert_eq!(mul_wide(a, b), (a * b, 0)); + } + assert_eq!(mul_wide(u128::MAX, u128::MAX), (1, u128::MAX - 1)); + assert_eq!(mul_wide(1u128 << 127, 2), (0, 1)); + } +} diff --git a/crates/field/src/lib.rs b/crates/field/src/lib.rs index 752f8d68..6993b6a8 100644 --- a/crates/field/src/lib.rs +++ b/crates/field/src/lib.rs @@ -5,6 +5,8 @@ mod codec; pub mod dynamic; pub mod fq; pub mod gf128; +pub(crate) mod helpers; pub use fq::{Fq, FqDefault, Q100}; pub use gf128::{F128, FixedBasePow, Wide256}; +pub use helpers::MAX_MODULUS_BITS; diff --git a/crates/transcript/src/challenge.rs b/crates/transcript/src/challenge.rs index b651f794..56f4606e 100644 --- a/crates/transcript/src/challenge.rs +++ b/crates/transcript/src/challenge.rs @@ -34,23 +34,29 @@ impl VerifierState<'_> { } } +/// Sample `u128` whose residue modulo `modulus` is uniform, from successive +/// squeezes. +/// +/// The u128 range is not generally an exact multiple of the modulus. Reject +/// its incomplete final interval before reducing so every residue has the +/// same number of preimages. +fn unbiased_u128(modulus: u128, mut next_u128: impl FnMut() -> u128) -> u128 { + let rejection_remainder = (u128::MAX % modulus + 1) % modulus; + let max_accepted = u128::MAX - rejection_remainder; + + loop { + let candidate = next_u128(); + if candidate <= max_accepted { + return candidate; + } + } +} + impl TranscriptChallenge for Fq { - fn from_squeezes(mut next_u128: impl FnMut() -> u128) -> Self { + fn from_squeezes(next_u128: impl FnMut() -> u128) -> Self { // Validate the const-generic modulus before using it below. let _ = Self::BITS; - - // The u128 range is not generally an exact multiple of Q. Reject its - // incomplete final interval before reducing so every residue has the - // same number of preimages. - let rejection_remainder = (u128::MAX % Q + 1) % Q; - let max_accepted = u128::MAX - rejection_remainder; - - loop { - let candidate = next_u128(); - if candidate <= max_accepted { - return Self::from(candidate); - } - } + Self::from(unbiased_u128(Q, next_u128)) } } @@ -96,13 +102,15 @@ impl TranscriptChallenge for F128 { impl TranscriptChallenge for DynField { fn from_squeezes(next_u128: impl FnMut() -> u128) -> Self { - todo!() + Self::from(unbiased_u128(DynField::config().modulus(), next_u128)) } } #[cfg(test)] mod tests { + use field::dynamic::DynField; use field::{F128, Q100}; + use spongefish::Encoding; use super::TranscriptChallenge; use crate::{build_prover, build_verifier}; @@ -131,6 +139,25 @@ mod tests { assert_eq!(challenge, F::from(Q100 - 1)); } + /// Under the same modulus, the same stream gives the same element. + #[test] + fn dyn_field_squeezes_as_fq() { + // SAFETY: process-wide, and the only modulus this binary installs. + unsafe { DynField::set_modulus(Q100) }; + + let mut typed = build_prover(SESSION, INSTANCE); + let dynamic = typed.squeeze::(); + let mut fixed = build_prover(SESSION, INSTANCE); + let fq = fixed.squeeze::(); + assert_eq!(dynamic.encode().as_ref(), fq.encode().as_ref()); + + let rejection_remainder = (u128::MAX % Q100 + 1) % Q100; + let max_accepted = u128::MAX - rejection_remainder; + let mut candidates = [max_accepted + 1, max_accepted].into_iter(); + let challenge = DynField::from_squeezes(|| candidates.next().unwrap()); + assert_eq!(challenge, DynField::from(Q100 - 1)); + } + #[test] fn prover_and_verifier_squeeze_in_lockstep() { let mut prover = build_prover(SESSION, INSTANCE); From c5541e9a5e82b601277bd6eef5aa73a966219b9a Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Wed, 30 Sep 2026 15:50:22 +0100 Subject: [PATCH 03/11] Define prime bits depending on the claim shape --- crates/common/src/params.rs | 109 +++++++++++++++++++++++++---- crates/transcript/src/challenge.rs | 9 ++- tooling/cli/src/end_to_end.rs | 19 ++--- 3 files changed, 107 insertions(+), 30 deletions(-) diff --git a/crates/common/src/params.rs b/crates/common/src/params.rs index 4eca2607..be6e297a 100644 --- a/crates/common/src/params.rs +++ b/crates/common/src/params.rs @@ -2,7 +2,7 @@ use crate::{BitTable, BitzClaimField, Shape, TableError, VirtualMap}; use field::{ - F128, + F128, FqDefault, MAX_MODULUS_BITS, gf128::{MULT_ORDER, is_generator}, }; use num_traits::{CheckedMul, ToBytes}; @@ -18,6 +18,35 @@ pub enum ParamsError { /// The generator's order is not the full group, so a fold is not the only /// exponent producing its image. GeneratorOrderNotFull, + /// The fold gate leaves the fingerprint prime narrower than + /// [`MIN_PRIME_BITS`]: the shape is too tall. + PrimeTooNarrow, +} + +/// The narrowest fingerprint prime a proof is run over: the width of the +/// default modulus. The PIOP's soundness error is of order `1/q`, so no shape +/// may push the prime below what the fixed modulus gave. +pub const MIN_PRIME_BITS: u32 = FqDefault::BITS; + +/// Width of the fingerprint prime for `shape`: the largest `bits` such that +/// every prime in `[2^(bits-1), 2^bits)` passes the fold gate of +/// [`BitZParams::new`], capped by the field's [`MAX_MODULUS_BITS`]. +/// +/// The prime is drawn from this interval by both sides, so the width must +/// be fixed by the shape. +/// +/// Wider is sounder, so the interval is the widest the gate admits. The gate +/// is the paper's `q < (|K| - 1) / k_1`, and with `k_1 = 2^log_rows` that is +/// exactly `128 - log_rows` bits. +pub fn prime_bits(shape: &Shape) -> Result { + // `rows * q < ord(g)` admits `q <= (ord(g) - 1) / rows`. + let max_modulus = (MULT_ORDER - 1) / shape.rows() as u128; + // `2^bits - 1 <= max_modulus < 2^(bits + 1) - 1`. + let bits = (max_modulus + 1).ilog2().min(MAX_MODULUS_BITS); + if bits < MIN_PRIME_BITS { + return Err(ParamsError::PrimeTooNarrow); + } + Ok(bits) } /// The shape, the modulus and the generator: what a proof is fixed against. @@ -41,17 +70,19 @@ impl BitZParams { // `g^{} = g^{eta_j}` implies integer equality only if both // sides, each in `[0, k_1 (Q - 1)]`, differ by less than `ord(g)`. - // The paper asks for `Q < (|K| - 1) / k_1`; the PoC's slightly stricter - // `(k_1 + 1)(Q - 1) < ord(g)` is kept, and it implies the paper's. + // The bound is the paper's `Q < (|K| - 1) / k_1`, under which Round 1 + // of 4.3. "The BitZ IOP for the core LinBitsRings relation" is + // skipped: `k_1 Q < ord(g)`. With `k_1` a power of two it admits every + // `(128 - log_rows)`-bit prime, so a prime of the width [`prime_bits`] + // derives always passes. // // `ord(g) = 2^128 - 1` is a property of `F128`, established by the // generator gate below. A product that overflows exceeds it too. let f128_order = F::Integer::from(MULT_ORDER); - let max_f = F::max_value().lift(); - let rows_plus_one = F::Integer::from(shape.rows() as u64 + 1); - if !max_f - .checked_mul(&rows_plus_one) - .is_some_and(|gap| gap < f128_order) + let rows = F::Integer::from(shape.rows() as u64); + if !rows + .checked_mul(&F::modulus()) + .is_some_and(|reach| reach < f128_order) { return Err(ParamsError::FoldBoundExceeded); } @@ -300,12 +331,11 @@ mod tests { #[test] fn rejects_a_shape_the_modulus_is_too_large_for() { - // `t = 13` is the widest row count this prime admits; 14 is not. The - // `+1` is what separates the two: `k_1 (Q - 1)` alone would still fit - // at `t = 14`. - assert!(params_at(Shape::new(13, 22).unwrap()).is_ok()); + // `t = 14` is the widest row count this prime admits: `2^14 Q114` is + // `2^128 - 11 * 2^14`, under `ord(g)`; `2^15 Q114` is not. + assert!(params_at(Shape::new(14, 21).unwrap()).is_ok()); assert_eq!( - params_at(Shape::new(14, 21).unwrap()).err(), + params_at(Shape::new(15, 20).unwrap()).err(), Some(ParamsError::FoldBoundExceeded) ); } @@ -318,6 +348,59 @@ mod tests { ); } + /// The gate of [`BitZParams::new`] at the top of a `bits`-bit interval. + fn gate_admits(bits: u32, shape: &Shape) -> bool { + let top = (1u128 << bits) - 1; + top.checked_mul(shape.rows() as u128) + .is_some_and(|reach| reach < MULT_ORDER) + } + + /// One shape per admissible row count. + fn shapes_by_rows() -> impl Iterator { + use crate::shape::{MAX_LOG_BITS, MIN_LOG_BITS, PACK_BITS}; + (PACK_BITS as usize..=MAX_LOG_BITS) + .map(|log_rows| Shape::new(log_rows, MIN_LOG_BITS.saturating_sub(log_rows)).unwrap()) + } + + #[test] + fn the_prime_width_is_the_widest_interval_the_gate_admits() { + for shape in shapes_by_rows() { + match prime_bits(&shape) { + Ok(bits) => { + assert!(bits >= MIN_PRIME_BITS, "{shape:?}"); + assert!(gate_admits(bits, &shape), "{shape:?}"); + assert!( + bits == MAX_MODULUS_BITS || !gate_admits(bits + 1, &shape), + "{shape:?}" + ); + } + Err(error) => { + assert_eq!(error, ParamsError::PrimeTooNarrow, "{shape:?}"); + assert!(!gate_admits(MIN_PRIME_BITS, &shape), "{shape:?}"); + } + } + } + } + + #[test] + fn the_prime_width_at_the_row_counts_in_use() { + for (log_rows, bits) in [(7, 121), (14, 114), (21, 107), (28, 100)] { + let shape = Shape::new(log_rows, 22usize.saturating_sub(log_rows)).unwrap(); + assert_eq!(prime_bits(&shape), Ok(bits), "{log_rows}"); + } + let shape = Shape::new(29, 0).unwrap(); + assert_eq!(prime_bits(&shape), Err(ParamsError::PrimeTooNarrow)); + } + + /// `Q114` is admitted by exactly the shapes whose width reaches it. + #[test] + fn the_prime_width_agrees_with_the_gate() { + for shape in shapes_by_rows() { + let reaches = prime_bits(&shape).is_ok_and(|bits| bits >= F::BITS); + assert_eq!(params_at(shape).is_ok(), reaches, "{shape:?}"); + } + } + #[test] fn the_fold_bound_is_the_widest_column_sum() { let params = params_at(shape()).unwrap(); diff --git a/crates/transcript/src/challenge.rs b/crates/transcript/src/challenge.rs index 56f4606e..f136a153 100644 --- a/crates/transcript/src/challenge.rs +++ b/crates/transcript/src/challenge.rs @@ -69,12 +69,11 @@ impl TranscriptChallenge for Fq { /// `[2^(bits-1), 2^bits)`: rejection-sample odd candidates from `next_u128` /// and test them by Miller-Rabin with bases squeezed from the same stream, as /// `ext_proj::sample_prime_context` does in `f2z-pcs`. Until then the fixed -/// modulus is returned and nothing is squeezed. +/// modulus is returned whatever width is asked for, and nothing is squeezed. pub fn prime_from_squeezes(_next_u128: impl FnMut() -> u128, bits: u32) -> u128 { - assert_eq!( - bits, - field::FqDefault::BITS, - "the placeholder draw only knows the fixed modulus" + assert!( + bits <= field::MAX_MODULUS_BITS, + "the field cannot hold a prime this wide" ); field::Q100 } diff --git a/tooling/cli/src/end_to_end.rs b/tooling/cli/src/end_to_end.rs index ce598178..df220b71 100644 --- a/tooling/cli/src/end_to_end.rs +++ b/tooling/cli/src/end_to_end.rs @@ -29,16 +29,6 @@ use spartan::{ const SESSION: &[u8] = b"bitz/circuit-e2e/v1"; const WINDOW: u32 = 8; -/// Width of the fingerprint prime: that of `F`'s modulus until the draw exists. -/// -/// TODO(random-prime): derive it from the claim shape as `f2z-pcs` does, -/// `min(113, 128 - log_rows)` with the top of the interval capped by the fold -/// gate of [`BitZParams`]. -fn prime_bits() -> u32 { - todo!() - //u128::BITS - F::modulus().leading_zeros() -} - /// A trusted, deterministic circuit and its public inputs. Implementations must /// emit identical operations for symbolic and concrete backends and constrain /// every public input/output. The proved constraints are interpreted modulo @@ -101,6 +91,8 @@ pub struct CircuitProofSystem { matrices: PreparedConstraintMatrices, map: MaterializedMTranspose, params: BitZParams, + /// Width of the fingerprint prime, fixed by the claim shape. + prime_bits: u32, committed_shape: Shape, pcs: Pcs, } @@ -173,6 +165,8 @@ where }; let params = BitZParams::new(claim_shape, smallest_generator()) .map_err(|_| Error::Configuration("inadmissible BitZ parameters"))?; + let prime_bits = common::prime_bits(&claim_shape) + .map_err(|_| Error::Configuration("shape too tall for the fingerprint prime"))?; let pcs = Pcs::new(&committed_shape, LigeritoProfile::Fast, HashKind::Blake3) .map_err(|_| Error::Configuration("unsupported PCS shape"))?; Ok(Self { @@ -181,6 +175,7 @@ where matrices, map, params, + prime_bits, committed_shape, pcs, }) @@ -273,7 +268,7 @@ where } // TODO(random-prime): bind the integer constraint digest before the // draw, then build the parameters, matrices and witness under `prime`. - let prime = transcript.squeeze_prime(prime_bits::()); + let prime = transcript.squeeze_prime(self.prime_bits); let prime = F::Integer::from(prime); debug_assert_eq!(prime, F::modulus()); let (spartan, terminal) = prove_spartan_piop( @@ -333,7 +328,7 @@ where } // TODO(random-prime): as in `prove`, the parameters and matrices must // be built under `prime`. - let prime = transcript.squeeze_prime(prime_bits::()); + let prime = transcript.squeeze_prime(self.prime_bits); let prime = F::Integer::from(prime); debug_assert_eq!(prime, F::modulus()); let terminal = verify_spartan_proof(&mut transcript, &self.matrices, &proof.spartan) From c4565234967763912bc97bdbb2a269234648c308 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Wed, 30 Sep 2026 23:00:36 +0100 Subject: [PATCH 04/11] Prime sampling --- Cargo.lock | 1 + crates/transcript/Cargo.toml | 1 + crates/transcript/src/challenge.rs | 2 + crates/transcript/src/challenge/prime.rs | 501 +++++++++++++++++++++++ 4 files changed, 505 insertions(+) create mode 100644 crates/transcript/src/challenge/prime.rs diff --git a/Cargo.lock b/Cargo.lock index 3ca0cdb6..02379fe1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1822,6 +1822,7 @@ version = "0.1.0" dependencies = [ "field", "spongefish", + "thiserror", ] [[package]] diff --git a/crates/transcript/Cargo.toml b/crates/transcript/Cargo.toml index ab3f2373..cf925669 100644 --- a/crates/transcript/Cargo.toml +++ b/crates/transcript/Cargo.toml @@ -8,3 +8,4 @@ license.workspace = true [dependencies] field = { workspace = true, features = ["spongefish"] } spongefish = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/transcript/src/challenge.rs b/crates/transcript/src/challenge.rs index f136a153..99eac935 100644 --- a/crates/transcript/src/challenge.rs +++ b/crates/transcript/src/challenge.rs @@ -1,5 +1,7 @@ //! Typed Fiat–Shamir challenge sampling. +pub mod prime; + use crate::{ProverState, VerifierState}; use field::dynamic::DynField; use field::{F128, Fq}; diff --git a/crates/transcript/src/challenge/prime.rs b/crates/transcript/src/challenge/prime.rs new file mode 100644 index 00000000..a7f59a70 --- /dev/null +++ b/crates/transcript/src/challenge/prime.rs @@ -0,0 +1,501 @@ +//! Public prime sampling from the challenge stream. +//! +//! Mirrors `f2z-pcs`: `ext_proj::sample_prime_context` fixes a whole-search +//! budget from the interval, then `field::sample_prime_public` draws uniform +//! odd candidates, sieves them by the primes up to 53 and runs Miller–Rabin +//! with bases drawn from the same stream. Everything here is public, so the +//! arithmetic is variable-time. + +use std::ops::RangeInclusive; +use thiserror::Error; + +/// The whole search accepts a composite with probability below +/// `2^-SECURITY_BITS`: `f2z-pcs` targets 128 bits and adds 16 of slack. +const SECURITY_BITS: u32 = 144; + +/// Trial-division sieve. +const SMALL_PRIMES_TABLE: [u128; 16] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53]; + +/// Why a draw produced no prime. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)] +pub enum PrimeSamplingError { + #[error("prime interval is empty: min {min} exceeds max {max}")] + InvalidInterval { min: u128, max: u128 }, + #[error("prime interval [{min}, {max}] contains no odd candidate")] + NoOddCandidate { min: u128, max: u128 }, + #[error("no prime found in [{min}, {max}] after {attempts} transcript candidates")] + Exhausted { min: u128, max: u128, attempts: u64 }, +} + +/// The whole-search budget of one draw, a function of the interval alone. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SearchPolicy { + /// Candidates drawn before the search gives up. + pub max_candidates: u64, + /// Miller–Rabin bases per candidate that passes the sieve. + pub rounds: u32, +} + +impl SearchPolicy { + /// 64 candidates per bit of `max`, one if `first` is the only odd candidate; + /// and enough rounds that the union bound over every candidate keeps the composite + /// acceptance of the search below `2^-SECURITY_BITS`. + pub fn for_interval(first: u128, max: u128) -> Self { + let last_odd = max - u128::from(max & 1 == 0); + let max_candidates = if first == last_odd { + 1 + } else { + 64 * u64::from(u128::BITS - max.leading_zeros()) + }; + let candidate_bits = u64::BITS - (max_candidates - 1).leading_zeros(); + Self { + max_candidates, + rounds: (SECURITY_BITS + candidate_bits).div_ceil(2), + } + } +} + +/// The primes of exactly `bits` bits, `[2^(bits-1), 2^bits)`. +pub fn interval(bits: u32) -> RangeInclusive { + assert!((1..=u128::BITS).contains(&bits), "prime width out of range"); + let top = 1u128 << (bits - 1); + top..=top | (top - 1) +} + +/// A prime from `interval` by successive `u128` draws, the same on both sides: +/// the fingerprint prime of 5. "An end-to-end BitZ-based SNARK over any +/// finitely generated ring", Step 2, drawn once per proof after the commitment +/// and the statement are absorbed and before the PIOP. +/// +/// Odd candidates are uniform in the interval. Each passes the sieve, then +/// [`SearchPolicy::rounds`] Miller–Rabin bases uniform in `[2, candidate - 2]`, +/// every base a fresh draw. Candidates and bases are drawn by masking to the +/// bit length of their range and rejecting above it, so at most half of the +/// draws are wasted. The bounded search is not exactly uniform over primes. +pub fn sample( + mut next_u128: impl FnMut() -> u128, + interval: &RangeInclusive, +) -> Result { + let (min, max) = (*interval.start(), *interval.end()); + if min > max { + return Err(PrimeSamplingError::InvalidInterval { min, max }); + } + let first = min.max(3) | 1; + if first > max { + return Err(PrimeSamplingError::NoOddCandidate { min, max }); + } + let policy = SearchPolicy::for_interval(first, max); + // The odd candidates are `first + 2 * index` for `index < count`. + let count = (max - first) / 2 + 1; + for _ in 0..policy.max_candidates { + let candidate = first + 2 * sample_below(&mut next_u128, count); + if is_probable_prime(candidate, policy.rounds, &mut next_u128) { + return Ok(candidate); + } + } + Err(PrimeSamplingError::Exhausted { + min, + max, + attempts: policy.max_candidates, + }) +} + +/// Uniform in `[0, bound)`: draws masked to the bit length of `bound - 1`, +/// accepted below `bound`. Nothing is drawn when the answer is forced. +fn sample_below(next_u128: &mut impl FnMut() -> u128, bound: u128) -> u128 { + debug_assert!(bound > 0, "empty sampling range"); + let bits = u128::BITS - (bound - 1).leading_zeros(); + if bits == 0 { + return 0; + } + let mask = u128::MAX >> (u128::BITS - bits); + loop { + let candidate = next_u128() & mask; + if candidate < bound { + return candidate; + } + } +} + +/// Trial division, then `rounds` strong tests with bases drawn uniformly from +/// `[2, candidate - 2]`. A composite survives one round with probability at +/// most `1/4`. +fn is_probable_prime(candidate: u128, rounds: u32, next_u128: &mut impl FnMut() -> u128) -> bool { + if let Some(decided) = small_primes_sieve(candidate) { + return decided; + } + let field = Montgomery::new(candidate); + // `candidate - 1 = odd * 2^twos`. + let twos = (candidate - 1).trailing_zeros(); + let odd = (candidate - 1) >> twos; + (0..rounds).all(|_| { + let base = 2 + sample_below(next_u128, candidate - 3); + field.strong_round(base, odd, twos) + }) +} + +/// `Some` when the sieve decides `candidate`. +fn small_primes_sieve(candidate: u128) -> Option { + if candidate < 2 { + return Some(false); + } + for prime in SMALL_PRIMES_TABLE { + if candidate == prime { + return Some(true); + } + if candidate.is_multiple_of(prime) { + return Some(false); + } + } + None +} + +/// Montgomery arithmetic modulo an odd `q` with `R = 2^128`, the two-limb +/// schedule of `f2z-pcs`'s `field::prime::montgomery128`. Values are residues +/// times `R`, held below `q`. +#[derive(Clone, Debug)] +struct Montgomery { + modulus: u128, + /// `-q^-1 mod 2^64`. + neg_inv: u64, + /// `R mod q`, the form of one. + one: u128, + /// `R^2 mod q`, which takes an integer into the form. + r2: u128, +} + +impl Montgomery { + fn new(modulus: u128) -> Self { + debug_assert!( + modulus & 1 == 1 && modulus >= 3, + "Montgomery modulus must be odd" + ); + // Newton iteration doubles the correct low bits: 1, 2, ..., 64. + let mut inv = 1u64; + for _ in 0..6 { + inv = inv.wrapping_mul(2u64.wrapping_sub((modulus as u64).wrapping_mul(inv))); + } + let one = (u128::MAX % modulus + 1) % modulus; + let mut r2 = one; + for _ in 0..u128::BITS { + let (doubled, carry) = r2.overflowing_add(r2); + r2 = if carry || doubled >= modulus { + doubled.wrapping_sub(modulus) + } else { + doubled + }; + } + Self { + modulus, + neg_inv: inv.wrapping_neg(), + one, + r2, + } + } + + /// `a * R mod q` for `a < q`. + fn to_form(&self, a: u128) -> u128 { + self.mul(a, self.r2) + } + + /// `a * b / R mod q` for `a, b < q`: schoolbook product, then REDC one + /// 64-bit limb at a time, then one conditional subtraction. + fn mul(&self, a: u128, b: u128) -> u128 { + let lo = |x: u128| u128::from(x as u64); + let q = self.modulus; + let (q0, q1) = (lo(q), q >> 64); + let (a0, a1) = (lo(a), a >> 64); + let (b0, b1) = (lo(b), b >> 64); + + let p00 = a0 * b0; + let p01 = a0 * b1; + let p10 = a1 * b0; + let p11 = a1 * b1; + let mid = (p00 >> 64) + lo(p01) + lo(p10); + let mid2 = (mid >> 64) + (p01 >> 64) + (p10 >> 64) + lo(p11); + let (t0, t1, t2, t3) = (lo(p00), lo(mid), lo(mid2), (mid2 >> 64) + (p11 >> 64)); + + // `m0 * q` clears limb 0. + let m0 = u128::from((t0 as u64).wrapping_mul(self.neg_inv)); + let (mq0, mq1) = (m0 * q0, m0 * q1); + let c0 = t0 + lo(mq0); + debug_assert_eq!(lo(c0), 0); + let c1 = t1 + (mq0 >> 64) + lo(mq1) + (c0 >> 64); + let c2 = t2 + (mq1 >> 64) + (c1 >> 64); + let c3 = t3 + (c2 >> 64); + // `m1 * q` clears limb 1. + let m1 = u128::from((c1 as u64).wrapping_mul(self.neg_inv)); + let (n0, n1) = (m1 * q0, m1 * q1); + let d1 = lo(c1) + lo(n0); + debug_assert_eq!(lo(d1), 0); + let d2 = lo(c2) + (n0 >> 64) + lo(n1) + (d1 >> 64); + let d3 = lo(c3) + (n1 >> 64) + (d2 >> 64); + + // The result is below `2q`; the two carries cannot both be set. + let overflow = (c3 >> 64) | (d3 >> 64) != 0; + let result = lo(d2) | (lo(d3) << 64); + if overflow || result >= q { + result.wrapping_sub(q) + } else { + result + } + } + + /// `base^exponent` in the form, by left-to-right square-and-multiply. + fn pow(&self, base: u128, exponent: u128) -> u128 { + let mut result = self.one; + for i in (0..u128::BITS - exponent.leading_zeros()).rev() { + result = self.mul(result, result); + if (exponent >> i) & 1 == 1 { + result = self.mul(result, base); + } + } + result + } + + /// One Miller–Rabin round for `q - 1 = odd * 2^twos`: `base^odd` is `±1`, + /// or one of its next `twos - 1` squarings is `-1`. + fn strong_round(&self, base: u128, odd: u128, twos: u32) -> bool { + let minus_one = self.modulus - self.one; + let mut value = self.pow(self.to_form(base), odd); + if value == self.one || value == minus_one { + return true; + } + (1..twos).any(|_| { + value = self.mul(value, value); + value == minus_one + }) + } +} + +#[cfg(test)] +#[allow(clippy::reversed_empty_ranges)] +mod tests { + use field::Q100; + + use super::*; + + /// SplitMix64, two outputs per draw. + fn stream(mut state: u64) -> impl FnMut() -> u128 { + let mut next_u64 = move || { + state = state.wrapping_add(0x9e37_79b9_7f4a_7c15); + let mut z = state; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + z ^ (z >> 31) + }; + move || u128::from(next_u64()) | (u128::from(next_u64()) << 64) + } + + /// `a * b mod q` by doubling, for any `q >= 2`. + fn mul_mod_reference(a: u128, b: u128, q: u128) -> u128 { + let add_mod = |a: u128, b: u128| { + let (sum, carry) = a.overflowing_add(b); + if carry || sum >= q { + sum.wrapping_sub(q) + } else { + sum + } + }; + let mut result = 0; + let mut addend = a % q; + for i in 0..u128::BITS - b.leading_zeros() { + if (b >> i) & 1 == 1 { + result = add_mod(result, addend); + } + addend = add_mod(addend, addend); + } + result + } + + /// Exact primality below `2^40`. + fn is_prime_by_trial_division(n: u128) -> bool { + assert!(n < 1 << 40); + if n < 2 || (n > 2 && n & 1 == 0) { + return false; + } + let mut divisor = 3; + while divisor * divisor <= n { + if n.is_multiple_of(divisor) { + return false; + } + divisor += 2; + } + true + } + + const PRIMES: [u128; 11] = [ + 2, + 3, + 53, + 59, + 61, + (1 << 61) - 1, + Q100, + (1 << 89) - 1, + (1 << 107) - 1, + (1 << 127) - 1, + u128::MAX - 158, + ]; + // Carmichael numbers, strong pseudoprimes to base 2, and two products + // of large primes, one of them full-width. + const COMPOSITES: [u128; 12] = [ + 0, + 1, + 561, + 1105, + 1729, + 2047, + 3277, + 4033, + (1 << 100) - 1, + (1 << 127) + 1, + ((1 << 64) - 59) * ((1 << 64) - 83), + u128::MAX, + ]; + + #[test] + fn montgomery_multiplication_matches_the_reference() { + let mut draw = stream(1); + for modulus in [ + 59, + (1 << 61) - 1, + Q100, + (1 << 127) - 1, + u128::MAX - 158, + u128::MAX, + ] { + let field = Montgomery::new(modulus); + assert_eq!(field.to_form(1), field.one); + assert_eq!(field.mul(field.one, 1), 1); + for _ in 0..64 { + let (a, b) = (draw() % modulus, draw() % modulus); + let product = field.mul(field.mul(field.to_form(a), field.to_form(b)), 1); + assert_eq!( + product, + mul_mod_reference(a, b, modulus), + "{a} * {b} mod {modulus}" + ); + } + for a in [0, 1, modulus - 1] { + let product = field.mul(field.mul(field.to_form(a), field.to_form(a)), 1); + assert_eq!( + product, + mul_mod_reference(a, a, modulus), + "{a}^2 mod {modulus}" + ); + } + } + } + + #[test] + fn probable_prime_decides_known_primes_and_composites() { + let mut draw = stream(2); + for prime in PRIMES { + assert!(is_probable_prime(prime, 16, &mut draw), "{prime} is prime"); + } + for composite in COMPOSITES { + assert!( + !is_probable_prime(composite, 16, &mut draw), + "{composite} is composite" + ); + } + } + + #[test] + fn sampling_below_masks_then_rejects() { + let mut draws = [7u128, 5, 8 | 3].into_iter(); + assert_eq!(sample_below(&mut || draws.next().unwrap(), 5), 3); + assert!(draws.next().is_none()); + + let mut draws = 0; + assert_eq!( + sample_below( + &mut || { + draws += 1; + u128::MAX + }, + 8 + ), + 7 + ); + assert_eq!(draws, 1); + + assert_eq!(sample_below(&mut || panic!("forced answer draws"), 1), 0); + } + + #[test] + fn sampled_primes_are_prime_and_in_the_interval() { + let range = interval(40); + for seed in 0..32 { + let prime = sample(stream(seed), &range).unwrap(); + assert!(range.contains(&prime), "seed {seed}: {prime} out of range"); + assert!( + is_prime_by_trial_division(prime), + "seed {seed}: {prime} is composite" + ); + } + for width in [2, 64, 100, 113, 126, 128] { + let range = interval(width); + let prime = sample(stream(u64::from(width)), &range).unwrap(); + assert!(range.contains(&prime), "{width} bits: {prime} out of range"); + assert!(is_probable_prime(prime, 8, &mut stream(99))); + } + } + + #[test] + fn interval_edge_cases() { + let draw = || stream(3); + assert_eq!( + sample(draw(), &(5..=3)), + Err(PrimeSamplingError::InvalidInterval { min: 5, max: 3 }) + ); + assert_eq!( + sample(draw(), &(100..=100)), + Err(PrimeSamplingError::NoOddCandidate { min: 100, max: 100 }) + ); + assert_eq!( + sample(draw(), &(1..=2)), + Err(PrimeSamplingError::NoOddCandidate { min: 1, max: 2 }) + ); + assert_eq!(sample(draw(), &(101..=101)), Ok(101)); + assert_eq!(sample(draw(), &(100..=102)), Ok(101)); + assert!([5, 7].contains(&sample(draw(), &(4..=9)).unwrap())); + assert_eq!( + sample(draw(), &(25..=27)), + Err(PrimeSamplingError::Exhausted { + min: 25, + max: 27, + attempts: 320 + }) + ); + } + + #[test] + fn search_policy_matches_the_poc_budget() { + let interval = interval(100); + assert_eq!(*interval.start(), 1 << 99); + assert_eq!(*interval.end(), (1 << 100) - 1); + assert_eq!( + SearchPolicy::for_interval(*interval.start(), *interval.end()), + SearchPolicy { + max_candidates: 6400, + rounds: 79 + } + ); + assert_eq!( + SearchPolicy::for_interval(1 << 63, u64::MAX.into()), + SearchPolicy { + max_candidates: 4096, + rounds: 78 + } + ); + assert_eq!( + SearchPolicy::for_interval(101, 101), + SearchPolicy { + max_candidates: 1, + rounds: 72 + } + ); + } +} From 461810d49a98050c34443581c59ba1227b3e106c Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Wed, 30 Sep 2026 23:41:25 +0100 Subject: [PATCH 05/11] Prime sampling, v2 --- crates/common/src/params.rs | 4 +- crates/field/src/dynamic.rs | 56 +-- crates/field/src/fq.rs | 56 +-- crates/field/src/helpers.rs | 228 +++++++--- crates/field/src/lib.rs | 2 +- crates/transcript/src/challenge.rs | 16 +- crates/transcript/src/challenge/prime.rs | 513 +++-------------------- crates/transcript/src/lib.rs | 2 +- 8 files changed, 271 insertions(+), 606 deletions(-) diff --git a/crates/common/src/params.rs b/crates/common/src/params.rs index be6e297a..8a2bb6fd 100644 --- a/crates/common/src/params.rs +++ b/crates/common/src/params.rs @@ -26,7 +26,7 @@ pub enum ParamsError { /// The narrowest fingerprint prime a proof is run over: the width of the /// default modulus. The PIOP's soundness error is of order `1/q`, so no shape /// may push the prime below what the fixed modulus gave. -pub const MIN_PRIME_BITS: u32 = FqDefault::BITS; +pub const MIN_PRIME_BITS: u32 = FqDefault::META.bits; /// Width of the fingerprint prime for `shape`: the largest `bits` such that /// every prime in `[2^(bits-1), 2^bits)` passes the fold gate of @@ -396,7 +396,7 @@ mod tests { #[test] fn the_prime_width_agrees_with_the_gate() { for shape in shapes_by_rows() { - let reaches = prime_bits(&shape).is_ok_and(|bits| bits >= F::BITS); + let reaches = prime_bits(&shape).is_ok_and(|bits| bits >= F::META.bits); assert_eq!(params_at(shape).is_ok(), reaches, "{shape:?}"); } } diff --git a/crates/field/src/dynamic.rs b/crates/field/src/dynamic.rs index 5961b571..54bcd6d2 100644 --- a/crates/field/src/dynamic.rs +++ b/crates/field/src/dynamic.rs @@ -14,43 +14,15 @@ use std::fmt::Display; use std::iter::{Product, Sum}; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -/// Global config for [`DynField`] shared among all instances: the modulus and -/// what its Barrett reduction precomputes from it. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DynFieldConfig { - modulus: u128, - /// Bit length of the modulus. - bits: u32, - /// `floor(2^(2 * bits) / modulus)`, Barrett's reciprocal. - mu: u128, -} - -impl DynFieldConfig { - /// Whether [`DynField::set_modulus`] has run. - fn initialized(&self) -> bool { - self.modulus != 0 - } - - pub fn modulus(&self) -> u128 { - debug_assert!(self.initialized()); - self.modulus - } - - /// Bit length of the modulus. - pub fn bits(&self) -> u32 { - debug_assert!(self.initialized()); - self.bits - } -} - -/// The config, one atomic per word. +/// The installed [`FieldMetadata`], one atomic per word: the config shared by +/// every [`DynField`]. /// /// Written only by [`DynField::set_modulus`], read by every operation. Its /// contract rules out a writer concurrent with a reader, so the words need /// neither a lock nor an ordering among them: `Relaxed` loads are plain /// loads, and every thread reads the same cache line without writing it. mod global { - use super::DynFieldConfig; + use crate::helpers::FieldMetadata; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering::Relaxed}; static MODULUS_LO: AtomicU64 = AtomicU64::new(0); @@ -75,15 +47,15 @@ mod global { } #[inline(always)] - pub(super) fn load() -> DynFieldConfig { - DynFieldConfig { + pub(super) fn load() -> FieldMetadata { + FieldMetadata { modulus: modulus(), bits: BITS.load(Relaxed), mu: load_u128(&MU_LO, &MU_HI), } } - pub(super) fn store(cfg: &DynFieldConfig) { + pub(super) fn store(cfg: &FieldMetadata) { store_u128(&MODULUS_LO, &MODULUS_HI, cfg.modulus); store_u128(&MU_LO, &MU_HI, cfg.mu); BITS.store(cfg.bits, Relaxed); @@ -124,16 +96,14 @@ impl DynField { // `DynField` claims to be a prime field, so it enforces that itself // rather than trusting whoever drew the modulus. assert!(helpers::is_prime(modulus), "modulus must be prime"); - let bits = u128::BITS - modulus.leading_zeros(); - let mu = helpers::barrett_mu(modulus, bits); - global::store(&DynFieldConfig { modulus, bits, mu }); + global::store(&helpers::FieldMetadata::new(modulus)); } /// The installed modulus and its reduction constants. #[inline(always)] - pub fn config() -> DynFieldConfig { + pub fn config() -> helpers::FieldMetadata { let cfg = global::load(); - debug_assert!(cfg.initialized(), "Field modulus has not been set yet!"); + debug_assert!(cfg.modulus != 0, "Field modulus has not been set yet!"); cfg } } @@ -337,9 +307,7 @@ impl SubAssign for DynField { impl MulAssign for DynField { #[inline(always)] fn mul_assign(&mut self, rhs: Self) { - let cfg = Self::config(); - let (lo, hi) = helpers::mul_wide(self.reduced_value, rhs.reduced_value); - self.reduced_value = helpers::barrett_reduce(lo, hi, cfg.modulus, cfg.mu, cfg.bits); + self.reduced_value = Self::config().mul(self.reduced_value, rhs.reduced_value); } } @@ -542,8 +510,8 @@ mod tests { fn config_reports_the_installed_modulus() { with_modulus(Q100, || { let cfg = DynField::config(); - assert_eq!(cfg.modulus(), Q100); - assert_eq!(cfg.bits(), FqDefault::BITS); + assert_eq!(cfg.modulus, Q100); + assert_eq!(cfg.bits, FqDefault::META.bits); assert_eq!(DynField::modulus(), Q100); assert_eq!(DynField::modulus_minus_one_div_two(), (Q100 - 1) / 2); assert_eq!(DynField::min_value(), DynField::ZERO); diff --git a/crates/field/src/fq.rs b/crates/field/src/fq.rs index 36432216..7799fd4f 100644 --- a/crates/field/src/fq.rs +++ b/crates/field/src/fq.rs @@ -1,6 +1,6 @@ //! Arithmetic modulo a prime fixed at compile time. -use crate::helpers; +use crate::helpers::{self, FieldMetadata}; use crypto_primitives::{ConstBaseField, LiftElement, WithAssociatedInteger}; use crypto_primitives_proc_macros::InfallibleCheckedOp; use num_traits::{ @@ -37,22 +37,11 @@ pub type FqDefault = Fq; pub struct Fq(u128); impl Fq { - /// Bit length of the modulus, derived so it cannot disagree with `Q`. - /// - /// Its asserts are the only check on `Q`, and an associated constant is - /// evaluated where it is used, so an operation that does not need the - /// value reads it anyway rather than accept a modulus out of range. - pub const BITS: u32 = { + pub const META: FieldMetadata = { assert!(Q >= 3, "modulus must be at least 3"); - assert!(Q % 2 == 1, "modulus must be odd"); - assert!( - Q < 1u128 << helpers::MAX_MODULUS_BITS, - "modulus must be below 2^126" - ); - // `Fq` claims to be a prime field, so it enforces that itself rather - // than trusting whoever names the constant. + // `Fq` claims to be a prime field, so it enforces that itself assert!(helpers::is_prime(Q), "modulus must be prime"); - 128 - Q.leading_zeros() + FieldMetadata::new(Q) }; /// Constructs an element from two little-endian `u64` limbs and reduces it @@ -60,13 +49,6 @@ impl Fq { pub fn from_limbs(low: u64, high: u64) -> Self { Self::from(u128::from(low) | (u128::from(high) << 64)) } - - const MU: u128 = helpers::barrett_mu(Q, Self::BITS); - - /// `(lo, hi) mod Q` by [`barrett_reduce`]. - const fn reduce_wide(lo: u128, hi: u128) -> Self { - Self(helpers::barrett_reduce(lo, hi, Q, Self::MU, Self::BITS)) - } } impl Display for Fq { @@ -79,7 +61,7 @@ impl Display for Fq { impl Distribution> for StandardUniform { fn sample(&self, rng: &mut R) -> Fq { // Force validation of the const-generic modulus before using it below. - let _ = Fq::::BITS; + let _ = Fq::::META; // A u128 range is not generally an exact multiple of Q. Reject its // incomplete final interval before reducing so every residue has the @@ -123,7 +105,7 @@ impl ConstOne for Fq { /// Reduces its input, so any `u128` is accepted. impl From for Fq { fn from(value: u128) -> Self { - let _ = Self::BITS; + let _ = Self::META; Self(value % Q) } } @@ -150,7 +132,7 @@ impl From for Fq { impl Neg for Fq { type Output = Self; fn neg(self) -> Self { - let _ = Self::BITS; + let _ = Self::META; Self(if self.0 == 0 { 0 } else { Q - self.0 }) } } @@ -158,7 +140,7 @@ impl Neg for Fq { impl Add for Fq { type Output = Self; fn add(self, rhs: Self) -> Self { - let _ = Self::BITS; + let _ = Self::META; // Both operands are below `Q < 2^126`, so the sum cannot wrap. let s = self.0 + rhs.0; Self(if s >= Q { s - Q } else { s }) @@ -168,7 +150,7 @@ impl Add for Fq { impl Sub for Fq { type Output = Self; fn sub(self, rhs: Self) -> Self { - let _ = Self::BITS; + let _ = Self::META; Self(if self.0 >= rhs.0 { self.0 - rhs.0 } else { @@ -179,9 +161,9 @@ impl Sub for Fq { impl Mul for Fq { type Output = Self; - fn mul(self, rhs: Self) -> Self { - let (lo, hi) = helpers::mul_wide(self.0, rhs.0); - Self::reduce_wide(lo, hi) + fn mul(mut self, rhs: Self) -> Self { + self.0 = Self::META.mul(self.0, rhs.0); + self } } @@ -345,14 +327,14 @@ impl Bounded for Fq { /// The largest residue, `Q - 1`. fn max_value() -> Self { - let _ = Self::BITS; + let _ = Self::META; Self(Q - 1) } } impl ConstBaseField for Fq { const MODULUS: Self::Integer = { - let _ = Self::BITS; + let _ = Self::META; Q }; const MODULUS_MINUS_ONE_DIV_TWO: Self::Integer = (Q - 1) / 2; @@ -377,7 +359,7 @@ mod tests { #[test] fn base_field_metadata() { - assert_eq!(FqDefault::MODULUS, Q100); + assert_eq!(FqDefault::META.modulus, Q100); assert_eq!(FqDefault::MODULUS_MINUS_ONE_DIV_TWO, (Q100 - 1) / 2); assert_eq!(FqDefault::modulus(), Q100); assert_eq!(FqDefault::min_value(), FqDefault::ZERO); @@ -451,11 +433,11 @@ mod tests { #[test] fn bits_matches_the_modulus() { - assert_eq!(Fq::::BITS, 100); - assert_eq!(Fq::::BITS, 8); + assert_eq!(Fq::::META.bits, 100); + assert_eq!(Fq::::META.bits, 8); // The defining bracket, `2^(BITS-1) <= Q < 2^BITS`. - const { assert!(1u128 << (Fq::::BITS - 1) <= Q100) }; - const { assert!(Q100 < 1u128 << Fq::::BITS) }; + const { assert!(1u128 << (Fq::::META.bits - 1) <= Q100) }; + const { assert!(Q100 < 1u128 << Fq::::META.bits) }; } #[test] diff --git a/crates/field/src/helpers.rs b/crates/field/src/helpers.rs index 501042e7..882f3c52 100644 --- a/crates/field/src/helpers.rs +++ b/crates/field/src/helpers.rs @@ -4,72 +4,58 @@ /// below `2^81.4`. const PRIMALITY_BASES: [u128; 13] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41]; -/// Whether `candidate` is prime. -/// -/// `const` because [`Fq`] asserts it on its own modulus, which makes a -/// composite `Q` a build failure rather than a type that quietly is not a -/// field. The loops are written out for the same reason: iterator combinators -/// are not available in a const context. -/// -/// # What this does not decide -/// -/// Miller-Rabin against a fixed base set is **proven** only for -/// `n < 3_317_044_064_679_887_385_961_981`, about `2^81.4`. Moduli above that -/// are not decided by any theorem here, and because the bases are public, -/// someone choosing `Q` could in principle construct a composite that passes -/// all of them. Since `Q` is a compile-time constant, doing so means editing -/// the source rather than forging a proof. -pub const fn is_prime(candidate: u128) -> bool { +/// Every modulus is below `2^MAX_MODULUS_BITS`: the bound of +/// [`barrett_reduce`], which every prime field here reduces by. +pub const MAX_MODULUS_BITS: u32 = 126; + +/// `Some` when trial division by [`PRIMALITY_BASES`] decides +/// `candidate`. +pub const fn sieve(candidate: u128) -> Option { if candidate < 2 { - return false; + return Some(false); } - let mut index = 0; while index < PRIMALITY_BASES.len() { - let base = PRIMALITY_BASES[index]; - if candidate == base { - return true; + let prime = PRIMALITY_BASES[index]; + if candidate == prime { + return Some(true); } - if candidate.is_multiple_of(base) { - return false; + if candidate.is_multiple_of(prime) { + return Some(false); } index += 1; } + None +} - // `candidate - 1 = odd * 2^shift`. - let shift = (candidate - 1).trailing_zeros(); - let odd = (candidate - 1) >> shift; - +/// Whether `candidate`, below `2^MAX_MODULUS_BITS`, is prime. +/// +/// Note that **this is not supposed to be used for testing adversarial primes**! +/// (See [`transcript::challenge::prime::sample`]) +pub const fn is_prime(candidate: u128) -> bool { + if let Some(decided) = sieve(candidate) { + return decided; + } + let modulus = FieldMetadata::new(candidate); let mut index = 0; while index < PRIMALITY_BASES.len() { - let mut witness = pow_mod(PRIMALITY_BASES[index], odd, candidate); - if witness != 1 && witness != candidate - 1 { - let mut round = 1; - loop { - if round >= shift { - return false; - } - witness = mul_mod(witness, witness, candidate); - if witness == candidate - 1 { - break; - } - round += 1; - } + if !modulus.strong_round(PRIMALITY_BASES[index]) { + return false; } index += 1; } true } -/// `(a + b) mod q`, for `a, b < q < 2^126`. The sum stays below `2^127`. -pub const fn add_mod(a: u128, b: u128, q: u128) -> u128 { +/// `(a + b) mod q`, for `a, b < q < 2^127`. The sum cannot wrap. +const fn add_mod(a: u128, b: u128, q: u128) -> u128 { let sum = a + b; if sum >= q { sum - q } else { sum } } -/// `(a * b) mod q` by doubling, avoiding the 256-bit product a u128 cannot -/// hold. Barrett is not an option here: `MU` depends on `BITS`, which is the -/// constant this feeds. +/// `(a * b) mod q` by doubling, for any `q` below `2^127`: slow, but +/// independent of Barrett, so it is the reference [`FieldMetadata`] is checked +/// against. pub const fn mul_mod(a: u128, b: u128, q: u128) -> u128 { let mut result = 0u128; let mut addend = a % q; @@ -84,21 +70,6 @@ pub const fn mul_mod(a: u128, b: u128, q: u128) -> u128 { result } -/// `(base ^ exponent) mod q`, by square-and-multiply. -pub const fn pow_mod(base: u128, exponent: u128, q: u128) -> u128 { - let mut result = 1u128 % q; - let mut square = base % q; - let mut remaining = exponent; - while remaining != 0 { - if remaining & 1 == 1 { - result = mul_mod(result, square, q); - } - square = mul_mod(square, square, q); - remaining >>= 1; - } - result -} - /// Full 128x128 -> 256-bit product as `(low, high)`. pub const fn mul_wide(a: u128, b: u128) -> (u128, u128) { let (a_lo, a_hi) = (a as u64 as u128, a >> 64); @@ -145,10 +116,6 @@ pub const fn barrett_mu(q: u128, k: u32) -> u128 { } } -/// Every modulus is below `2^MAX_MODULUS_BITS`: the bound of -/// [`barrett_reduce`], which every prime field here reduces by. -pub const MAX_MODULUS_BITS: u32 = 126; - /// `(lo, hi) mod q` for `q` of `k` bits and `mu` its reciprocal from /// [`barrett_mu`]: Barrett reduction, Handbook of Applied Cryptography /// Algorithm 14.42, after Barrett, CRYPTO '86, LNCS 263:311-323. Estimate the @@ -176,6 +143,77 @@ pub const fn barrett_reduce(lo: u128, hi: u128, q: u128, mu: u128, k: u32) -> u1 r } +/// A modulus in `[2, 2^MAX_MODULUS_BITS)` with its Barrett reciprocal: the +/// arithmetic of a modulus that is a value rather than a type, in `const` and +/// at runtime alike. What [`DynField`](crate::dynamic::DynField) installs. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FieldMetadata { + pub modulus: u128, + /// Bit length of the modulus. + pub bits: u32, + /// `floor(2^(2 * bits) / modulus)`. + pub mu: u128, +} + +impl FieldMetadata { + pub const fn new(modulus: u128) -> Self { + assert!( + modulus >= 2 && modulus < 1 << MAX_MODULUS_BITS, + "modulus out of range" + ); + assert!(modulus == 2 || modulus % 2 == 1, "modulus > 2 must be odd"); + let bits = u128::BITS - modulus.leading_zeros(); + Self { + modulus, + bits, + mu: barrett_mu(modulus, bits), + } + } + + /// `(a * b) mod q` for `a, b < q`. + #[inline] + pub const fn mul(&self, a: u128, b: u128) -> u128 { + let (lo, hi) = mul_wide(a, b); + barrett_reduce(lo, hi, self.modulus, self.mu, self.bits) + } + + /// `(base ^ exponent) mod q` for `base < q`, by square-and-multiply. + pub const fn pow(&self, base: u128, exponent: u128) -> u128 { + let mut result = 1 % self.modulus; + let mut bit = u128::BITS - exponent.leading_zeros(); + while bit > 0 { + bit -= 1; + result = self.mul(result, result); + if (exponent >> bit) & 1 == 1 { + result = self.mul(result, base); + } + } + result + } + + /// One strong probable-prime round of `q`, odd and at least 3, at `base` + /// in `[1, q - 1]`: with `q - 1 = odd * 2^shift`, `base^odd` is `±1` or + /// one of its next `shift - 1` squarings is `-1`. A prime passes every + /// base; a composite passes at most a quarter of them. + pub const fn strong_round(&self, base: u128) -> bool { + let shift = (self.modulus - 1).trailing_zeros(); + let odd = (self.modulus - 1) >> shift; + let mut witness = self.pow(base, odd); + if witness == 1 || witness == self.modulus - 1 { + return true; + } + let mut round = 1; + while round < shift { + witness = self.mul(witness, witness); + if witness == self.modulus - 1 { + return true; + } + round += 1; + } + false + } +} + #[cfg(test)] pub mod tests { use super::*; @@ -214,6 +252,31 @@ pub mod tests { } } + #[test] + fn the_sieve_decides_only_what_trial_division_can() { + assert_eq!(sieve(0), Some(false)); + assert_eq!(sieve(1), Some(false)); + assert_eq!(sieve(2), Some(true)); + assert_eq!(sieve(41), Some(true)); + assert_eq!(sieve(43), None); + assert_eq!(sieve(1 << 100), Some(false)); + assert_eq!(sieve((1 << 100) - 15), None); + } + + #[test] + fn a_strong_round_catches_what_its_base_witnesses() { + // `2047 = 23 * 89` is a strong pseudoprime to base 2 and nothing else + // small; `1_373_653` is one to bases 2 and 3. + assert!(FieldMetadata::new(2047).strong_round(2)); + assert!(!FieldMetadata::new(2047).strong_round(3)); + assert!(FieldMetadata::new(1_373_653).strong_round(2)); + assert!(FieldMetadata::new(1_373_653).strong_round(3)); + assert!(!FieldMetadata::new(1_373_653).strong_round(5)); + // A prime passes every base. + let prime = FieldMetadata::new((1 << 61) - 1); + assert!((1..64).all(|base| prime.strong_round(base))); + } + /// `(hi:lo) mod q` by binary long division — the independent reference /// Barrett is checked against. Structurally unlike Barrett, which /// estimates a quotient and corrects. @@ -238,6 +301,43 @@ pub mod tests { mod_reference(lo, hi, q) } + fn u128_of(rng: &mut Pcg64) -> u128 { + (rng.next_u64() as u128) << 64 | rng.next_u64() as u128 + } + + #[test] + fn the_modulus_multiplies_and_exponentiates_like_the_references() { + let mut rng = Pcg64::seed_from_u64(405); + for q in [ + 2u128, + 3, + 59, + 251, + (1 << 100) - 15, + (1 << 114) - 11, + (1 << 126) - 1, + ] { + let modulus = FieldMetadata::new(q); + for _ in 0..64 { + let (a, b) = (u128_of(&mut rng) % q, u128_of(&mut rng) % q); + assert_eq!( + modulus.mul(a, b), + mulmod_reference(a, b, q), + "{a} * {b} mod {q}" + ); + assert_eq!(modulus.mul(a, b), mul_mod(a, b, q), "{a} * {b} mod {q}"); + } + for a in [0, 1, q - 1] { + assert_eq!(modulus.mul(a, a), mul_mod(a, a, q), "{a}^2 mod {q}"); + let mut power = 1 % q; + for exponent in 0..20u128 { + assert_eq!(modulus.pow(a, exponent), power, "{a}^{exponent} mod {q}"); + power = mul_mod(power, a, q); + } + } + } + } + #[test] fn mul_wide_matches_u128_on_small_inputs() { let mut rng = Pcg64::seed_from_u64(401); diff --git a/crates/field/src/lib.rs b/crates/field/src/lib.rs index 6993b6a8..116d8aa6 100644 --- a/crates/field/src/lib.rs +++ b/crates/field/src/lib.rs @@ -5,7 +5,7 @@ mod codec; pub mod dynamic; pub mod fq; pub mod gf128; -pub(crate) mod helpers; +pub mod helpers; pub use fq::{Fq, FqDefault, Q100}; pub use gf128::{F128, FixedBasePow, Wide256}; diff --git a/crates/transcript/src/challenge.rs b/crates/transcript/src/challenge.rs index 99eac935..d6c0371c 100644 --- a/crates/transcript/src/challenge.rs +++ b/crates/transcript/src/challenge.rs @@ -57,7 +57,7 @@ fn unbiased_u128(modulus: u128, mut next_u128: impl FnMut() -> u128) -> u128 { impl TranscriptChallenge for Fq { fn from_squeezes(next_u128: impl FnMut() -> u128) -> Self { // Validate the const-generic modulus before using it below. - let _ = Self::BITS; + let _ = Self::META; Self::from(unbiased_u128(Q, next_u128)) } } @@ -67,11 +67,9 @@ impl TranscriptChallenge for Fq { /// any finitely generated ring", Step 2, drawn once per proof after the /// commitment and the statement are absorbed and before the PIOP. /// -/// TODO(random-prime): draw it. Uniform among the primes in -/// `[2^(bits-1), 2^bits)`: rejection-sample odd candidates from `next_u128` -/// and test them by Miller-Rabin with bases squeezed from the same stream, as -/// `ext_proj::sample_prime_context` does in `f2z-pcs`. Until then the fixed -/// modulus is returned whatever width is asked for, and nothing is squeezed. +/// TODO(random-prime): draw it with [`prime::sample`] once the e2e builds its +/// parameters under the drawn prime. Until then the fixed modulus is returned +/// whatever width is asked for, and nothing is squeezed. pub fn prime_from_squeezes(_next_u128: impl FnMut() -> u128, bits: u32) -> u128 { assert!( bits <= field::MAX_MODULUS_BITS, @@ -103,7 +101,7 @@ impl TranscriptChallenge for F128 { impl TranscriptChallenge for DynField { fn from_squeezes(next_u128: impl FnMut() -> u128) -> Self { - Self::from(unbiased_u128(DynField::config().modulus(), next_u128)) + Self::from(unbiased_u128(DynField::config().modulus, next_u128)) } } @@ -178,12 +176,12 @@ mod tests { #[test] fn prover_and_verifier_squeeze_the_same_prime() { let mut prover = build_prover(SESSION, INSTANCE); - let prime = prover.squeeze_prime(F::BITS); + let prime = prover.squeeze_prime(F::META.bits); let after = prover.squeeze::(); let proof = prover.finish(); let mut verifier = build_verifier(SESSION, INSTANCE, &proof); - assert_eq!(verifier.squeeze_prime(F::BITS), prime); + assert_eq!(verifier.squeeze_prime(F::META.bits), prime); assert_eq!(verifier.squeeze::(), after); verifier.check_eof().unwrap(); } diff --git a/crates/transcript/src/challenge/prime.rs b/crates/transcript/src/challenge/prime.rs index a7f59a70..24b8f8f2 100644 --- a/crates/transcript/src/challenge/prime.rs +++ b/crates/transcript/src/challenge/prime.rs @@ -1,114 +1,57 @@ //! Public prime sampling from the challenge stream. //! -//! Mirrors `f2z-pcs`: `ext_proj::sample_prime_context` fixes a whole-search -//! budget from the interval, then `field::sample_prime_public` draws uniform -//! odd candidates, sieves them by the primes up to 53 and runs Miller–Rabin -//! with bases drawn from the same stream. Everything here is public, so the -//! arithmetic is variable-time. - -use std::ops::RangeInclusive; -use thiserror::Error; - -/// The whole search accepts a composite with probability below -/// `2^-SECURITY_BITS`: `f2z-pcs` targets 128 bits and adds 16 of slack. -const SECURITY_BITS: u32 = 144; - -/// Trial-division sieve. -const SMALL_PRIMES_TABLE: [u128; 16] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53]; - -/// Why a draw produced no prime. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)] -pub enum PrimeSamplingError { - #[error("prime interval is empty: min {min} exceeds max {max}")] - InvalidInterval { min: u128, max: u128 }, - #[error("prime interval [{min}, {max}] contains no odd candidate")] - NoOddCandidate { min: u128, max: u128 }, - #[error("no prime found in [{min}, {max}] after {attempts} transcript candidates")] - Exhausted { min: u128, max: u128, attempts: u64 }, -} - -/// The whole-search budget of one draw, a function of the interval alone. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct SearchPolicy { - /// Candidates drawn before the search gives up. - pub max_candidates: u64, - /// Miller–Rabin bases per candidate that passes the sieve. - pub rounds: u32, -} - -impl SearchPolicy { - /// 64 candidates per bit of `max`, one if `first` is the only odd candidate; - /// and enough rounds that the union bound over every candidate keeps the composite - /// acceptance of the search below `2^-SECURITY_BITS`. - pub fn for_interval(first: u128, max: u128) -> Self { - let last_odd = max - u128::from(max & 1 == 0); - let max_candidates = if first == last_odd { - 1 - } else { - 64 * u64::from(u128::BITS - max.leading_zeros()) - }; - let candidate_bits = u64::BITS - (max_candidates - 1).leading_zeros(); - Self { - max_candidates, - rounds: (SECURITY_BITS + candidate_bits).div_ceil(2), - } - } -} - -/// The primes of exactly `bits` bits, `[2^(bits-1), 2^bits)`. -pub fn interval(bits: u32) -> RangeInclusive { - assert!((1..=u128::BITS).contains(&bits), "prime width out of range"); - let top = 1u128 << (bits - 1); - top..=top | (top - 1) -} - -/// A prime from `interval` by successive `u128` draws, the same on both sides: -/// the fingerprint prime of 5. "An end-to-end BitZ-based SNARK over any -/// finitely generated ring", Step 2, drawn once per proof after the commitment -/// and the statement are absorbed and before the PIOP. +//! As in `f2z-pcs`: candidates uniform among the odd integers of the requested +//! width, sieved by the small primes, then Miller–Rabin with bases drawn from +//! the same stream, so a prover grinding the transcript faces fresh bases at +//! every candidate. Everything here is public, so the arithmetic is +//! variable-time. + +use field::MAX_MODULUS_BITS; +use field::helpers::{self, FieldMetadata}; + +/// Miller–Rabin bases per candidate. A composite passes a base with +/// probability at most `1/4`, so it survives with probability at most +/// `2^-160`, and a search of up to `2^16` candidates accepts a composite with +/// probability below `2^-144`: the 128 bits `f2z-pcs` targets and its 16 of +/// slack. A longer search does not happen: at least one odd integer in 44 is +/// prime below `2^126`, so `2^16` composites in a row have probability below +/// `2^-2000`. +const MILLER_RABIN_ROUNDS: u32 = 80; + +/// A prime of exactly `bits` bits, `2 <= bits <= MAX_MODULUS_BITS`, from +/// successive `u128` draws, the same on both sides: the fingerprint prime of +/// 5. "An end-to-end BitZ-based SNARK over any finitely generated ring", +/// Step 2, drawn once per proof after the commitment and the statement are +/// absorbed and before the PIOP. /// -/// Odd candidates are uniform in the interval. Each passes the sieve, then -/// [`SearchPolicy::rounds`] Miller–Rabin bases uniform in `[2, candidate - 2]`, -/// every base a fresh draw. Candidates and bases are drawn by masking to the -/// bit length of their range and rejecting above it, so at most half of the -/// draws are wasted. The bounded search is not exactly uniform over primes. -pub fn sample( - mut next_u128: impl FnMut() -> u128, - interval: &RangeInclusive, -) -> Result { - let (min, max) = (*interval.start(), *interval.end()); - if min > max { - return Err(PrimeSamplingError::InvalidInterval { min, max }); - } - let first = min.max(3) | 1; - if first > max { - return Err(PrimeSamplingError::NoOddCandidate { min, max }); - } - let policy = SearchPolicy::for_interval(first, max); - // The odd candidates are `first + 2 * index` for `index < count`. - let count = (max - first) / 2 + 1; - for _ in 0..policy.max_candidates { - let candidate = first + 2 * sample_below(&mut next_u128, count); - if is_probable_prime(candidate, policy.rounds, &mut next_u128) { - return Ok(candidate); +/// Each candidate is one draw, uniform among the odd integers in +/// `[2^(bits-1), 2^bits)`; each base is uniform in `[2, candidate - 2]` by +/// rejection. The search stops at the first candidate that passes, which is +/// not exactly uniform over the primes, as in `f2z-pcs`. +pub fn sample(mut next_u128: impl FnMut() -> u128, bits: u32) -> u128 { + assert!( + (2..=MAX_MODULUS_BITS).contains(&bits), + "prime width out of range" + ); + let bottom = 1u128 << (bits - 1); + loop { + let candidate = bottom | (next_u128() & (bottom - 1)) | 1; + let is_prime = helpers::sieve(candidate).unwrap_or_else(|| { + let modulus = FieldMetadata::new(candidate); + (0..MILLER_RABIN_ROUNDS) + .all(|_| modulus.strong_round(2 + sample_below(&mut next_u128, candidate - 3))) + }); + if is_prime { + return candidate; } } - Err(PrimeSamplingError::Exhausted { - min, - max, - attempts: policy.max_candidates, - }) } -/// Uniform in `[0, bound)`: draws masked to the bit length of `bound - 1`, -/// accepted below `bound`. Nothing is drawn when the answer is forced. +/// Uniform in `[0, bound)` for `bound >= 2`: draws masked to the bit length +/// of `bound - 1`, accepted below `bound`, so at most half are rejected. fn sample_below(next_u128: &mut impl FnMut() -> u128, bound: u128) -> u128 { - debug_assert!(bound > 0, "empty sampling range"); - let bits = u128::BITS - (bound - 1).leading_zeros(); - if bits == 0 { - return 0; - } - let mask = u128::MAX >> (u128::BITS - bits); + debug_assert!(bound >= 2, "nothing to sample"); + let mask = u128::MAX >> (bound - 1).leading_zeros(); loop { let candidate = next_u128() & mask; if candidate < bound { @@ -117,162 +60,8 @@ fn sample_below(next_u128: &mut impl FnMut() -> u128, bound: u128) -> u128 { } } -/// Trial division, then `rounds` strong tests with bases drawn uniformly from -/// `[2, candidate - 2]`. A composite survives one round with probability at -/// most `1/4`. -fn is_probable_prime(candidate: u128, rounds: u32, next_u128: &mut impl FnMut() -> u128) -> bool { - if let Some(decided) = small_primes_sieve(candidate) { - return decided; - } - let field = Montgomery::new(candidate); - // `candidate - 1 = odd * 2^twos`. - let twos = (candidate - 1).trailing_zeros(); - let odd = (candidate - 1) >> twos; - (0..rounds).all(|_| { - let base = 2 + sample_below(next_u128, candidate - 3); - field.strong_round(base, odd, twos) - }) -} - -/// `Some` when the sieve decides `candidate`. -fn small_primes_sieve(candidate: u128) -> Option { - if candidate < 2 { - return Some(false); - } - for prime in SMALL_PRIMES_TABLE { - if candidate == prime { - return Some(true); - } - if candidate.is_multiple_of(prime) { - return Some(false); - } - } - None -} - -/// Montgomery arithmetic modulo an odd `q` with `R = 2^128`, the two-limb -/// schedule of `f2z-pcs`'s `field::prime::montgomery128`. Values are residues -/// times `R`, held below `q`. -#[derive(Clone, Debug)] -struct Montgomery { - modulus: u128, - /// `-q^-1 mod 2^64`. - neg_inv: u64, - /// `R mod q`, the form of one. - one: u128, - /// `R^2 mod q`, which takes an integer into the form. - r2: u128, -} - -impl Montgomery { - fn new(modulus: u128) -> Self { - debug_assert!( - modulus & 1 == 1 && modulus >= 3, - "Montgomery modulus must be odd" - ); - // Newton iteration doubles the correct low bits: 1, 2, ..., 64. - let mut inv = 1u64; - for _ in 0..6 { - inv = inv.wrapping_mul(2u64.wrapping_sub((modulus as u64).wrapping_mul(inv))); - } - let one = (u128::MAX % modulus + 1) % modulus; - let mut r2 = one; - for _ in 0..u128::BITS { - let (doubled, carry) = r2.overflowing_add(r2); - r2 = if carry || doubled >= modulus { - doubled.wrapping_sub(modulus) - } else { - doubled - }; - } - Self { - modulus, - neg_inv: inv.wrapping_neg(), - one, - r2, - } - } - - /// `a * R mod q` for `a < q`. - fn to_form(&self, a: u128) -> u128 { - self.mul(a, self.r2) - } - - /// `a * b / R mod q` for `a, b < q`: schoolbook product, then REDC one - /// 64-bit limb at a time, then one conditional subtraction. - fn mul(&self, a: u128, b: u128) -> u128 { - let lo = |x: u128| u128::from(x as u64); - let q = self.modulus; - let (q0, q1) = (lo(q), q >> 64); - let (a0, a1) = (lo(a), a >> 64); - let (b0, b1) = (lo(b), b >> 64); - - let p00 = a0 * b0; - let p01 = a0 * b1; - let p10 = a1 * b0; - let p11 = a1 * b1; - let mid = (p00 >> 64) + lo(p01) + lo(p10); - let mid2 = (mid >> 64) + (p01 >> 64) + (p10 >> 64) + lo(p11); - let (t0, t1, t2, t3) = (lo(p00), lo(mid), lo(mid2), (mid2 >> 64) + (p11 >> 64)); - - // `m0 * q` clears limb 0. - let m0 = u128::from((t0 as u64).wrapping_mul(self.neg_inv)); - let (mq0, mq1) = (m0 * q0, m0 * q1); - let c0 = t0 + lo(mq0); - debug_assert_eq!(lo(c0), 0); - let c1 = t1 + (mq0 >> 64) + lo(mq1) + (c0 >> 64); - let c2 = t2 + (mq1 >> 64) + (c1 >> 64); - let c3 = t3 + (c2 >> 64); - // `m1 * q` clears limb 1. - let m1 = u128::from((c1 as u64).wrapping_mul(self.neg_inv)); - let (n0, n1) = (m1 * q0, m1 * q1); - let d1 = lo(c1) + lo(n0); - debug_assert_eq!(lo(d1), 0); - let d2 = lo(c2) + (n0 >> 64) + lo(n1) + (d1 >> 64); - let d3 = lo(c3) + (n1 >> 64) + (d2 >> 64); - - // The result is below `2q`; the two carries cannot both be set. - let overflow = (c3 >> 64) | (d3 >> 64) != 0; - let result = lo(d2) | (lo(d3) << 64); - if overflow || result >= q { - result.wrapping_sub(q) - } else { - result - } - } - - /// `base^exponent` in the form, by left-to-right square-and-multiply. - fn pow(&self, base: u128, exponent: u128) -> u128 { - let mut result = self.one; - for i in (0..u128::BITS - exponent.leading_zeros()).rev() { - result = self.mul(result, result); - if (exponent >> i) & 1 == 1 { - result = self.mul(result, base); - } - } - result - } - - /// One Miller–Rabin round for `q - 1 = odd * 2^twos`: `base^odd` is `±1`, - /// or one of its next `twos - 1` squarings is `-1`. - fn strong_round(&self, base: u128, odd: u128, twos: u32) -> bool { - let minus_one = self.modulus - self.one; - let mut value = self.pow(self.to_form(base), odd); - if value == self.one || value == minus_one { - return true; - } - (1..twos).any(|_| { - value = self.mul(value, value); - value == minus_one - }) - } -} - #[cfg(test)] -#[allow(clippy::reversed_empty_ranges)] mod tests { - use field::Q100; - use super::*; /// SplitMix64, two outputs per draw. @@ -287,215 +76,43 @@ mod tests { move || u128::from(next_u64()) | (u128::from(next_u64()) << 64) } - /// `a * b mod q` by doubling, for any `q >= 2`. - fn mul_mod_reference(a: u128, b: u128, q: u128) -> u128 { - let add_mod = |a: u128, b: u128| { - let (sum, carry) = a.overflowing_add(b); - if carry || sum >= q { - sum.wrapping_sub(q) - } else { - sum - } - }; - let mut result = 0; - let mut addend = a % q; - for i in 0..u128::BITS - b.leading_zeros() { - if (b >> i) & 1 == 1 { - result = add_mod(result, addend); - } - addend = add_mod(addend, addend); - } - result - } - - /// Exact primality below `2^40`. - fn is_prime_by_trial_division(n: u128) -> bool { - assert!(n < 1 << 40); - if n < 2 || (n > 2 && n & 1 == 0) { - return false; - } - let mut divisor = 3; - while divisor * divisor <= n { - if n.is_multiple_of(divisor) { - return false; - } - divisor += 2; - } - true - } - - const PRIMES: [u128; 11] = [ - 2, - 3, - 53, - 59, - 61, - (1 << 61) - 1, - Q100, - (1 << 89) - 1, - (1 << 107) - 1, - (1 << 127) - 1, - u128::MAX - 158, - ]; - // Carmichael numbers, strong pseudoprimes to base 2, and two products - // of large primes, one of them full-width. - const COMPOSITES: [u128; 12] = [ - 0, - 1, - 561, - 1105, - 1729, - 2047, - 3277, - 4033, - (1 << 100) - 1, - (1 << 127) + 1, - ((1 << 64) - 59) * ((1 << 64) - 83), - u128::MAX, - ]; - + /// `is_prime` is exact below `2^81.4` and a strong probable-prime test + /// above, which is all a check on the sampler's own test needs. #[test] - fn montgomery_multiplication_matches_the_reference() { - let mut draw = stream(1); - for modulus in [ - 59, - (1 << 61) - 1, - Q100, - (1 << 127) - 1, - u128::MAX - 158, - u128::MAX, - ] { - let field = Montgomery::new(modulus); - assert_eq!(field.to_form(1), field.one); - assert_eq!(field.mul(field.one, 1), 1); - for _ in 0..64 { - let (a, b) = (draw() % modulus, draw() % modulus); - let product = field.mul(field.mul(field.to_form(a), field.to_form(b)), 1); + fn sampled_primes_are_prime_and_of_the_requested_width() { + for bits in [2, 3, 8, 40, 64, 100, 113, MAX_MODULUS_BITS] { + for seed in 0..8 { + let prime = sample(stream(seed | u64::from(bits) << 8), bits); assert_eq!( - product, - mul_mod_reference(a, b, modulus), - "{a} * {b} mod {modulus}" + u128::BITS - prime.leading_zeros(), + bits, + "{bits} bits, seed {seed}" ); - } - for a in [0, 1, modulus - 1] { - let product = field.mul(field.mul(field.to_form(a), field.to_form(a)), 1); - assert_eq!( - product, - mul_mod_reference(a, a, modulus), - "{a}^2 mod {modulus}" + assert!( + helpers::is_prime(prime), + "{bits} bits, seed {seed}: {prime} is composite" ); } } } #[test] - fn probable_prime_decides_known_primes_and_composites() { - let mut draw = stream(2); - for prime in PRIMES { - assert!(is_probable_prime(prime, 16, &mut draw), "{prime} is prime"); - } - for composite in COMPOSITES { - assert!( - !is_probable_prime(composite, 16, &mut draw), - "{composite} is composite" - ); - } + fn the_prime_is_a_function_of_the_stream() { + assert_eq!(sample(stream(7), 100), sample(stream(7), 100)); + assert_ne!(sample(stream(7), 100), sample(stream(8), 100)); } #[test] fn sampling_below_masks_then_rejects() { + // Bound 5 masks to three bits: 7 and 5 are rejected, `11 & 7 = 3` is not. let mut draws = [7u128, 5, 8 | 3].into_iter(); assert_eq!(sample_below(&mut || draws.next().unwrap(), 5), 3); assert!(draws.next().is_none()); - - let mut draws = 0; - assert_eq!( - sample_below( - &mut || { - draws += 1; - u128::MAX - }, - 8 - ), - 7 - ); - assert_eq!(draws, 1); - - assert_eq!(sample_below(&mut || panic!("forced answer draws"), 1), 0); - } - - #[test] - fn sampled_primes_are_prime_and_in_the_interval() { - let range = interval(40); - for seed in 0..32 { - let prime = sample(stream(seed), &range).unwrap(); - assert!(range.contains(&prime), "seed {seed}: {prime} out of range"); - assert!( - is_prime_by_trial_division(prime), - "seed {seed}: {prime} is composite" - ); - } - for width in [2, 64, 100, 113, 126, 128] { - let range = interval(width); - let prime = sample(stream(u64::from(width)), &range).unwrap(); - assert!(range.contains(&prime), "{width} bits: {prime} out of range"); - assert!(is_probable_prime(prime, 8, &mut stream(99))); - } - } - - #[test] - fn interval_edge_cases() { - let draw = || stream(3); - assert_eq!( - sample(draw(), &(5..=3)), - Err(PrimeSamplingError::InvalidInterval { min: 5, max: 3 }) - ); - assert_eq!( - sample(draw(), &(100..=100)), - Err(PrimeSamplingError::NoOddCandidate { min: 100, max: 100 }) - ); - assert_eq!( - sample(draw(), &(1..=2)), - Err(PrimeSamplingError::NoOddCandidate { min: 1, max: 2 }) - ); - assert_eq!(sample(draw(), &(101..=101)), Ok(101)); - assert_eq!(sample(draw(), &(100..=102)), Ok(101)); - assert!([5, 7].contains(&sample(draw(), &(4..=9)).unwrap())); - assert_eq!( - sample(draw(), &(25..=27)), - Err(PrimeSamplingError::Exhausted { - min: 25, - max: 27, - attempts: 320 - }) - ); } #[test] - fn search_policy_matches_the_poc_budget() { - let interval = interval(100); - assert_eq!(*interval.start(), 1 << 99); - assert_eq!(*interval.end(), (1 << 100) - 1); - assert_eq!( - SearchPolicy::for_interval(*interval.start(), *interval.end()), - SearchPolicy { - max_candidates: 6400, - rounds: 79 - } - ); - assert_eq!( - SearchPolicy::for_interval(1 << 63, u64::MAX.into()), - SearchPolicy { - max_candidates: 4096, - rounds: 78 - } - ); - assert_eq!( - SearchPolicy::for_interval(101, 101), - SearchPolicy { - max_candidates: 1, - rounds: 72 - } - ); + #[should_panic(expected = "prime width out of range")] + fn rejects_a_width_the_field_cannot_hold() { + sample(stream(0), MAX_MODULUS_BITS + 1); } } diff --git a/crates/transcript/src/lib.rs b/crates/transcript/src/lib.rs index eb9b981d..df6fd352 100644 --- a/crates/transcript/src/lib.rs +++ b/crates/transcript/src/lib.rs @@ -33,7 +33,7 @@ mod proof; mod prover; mod verifier; -pub use challenge::TranscriptChallenge; +pub use challenge::{TranscriptChallenge, prime}; pub use domain::{PROTOCOL_LABEL, build_prover, build_verifier}; pub use proof::Proof; pub use prover::ProverState; From 4ec900bbef4e69728701b1e73ee365aa57c26d29 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Thu, 1 Oct 2026 09:43:21 +0100 Subject: [PATCH 06/11] Wire in dynamic field --- crates/circuit/src/constraints.rs | 45 ++++- crates/circuit/src/matrix_products.rs | 64 +++++- crates/circuit/src/witgen.rs | 4 +- crates/field/src/dynamic.rs | 41 ++-- crates/field/src/lib.rs | 16 ++ crates/spartan/src/piop.rs | 2 +- crates/spartan/src/sumcheck.rs | 2 +- crates/transcript/src/challenge.rs | 97 ++++----- crates/transcript/src/lib.rs | 2 +- crates/verifier/src/verify.rs | 4 +- tooling/cli/benches/circuits.rs | 12 +- tooling/cli/benches/sha256_spartan.rs | 9 +- tooling/cli/src/benchmark.rs | 14 +- tooling/cli/src/cmd/circuit_e2e.rs | 10 +- tooling/cli/src/end_to_end.rs | 274 ++++++++++++++++---------- tooling/cli/src/lib.rs | 17 +- tooling/cli/tests/circuits.rs | 30 +-- tooling/cli/tests/end_to_end.rs | 16 +- tooling/cli/tests/sha256_spartan.rs | 8 +- 19 files changed, 414 insertions(+), 253 deletions(-) diff --git a/crates/circuit/src/constraints.rs b/crates/circuit/src/constraints.rs index 03dd51bb..8bc3dc47 100644 --- a/crates/circuit/src/constraints.rs +++ b/crates/circuit/src/constraints.rs @@ -96,7 +96,7 @@ impl SparseMatrix { } impl SparseMatrix { - fn map_values_with(self, map: M) -> SparseMatrix + fn map_values(self, map: M) -> SparseMatrix where D: Send + Sync, M: Fn(C) -> D + Send + Sync, @@ -116,6 +116,27 @@ impl SparseMatrix { columns: self.columns, } } + + fn map_values_ref(&self, map: M) -> SparseMatrix + where + D: Send + Sync, + M: Fn(&C) -> D + Send + Sync, + { + SparseMatrix { + rows: self + .rows + .par_iter() + .map(|row| SparseRow { + entries: row + .entries + .iter() + .map(|(column, coefficient)| (*column, map(coefficient))) + .collect(), + }) + .collect(), + columns: self.columns, + } + } } /// Why a sparse row representation is malformed. @@ -279,9 +300,25 @@ impl ConstraintMatrices { { ConstraintMatrices { m: self.m, - a: self.a.map_values_with(map), - b: self.b.map_values_with(map), - c: self.c.map_values_with(map), + a: self.a.map_values(map), + b: self.b.map_values(map), + c: self.c.map_values(map), + } + } + + /// Maps every A/B/C coefficient, keeping the matrices. + /// + /// The Boolean `M` matrix and sparse topology are cloned unchanged. + pub fn map_coefficients_ref(&self, map: M) -> ConstraintMatrices + where + D: Send + Sync, + M: Fn(&R) -> D + Copy + Send + Sync, + { + ConstraintMatrices { + m: self.m.clone(), + a: self.a.map_values_ref(map), + b: self.b.map_values_ref(map), + c: self.c.map_values_ref(map), } } } diff --git a/crates/circuit/src/matrix_products.rs b/crates/circuit/src/matrix_products.rs index e536ea03..b6b4e554 100644 --- a/crates/circuit/src/matrix_products.rs +++ b/crates/circuit/src/matrix_products.rs @@ -9,6 +9,7 @@ use crate::witgen::Z as Integer; use crate::{BitWidth, IntoWords}; +use common::BitzConstraintRing; use num_traits::{One, Zero}; use rayon::prelude::*; use std::cmp::Ordering; @@ -242,6 +243,7 @@ fn add_mod_words( /// A dense vector of canonical runtime-field elements. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ModularVector { + /// Vector of canonical field element in little-endian limbs form, in row order. values: Vec<[u64; PRIME_LIMBS]>, } @@ -256,7 +258,7 @@ impl ModularVector { self.values.is_empty() } - /// Dense canonical field elements in row order. + /// Vector of canonical field element in little-endian limbs form, in row order. pub fn values(&self) -> &[[u64; PRIME_LIMBS]] { &self.values } @@ -267,12 +269,13 @@ impl ModularVector { } } -impl From<&ModularVector<2>> for Vec { +/// Converts two little-endian limbs, treating them as one `u128`. +impl> From<&ModularVector<2>> for Vec { fn from(values: &ModularVector<2>) -> Self { values .values() .iter() - .map(|&[low, high]| field::FqDefault::from_limbs(low, high)) + .map(|&[low, high]| T::from(u128::from(low) | (u128::from(high) << 64))) .collect() } } @@ -354,6 +357,17 @@ impl From<&num_bigint::BigInt> for StoredInteger { } } +impl From<&StoredInteger> for num_bigint::BigInt { + fn from(value: &StoredInteger) -> Self { + let bytes = value + .words() + .iter() + .flat_map(|word| word.to_le_bytes()) + .collect::>(); + Self::from_signed_bytes_le(&bytes) + } +} + impl From<&Integer> for StoredInteger { /// Stores a gadget-local fixed integer without changing its value. fn from(value: &Integer) -> Self { @@ -401,6 +415,22 @@ impl IntegerProducts { self.c_mw.push(StoredInteger::from(&c)); } + /// Whether `A(Mw) * B(Mw) = C(Mw)` holds row by row over the integers + /// (hence modulo every prime). + pub fn is_satisfied(&self) -> bool + where + R: BitzConstraintRing + for<'a> From<&'a StoredInteger>, + { + self.a_mw.len() == self.b_mw.len() + && self.a_mw.len() == self.c_mw.len() + && self + .a_mw + .par_iter() + .zip(&self.b_mw) + .zip(&self.c_mw) + .all(|((a, b), c)| R::from(a) * R::from(b) == R::from(c)) + } + /// Reduces every materialized element modulo `modulus`. /// /// Vectors with at least 32,768 entries use Rayon. Smaller vectors remain @@ -446,12 +476,7 @@ mod tests { type R = num_bigint::BigInt; fn stored_ring(value: &StoredInteger) -> R { - let bytes = value - .words() - .iter() - .flat_map(|word| word.to_le_bytes()) - .collect::>(); - R::from_signed_bytes_le(&bytes) + R::from(value) } fn direct_row(row: &SparseRow, integer_witness: &PackedWitness) -> R { @@ -540,6 +565,27 @@ mod tests { } } + #[test] + fn products_are_satisfied_over_the_integers_only_when_exact() { + let mut products = IntegerProducts::default(); + assert!(products.is_satisfied::()); + let z = |value: i128| Integer::<2>::from(value); + products.push(z(-3), z(5), z(-15)); + products.push( + z(i128::from(i64::MIN)), + z(i128::from(i64::MIN)), + z(1 << 126), + ); + assert!(products.is_satisfied::()); + products.push(z(2), z(2), z(5)); + assert!(!products.is_satisfied::()); + products.c_mw.pop(); + assert!( + !products.is_satisfied::(), + "a missing row is not satisfied" + ); + } + #[test] fn arbitrary_bigints_round_trip_through_stored_integers() { let boundary = R::one() << 128_usize; diff --git a/crates/circuit/src/witgen.rs b/crates/circuit/src/witgen.rs index 2f04d25a..759cdfd7 100644 --- a/crates/circuit/src/witgen.rs +++ b/crates/circuit/src/witgen.rs @@ -612,8 +612,8 @@ impl ProductWitgen { /// Consumes the runner into `w`, `M * w`, and exact matrix products. pub fn into_parts(self) -> (PackedWitness, PackedWitness, IntegerProducts) { - let (witness, integer_witness) = self.witgen.into_witnesses(); - (witness, integer_witness, self.products) + let (bool_witness, integer_witness) = self.witgen.into_witnesses(); + (bool_witness, integer_witness, self.products) } } diff --git a/crates/field/src/dynamic.rs b/crates/field/src/dynamic.rs index 54bcd6d2..18d25c93 100644 --- a/crates/field/src/dynamic.rs +++ b/crates/field/src/dynamic.rs @@ -2,7 +2,7 @@ //! //! We only expect to have one of those at any given time, so the modulus is shared globally. -use crate::helpers; +use crate::{FieldWithDynamicModulus, helpers}; use crypto_primitives::{BaseField, LiftElement, WithAssociatedInteger}; use crypto_primitives_proc_macros::InfallibleCheckedOp; use num_traits::{ @@ -75,18 +75,21 @@ pub struct DynField { } impl DynField { + /// The installed modulus and its reduction constants. + #[inline(always)] + pub fn config() -> helpers::FieldMetadata { + let cfg = global::load(); + debug_assert!(cfg.modulus != 0, "Field modulus has not been set yet!"); + cfg + } +} + +impl FieldWithDynamicModulus for DynField { /// Set modulus globally. /// /// The modulus must be an odd prime below `2^126`, the bound of the /// Barrett reduction shared with [`Fq`](crate::Fq); anything else panics. - /// - /// # Safety - /// - /// No [`DynField`] value may be alive, or they will silently become invalid. - /// No [`DynField`] operation may be in flight on another thread: the config - /// is stored word by word, and a value keeps the representative it had under - /// the previous modulus. - pub unsafe fn set_modulus(modulus: u128) { + unsafe fn set_modulus(modulus: u128) { assert!(modulus >= 3, "modulus must be at least 3"); assert_eq!(modulus % 2, 1, "modulus must be odd"); assert!( @@ -98,14 +101,6 @@ impl DynField { assert!(helpers::is_prime(modulus), "modulus must be prime"); global::store(&helpers::FieldMetadata::new(modulus)); } - - /// The installed modulus and its reduction constants. - #[inline(always)] - pub fn config() -> helpers::FieldMetadata { - let cfg = global::load(); - debug_assert!(cfg.modulus != 0, "Field modulus has not been set yet!"); - cfg - } } // @@ -431,20 +426,20 @@ impl LiftElement for DynField { // Other // -#[cfg(test)] -pub(crate) mod test_support { - use super::DynField; +#[allow(dead_code)] // Cannot be gated for #[cfg(test)] or it won't be accessible to other crates +pub mod test_support { + use super::*; use std::sync::{Mutex, PoisonError}; - /// Runs `test` under `modulus`. The modulus is process-wide and the test + /// Runs `test_code` under `modulus`. The modulus is process-wide and the test /// harness is multi-threaded, so every test that needs one holds this /// lock for its whole run. - pub(crate) fn with_modulus(modulus: u128, test: impl FnOnce() -> T) -> T { + pub fn with_modulus(modulus: u128, test_code: impl FnOnce() -> T) -> T { static LOCK: Mutex<()> = Mutex::new(()); let _guard = LOCK.lock().unwrap_or_else(PoisonError::into_inner); // SAFETY: the lock keeps every other test's values and operations out. unsafe { DynField::set_modulus(modulus) }; - test() + test_code() } } diff --git a/crates/field/src/lib.rs b/crates/field/src/lib.rs index 116d8aa6..45b5c326 100644 --- a/crates/field/src/lib.rs +++ b/crates/field/src/lib.rs @@ -7,6 +7,22 @@ pub mod fq; pub mod gf128; pub mod helpers; +pub use dynamic::DynField; pub use fq::{Fq, FqDefault, Q100}; pub use gf128::{F128, FixedBasePow, Wide256}; pub use helpers::MAX_MODULUS_BITS; + +/// A prime field whose modulus is installed at run time, so a proof can run +/// over a prime its transcript draws. +pub trait FieldWithDynamicModulus { + /// Installs `modulus` process-wide. + /// + /// # Safety + /// + /// No instances of this field value may be alive, in any thread, or they may + /// become invalid. + /// + /// Despite that, no low-level Rust guarantees are violated and thus no true + /// Undefined Behavior™ could happen under any circumstances. + unsafe fn set_modulus(modulus: u128); +} diff --git a/crates/spartan/src/piop.rs b/crates/spartan/src/piop.rs index 332a16a0..7b6735aa 100644 --- a/crates/spartan/src/piop.rs +++ b/crates/spartan/src/piop.rs @@ -5,7 +5,7 @@ use common::BitzField; use poly::{ DenseMultilinearExtension, MleClaimError, ScaledMleEvaluationClaim, make_equality_factors, }; -use transcript::{ProverState, VerifierState}; +use transcript::{ProverState, SqueezableTranscript, VerifierState}; use crate::matrix::{PreparedConstraintMatrices, SpartanMatrixError, build_assignment_mle}; use crate::sumcheck::{ diff --git a/crates/spartan/src/sumcheck.rs b/crates/spartan/src/sumcheck.rs index c2d9b6c6..ed912777 100644 --- a/crates/spartan/src/sumcheck.rs +++ b/crates/spartan/src/sumcheck.rs @@ -32,7 +32,7 @@ use crypto_primitives::Semiring; use poly::DenseMultilinearExtension; use rayon::prelude::*; use std::array; -use transcript::{ProverState, VerifierState}; +use transcript::{ProverState, SqueezableTranscript, VerifierState}; /// Failures produced while reducing or checking a sumcheck claim. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/transcript/src/challenge.rs b/crates/transcript/src/challenge.rs index d6c0371c..c0242138 100644 --- a/crates/transcript/src/challenge.rs +++ b/crates/transcript/src/challenge.rs @@ -22,18 +22,32 @@ pub trait TranscriptChallenge: Sized { fn from_squeezes(next_u128: impl FnMut() -> u128) -> Self; } -impl ProverState { +pub trait SqueezableTranscript { /// Samples a typed Fiat–Shamir challenge from this transcript. - pub fn squeeze(&mut self) -> T { + fn squeeze(&mut self) -> T; + + /// Squeezes a `bits`-bit prime from this transcript. + fn squeeze_prime(&mut self, bits: u32) -> u128; +} + +impl SqueezableTranscript for ProverState { + fn squeeze(&mut self) -> T { T::from_squeezes(|| self.verifier_message::()) } + + fn squeeze_prime(&mut self, bits: u32) -> u128 { + prime::sample(|| self.verifier_message::(), bits) + } } -impl VerifierState<'_> { - /// Samples a typed Fiat–Shamir challenge from this transcript. - pub fn squeeze(&mut self) -> T { +impl SqueezableTranscript for VerifierState<'_> { + fn squeeze(&mut self) -> T { T::from_squeezes(|| self.verifier_message::()) } + + fn squeeze_prime(&mut self, bits: u32) -> u128 { + prime::sample(|| self.verifier_message::(), bits) + } } /// Sample `u128` whose residue modulo `modulus` is uniform, from successive @@ -62,33 +76,9 @@ impl TranscriptChallenge for Fq { } } -/// A `bits`-bit prime from successive `u128` squeezes, the same on both -/// sides: the fingerprint prime of 5. "An end-to-end BitZ-based SNARK over -/// any finitely generated ring", Step 2, drawn once per proof after the -/// commitment and the statement are absorbed and before the PIOP. -/// -/// TODO(random-prime): draw it with [`prime::sample`] once the e2e builds its -/// parameters under the drawn prime. Until then the fixed modulus is returned -/// whatever width is asked for, and nothing is squeezed. -pub fn prime_from_squeezes(_next_u128: impl FnMut() -> u128, bits: u32) -> u128 { - assert!( - bits <= field::MAX_MODULUS_BITS, - "the field cannot hold a prime this wide" - ); - field::Q100 -} - -impl ProverState { - /// Squeezes a `bits`-bit prime from this transcript. - pub fn squeeze_prime(&mut self, bits: u32) -> u128 { - prime_from_squeezes(|| self.verifier_message::(), bits) - } -} - -impl VerifierState<'_> { - /// Squeezes a `bits`-bit prime from this transcript. - pub fn squeeze_prime(&mut self, bits: u32) -> u128 { - prime_from_squeezes(|| self.verifier_message::(), bits) +impl TranscriptChallenge for DynField { + fn from_squeezes(next_u128: impl FnMut() -> u128) -> Self { + Self::from(unbiased_u128(DynField::config().modulus, next_u128)) } } @@ -99,20 +89,12 @@ impl TranscriptChallenge for F128 { } } -impl TranscriptChallenge for DynField { - fn from_squeezes(next_u128: impl FnMut() -> u128) -> Self { - Self::from(unbiased_u128(DynField::config().modulus, next_u128)) - } -} - #[cfg(test)] mod tests { - use field::dynamic::DynField; - use field::{F128, Q100}; - use spongefish::Encoding; - - use super::TranscriptChallenge; + use super::*; use crate::{build_prover, build_verifier}; + use field::Q100; + use spongefish::Encoding; const SESSION: &[u8] = b"transcript/typed-challenge/test"; const INSTANCE: &[u8] = b"fq-rejection-sampling"; @@ -141,20 +123,19 @@ mod tests { /// Under the same modulus, the same stream gives the same element. #[test] fn dyn_field_squeezes_as_fq() { - // SAFETY: process-wide, and the only modulus this binary installs. - unsafe { DynField::set_modulus(Q100) }; - - let mut typed = build_prover(SESSION, INSTANCE); - let dynamic = typed.squeeze::(); - let mut fixed = build_prover(SESSION, INSTANCE); - let fq = fixed.squeeze::(); - assert_eq!(dynamic.encode().as_ref(), fq.encode().as_ref()); - - let rejection_remainder = (u128::MAX % Q100 + 1) % Q100; - let max_accepted = u128::MAX - rejection_remainder; - let mut candidates = [max_accepted + 1, max_accepted].into_iter(); - let challenge = DynField::from_squeezes(|| candidates.next().unwrap()); - assert_eq!(challenge, DynField::from(Q100 - 1)); + field::dynamic::test_support::with_modulus(Q100, || { + let mut typed = build_prover(SESSION, INSTANCE); + let dynamic = typed.squeeze::(); + let mut fixed = build_prover(SESSION, INSTANCE); + let fq = fixed.squeeze::(); + assert_eq!(dynamic.encode().as_ref(), fq.encode().as_ref()); + + let rejection_remainder = (u128::MAX % Q100 + 1) % Q100; + let max_accepted = u128::MAX - rejection_remainder; + let mut candidates = [max_accepted + 1, max_accepted].into_iter(); + let challenge = DynField::from_squeezes(|| candidates.next().unwrap()); + assert_eq!(challenge, DynField::from(Q100 - 1)); + }); } #[test] @@ -179,6 +160,8 @@ mod tests { let prime = prover.squeeze_prime(F::META.bits); let after = prover.squeeze::(); let proof = prover.finish(); + assert_eq!(u128::BITS - prime.leading_zeros(), F::META.bits); + assert!(field::helpers::is_prime(prime)); let mut verifier = build_verifier(SESSION, INSTANCE, &proof); assert_eq!(verifier.squeeze_prime(F::META.bits), prime); diff --git a/crates/transcript/src/lib.rs b/crates/transcript/src/lib.rs index df6fd352..a4663ba1 100644 --- a/crates/transcript/src/lib.rs +++ b/crates/transcript/src/lib.rs @@ -33,7 +33,7 @@ mod proof; mod prover; mod verifier; -pub use challenge::{TranscriptChallenge, prime}; +pub use challenge::{SqueezableTranscript, TranscriptChallenge}; pub use domain::{PROTOCOL_LABEL, build_prover, build_verifier}; pub use proof::Proof; pub use prover::ProverState; diff --git a/crates/verifier/src/verify.rs b/crates/verifier/src/verify.rs index 7b47d9f0..1376265c 100644 --- a/crates/verifier/src/verify.rs +++ b/crates/verifier/src/verify.rs @@ -143,8 +143,8 @@ impl BitZVerifier { claim: &LinearClaim, transcript: &mut VerifierState<'_>, ) -> Result { - // Step 2 is absent: the field modulus is fixed, and BitZParams::new - // checks its fold bound. + // Step 2 is skipped: BitZParams::new checks the claim field bound + // `q < (|K| - 1) / k_1`, so a fold cannot wrap in the exponent. // Step 3: read the folds, range-check them, reconstruct against mu. let fold = self diff --git a/tooling/cli/benches/circuits.rs b/tooling/cli/benches/circuits.rs index 822d3b08..dfb09858 100644 --- a/tooling/cli/benches/circuits.rs +++ b/tooling/cli/benches/circuits.rs @@ -1,15 +1,15 @@ //! End-to-end and per-stage benchmarks; instance generation is outside timing. use bitz_cli::{ - ProjectBigIntToFq, benchmark, + ProjectBigIntToField, benchmark, circuits::{BuiltinCircuit, CircuitInstance}, end_to_end::CircuitProofSystem, }; use divan::Bencher; type R = num_bigint::BigInt; -type F = field::FqDefault; -type Proj = ProjectBigIntToFq; +type F = field::DynField; +type Proj = ProjectBigIntToField; fn main() { divan::main(); @@ -21,10 +21,10 @@ fn instance(circuit: BuiltinCircuit) -> (CircuitInstance, Vec) { (statement, inputs) } -fn setup(circuit: BuiltinCircuit) -> (CircuitProofSystem, Vec) { +fn setup(circuit: BuiltinCircuit) -> (CircuitProofSystem, Vec) { let (statement, inputs) = instance(circuit); ( - CircuitProofSystem::<_, F>::new::(statement).unwrap(), + CircuitProofSystem::<_, F, _, Proj>::new(statement).unwrap(), inputs, ) } @@ -43,7 +43,7 @@ fn circuit_setup(bencher: Bencher, circuit: BuiltinCircuit) { bencher .with_inputs(|| CircuitInstance::random(circuit, None, None).unwrap()) .bench_local_values(|statement| { - CircuitProofSystem::<_, F>::new::(statement).unwrap() + CircuitProofSystem::<_, F, _, Proj>::new(statement).unwrap() }); } diff --git a/tooling/cli/benches/sha256_spartan.rs b/tooling/cli/benches/sha256_spartan.rs index 0066d51b..47736eda 100644 --- a/tooling/cli/benches/sha256_spartan.rs +++ b/tooling/cli/benches/sha256_spartan.rs @@ -6,8 +6,8 @@ use std::sync::{Arc, Mutex}; -use bitz_cli::{ProjectBigIntToFq, ProjectConstraint}; -use circuit::constraints::ConstraintGenerator; +use bitz_cli::{ProjectBigIntToField, ProjectConstraint}; +use circuit::constraints::{ConstraintGenerator, ConstraintMatrices}; use circuit::sha256::sha256_block_aligned_circuit; use circuit::witgen::ProductWitgen; use divan::{AllocProfiler, Bencher, black_box}; @@ -30,7 +30,7 @@ const BLOCKS: &[usize] = &[608]; type R = num_bigint::BigInt; type F = field::FqDefault; -type Proj = ProjectBigIntToFq; +type Proj = ProjectBigIntToField; /// An R1CS instance with a witness satisfying it. #[derive(Debug, Clone)] @@ -85,7 +85,8 @@ fn build(blocks: usize) -> R1csInstanceWitness { // Lower to Q100 and pad to the Boolean domains. let projection = >::prepare(); - let matrices = integer_matrices.map_coefficients(|c| projection.project(&c)); + let matrices: ConstraintMatrices = + integer_matrices.map_coefficients(|c| projection.project(&c)); let products = build_product_mles(&exact_products, matrices.a.row_count()).unwrap(); let assignment = build_assignment_mle::(&assignment_bits, matrices.a.column_count()).unwrap(); diff --git a/tooling/cli/src/benchmark.rs b/tooling/cli/src/benchmark.rs index 85115321..3fd02ce6 100644 --- a/tooling/cli/src/benchmark.rs +++ b/tooling/cli/src/benchmark.rs @@ -2,9 +2,10 @@ use crate::ProjectConstraint; use crate::end_to_end::{CircuitProofSystem, CircuitStatement, CircuitStats, Error}; -use circuit::matrix_products::ModularVector; +use circuit::matrix_products::StoredInteger; use circuit::{BitWidth, IntoWords}; use common::{BitzClaimField, BitzConstraintRing}; +use field::FieldWithDynamicModulus; use std::{ fmt, time::{Duration, Instant}, @@ -25,14 +26,13 @@ pub struct Timings { pub fn run(statement: S, inputs: &[bool]) -> Result where S: CircuitStatement, - F: BitzClaimField, + F: BitzClaimField + FieldWithDynamicModulus, F::Integer: BitWidth + IntoWords, - Vec: for<'a> From<&'a ModularVector<2>>, - R: BitzConstraintRing, + R: BitzConstraintRing + for<'a> From<&'a StoredInteger>, Proj: ProjectConstraint, { let started = Instant::now(); - let prepared = CircuitProofSystem::::new::(statement)?; + let prepared = CircuitProofSystem::<_, _, _, Proj>::new(statement)?; let setup = started.elapsed(); let circuit = prepared.stats(); tracing::info!( @@ -41,6 +41,7 @@ where assignment_bits = circuit.assignment_bits, committed_bits = circuit.committed_bits, padded_committed_bits = circuit.padded_committed_bits, + prime_bits = circuit.prime_bits, "Circuit prepared", ); let started = Instant::now(); @@ -69,12 +70,13 @@ impl fmt::Display for Timings { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "opening_path={:?} constraints={} assignment_bits={} committed_bits={} padded_committed_bits={} setup_ms={:.3} witness_ms={:.3} commit_ms={:.3} prove_ms={:.3} total_prove_ms={:.3} verify_ms={:.3}", + "opening_path={:?} constraints={} assignment_bits={} committed_bits={} padded_committed_bits={} prime_bits={} setup_ms={:.3} witness_ms={:.3} commit_ms={:.3} prove_ms={:.3} total_prove_ms={:.3} verify_ms={:.3}", self.circuit.opening_path, self.circuit.constraints, self.circuit.assignment_bits, self.circuit.committed_bits, self.circuit.padded_committed_bits, + self.circuit.prime_bits, self.setup.as_secs_f64() * 1000., self.witness.as_secs_f64() * 1000., self.commit.as_secs_f64() * 1000., diff --git a/tooling/cli/src/cmd/circuit_e2e.rs b/tooling/cli/src/cmd/circuit_e2e.rs index 31ff5a57..4f5184c1 100644 --- a/tooling/cli/src/cmd/circuit_e2e.rs +++ b/tooling/cli/src/cmd/circuit_e2e.rs @@ -8,10 +8,10 @@ use { }, }; -type F = field::FqDefault; -type Proj = bitz_cli::ProjectBigIntToFq; +type F = field::DynField; +type Proj = bitz_cli::ProjectBigIntToField; -/// Prove generated circuit constraints over Q100 and verify the proof. +/// Prove generated circuit constraints over a transcript-drawn prime and verify the proof. #[derive(FromArgs, PartialEq, Debug)] #[argh(subcommand, name = "circuit-e2e")] pub struct Args { @@ -48,7 +48,7 @@ impl Command for Args { "circuit_e2e", circuit = %self.circuit, threads = rayon::current_num_threads(), - field = "Q100", + field = "random-prime", pcs = "Fast", hash = "Blake3", ).entered(); @@ -58,7 +58,7 @@ impl Command for Args { let inputs = statement.inputs.clone(); let timings = benchmark::run::<_, F, _, Proj>(statement, &inputs)?; tracing::info!("Proof verified successfully"); - println!("circuit={} threads={} field=Q100 pcs=Fast hash=Blake3 relation=Q100-r1cs constraints_verified=true", self.circuit, rayon::current_num_threads()); + println!("circuit={} threads={} field=random-prime pcs=Fast hash=Blake3 relation=r1cs constraints_verified=true", self.circuit, rayon::current_num_threads()); println!("{timings}"); Ok(()) }) diff --git a/tooling/cli/src/end_to_end.rs b/tooling/cli/src/end_to_end.rs index df220b71..ed296350 100644 --- a/tooling/cli/src/end_to_end.rs +++ b/tooling/cli/src/end_to_end.rs @@ -1,9 +1,14 @@ -//! Circuit constraints over a prime field, reduced by Spartan and opened through direct or virtual BitZ. +//! Circuit constraints over a prime field, reduced by Spartan and opened +//! through direct or virtual BitZ. +//! +//! The field's modulus is sampled at a runtime once the commitment and the +//! statement are absorbed. +use circuit::matrix_products::StoredInteger; use circuit::{ BitWidth, Circuit, IntoWords, - constraints::{ConstraintGenerator, SparseBoolMatrix}, - matrix_products::ModularVector, + constraints::{ConstraintGenerator, ConstraintMatrices, SparseBoolMatrix}, + matrix_products::IntegerProducts, matrix_transpose::{MTransposeGenerator, MaterializedMTranspose}, witgen::{PackedWitness, ProductWitgen}, }; @@ -12,27 +17,37 @@ use common::{ VirtualMap, VirtualStatement, shape::{MIN_LOG_BITS, PACK_BITS}, }; -use field::{F128, gf128::smallest_generator}; +use field::{F128, FieldWithDynamicModulus, gf128::smallest_generator}; use num_traits::{ConstOne, ConstZero}; use pcs::{CommitScheme, HashKind, LigeritoProfile, Pcs, ProverData, StatementBinding}; -use poly::{DenseMultilinearExtension, ScaledMleEvaluationClaim}; +use poly::ScaledMleEvaluationClaim; use prover::{BitZProver, VirtualWitness}; -use transcript::{ProverState, PublicTranscript, build_prover, build_verifier}; +use std::marker::PhantomData; +use std::sync::{Mutex, MutexGuard, PoisonError}; +use transcript::{ + ProverState, PublicTranscript, SqueezableTranscript, build_prover, build_verifier, +}; use verifier::BitZVerifier; use crate::ProjectConstraint; use spartan::{ - PreparedConstraintMatrices, R1csProductMles, SpartanPiopProof, build_assignment_mle, + PreparedConstraintMatrices, SpartanMatrixError, SpartanPiopProof, build_assignment_mle, build_product_mles, prove_spartan_piop, verify_spartan_proof, }; -const SESSION: &[u8] = b"bitz/circuit-e2e/v1"; +const SESSION: &[u8] = b"bitz/circuit-e2e/v2"; const WINDOW: u32 = 8; +/// Held for the whole of a proof or a verification: the modulus they install +/// is process-wide. +static DYNAMIC_MODULUS_LOCK: Mutex<()> = Mutex::new(()); + +type ModulusLock<'a> = MutexGuard<'a, ()>; + /// A trusted, deterministic circuit and its public inputs. Implementations must /// emit identical operations for symbolic and concrete backends and constrain /// every public input/output. The proved constraints are interpreted modulo -/// the modulus of the field the proof system is built over. +/// the prime the proof draws. pub trait CircuitStatement { fn domain(&self) -> &'static [u8]; fn public_bytes(&self) -> Vec; @@ -47,7 +62,7 @@ pub enum Error { #[error("invalid proof configuration: {0}")] Configuration(&'static str), #[error("matrix preparation failed: {0:?}")] - Matrix(spartan::SpartanMatrixError), + Matrix(SpartanMatrixError), #[error("circuit witness does not satisfy the constraints")] Unsatisfied, #[error("Spartan failed: {0:?}")] @@ -81,28 +96,36 @@ pub struct CircuitStats { /// Meaningful committed witness bits before PCS zero padding. pub committed_bits: usize, pub padded_committed_bits: usize, + /// Width of the fingerprint prime each proof draws. + pub prime_bits: u32, } -/// A circuit prepared for proving over the prime field `F`. +/// A circuit prepared for proving over the prime field `F`, whose modulus +/// each proof draws; `Proj` projects the constraints from `R` onto it. #[derive(Debug)] -pub struct CircuitProofSystem { +pub struct CircuitProofSystem { statement: S, opening_path: OpeningPath, - matrices: PreparedConstraintMatrices, + /// Exact, projected onto `F` under the drawn prime. + matrices: ConstraintMatrices, map: MaterializedMTranspose, - params: BitZParams, + claim_shape: Shape, + generator: F128, /// Width of the fingerprint prime, fixed by the claim shape. prime_bits: u32, committed_shape: Shape, pcs: Pcs, + _phantom: PhantomData<(F, Proj)>, } +/// A witness kept exact: it takes field values once the prime is drawn. #[derive(Debug)] -pub struct Witness { - committed: Vec, - assignment_bits: Vec, - assignment: DenseMultilinearExtension, - products: R1csProductMles, +pub struct Witness { + committed_bits: Vec, + /// Only exist on a virtual opening path + virtual_bits: Option>, + assignment: PackedWitness, + products: IntegerProducts, } /// Commitment data and the transcript that sampled its OOD claim. @@ -111,6 +134,7 @@ pub struct CommittedWitness { transcript: ProverState, } +/// The residues are under the prime the transcript yields for `root`. #[derive(Clone, Debug)] pub struct Proof { pub root: Root, @@ -118,43 +142,37 @@ pub struct Proof { pub opening: transcript::Proof, } -impl CircuitProofSystem +impl CircuitProofSystem where S: CircuitStatement, - F: BitzClaimField, + F: BitzClaimField + FieldWithDynamicModulus, F::Integer: BitWidth + IntoWords, - Vec: for<'a> From<&'a ModularVector<2>>, // Needed for `build_product_mles` + R: BitzConstraintRing + for<'a> From<&'a StoredInteger>, + Proj: ProjectConstraint, { #[tracing::instrument(name = "setup", skip_all)] - pub fn new(statement: S) -> Result - where - R: BitzConstraintRing, - Proj: ProjectConstraint, - { + pub fn new(statement: S) -> Result { let mut constraints = ConstraintGenerator::::new(statement.input_bits()); let inputs: Vec<_> = (0..statement.input_bits()) .map(|i| constraints.input(i)) .collect(); statement.synthesize(&mut constraints, &inputs)?; - let projection = Proj::prepare(); - let matrices = PreparedConstraintMatrices::new( - constraints - .into_matrices() - .map_coefficients(|c| projection.project(&c)), - ) - .map_err(Error::Matrix)?; + let matrices = constraints.into_matrices(); + matrices + .validate_shape() + .map_err(|_| Error::Matrix(SpartanMatrixError::InvalidR1csShape))?; let mut generator = MTransposeGenerator::new(statement.input_bits()); let inputs = generator.take_inputs(); statement.synthesize(&mut generator, &inputs)?; let map = generator.finish(); - if map.h_len() != matrices.matrices().a.column_count() { + if map.h_len() != matrices.a.column_count() { return Err(Error::Configuration("map and assignment dimensions differ")); } - if map.f_len() != matrices.matrices().m.column_count() { + if map.f_len() != matrices.m.column_count() { return Err(Error::Configuration("map and witness dimensions differ")); } let claim_shape = shape_for(map.h_len())?; - let opening_path = if is_identity(&matrices.matrices().m) { + let opening_path = if is_identity(&matrices.m) { OpeningPath::Direct } else { OpeningPath::Virtual @@ -163,8 +181,6 @@ where OpeningPath::Direct => claim_shape, OpeningPath::Virtual => shape_for(map.f_len() - 1)?, }; - let params = BitZParams::new(claim_shape, smallest_generator()) - .map_err(|_| Error::Configuration("inadmissible BitZ parameters"))?; let prime_bits = common::prime_bits(&claim_shape) .map_err(|_| Error::Configuration("shape too tall for the fingerprint prime"))?; let pcs = Pcs::new(&committed_shape, LigeritoProfile::Fast, HashKind::Blake3) @@ -174,28 +190,31 @@ where opening_path, matrices, map, - params, + claim_shape, + generator: smallest_generator(), prime_bits, committed_shape, pcs, + _phantom: PhantomData, }) } pub fn stats(&self) -> CircuitStats { CircuitStats { opening_path: self.opening_path, - constraints: self.matrices.matrices().a.row_count(), + constraints: self.matrices.a.row_count(), assignment_bits: self.map.h_len(), committed_bits: match self.opening_path { OpeningPath::Direct => self.map.h_len(), OpeningPath::Virtual => self.map.f_len() - 1, }, padded_committed_bits: self.pcs.bit_len(), + prime_bits: self.prime_bits, } } #[tracing::instrument(name = "witness", skip_all)] - pub fn witness(&self, inputs: &[bool]) -> Result, Error> { + pub fn witness(&self, inputs: &[bool]) -> Result { if inputs.len() != self.statement.input_bits() { return Err(Error::Input("wrong witness input length")); } @@ -205,27 +224,22 @@ where if f.bit_len() + 1 != self.map.f_len() || h.bit_len() != self.map.h_len() { return Err(Error::Input("circuit replay changed witness dimensions")); } - let products = build_product_mles(&products, self.matrices.matrices().a.row_count()) - .map_err(Error::Matrix)?; - if products - .az - .iter() - .zip(products.bz.iter()) - .zip(products.cz.iter()) - .any(|((&a, &b), &c)| a * b != c) - { + if products.a_mw.len() != self.matrices.a.row_count() { + return Err(Error::Input("circuit replay changed constraint count")); + } + // Over the integers, so modulo whichever prime is drawn. + if !products.is_satisfied::() { return Err(Error::Unsatisfied); } - let assignment = build_assignment_mle(&h, self.map.h_len()).map_err(Error::Matrix)?; - let assignment_bits = pack(&h, *self.params.shape()); - let (committed, assignment_bits) = match self.opening_path { - OpeningPath::Direct => (assignment_bits, Vec::new()), - OpeningPath::Virtual => (pack(&f, self.committed_shape), assignment_bits), + let assignment_bits = pack(&h, self.claim_shape); + let (committed_bits, virtual_bits) = match self.opening_path { + OpeningPath::Direct => (assignment_bits, None), + OpeningPath::Virtual => (pack(&f, self.committed_shape), Some(assignment_bits)), }; Ok(Witness { - committed, - assignment_bits, - assignment, + committed_bits, + virtual_bits, + assignment: h, products, }) } @@ -233,22 +247,22 @@ where /// Commits and sends the initial OOD evaluation before any PIOP challenge. /// The returned state retains both PCS data and the transcript for proving. #[tracing::instrument(name = "commit", skip_all)] - pub fn commit(&self, witness: &Witness) -> Result { + pub fn commit(&self, witness: &Witness) -> Result { let mut transcript = build_prover(SESSION, self.statement.domain()); let (_, data) = self .pcs - .commit(&witness.committed, &mut transcript) + .commit(&witness.committed_bits, &mut transcript) .map_err(Error::Commit)?; Ok(CommittedWitness { data, transcript }) } - /// Continues the commitment transcript through Spartan and the BitZ opening. + /// Continues the commitment transcript through the prime draw, Spartan + /// and the BitZ opening. #[tracing::instrument(name = "prove", skip_all, fields(opening_path = ?self.opening_path))] - pub fn prove( - &self, - witness: Witness, - commitment: CommittedWitness, - ) -> Result, Error> { + pub fn prove(&self, witness: Witness, commitment: CommittedWitness) -> Result, Error> { + let prime_lock = DYNAMIC_MODULUS_LOCK + .lock() + .unwrap_or_else(PoisonError::into_inner); let CommittedWitness { data, mut transcript, @@ -259,42 +273,46 @@ where self.pcs .prove_lin( &data, - witness.committed.clone(), + witness.committed_bits.clone(), &self.constant_query(), StatementBinding::Bind, &mut transcript, ) .map_err(Error::ConstantProve)?; } - // TODO(random-prime): bind the integer constraint digest before the - // draw, then build the parameters, matrices and witness under `prime`. let prime = transcript.squeeze_prime(self.prime_bits); - let prime = F::Integer::from(prime); - debug_assert_eq!(prime, F::modulus()); - let (spartan, terminal) = prove_spartan_piop( - &mut transcript, - &self.matrices, - &witness.products, - &witness.assignment, - ) - .map_err(Error::Spartan)?; - let claim = opening_claim(&self.params, &terminal)?; - let prover = BitZProver::new(self.params, WINDOW); + let (params, matrices) = self.under_prime(prime, &prime_lock, &mut transcript)?; + let products = build_product_mles(&witness.products, self.matrices.a.row_count()) + .map_err(Error::Matrix)?; + let assignment = + build_assignment_mle(&witness.assignment, self.map.h_len()).map_err(Error::Matrix)?; + let (spartan, terminal) = + prove_spartan_piop(&mut transcript, &matrices, &products, &assignment) + .map_err(Error::Spartan)?; + let claim = opening_claim(¶ms, &terminal)?; + let prover = BitZProver::new(params, WINDOW); match self.opening_path { - OpeningPath::Direct => { - prover.prove(&claim, &self.pcs, &data, witness.committed, &mut transcript) - } + OpeningPath::Direct => prover.prove( + &claim, + &self.pcs, + &data, + witness.committed_bits, + &mut transcript, + ), OpeningPath::Virtual => { let statement = - VirtualStatement::new(self.params, self.committed_shape, &self.map, &claim) + VirtualStatement::new(params, self.committed_shape, &self.map, &claim) .map_err(|_| Error::Configuration("invalid virtual statement"))?; prover.prove_virtual( &statement, &self.pcs, &data, VirtualWitness { - committed_bits: witness.committed, - virtual_bits: &witness.assignment_bits, + committed_bits: witness.committed_bits, + virtual_bits: witness + .virtual_bits + .as_ref() + .expect("virtual_bits must exist on virtual path"), }, &mut transcript, ) @@ -310,6 +328,9 @@ where #[tracing::instrument(name = "verify", skip_all)] pub fn verify(&self, proof: &Proof) -> Result<(), Error> { + let prime_lock = DYNAMIC_MODULUS_LOCK + .lock() + .unwrap_or_else(PoisonError::into_inner); let mut transcript = build_verifier(SESSION, self.statement.domain(), &proof.opening); let commitment = self .pcs @@ -326,22 +347,19 @@ where ) .map_err(Error::ConstantVerify)?; } - // TODO(random-prime): as in `prove`, the parameters and matrices must - // be built under `prime`. let prime = transcript.squeeze_prime(self.prime_bits); - let prime = F::Integer::from(prime); - debug_assert_eq!(prime, F::modulus()); - let terminal = verify_spartan_proof(&mut transcript, &self.matrices, &proof.spartan) + let (params, matrices) = self.under_prime(prime, &prime_lock, &mut transcript)?; + let terminal = verify_spartan_proof(&mut transcript, &matrices, &proof.spartan) .map_err(Error::Spartan)?; - let claim = opening_claim(&self.params, &terminal)?; - let verifier = BitZVerifier::new(self.params, WINDOW); + let claim = opening_claim(¶ms, &terminal)?; + let verifier = BitZVerifier::new(params, WINDOW); match self.opening_path { OpeningPath::Direct => { verifier.verify_with_commitment(&claim, &self.pcs, &commitment, transcript) } OpeningPath::Virtual => { let statement = - VirtualStatement::new(self.params, self.committed_shape, &self.map, &claim) + VirtualStatement::new(params, self.committed_shape, &self.map, &claim) .map_err(|_| Error::Configuration("invalid virtual statement"))?; verifier.verify_virtual_with_commitment( &statement, @@ -354,6 +372,29 @@ where .map_err(Error::Verify) } + /// Installs the sampled prime and builds the parameters and the matrices + /// under it. The parameters' frame carries the prime into the transcript. + fn under_prime( + &self, + prime: u128, + _prime_lock: &ModulusLock<'_>, + transcript: &mut impl PublicTranscript, + ) -> Result<(BitZParams, PreparedConstraintMatrices), Error> { + tracing::info!(bits = self.prime_bits, prime = %prime, "Fingerprint prime drawn"); + // SAFETY: No value of `F` exists yet, and `DYNAMIC_MODULUS_LOCK` is held, so + // no operation is in flight elsewhere. + unsafe { F::set_modulus(prime) }; + let params = BitZParams::new(self.claim_shape, self.generator) + .map_err(|_| Error::Configuration("inadmissible BitZ parameters"))?; + transcript.public_message(¶ms); + let projection = Proj::prepare(); + let matrices = self + .matrices + .map_coefficients_ref(|c| projection.project(c)); + let matrices = PreparedConstraintMatrices::new(matrices).map_err(Error::Matrix)?; + Ok((params, matrices)) + } + // Direct commitment includes h[0]; unlike the virtual map, it does not // supply that coordinate as a fixed one. Opening at zero enforces h[0] = 1. fn constant_query(&self) -> OpeningQuery { @@ -363,12 +404,15 @@ where } } + /// Everything public and fixed before the prime is drawn. fn bind(&self, transcript: &mut impl PublicTranscript, root: Root) { let public = self.statement.public_bytes(); transcript.public_message(&(public.len() as u64)); transcript.public_message(public.as_slice()); transcript.public_message(&root.0); - transcript.public_message(&self.params); + transcript.public_message(&(self.claim_shape.log_rows() as u64)); + transcript.public_message(&(self.claim_shape.log_columns() as u64)); + transcript.public_message(&self.generator); transcript.public_message(&self.pcs); transcript.public_message(&self.map.digest()); transcript.public_message(b"bitz/circuit-opening-path/v1"); @@ -429,10 +473,13 @@ fn opening_claim( #[cfg(test)] mod tests { use super::*; - use crate::ProjectBigIntToFq; + use crate::ProjectBigIntToField; + use poly::DenseMultilinearExtension; - type F = field::FqDefault; - type Proj = ProjectBigIntToFq; + type F = field::DynField; + type R = num_bigint::BigInt; + type Proj = ProjectBigIntToField; + type System = CircuitProofSystem; #[test] fn only_exact_identity_maps_select_direct_opening() { @@ -473,7 +520,7 @@ mod tests { #[test] fn commitment_sends_ood_before_proving() { - let system = CircuitProofSystem::<_, F>::new::<_, Proj>(IdentityBit).unwrap(); + let system = System::new(IdentityBit).unwrap(); let witness = system.witness(&[true]).unwrap(); let committed = system.commit(&witness).unwrap(); let proof = committed.transcript.finish(); @@ -489,7 +536,7 @@ mod tests { #[test] fn direct_opening_requires_constant_one_on_both_sides() { - let mut system = CircuitProofSystem::<_, F>::new::<_, Proj>(IdentityBit).unwrap(); + let mut system = System::new(IdentityBit).unwrap(); let witness = system.witness(&[true]).unwrap(); let data = system.commit(&witness).unwrap(); let mut proof = system.prove(witness, data).unwrap(); @@ -499,7 +546,7 @@ mod tests { system.opening_path = OpeningPath::Direct; let mut bad_witness = system.witness(&[true]).unwrap(); - bad_witness.committed.fill(F128::ZERO); + bad_witness.committed_bits.fill(F128::ZERO); let bad_data = system.commit(&bad_witness).unwrap(); assert!(matches!( system.prove(bad_witness, bad_data), @@ -533,8 +580,31 @@ mod tests { )); } + /// The prime is the transcript's: the same statement and commitment + /// yield it again, another statement or commitment yields another. + #[test] + fn the_prime_follows_the_transcript() { + let system = System::new(IdentityBit).unwrap(); + let witness = system.witness(&[true]).unwrap(); + let root = system.commit(&witness).unwrap().data.root(); + let draw = |domain: &'static [u8], root: Root| { + let mut transcript = build_prover(SESSION, domain); + system.bind(&mut transcript, root); + transcript.squeeze_prime(system.prime_bits) + }; + let prime = draw(system.statement.domain(), root); + assert_eq!(u128::BITS - prime.leading_zeros(), system.prime_bits); + assert!(field::helpers::is_prime(prime)); + assert_eq!(draw(system.statement.domain(), root), prime); + assert_ne!(draw(b"another/statement", root), prime); + assert_ne!(draw(system.statement.domain(), Root([0; 32])), prime); + } + + /// `opening_claim` is generic, so a fixed field keeps this test off the + /// installed modulus. #[test] fn scaled_claim_conversion_preserves_values_and_zero_scale() { + type F = field::FqDefault; let params = BitZParams::::new(Shape::new(7, 15).unwrap(), smallest_generator()).unwrap(); let assignment = diff --git a/tooling/cli/src/lib.rs b/tooling/cli/src/lib.rs index 56990eec..96e2b968 100644 --- a/tooling/cli/src/lib.rs +++ b/tooling/cli/src/lib.rs @@ -4,7 +4,7 @@ pub mod benchmark; pub mod circuits; pub mod end_to_end; -use field::Fq; +use common::BitzClaimField; use num_bigint::BigInt; use num_traits::{Signed, ToPrimitive}; @@ -17,24 +17,27 @@ pub trait ProjectConstraint: Send + Sync { fn project(&self, constraint: &R) -> F; } -/// Projects [`BigInt`] constraints onto [`Fq`] by reducing it canonically modulo `Q`. -pub struct ProjectBigIntToFq { +/// Projects [`BigInt`] constraints onto a prime field by reducing them +/// canonically modulo its modulus, read when prepared. +#[derive(Debug, Clone)] +pub struct ProjectBigIntToField { modulus: BigInt, } -impl ProjectConstraint> for ProjectBigIntToFq { +impl ProjectConstraint for ProjectBigIntToField { fn prepare() -> Self { + let modulus = F::modulus().to_u128().expect("the modulus fits a u128"); Self { - modulus: BigInt::from(Q), + modulus: BigInt::from(modulus), } } - fn project(&self, constraint: &BigInt) -> Fq { + fn project(&self, constraint: &BigInt) -> F { let mut reduced = constraint % &self.modulus; if reduced.is_negative() { reduced += &self.modulus; } - Fq::from( + F::from( reduced .to_u128() .expect("a canonical residue always fits a u128"), diff --git a/tooling/cli/tests/circuits.rs b/tooling/cli/tests/circuits.rs index 063c5686..0569baf8 100644 --- a/tooling/cli/tests/circuits.rs +++ b/tooling/cli/tests/circuits.rs @@ -1,5 +1,5 @@ use bitz_cli::{ - ProjectBigIntToFq, + ProjectBigIntToField, circuits::{BuiltinCircuit, CircuitInstance}, end_to_end::{CircuitProofSystem, CircuitStatement, Error}, }; @@ -10,11 +10,14 @@ use circuit::{ use num_traits::{Signed, ToPrimitive}; type R = num_bigint::BigInt; -type F = field::FqDefault; -type Proj = ProjectBigIntToFq; +type F = field::DynField; +type Proj = ProjectBigIntToField; +/// Every residual is below the narrowest prime a proof may draw, so no +/// Boolean assignment satisfies a row modulo the prime without satisfying it +/// over the integers. #[test] -fn sha_constraint_residuals_cannot_wrap_modulo_q100() { +fn sha_constraint_residuals_cannot_wrap_modulo_the_prime() { for circuit in BuiltinCircuit::ALL { let statement = CircuitInstance::random(circuit, None, None).unwrap(); let mut generator = ConstraintGenerator::::new(statement.input_bits()); @@ -45,7 +48,10 @@ fn sha_constraint_residuals_cannot_wrap_modulo_q100() { .unwrap() .checked_add(bound(c)) .unwrap(); - assert!(residual_bound < field::Q100, "{circuit}: residual may wrap"); + assert!( + residual_bound < 1u128 << (common::MIN_PRIME_BITS - 1), + "{circuit}: residual may wrap" + ); } } } @@ -55,7 +61,7 @@ fn supported_sha_circuits_prove_and_verify() { for circuit in BuiltinCircuit::ALL { let statement = CircuitInstance::random(circuit, None, None).unwrap(); let inputs = statement.inputs.clone(); - let system = CircuitProofSystem::<_, F>::new::(statement).unwrap(); + let system = CircuitProofSystem::<_, F, R, Proj>::new(statement).unwrap(); let witness = system.witness(&inputs).unwrap(); let data = system.commit(&witness).unwrap(); let proof = system.prove(witness, data).unwrap(); @@ -78,17 +84,17 @@ fn sha_compression_matches_abc_and_binds_public_values() { inputs: inputs.clone(), output: bits(&ABC_DIGEST), }; - let system = CircuitProofSystem::<_, F>::new::(statement.clone()).unwrap(); + let system = CircuitProofSystem::<_, F, R, Proj>::new(statement.clone()).unwrap(); let witness = system.witness(&inputs).unwrap(); let data = system.commit(&witness).unwrap(); let proof = system.prove(witness, data).unwrap(); - CircuitProofSystem::<_, F>::new::(statement.clone()) + CircuitProofSystem::<_, F, R, Proj>::new(statement.clone()) .unwrap() .verify(&proof) .unwrap(); let mut changed = statement.clone(); changed.output[0] ^= true; - let wrong_output = CircuitProofSystem::<_, F>::new::(changed).unwrap(); + let wrong_output = CircuitProofSystem::<_, F, R, Proj>::new(changed).unwrap(); assert!(wrong_output.verify(&proof).is_err()); assert!(matches!( wrong_output.witness(&inputs), @@ -97,7 +103,7 @@ fn sha_compression_matches_abc_and_binds_public_values() { let mut changed = statement; changed.inputs[0] ^= true; assert!( - CircuitProofSystem::<_, F>::new::(changed) + CircuitProofSystem::<_, F, R, Proj>::new(changed) .unwrap() .verify(&proof) .is_err() @@ -114,7 +120,7 @@ fn variable_lengths_custom_state_and_invalid_dimensions() { ] { let statement = CircuitInstance::random(circuit, count, state).unwrap(); let inputs = statement.inputs.clone(); - let system = CircuitProofSystem::<_, F>::new::(statement).unwrap(); + let system = CircuitProofSystem::<_, F, R, Proj>::new(statement).unwrap(); system.witness(&inputs).unwrap(); } for (circuit, count) in [ @@ -131,5 +137,5 @@ fn variable_lengths_custom_state_and_invalid_dimensions() { let mut malformed = CircuitInstance::random(BuiltinCircuit::Sha256Compression, None, None).unwrap(); malformed.inputs.pop(); - assert!(CircuitProofSystem::<_, F>::new::(malformed).is_err()); + assert!(CircuitProofSystem::<_, F, R, Proj>::new(malformed).is_err()); } diff --git a/tooling/cli/tests/end_to_end.rs b/tooling/cli/tests/end_to_end.rs index f9d387ce..48236bb8 100644 --- a/tooling/cli/tests/end_to_end.rs +++ b/tooling/cli/tests/end_to_end.rs @@ -1,14 +1,14 @@ -use bitz_cli::ProjectBigIntToFq; +use bitz_cli::ProjectBigIntToField; use bitz_cli::end_to_end::{CircuitProofSystem, CircuitStatement, Error, OpeningPath, Proof}; use circuit::Circuit; use pcs::VerifyError; type R = num_bigint::BigInt; -type F = field::FqDefault; -type Proj = ProjectBigIntToFq; +type F = field::DynField; +type Proj = ProjectBigIntToField; fn rejects_changed_or_missing_ood( - system: &CircuitProofSystem, + system: &CircuitProofSystem, proof: &Proof, ) { // These Fast-profile fixtures have zero initial grinding bits, so the first @@ -51,13 +51,13 @@ impl CircuitStatement for PublicBit { #[test] fn generic_driver_accepts_a_non_sha_circuit() { - let prepared = CircuitProofSystem::<_, F>::new::(PublicBit).unwrap(); + let prepared = CircuitProofSystem::<_, F, R, Proj>::new(PublicBit).unwrap(); assert_eq!(prepared.stats().opening_path, OpeningPath::Direct); assert_eq!(prepared.stats().committed_bits, 2); let witness = prepared.witness(&[true]).unwrap(); let data = prepared.commit(&witness).unwrap(); let proof = prepared.prove(witness, data).unwrap(); - CircuitProofSystem::<_, F>::new::(PublicBit) + CircuitProofSystem::<_, F, R, Proj>::new(PublicBit) .unwrap() .verify(&proof) .unwrap(); @@ -123,14 +123,14 @@ impl CircuitStatement for PublicXor { #[test] fn nonidentity_map_uses_virtual_opening_and_checks_xor_relation() { - let system = CircuitProofSystem::<_, F>::new::(PublicXor).unwrap(); + let system = CircuitProofSystem::<_, F, R, Proj>::new(PublicXor).unwrap(); assert_eq!(system.stats().opening_path, OpeningPath::Virtual); assert_eq!(system.stats().assignment_bits, 3); assert_eq!(system.stats().committed_bits, 2); let witness = system.witness(&[true, false]).unwrap(); let data = system.commit(&witness).unwrap(); let proof = system.prove(witness, data).unwrap(); - CircuitProofSystem::<_, F>::new::(PublicXor) + CircuitProofSystem::<_, F, R, Proj>::new(PublicXor) .unwrap() .verify(&proof) .unwrap(); diff --git a/tooling/cli/tests/sha256_spartan.rs b/tooling/cli/tests/sha256_spartan.rs index 82b29d17..22562735 100644 --- a/tooling/cli/tests/sha256_spartan.rs +++ b/tooling/cli/tests/sha256_spartan.rs @@ -1,4 +1,5 @@ -use bitz_cli::{ProjectBigIntToFq, ProjectConstraint}; +use bitz_cli::{ProjectBigIntToField, ProjectConstraint}; +use circuit::constraints::ConstraintMatrices; use circuit::{ constraints::ConstraintGenerator, sha256::{ @@ -16,7 +17,7 @@ use transcript::{build_prover, build_verifier}; type R = num_bigint::BigInt; type F = field::FqDefault; -type Proj = ProjectBigIntToFq; +type Proj = ProjectBigIntToField; const SESSION: &[u8] = b"spartan/piop/sha256-compression/v1"; const INSTANCE: &[u8] = b"abc-single-compression"; @@ -55,7 +56,8 @@ fn sha256_compression_verifies_through_spartan_piop() { assert_assignment_matches(&recomputed_assignment, &recorded_assignment); let projection = >::prepare(); - let matrices = integer_matrices.map_coefficients(|c| projection.project(&c)); + let matrices: ConstraintMatrices = + integer_matrices.map_coefficients(|c| projection.project(&c)); let products = build_product_mles(&exact_products, matrices.a.row_count()).unwrap(); let assignment = build_assignment_mle(&recorded_assignment, matrices.a.column_count()).unwrap(); let matrices = PreparedConstraintMatrices::new(matrices).unwrap(); From b9bb7c1b3542fe9aad83abd7cf5ae3114e637cfb Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Mon, 5 Oct 2026 19:47:30 +0100 Subject: [PATCH 07/11] Optimization: Restructure constraint matrices preparation to setup `PreparedIntegerMatrices` --- crates/circuit/src/constraints.rs | 19 +-- crates/spartan/src/lib.rs | 3 +- crates/spartan/src/matrix.rs | 238 ++++++++++++++++++++++-------- crates/spartan/src/piop.rs | 2 +- tooling/cli/src/end_to_end.rs | 34 ++--- 5 files changed, 200 insertions(+), 96 deletions(-) diff --git a/crates/circuit/src/constraints.rs b/crates/circuit/src/constraints.rs index 8bc3dc47..f18d2389 100644 --- a/crates/circuit/src/constraints.rs +++ b/crates/circuit/src/constraints.rs @@ -117,7 +117,8 @@ impl SparseMatrix { } } - fn map_values_ref(&self, map: M) -> SparseMatrix + /// Maps every coefficient, keeping this matrix. + pub fn map_values_ref(&self, map: M) -> SparseMatrix where D: Send + Sync, M: Fn(&C) -> D + Send + Sync, @@ -305,22 +306,6 @@ impl ConstraintMatrices { c: self.c.map_values(map), } } - - /// Maps every A/B/C coefficient, keeping the matrices. - /// - /// The Boolean `M` matrix and sparse topology are cloned unchanged. - pub fn map_coefficients_ref(&self, map: M) -> ConstraintMatrices - where - D: Send + Sync, - M: Fn(&R) -> D + Copy + Send + Sync, - { - ConstraintMatrices { - m: self.m.clone(), - a: self.a.map_values_ref(map), - b: self.b.map_values_ref(map), - c: self.c.map_values_ref(map), - } - } } impl ConstraintMatrices { diff --git a/crates/spartan/src/lib.rs b/crates/spartan/src/lib.rs index 70a43113..d3358e0a 100644 --- a/crates/spartan/src/lib.rs +++ b/crates/spartan/src/lib.rs @@ -7,7 +7,8 @@ pub mod piop; pub mod sumcheck; pub use matrix::{ - PreparedConstraintMatrices, SpartanMatrixError, build_assignment_mle, build_product_mles, + PreparedConstraintMatrices, PreparedIntegerMatrices, SpartanMatrixError, build_assignment_mle, + build_product_mles, }; pub use piop::{ SpartanError, SpartanPiopProof, prove_spartan_piop, verify_spartan_proof, diff --git a/crates/spartan/src/matrix.rs b/crates/spartan/src/matrix.rs index 83e8bea0..faf7862a 100644 --- a/crates/spartan/src/matrix.rs +++ b/crates/spartan/src/matrix.rs @@ -1,6 +1,6 @@ //! R1CS matrix preparation and the sparse kernels used by Spartan. -use circuit::constraints::{ConstraintMatrices, SparseMatrix}; +use circuit::constraints::{ConstraintMatrices, SparseBoolMatrix, SparseMatrix}; use circuit::matrix_products::{IntegerProducts, ModularVector, RuntimeModulus}; use circuit::witgen::PackedWitness; use circuit::{BitWidth, IntoWords}; @@ -8,6 +8,7 @@ use common::{BitzClaimField, BitzField}; use poly::DenseMultilinearExtension; use rayon::prelude::*; use sha2::{Digest, Sha256}; +use std::sync::Arc; use transcript::Encoding; use crate::sumcheck::R1csProductMles; @@ -26,17 +27,31 @@ pub enum SpartanMatrixError { InvalidMleOperation, } -/// Immutable field-valued constraint matrices prepared for repeated Spartan -/// proofs and verification. -/// -/// Shape validation, Boolean-domain sizing, canonical statement hashing and -/// the column chunking are performed once during construction rather than -/// inside the prover or verifier. +/// Constraint matrices over their coefficient ring, prepared once for every +/// proof: shape validated, Boolean domains sized, nonzeros chunked by column. +/// None of that depends on a modulus, so [`Self::project`] shares it and +/// only reduces the coefficients. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct PreparedConstraintMatrices { +pub struct PreparedIntegerMatrices { matrices: ConstraintMatrices, /// Nonzeros of `a`, `b` and `c` grouped by column chunk. - column_chunks: [ColumnChunkIndex; 3], + column_chunks: Arc<[ColumnChunkIndex; 3]>, + num_row_vars: usize, + num_column_vars: usize, +} + +/// Immutable field-valued constraint matrices prepared for repeated Spartan +/// proofs and verification: `A`, `B` and `C` over the field, their column +/// chunking, the Boolean domain sizes and the canonical statement digest. +/// +/// Built by [`PreparedIntegerMatrices::project`] under a drawn modulus, or by +/// [`Self::new`] from matrices already over the field. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PreparedConstraintMatrices { + a: SparseMatrix, + b: SparseMatrix, + c: SparseMatrix, + column_chunks: Arc<[ColumnChunkIndex; 3]>, digest: [u8; 32], num_row_vars: usize, num_column_vars: usize, @@ -92,20 +107,68 @@ impl ColumnChunkIndex { } } -impl PreparedConstraintMatrices { - pub fn new(matrices: ConstraintMatrices) -> Result { +/// Groups `a`, `b` and `c` by column chunk over the padded column domain. +fn column_chunks( + matrices: &ConstraintMatrices, + num_column_vars: usize, +) -> [ColumnChunkIndex; 3] { + let num_columns = 1_usize << num_column_vars; + let chunk_len = num_columns.min(1_usize << BIND_CHUNK_COLUMN_VARS); + let chunk_count = num_columns / chunk_len; + [&matrices.a, &matrices.b, &matrices.c] + .map(|matrix| ColumnChunkIndex::new(matrix, chunk_len, chunk_count)) +} + +impl PreparedIntegerMatrices { + pub fn new(matrices: ConstraintMatrices) -> Result { let (num_row_vars, num_column_vars) = r1cs_num_vars(&matrices)?; + let column_chunks = Arc::new(column_chunks(&matrices, num_column_vars)); + Ok(Self { + matrices, + column_chunks, + num_row_vars, + num_column_vars, + }) + } - let num_columns = 1_usize << num_column_vars; - let chunk_len = num_columns.min(1_usize << BIND_CHUNK_COLUMN_VARS); - let chunk_count = num_columns / chunk_len; + pub fn matrices(&self) -> &ConstraintMatrices { + &self.matrices + } - let column_chunks = [&matrices.a, &matrices.b, &matrices.c] - .map(|matrix| ColumnChunkIndex::new(matrix, chunk_len, chunk_count)); - let digest = constraint_matrix_digest(&matrices)?; + /// The matrices with `map` applied to every coefficient of `A`, `B` and + /// `C`: the per-proof work. The chunking is shared; the digest is over the + /// projected coefficients, as in [`PreparedConstraintMatrices::new`]. + pub fn project(&self, map: M) -> Result, SpartanMatrixError> + where + F: BitzField, + M: Fn(&R) -> F + Copy + Send + Sync, + { + let [a, b, c] = [&self.matrices.a, &self.matrices.b, &self.matrices.c] + .map(|matrix| matrix.map_values_ref(map)); + let digest = constraint_matrix_digest(&self.matrices.m, [&a, &b, &c])?; + Ok(PreparedConstraintMatrices { + a, + b, + c, + column_chunks: Arc::clone(&self.column_chunks), + digest, + num_row_vars: self.num_row_vars, + num_column_vars: self.num_column_vars, + }) + } +} +impl PreparedConstraintMatrices { + /// Prepares matrices already over the field, without copying them. + pub fn new(matrices: ConstraintMatrices) -> Result { + let (num_row_vars, num_column_vars) = r1cs_num_vars(&matrices)?; + let column_chunks = Arc::new(column_chunks(&matrices, num_column_vars)); + let ConstraintMatrices { m, a, b, c } = matrices; + let digest = constraint_matrix_digest(&m, [&a, &b, &c])?; Ok(Self { - matrices, + a, + b, + c, column_chunks, digest, num_row_vars, @@ -115,22 +178,34 @@ impl PreparedConstraintMatrices { /// Returns human-readable info about R1CS matrices and their nonzero entries. pub fn short_debug_info(&self) -> String { - let nonzeros: usize = [&self.matrices.a, &self.matrices.b, &self.matrices.c] + let nonzeros: usize = self + .matrices() .iter() .flat_map(|matrix| matrix.rows()) .map(|row| row.entries().len()) .sum(); format!( "{} r1cs rows -> 2^{}, {} h entries -> 2^{}, {nonzeros} nonzeros", - self.matrices.a.row_count(), + self.row_count(), self.num_row_vars(), - self.matrices.a.column_count(), + self.column_count(), self.num_column_vars(), ) } - pub fn matrices(&self) -> &ConstraintMatrices { - &self.matrices + /// `A`, `B` and `C`. + pub fn matrices(&self) -> [&SparseMatrix; 3] { + [&self.a, &self.b, &self.c] + } + + /// Number of R1CS rows before padding. + pub fn row_count(&self) -> usize { + self.a.row_count() + } + + /// Number of assignment entries before padding, the constant included. + pub const fn column_count(&self) -> usize { + self.a.column_count() } pub const fn digest(&self) -> &[u8; 32] { @@ -249,7 +324,7 @@ impl PreparedConstraintMatrices { rho: F, ) -> Result, SpartanMatrixError> { bind_and_batch_with_num_vars( - &self.matrices, + self.matrices(), &self.column_chunks, row_point, rho, @@ -272,7 +347,7 @@ impl PreparedConstraintMatrices { column_point: &[F], ) -> Result { evaluate_batched_with_num_vars( - &self.matrices, + self.matrices(), &self.column_chunks, row_point, rho, @@ -284,7 +359,7 @@ impl PreparedConstraintMatrices { } fn bind_and_batch_with_num_vars( - matrices: &ConstraintMatrices, + matrices: [&SparseMatrix; 3], column_chunks: &[ColumnChunkIndex; 3], row_point: &[F], rho: F, @@ -300,9 +375,9 @@ fn bind_and_batch_with_num_vars( let row_weights = poly::eq_table(row_point); let batched = [ - (&matrices.a, &column_chunks[0], F::one()), - (&matrices.b, &column_chunks[1], rho), - (&matrices.c, &column_chunks[2], rho * rho), + (matrices[0], &column_chunks[0], F::one()), + (matrices[1], &column_chunks[1], rho), + (matrices[2], &column_chunks[2], rho * rho), ]; let chunk_len = column_chunks[0].chunk_len; @@ -332,7 +407,7 @@ fn bind_and_batch_with_num_vars( } fn evaluate_batched_with_num_vars( - matrices: &ConstraintMatrices, + matrices: [&SparseMatrix; 3], column_chunks: &[ColumnChunkIndex; 3], row_point: &[F], rho: F, @@ -355,9 +430,9 @@ fn evaluate_batched_with_num_vars( let row_weights = poly::eq_table(row_point); let batched = [ - (&matrices.a, &column_chunks[0], F::one()), - (&matrices.b, &column_chunks[1], rho), - (&matrices.c, &column_chunks[2], rho * rho), + (matrices[0], &column_chunks[0], F::one()), + (matrices[1], &column_chunks[1], rho), + (matrices[2], &column_chunks[2], rho * rho), ]; // All tasks share one cache-sized `low` table and a per-chunk factor. @@ -413,30 +488,23 @@ pub(crate) fn r1cs_num_vars( /// more than one field must bind the field choice in its transcript session or /// instance; canonical coefficient encodings need not identify their field. pub(crate) fn constraint_matrix_digest( - matrices: &ConstraintMatrices, + m: &SparseBoolMatrix, + matrices: [&SparseMatrix; 3], ) -> Result<[u8; 32], SpartanMatrixError> { - matrices - .validate_shape() - .map_err(|_| SpartanMatrixError::InvalidR1csShape)?; - let mut hash = Sha256::new(); hash.update(b"bitz/spartan/constraint-matrices/v1"); hash.update(b"M"); - hash_usize(&mut hash, matrices.m.row_count())?; - hash_usize(&mut hash, matrices.m.column_count())?; - for row in matrices.m.rows() { + hash_usize(&mut hash, m.row_count())?; + hash_usize(&mut hash, m.column_count())?; + for row in m.rows() { hash_usize(&mut hash, row.positions().len())?; for &column in row.positions() { hash_usize(&mut hash, column)?; } } - for (label, matrix) in [ - (b"A", &matrices.a), - (b"B", &matrices.b), - (b"C", &matrices.c), - ] { + for (label, matrix) in [b"A", b"B", b"C"].into_iter().zip(matrices) { hash.update(label); hash_sparse_matrix(&mut hash, matrix)?; } @@ -482,14 +550,13 @@ pub(crate) fn padded_num_vars(logical_len: usize) -> Result PreparedConstraintMatrices { + fn random_matrices(rng: &mut Pcg64) -> ConstraintMatrices { let m = SparseBoolMatrix::try_from_rows(1, vec![Vec::new(); COLUMNS]).unwrap(); let a = random_sparse_matrix(rng); let b = random_sparse_matrix(rng); let c = random_sparse_matrix(rng); - PreparedConstraintMatrices::new(ConstraintMatrices { m, a, b, c }).unwrap() + ConstraintMatrices { m, a, b, c } + } + + fn random_prepared_matrices(rng: &mut Pcg64) -> PreparedConstraintMatrices { + PreparedConstraintMatrices::new(random_matrices(rng)).unwrap() } fn random_point(rng: &mut Pcg64, len: usize) -> Vec { @@ -529,7 +600,7 @@ mod tests { /// `D(r_y)` by the direct triple loop over every nonzero. fn reference_evaluation( - matrices: &ConstraintMatrices, + matrices: [&SparseMatrix; 3], row_point: &[F], rho: F, column_point: &[F], @@ -537,11 +608,7 @@ mod tests { let row_weights = poly::eq_table(row_point); let column_weights = poly::eq_table(column_point); let mut evaluation = F::from(0u128); - for (matrix, batch_scale) in [ - (&matrices.a, F::from(1u128)), - (&matrices.b, rho), - (&matrices.c, rho * rho), - ] { + for (matrix, batch_scale) in matrices.into_iter().zip([F::from(1u128), rho, rho * rho]) { for (row, entries) in matrix.rows().iter().enumerate() { for &(column, coefficient) in entries.entries() { evaluation += @@ -556,11 +623,11 @@ mod tests { fn column_chunks_partition_every_row() { let mut rng = Pcg64::seed_from_u64(7); let prepared = random_prepared_matrices(&mut rng); - let matrices = prepared.matrices(); - for (matrix, index) in [&matrices.a, &matrices.b, &matrices.c] + for (matrix, index) in prepared + .matrices() .into_iter() - .zip(&prepared.column_chunks) + .zip(prepared.column_chunks.iter()) { let mut spans: Vec<_> = index .spans @@ -604,4 +671,57 @@ mod tests { let bound = prepared.bind_and_batch(&row_point, rho).unwrap(); assert_eq!(bound.evaluate(&column_point).unwrap(), expected); } + + /// Projecting integer matrices prepared once gives exactly what preparing + /// the projected matrices gives, and the chunking is shared, not rebuilt. + #[test] + fn projecting_prepared_integer_matrices_matches_preparing_the_projection() { + let mut rng = Pcg64::seed_from_u64(13); + let mut random_integer_matrix = || { + let rows = (0..ROWS) + .map(|_| { + let mut columns: Vec = (0..ENTRIES_PER_ROW) + .map(|_| rng.random_range(0..COLUMNS)) + .collect(); + columns.sort_unstable(); + columns.dedup(); + columns + .into_iter() + .map(|column| (column, i128::from(rng.random::()))) + .collect() + }) + .collect(); + SparseMatrix::try_from_rows(COLUMNS, rows).unwrap() + }; + let project = |coefficient: &i128| { + let magnitude = F::from(coefficient.unsigned_abs()); + if *coefficient < 0 { + -magnitude + } else { + magnitude + } + }; + + let integer_matrices = ConstraintMatrices { + m: SparseBoolMatrix::try_from_rows(1, vec![Vec::new(); COLUMNS]).unwrap(), + a: random_integer_matrix(), + b: random_integer_matrix(), + c: random_integer_matrix(), + }; + let prepared = PreparedIntegerMatrices::new(integer_matrices.clone()).unwrap(); + let projected = prepared.project(project).unwrap(); + let direct = + PreparedConstraintMatrices::new(integer_matrices.map_coefficients(|c| project(&c))) + .unwrap(); + assert_eq!(projected, direct); + assert!(Arc::ptr_eq( + &projected.column_chunks, + &prepared.project(project).unwrap().column_chunks + )); + assert_eq!(prepared.matrices().a.row_count(), projected.row_count()); + assert_eq!( + prepared.matrices().a.column_count(), + projected.column_count() + ); + } } diff --git a/crates/spartan/src/piop.rs b/crates/spartan/src/piop.rs index 7b6735aa..5a8271df 100644 --- a/crates/spartan/src/piop.rs +++ b/crates/spartan/src/piop.rs @@ -160,7 +160,7 @@ pub fn verify_spartan_with_mle_claim( mle_claim: &ScaledMleEvaluationClaim, assignment: &PackedWitness, ) -> Result<(), SpartanError> { - let assignment = build_assignment_mle::(assignment, matrices.matrices().a.column_count())?; + let assignment = build_assignment_mle::(assignment, matrices.column_count())?; let expected_claim = verify_spartan_proof(transcript, matrices, proof)?; if mle_claim != &expected_claim { return Err(SpartanError::InvalidMleClaim); diff --git a/tooling/cli/src/end_to_end.rs b/tooling/cli/src/end_to_end.rs index ed296350..cb4e2db6 100644 --- a/tooling/cli/src/end_to_end.rs +++ b/tooling/cli/src/end_to_end.rs @@ -7,7 +7,7 @@ use circuit::matrix_products::StoredInteger; use circuit::{ BitWidth, Circuit, IntoWords, - constraints::{ConstraintGenerator, ConstraintMatrices, SparseBoolMatrix}, + constraints::{ConstraintGenerator, SparseBoolMatrix}, matrix_products::IntegerProducts, matrix_transpose::{MTransposeGenerator, MaterializedMTranspose}, witgen::{PackedWitness, ProductWitgen}, @@ -31,8 +31,8 @@ use verifier::BitZVerifier; use crate::ProjectConstraint; use spartan::{ - PreparedConstraintMatrices, SpartanMatrixError, SpartanPiopProof, build_assignment_mle, - build_product_mles, prove_spartan_piop, verify_spartan_proof, + PreparedConstraintMatrices, PreparedIntegerMatrices, SpartanMatrixError, SpartanPiopProof, + build_assignment_mle, build_product_mles, prove_spartan_piop, verify_spartan_proof, }; const SESSION: &[u8] = b"bitz/circuit-e2e/v2"; @@ -106,8 +106,8 @@ pub struct CircuitStats { pub struct CircuitProofSystem { statement: S, opening_path: OpeningPath, - /// Exact, projected onto `F` under the drawn prime. - matrices: ConstraintMatrices, + /// Prepared once over the integers; projected onto `F` under the drawn prime. + matrices: PreparedIntegerMatrices, map: MaterializedMTranspose, claim_shape: Shape, generator: F128, @@ -157,22 +157,20 @@ where .map(|i| constraints.input(i)) .collect(); statement.synthesize(&mut constraints, &inputs)?; - let matrices = constraints.into_matrices(); - matrices - .validate_shape() - .map_err(|_| Error::Matrix(SpartanMatrixError::InvalidR1csShape))?; + let matrices = + PreparedIntegerMatrices::new(constraints.into_matrices()).map_err(Error::Matrix)?; let mut generator = MTransposeGenerator::new(statement.input_bits()); let inputs = generator.take_inputs(); statement.synthesize(&mut generator, &inputs)?; let map = generator.finish(); - if map.h_len() != matrices.a.column_count() { + if map.h_len() != matrices.matrices().a.column_count() { return Err(Error::Configuration("map and assignment dimensions differ")); } - if map.f_len() != matrices.m.column_count() { + if map.f_len() != matrices.matrices().m.column_count() { return Err(Error::Configuration("map and witness dimensions differ")); } let claim_shape = shape_for(map.h_len())?; - let opening_path = if is_identity(&matrices.m) { + let opening_path = if is_identity(&matrices.matrices().m) { OpeningPath::Direct } else { OpeningPath::Virtual @@ -202,7 +200,7 @@ where pub fn stats(&self) -> CircuitStats { CircuitStats { opening_path: self.opening_path, - constraints: self.matrices.a.row_count(), + constraints: self.matrices.matrices().a.row_count(), assignment_bits: self.map.h_len(), committed_bits: match self.opening_path { OpeningPath::Direct => self.map.h_len(), @@ -224,7 +222,7 @@ where if f.bit_len() + 1 != self.map.f_len() || h.bit_len() != self.map.h_len() { return Err(Error::Input("circuit replay changed witness dimensions")); } - if products.a_mw.len() != self.matrices.a.row_count() { + if products.a_mw.len() != self.matrices.matrices().a.row_count() { return Err(Error::Input("circuit replay changed constraint count")); } // Over the integers, so modulo whichever prime is drawn. @@ -282,8 +280,8 @@ where } let prime = transcript.squeeze_prime(self.prime_bits); let (params, matrices) = self.under_prime(prime, &prime_lock, &mut transcript)?; - let products = build_product_mles(&witness.products, self.matrices.a.row_count()) - .map_err(Error::Matrix)?; + let products = + build_product_mles(&witness.products, matrices.row_count()).map_err(Error::Matrix)?; let assignment = build_assignment_mle(&witness.assignment, self.map.h_len()).map_err(Error::Matrix)?; let (spartan, terminal) = @@ -390,8 +388,8 @@ where let projection = Proj::prepare(); let matrices = self .matrices - .map_coefficients_ref(|c| projection.project(c)); - let matrices = PreparedConstraintMatrices::new(matrices).map_err(Error::Matrix)?; + .project(|c| projection.project(c)) + .map_err(Error::Matrix)?; Ok((params, matrices)) } From c6d8d07f2d44ed06bfe2f11fb547904ad9eaf379 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Tue, 6 Oct 2026 15:54:06 +0100 Subject: [PATCH 08/11] Optimization: Hash constraint matrices over integers in the setup phase --- crates/circuit/src/matrix_products.rs | 30 ++++ crates/spartan/src/matrix.rs | 220 ++++++++++++++++++++------ tooling/cli/src/benchmark.rs | 1 + tooling/cli/src/end_to_end.rs | 6 +- 4 files changed, 206 insertions(+), 51 deletions(-) diff --git a/crates/circuit/src/matrix_products.rs b/crates/circuit/src/matrix_products.rs index b6b4e554..3e22ce02 100644 --- a/crates/circuit/src/matrix_products.rs +++ b/crates/circuit/src/matrix_products.rs @@ -368,6 +368,12 @@ impl From<&StoredInteger> for num_bigint::BigInt { } } +impl From<&i128> for StoredInteger { + fn from(value: &i128) -> Self { + Self::from(&Integer::<2>::from(*value)) + } +} + impl From<&Integer> for StoredInteger { /// Stores a gadget-local fixed integer without changing its value. fn from(value: &Integer) -> Self { @@ -586,6 +592,30 @@ mod tests { ); } + #[test] + fn every_source_stores_an_integer_alike() { + for value in [ + 0_i128, + 1, + -1, + 7, + -7, + i128::from(i64::MAX), + i128::from(i64::MIN), + 1 << 100, + -(1 << 100), + ] { + let from_bigint = StoredInteger::from(&R::from(value)); + assert_eq!(StoredInteger::from(&value), from_bigint, "{value}"); + assert_eq!( + StoredInteger::from(&Integer::<2>::from(value)), + from_bigint, + "{value}" + ); + assert_eq!(stored_ring(&from_bigint), R::from(value)); + } + } + #[test] fn arbitrary_bigints_round_trip_through_stored_integers() { let boundary = R::one() << 128_usize; diff --git a/crates/spartan/src/matrix.rs b/crates/spartan/src/matrix.rs index faf7862a..406061a8 100644 --- a/crates/spartan/src/matrix.rs +++ b/crates/spartan/src/matrix.rs @@ -1,15 +1,15 @@ //! R1CS matrix preparation and the sparse kernels used by Spartan. use circuit::constraints::{ConstraintMatrices, SparseBoolMatrix, SparseMatrix}; -use circuit::matrix_products::{IntegerProducts, ModularVector, RuntimeModulus}; +use circuit::matrix_products::{IntegerProducts, ModularVector, RuntimeModulus, StoredInteger}; use circuit::witgen::PackedWitness; use circuit::{BitWidth, IntoWords}; use common::{BitzClaimField, BitzField}; +use num_traits::ToPrimitive; use poly::DenseMultilinearExtension; use rayon::prelude::*; use sha2::{Digest, Sha256}; use std::sync::Arc; -use transcript::Encoding; use crate::sumcheck::R1csProductMles; @@ -28,14 +28,17 @@ pub enum SpartanMatrixError { } /// Constraint matrices over their coefficient ring, prepared once for every -/// proof: shape validated, Boolean domains sized, nonzeros chunked by column. -/// None of that depends on a modulus, so [`Self::project`] shares it and -/// only reduces the coefficients. +/// proof: shape validated, Boolean domains sized, nonzeros chunked by column, +/// statement digested. None of that depends on a modulus, so +/// [`Self::project`] shares it and only reduces the coefficients. #[derive(Clone, Debug, Eq, PartialEq)] pub struct PreparedIntegerMatrices { matrices: ConstraintMatrices, /// Nonzeros of `a`, `b` and `c` grouped by column chunk. column_chunks: Arc<[ColumnChunkIndex; 3]>, + /// [`integer_matrix_digest`]: over the integers, so it exists before a + /// modulus does. + digest: [u8; 32], num_row_vars: usize, num_column_vars: usize, } @@ -52,6 +55,8 @@ pub struct PreparedConstraintMatrices { b: SparseMatrix, c: SparseMatrix, column_chunks: Arc<[ColumnChunkIndex; 3]>, + /// If this struct produced by an integer matrix, digest carries over, + /// otherwise it's built by hashing field elements via [`constraint_matrix_digest`] digest: [u8; 32], num_row_vars: usize, num_column_vars: usize, @@ -119,13 +124,19 @@ fn column_chunks( .map(|matrix| ColumnChunkIndex::new(matrix, chunk_len, chunk_count)) } -impl PreparedIntegerMatrices { +impl PreparedIntegerMatrices +where + R: Send + Sync + ToPrimitive, + for<'a> StoredInteger: From<&'a R>, +{ pub fn new(matrices: ConstraintMatrices) -> Result { let (num_row_vars, num_column_vars) = r1cs_num_vars(&matrices)?; let column_chunks = Arc::new(column_chunks(&matrices, num_column_vars)); + let digest = integer_matrix_digest(&matrices)?; Ok(Self { matrices, column_chunks, + digest, num_row_vars, num_column_vars, }) @@ -135,31 +146,34 @@ impl PreparedIntegerMatrices { &self.matrices } + pub const fn digest(&self) -> &[u8; 32] { + &self.digest + } + /// The matrices with `map` applied to every coefficient of `A`, `B` and - /// `C`: the per-proof work. The chunking is shared; the digest is over the - /// projected coefficients, as in [`PreparedConstraintMatrices::new`]. - pub fn project(&self, map: M) -> Result, SpartanMatrixError> + /// `C`: the per-proof work. The chunking and the digest are shared. + pub fn project(&self, map: M) -> PreparedConstraintMatrices where F: BitzField, M: Fn(&R) -> F + Copy + Send + Sync, { let [a, b, c] = [&self.matrices.a, &self.matrices.b, &self.matrices.c] .map(|matrix| matrix.map_values_ref(map)); - let digest = constraint_matrix_digest(&self.matrices.m, [&a, &b, &c])?; - Ok(PreparedConstraintMatrices { + PreparedConstraintMatrices { a, b, c, column_chunks: Arc::clone(&self.column_chunks), - digest, + digest: self.digest, num_row_vars: self.num_row_vars, num_column_vars: self.num_column_vars, - }) + } } } impl PreparedConstraintMatrices { - /// Prepares matrices already over the field, without copying them. + /// Prepares matrices already over the field, without copying them; the + /// digest is `constraint_matrix_digest`. pub fn new(matrices: ConstraintMatrices) -> Result { let (num_row_vars, num_column_vars) = r1cs_num_vars(&matrices)?; let column_chunks = Arc::new(column_chunks(&matrices, num_column_vars)); @@ -482,17 +496,16 @@ pub(crate) fn r1cs_num_vars( } /// Canonically commits the complete public matrix statement before any -/// Fiat--Shamir challenge is sampled. -/// -/// The digest domain is intentionally field-neutral. A protocol that supports -/// more than one field must bind the field choice in its transcript session or -/// instance; canonical coefficient encodings need not identify their field. -pub(crate) fn constraint_matrix_digest( +/// Fiat--Shamir challenge is sampled: `domain`, the positions of `M`, then +/// `A`, `B` and `C` with every coefficient written by `encode`. +fn matrix_digest( + domain: &[u8], m: &SparseBoolMatrix, - matrices: [&SparseMatrix; 3], + matrices: [&SparseMatrix; 3], + encode: impl Fn(&mut Sha256, &C) -> Result<(), SpartanMatrixError>, ) -> Result<[u8; 32], SpartanMatrixError> { let mut hash = Sha256::new(); - hash.update(b"bitz/spartan/constraint-matrices/v1"); + hash.update(domain); hash.update(b"M"); hash_usize(&mut hash, m.row_count())?; @@ -506,30 +519,90 @@ pub(crate) fn constraint_matrix_digest( for (label, matrix) in [b"A", b"B", b"C"].into_iter().zip(matrices) { hash.update(label); - hash_sparse_matrix(&mut hash, matrix)?; + hash_usize(&mut hash, matrix.row_count())?; + hash_usize(&mut hash, matrix.column_count())?; + for row in matrix.rows() { + hash_usize(&mut hash, row.entries().len())?; + for (column, coefficient) in row.entries() { + hash_usize(&mut hash, *column)?; + encode(&mut hash, coefficient)?; + } + } } Ok(hash.finalize().into()) } -fn hash_sparse_matrix( - hash: &mut Sha256, - matrix: &SparseMatrix, -) -> Result<(), SpartanMatrixError> -where - F: Encoding<[u8]>, -{ - hash_usize(hash, matrix.row_count())?; - hash_usize(hash, matrix.column_count())?; - for row in matrix.rows() { - hash_usize(hash, row.entries().len())?; - for (column, coefficient) in row.entries() { - hash_usize(hash, *column)?; - let encoding = coefficient.encode(); +/// The statement digest over coefficients already in the field, encoded as +/// the transcript encodes them. +/// +/// The digest domain is intentionally field-neutral. A protocol that supports +/// more than one field must bind the field choice in its transcript session or +/// instance; canonical coefficient encodings need not identify their field. +pub(crate) fn constraint_matrix_digest( + m: &SparseBoolMatrix, + matrices: [&SparseMatrix; 3], +) -> Result<[u8; 32], SpartanMatrixError> { + matrix_digest( + b"bitz/spartan/constraint-matrices/v1", + m, + matrices, + |hash, coefficient| { + let encoding = transcript::Encoding::encode(coefficient); let bytes = encoding.as_ref(); hash_usize(hash, bytes.len())?; hash.update(bytes); - } + Ok(()) + }, + ) +} + +/// The statement digest over integer coefficients, each in its normalized +/// two's-complement words, so it is fixed before any modulus is drawn and +/// shared by every projection. Its own domain: it never equals the digest of +/// the projected matrices. A protocol that projects must bind the modulus in +/// its transcript before Spartan reads this digest, as the e2e does by +/// absorbing its parameters right after the draw. +pub(crate) fn integer_matrix_digest( + matrices: &ConstraintMatrices, +) -> Result<[u8; 32], SpartanMatrixError> +where + R: ToPrimitive, + for<'a> StoredInteger: From<&'a R>, +{ + matrix_digest( + b"bitz/spartan/integer-constraint-matrices/v1", + &matrices.m, + [&matrices.a, &matrices.b, &matrices.c], + |hash, coefficient| { + // Coefficients are small in practice; spare them the heap. + if let Some(value) = coefficient.to_i128() { + let (words, len) = canonical_words(value); + hash_words(hash, &words[..len]) + } else { + hash_words(hash, StoredInteger::from(coefficient).words()) + } + }, + ) +} + +/// The words [`StoredInteger`] holds for `value`, and how many: little-endian +/// two's complement with a redundant sign word dropped, none for zero. +fn canonical_words(value: i128) -> ([u64; 2], usize) { + let (low, high) = (value as u64, (value >> 64) as u64); + let sign_extended = (high == 0 && low >> 63 == 0) || (high == u64::MAX && low >> 63 == 1); + let len = match (value == 0, sign_extended) { + (true, _) => 0, + (false, true) => 1, + (false, false) => 2, + }; + ([low, high], len) +} + +fn hash_words(hash: &mut Sha256, words: &[u64]) -> Result<(), SpartanMatrixError> { + hash_usize(hash, words.len())?; + for word in words { + hash.update(word.to_le_bytes()); } Ok(()) } @@ -672,10 +745,35 @@ mod tests { assert_eq!(bound.evaluate(&column_point).unwrap(), expected); } - /// Projecting integer matrices prepared once gives exactly what preparing - /// the projected matrices gives, and the chunking is shared, not rebuilt. #[test] - fn projecting_prepared_integer_matrices_matches_preparing_the_projection() { + fn canonical_words_are_the_stored_integers() { + for value in [ + 0_i128, + 1, + -1, + i128::from(i64::MAX), + i128::from(i64::MIN), + i128::from(i64::MAX) + 1, + i128::from(i64::MIN) - 1, + i128::from(u64::MAX), + -i128::from(u64::MAX), + i128::MAX, + i128::MIN, + ] { + let (words, len) = super::canonical_words(value); + assert_eq!( + &words[..len], + circuit::matrix_products::StoredInteger::from(&value).words(), + "{value}" + ); + } + } + + /// Projecting integer matrices prepared once gives what preparing the + /// projected matrices gives, the chunking shared rather than rebuilt; only + /// the digest differs, each over its own coefficients and domain. + #[test] + fn projection_matches_direct_preparation_up_to_the_digest_domain() { let mut rng = Pcg64::seed_from_u64(13); let mut random_integer_matrix = || { let rows = (0..ROWS) @@ -709,19 +807,47 @@ mod tests { c: random_integer_matrix(), }; let prepared = PreparedIntegerMatrices::new(integer_matrices.clone()).unwrap(); - let projected = prepared.project(project).unwrap(); - let direct = - PreparedConstraintMatrices::new(integer_matrices.map_coefficients(|c| project(&c))) - .unwrap(); - assert_eq!(projected, direct); + let projected = prepared.project(project); + let direct = PreparedConstraintMatrices::new( + integer_matrices.clone().map_coefficients(|c| project(&c)), + ) + .unwrap(); + assert_eq!(projected.matrices(), direct.matrices()); + assert_eq!(projected.column_chunks, direct.column_chunks); + assert_eq!(projected.num_row_vars(), direct.num_row_vars()); + assert_eq!(projected.num_column_vars(), direct.num_column_vars()); + assert_ne!( + projected.digest(), + direct.digest(), + "the integer digest is its own domain" + ); + assert_eq!(projected.digest(), prepared.digest()); assert!(Arc::ptr_eq( &projected.column_chunks, - &prepared.project(project).unwrap().column_chunks + &prepared.project(project).column_chunks )); assert_eq!(prepared.matrices().a.row_count(), projected.row_count()); assert_eq!( prepared.matrices().a.column_count(), projected.column_count() ); + + // The integer digest is the integer matrices': the same under every + // projection, different as soon as a coefficient or a position is. + type Wide = field::Fq<{ (1 << 114) - 11 }>; + let wide = |coefficient: &i128| Wide::from(coefficient.unsigned_abs()); + assert_eq!(prepared.project(wide).digest(), prepared.digest()); + let mut negated = integer_matrices.clone(); + negated.a = negated.a.map_values_ref(|c| -c); + assert_ne!( + PreparedIntegerMatrices::new(negated).unwrap().digest(), + prepared.digest() + ); + let mut moved = integer_matrices; + moved.m = SparseBoolMatrix::try_from_rows(2, vec![vec![1]; COLUMNS]).unwrap(); + assert_ne!( + PreparedIntegerMatrices::new(moved).unwrap().digest(), + prepared.digest() + ); } } diff --git a/tooling/cli/src/benchmark.rs b/tooling/cli/src/benchmark.rs index 3fd02ce6..0ac921d1 100644 --- a/tooling/cli/src/benchmark.rs +++ b/tooling/cli/src/benchmark.rs @@ -29,6 +29,7 @@ where F: BitzClaimField + FieldWithDynamicModulus, F::Integer: BitWidth + IntoWords, R: BitzConstraintRing + for<'a> From<&'a StoredInteger>, + for<'a> StoredInteger: From<&'a R>, Proj: ProjectConstraint, { let started = Instant::now(); diff --git a/tooling/cli/src/end_to_end.rs b/tooling/cli/src/end_to_end.rs index cb4e2db6..abcd9fd8 100644 --- a/tooling/cli/src/end_to_end.rs +++ b/tooling/cli/src/end_to_end.rs @@ -148,6 +148,7 @@ where F: BitzClaimField + FieldWithDynamicModulus, F::Integer: BitWidth + IntoWords, R: BitzConstraintRing + for<'a> From<&'a StoredInteger>, + for<'a> StoredInteger: From<&'a R>, Proj: ProjectConstraint, { #[tracing::instrument(name = "setup", skip_all)] @@ -386,10 +387,7 @@ where .map_err(|_| Error::Configuration("inadmissible BitZ parameters"))?; transcript.public_message(¶ms); let projection = Proj::prepare(); - let matrices = self - .matrices - .project(|c| projection.project(c)) - .map_err(Error::Matrix)?; + let matrices = self.matrices.project(|c| projection.project(c)); Ok((params, matrices)) } From 00c2c88bda7b2643e299dd30b482396164dfb94e Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Tue, 6 Oct 2026 16:18:12 +0100 Subject: [PATCH 09/11] Optimization: Better BigInt to Field projection --- tooling/cli/src/lib.rs | 72 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/tooling/cli/src/lib.rs b/tooling/cli/src/lib.rs index 96e2b968..9aae2a99 100644 --- a/tooling/cli/src/lib.rs +++ b/tooling/cli/src/lib.rs @@ -21,21 +21,37 @@ pub trait ProjectConstraint: Send + Sync { /// canonically modulo its modulus, read when prepared. #[derive(Debug, Clone)] pub struct ProjectBigIntToField { - modulus: BigInt, + modulus: u128, + wide_modulus: BigInt, } impl ProjectConstraint for ProjectBigIntToField { fn prepare() -> Self { let modulus = F::modulus().to_u128().expect("the modulus fits a u128"); Self { - modulus: BigInt::from(modulus), + modulus, + wide_modulus: BigInt::from(modulus), } } fn project(&self, constraint: &BigInt) -> F { - let mut reduced = constraint % &self.modulus; + // Coefficients are small in practice; reduce them without the heap. + if let Some(value) = constraint.to_i128() { + let magnitude = value.unsigned_abs(); + let magnitude = if magnitude < self.modulus { + magnitude + } else { + magnitude % self.modulus + }; + return F::from(if value < 0 && magnitude != 0 { + self.modulus - magnitude + } else { + magnitude + }); + } + let mut reduced = constraint % &self.wide_modulus; if reduced.is_negative() { - reduced += &self.modulus; + reduced += &self.wide_modulus; } F::from( reduced @@ -44,3 +60,51 @@ impl ProjectConstraint for ProjectBigIntToField { ) } } + +#[cfg(test)] +mod tests { + use super::*; + use field::Q100; + use num_traits::{One, Pow}; + + type F = field::FqDefault; + + /// Every coefficient lands on its canonical residue, whichever path + /// reduces it. + #[test] + fn projection_is_canonical_reduction() { + let projection = >::prepare(); + let reference = |value: &BigInt| { + let modulus = BigInt::from(Q100); + let mut residue = value % &modulus; + if residue.is_negative() { + residue += &modulus; + } + F::from(residue.to_u128().unwrap()) + }; + let q = Q100 as i128; + let mut values: Vec = [ + 0, + 1, + -1, + 7, + -7, + q, + -q, + q + 1, + -q - 1, + 2 * q, + i128::MAX, + i128::MIN, + ] + .into_iter() + .map(BigInt::from) + .collect(); + let wide = BigInt::from(3).pow(130u32) + BigInt::one(); + values.extend([wide.clone(), -wide.clone(), &wide * &wide, -(&wide * &wide)]); + for value in &values { + let projected: F = projection.project(value); + assert_eq!(projected, reference(value), "{value}"); + } + } +} From fa4a9d5895e079a1d1e3b909c6e198eb6253af28 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Tue, 6 Oct 2026 16:22:49 +0100 Subject: [PATCH 10/11] Optimization: Flat storage for matrix coefficients --- crates/circuit/src/constraints.rs | 10 + crates/circuit/src/matrix_products.rs | 2 +- crates/spartan/src/matrix.rs | 656 ++++++++++++++------------ tooling/cli/src/end_to_end.rs | 10 +- 4 files changed, 381 insertions(+), 297 deletions(-) diff --git a/crates/circuit/src/constraints.rs b/crates/circuit/src/constraints.rs index f18d2389..f12d8054 100644 --- a/crates/circuit/src/constraints.rs +++ b/crates/circuit/src/constraints.rs @@ -29,6 +29,11 @@ impl SparseRow { pub fn entries(&self) -> &[(usize, C)] { &self.entries } + + /// The entries, consuming the row. + pub fn into_entries(self) -> Vec<(usize, C)> { + self.entries + } } /// A row-major sparse matrix. @@ -84,6 +89,11 @@ impl SparseMatrix { &self.rows } + /// The rows, consuming the matrix. + pub fn into_rows(self) -> Vec> { + self.rows + } + /// Number of rows. pub fn row_count(&self) -> usize { self.rows.len() diff --git a/crates/circuit/src/matrix_products.rs b/crates/circuit/src/matrix_products.rs index 3e22ce02..93ed24cb 100644 --- a/crates/circuit/src/matrix_products.rs +++ b/crates/circuit/src/matrix_products.rs @@ -258,7 +258,7 @@ impl ModularVector { self.values.is_empty() } - /// Vector of canonical field element in little-endian limbs form, in row order. + /// Slice of canonical field element in little-endian limbs form, in row order. pub fn values(&self) -> &[[u64; PRIME_LIMBS]] { &self.values } diff --git a/crates/spartan/src/matrix.rs b/crates/spartan/src/matrix.rs index 406061a8..5d07089f 100644 --- a/crates/spartan/src/matrix.rs +++ b/crates/spartan/src/matrix.rs @@ -28,40 +28,98 @@ pub enum SpartanMatrixError { } /// Constraint matrices over their coefficient ring, prepared once for every -/// proof: shape validated, Boolean domains sized, nonzeros chunked by column, -/// statement digested. None of that depends on a modulus, so -/// [`Self::project`] shares it and only reduces the coefficients. +/// proof: shape validated, Boolean domains sized, nonzeros laid out row by +/// row and chunked by column, statement digested. None of that depends on a +/// modulus, so [`Self::project`] shares it and only reduces the coefficients. #[derive(Clone, Debug, Eq, PartialEq)] pub struct PreparedIntegerMatrices { - matrices: ConstraintMatrices, - /// Nonzeros of `a`, `b` and `c` grouped by column chunk. - column_chunks: Arc<[ColumnChunkIndex; 3]>, - /// [`integer_matrix_digest`]: over the integers, so it exists before a + m: SparseBoolMatrix, + layout: Arc, + /// The coefficients of `A`, `B` and `C`, each in its layout's order. + values: [Vec; 3], + /// `integer_matrix_digest`: over the integers, so it exists before a /// modulus does. digest: [u8; 32], - num_row_vars: usize, - num_column_vars: usize, } /// Immutable field-valued constraint matrices prepared for repeated Spartan -/// proofs and verification: `A`, `B` and `C` over the field, their column -/// chunking, the Boolean domain sizes and the canonical statement digest. +/// proofs and verification: the coefficients of `A`, `B` and `C` over the +/// field, their shared layout and the canonical statement digest. /// /// Built by [`PreparedIntegerMatrices::project`] under a drawn modulus, or by -/// [`Self::new`] from matrices already over the field. +/// [`Self::new`] from matrices already over the field. The digest is stamped +/// by whichever built it: `project` carries the `integer_matrix_digest` its +/// source computed at setup, hashing nothing itself, while `new` hashes the +/// field coefficients it is given (`constraint_matrix_digest`). #[derive(Clone, Debug, Eq, PartialEq)] pub struct PreparedConstraintMatrices { - a: SparseMatrix, - b: SparseMatrix, - c: SparseMatrix, - column_chunks: Arc<[ColumnChunkIndex; 3]>, - /// If this struct produced by an integer matrix, digest carries over, - /// otherwise it's built by hashing field elements via [`constraint_matrix_digest`] + layout: Arc, + /// The coefficients of `A`, `B` and `C`, each in its layout's order. + values: [Vec; 3], digest: [u8; 32], +} + +/// Everything about `A`, `B` and `C` but their coefficients: positions in +/// compressed-row form, the column chunking and the Boolean domain sizes. +/// Modulus-free, so one layout serves the integer matrices and every +/// projection of them. +#[derive(Clone, Debug, Eq, PartialEq)] +struct Layout { + positions: [Positions; 3], + column_chunks: [ColumnChunkIndex; 3], + /// R1CS rows and assignment entries before padding. + row_count: usize, + column_count: usize, num_row_vars: usize, num_column_vars: usize, } +/// Where one matrix's nonzeros are: row `i` owns +/// `columns[row_starts[i]..row_starts[i + 1]]` and the same range of the +/// matrix's values. +#[derive(Clone, Debug, Eq, PartialEq)] +struct Positions { + row_starts: Vec, + columns: Vec, +} + +impl Positions { + /// Lays `matrix` out row by row, returning its coefficients in that order. + fn new(matrix: SparseMatrix) -> Result<(Self, Vec), SpartanMatrixError> { + let rows = matrix.into_rows(); + let nonzeros = rows.iter().map(|row| row.entries().len()).sum(); + let mut row_starts = Vec::with_capacity(rows.len() + 1); + let mut columns = Vec::with_capacity(nonzeros); + let mut values = Vec::with_capacity(nonzeros); + row_starts.push(0); + for row in rows { + for (column, coefficient) in row.into_entries() { + let column = + u32::try_from(column).map_err(|_| SpartanMatrixError::DomainTooLarge)?; + columns.push(column); + values.push(coefficient); + } + row_starts.push(columns.len()); + } + Ok(( + Self { + row_starts, + columns, + }, + values, + )) + } + + fn row_count(&self) -> usize { + self.row_starts.len() - 1 + } + + /// The positions of row `row`. + fn row(&self, row: usize) -> std::ops::Range { + self.row_starts[row]..self.row_starts[row + 1] + } +} + /// `bind_and_batch` splits the column domain into chunks of `2^16` columns and /// accumulates each chunk on its own thread. /// A chunk's slice of the dense table fits in cache. @@ -70,8 +128,8 @@ const BIND_CHUNK_COLUMN_VARS: usize = 16; /// The nonzeros of one sparse matrix grouped by column chunk. /// /// Chunk `c` owns columns `[c * chunk_len, (c + 1) * chunk_len)` and lists -/// the entries of every row that fall into it, in row order. The thread -/// accumulating chunk `c` reads only these entries and writes only these +/// the positions of every row that fall into it, in row order. The thread +/// accumulating chunk `c` reads only these positions and writes only these /// columns, so threads never share a column. #[derive(Clone, Debug, Eq, PartialEq)] struct ColumnChunkIndex { @@ -79,7 +137,7 @@ struct ColumnChunkIndex { spans: Vec>, } -/// The entries `[start, end)` of row `row`. +/// The positions `[start, end)` of row `row`. #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct RowSpan { row: usize, @@ -88,21 +146,21 @@ struct RowSpan { } impl ColumnChunkIndex { - /// Groups `matrix` into `chunk_count` chunks of `chunk_len` columns. - fn new(matrix: &SparseMatrix, chunk_len: usize, chunk_count: usize) -> Self { + /// Groups `positions` into `chunk_count` chunks of `chunk_len` columns. + fn new(positions: &Positions, chunk_len: usize, chunk_count: usize) -> Self { debug_assert!(chunk_len.is_power_of_two()); - debug_assert!(chunk_len * chunk_count >= matrix.column_count()); let mut spans = vec![Vec::new(); chunk_count]; - for (row, entries) in matrix.rows().iter().enumerate() { - let entries = entries.entries(); - let mut start = 0; - while start < entries.len() { + for row in 0..positions.row_count() { + let range = positions.row(row); + let mut start = range.start; + while start < range.end { // Columns increase within a row, so a chunk's entries are a // contiguous run. - let chunk = entries[start].0 / chunk_len; + let chunk = positions.columns[start] as usize / chunk_len; let end = start - + entries[start..].partition_point(|(column, _)| column / chunk_len == chunk); + + positions.columns[start..range.end] + .partition_point(|&column| column as usize / chunk_len == chunk); spans[chunk].push(RowSpan { row, start, end }); start = end; } @@ -112,16 +170,38 @@ impl ColumnChunkIndex { } } -/// Groups `a`, `b` and `c` by column chunk over the padded column domain. -fn column_chunks( - matrices: &ConstraintMatrices, - num_column_vars: usize, -) -> [ColumnChunkIndex; 3] { - let num_columns = 1_usize << num_column_vars; - let chunk_len = num_columns.min(1_usize << BIND_CHUNK_COLUMN_VARS); - let chunk_count = num_columns / chunk_len; - [&matrices.a, &matrices.b, &matrices.c] - .map(|matrix| ColumnChunkIndex::new(matrix, chunk_len, chunk_count)) +impl Layout { + /// Validates the shape, lays `a`, `b` and `c` out and chunks them, and + /// returns `m` and the coefficients in layout order. + #[allow(clippy::type_complexity)] + fn new( + matrices: ConstraintMatrices, + ) -> Result<(SparseBoolMatrix, Arc, [Vec; 3]), SpartanMatrixError> { + let (num_row_vars, num_column_vars) = r1cs_num_vars(&matrices)?; + let row_count = matrices.a.row_count(); + let column_count = matrices.a.column_count(); + let ConstraintMatrices { m, a, b, c } = matrices; + let (a, a_values) = Positions::new(a)?; + let (b, b_values) = Positions::new(b)?; + let (c, c_values) = Positions::new(c)?; + let positions = [a, b, c]; + + let num_columns = 1_usize << num_column_vars; + let chunk_len = num_columns.min(1_usize << BIND_CHUNK_COLUMN_VARS); + let chunk_count = num_columns / chunk_len; + let column_chunks = [&positions[0], &positions[1], &positions[2]] + .map(|positions| ColumnChunkIndex::new(positions, chunk_len, chunk_count)); + + let layout = Self { + positions, + column_chunks, + row_count, + column_count, + num_row_vars, + num_column_vars, + }; + Ok((m, Arc::new(layout), [a_values, b_values, c_values])) + } } impl PreparedIntegerMatrices @@ -130,20 +210,29 @@ where for<'a> StoredInteger: From<&'a R>, { pub fn new(matrices: ConstraintMatrices) -> Result { - let (num_row_vars, num_column_vars) = r1cs_num_vars(&matrices)?; - let column_chunks = Arc::new(column_chunks(&matrices, num_column_vars)); - let digest = integer_matrix_digest(&matrices)?; + let (m, layout, values) = Layout::new(matrices)?; + let digest = integer_matrix_digest(&m, &layout, [&values[0], &values[1], &values[2]])?; Ok(Self { - matrices, - column_chunks, + m, + layout, + values, digest, - num_row_vars, - num_column_vars, }) } - pub fn matrices(&self) -> &ConstraintMatrices { - &self.matrices + /// The Boolean witness matrix. + pub fn m(&self) -> &SparseBoolMatrix { + &self.m + } + + /// Number of R1CS rows before padding. + pub fn row_count(&self) -> usize { + self.layout.row_count + } + + /// Number of assignment entries before padding, the constant included. + pub fn column_count(&self) -> usize { + self.layout.column_count } pub const fn digest(&self) -> &[u8; 32] { @@ -151,53 +240,39 @@ where } /// The matrices with `map` applied to every coefficient of `A`, `B` and - /// `C`: the per-proof work. The chunking and the digest are shared. + /// `C`, each into one contiguous table: the per-proof work. The layout + /// and the digest are shared. pub fn project(&self, map: M) -> PreparedConstraintMatrices where F: BitzField, - M: Fn(&R) -> F + Copy + Send + Sync, + M: Fn(&R) -> F + Sync, { - let [a, b, c] = [&self.matrices.a, &self.matrices.b, &self.matrices.c] - .map(|matrix| matrix.map_values_ref(map)); + let values = [&self.values[0], &self.values[1], &self.values[2]] + .map(|values| values.par_iter().map(&map).collect()); PreparedConstraintMatrices { - a, - b, - c, - column_chunks: Arc::clone(&self.column_chunks), + layout: Arc::clone(&self.layout), + values, digest: self.digest, - num_row_vars: self.num_row_vars, - num_column_vars: self.num_column_vars, } } } impl PreparedConstraintMatrices { - /// Prepares matrices already over the field, without copying them; the - /// digest is `constraint_matrix_digest`. + /// Prepares matrices already over the field; the digest is + /// `constraint_matrix_digest`. pub fn new(matrices: ConstraintMatrices) -> Result { - let (num_row_vars, num_column_vars) = r1cs_num_vars(&matrices)?; - let column_chunks = Arc::new(column_chunks(&matrices, num_column_vars)); - let ConstraintMatrices { m, a, b, c } = matrices; - let digest = constraint_matrix_digest(&m, [&a, &b, &c])?; + let (m, layout, values) = Layout::new(matrices)?; + let digest = constraint_matrix_digest(&m, &layout, [&values[0], &values[1], &values[2]])?; Ok(Self { - a, - b, - c, - column_chunks, + layout, + values, digest, - num_row_vars, - num_column_vars, }) } /// Returns human-readable info about R1CS matrices and their nonzero entries. pub fn short_debug_info(&self) -> String { - let nonzeros: usize = self - .matrices() - .iter() - .flat_map(|matrix| matrix.rows()) - .map(|row| row.entries().len()) - .sum(); + let nonzeros: usize = self.values.iter().map(Vec::len).sum(); format!( "{} r1cs rows -> 2^{}, {} h entries -> 2^{}, {nonzeros} nonzeros", self.row_count(), @@ -207,31 +282,26 @@ impl PreparedConstraintMatrices { ) } - /// `A`, `B` and `C`. - pub fn matrices(&self) -> [&SparseMatrix; 3] { - [&self.a, &self.b, &self.c] - } - /// Number of R1CS rows before padding. pub fn row_count(&self) -> usize { - self.a.row_count() + self.layout.row_count } /// Number of assignment entries before padding, the constant included. - pub const fn column_count(&self) -> usize { - self.a.column_count() + pub fn column_count(&self) -> usize { + self.layout.column_count } pub const fn digest(&self) -> &[u8; 32] { &self.digest } - pub const fn num_row_vars(&self) -> usize { - self.num_row_vars + pub fn num_row_vars(&self) -> usize { + self.layout.num_row_vars } - pub const fn num_column_vars(&self) -> usize { - self.num_column_vars + pub fn num_column_vars(&self) -> usize { + self.layout.num_column_vars } } @@ -337,14 +407,40 @@ impl PreparedConstraintMatrices { row_point: &[F], rho: F, ) -> Result, SpartanMatrixError> { - bind_and_batch_with_num_vars( - self.matrices(), - &self.column_chunks, - row_point, - rho, - self.num_row_vars, - self.num_column_vars, - ) + let layout = &*self.layout; + if row_point.len() != layout.num_row_vars { + return Err(SpartanMatrixError::InvalidRowPointLength { + expected: layout.num_row_vars, + actual: row_point.len(), + }); + } + + let row_weights = poly::eq_table(row_point); + let batched = self.batched(rho); + let chunk_len = layout.column_chunks[0].chunk_len; + + // Every task owns one column chunk of the table and touches only the + // nonzeros landing in it: no two tasks write the same column, and each + // task's writes stay within a cache-sized slice. + let mut evaluations: Vec = + rayon::iter::repeat_n(F::zero(), 1usize << layout.num_column_vars).collect(); + evaluations + .par_chunks_mut(chunk_len) + .enumerate() + .for_each(|(chunk, output)| { + let base = chunk * chunk_len; + for (positions, index, values, batch_scale) in batched { + for span in &index.spans[chunk] { + let row_scale = row_weights[span.row] * batch_scale; + for k in span.start..span.end { + output[positions.columns[k] as usize - base] += row_scale * values[k]; + } + } + } + }); + + DenseMultilinearExtension::from_evaluations(layout.num_column_vars, evaluations) + .map_err(|_| SpartanMatrixError::InvalidMleOperation) } /// Directly evaluates @@ -360,127 +456,68 @@ impl PreparedConstraintMatrices { rho: F, column_point: &[F], ) -> Result { - evaluate_batched_with_num_vars( - self.matrices(), - &self.column_chunks, - row_point, - rho, - column_point, - self.num_row_vars, - self.num_column_vars, - ) - } -} - -fn bind_and_batch_with_num_vars( - matrices: [&SparseMatrix; 3], - column_chunks: &[ColumnChunkIndex; 3], - row_point: &[F], - rho: F, - num_row_vars: usize, - num_column_vars: usize, -) -> Result, SpartanMatrixError> { - if row_point.len() != num_row_vars { - return Err(SpartanMatrixError::InvalidRowPointLength { - expected: num_row_vars, - actual: row_point.len(), - }); - } - - let row_weights = poly::eq_table(row_point); - let batched = [ - (matrices[0], &column_chunks[0], F::one()), - (matrices[1], &column_chunks[1], rho), - (matrices[2], &column_chunks[2], rho * rho), - ]; - let chunk_len = column_chunks[0].chunk_len; - - // Every task owns one column chunk of the table and touches only the - // nonzeros landing in it: no two tasks write the same column, and each - // task's writes stay within a cache-sized slice. - let mut evaluations: Vec = - rayon::iter::repeat_n(F::zero(), 1usize << num_column_vars).collect(); - evaluations - .par_chunks_mut(chunk_len) - .enumerate() - .for_each(|(chunk, output)| { - let base = chunk * chunk_len; - for (matrix, index, batch_scale) in batched { - for span in &index.spans[chunk] { - let row_scale = row_weights[span.row] * batch_scale; - let entries = &matrix.rows()[span.row].entries()[span.start..span.end]; - for &(column, coefficient) in entries { - output[column - base] += row_scale * coefficient; - } - } - } - }); - - DenseMultilinearExtension::from_evaluations(num_column_vars, evaluations) - .map_err(|_| SpartanMatrixError::InvalidMleOperation) -} - -fn evaluate_batched_with_num_vars( - matrices: [&SparseMatrix; 3], - column_chunks: &[ColumnChunkIndex; 3], - row_point: &[F], - rho: F, - column_point: &[F], - num_row_vars: usize, - num_column_vars: usize, -) -> Result { - if row_point.len() != num_row_vars { - return Err(SpartanMatrixError::InvalidRowPointLength { - expected: num_row_vars, - actual: row_point.len(), - }); - } - if column_point.len() != num_column_vars { - return Err(SpartanMatrixError::InvalidColumnPointLength { - expected: num_column_vars, - actual: column_point.len(), - }); - } + let layout = &*self.layout; + if row_point.len() != layout.num_row_vars { + return Err(SpartanMatrixError::InvalidRowPointLength { + expected: layout.num_row_vars, + actual: row_point.len(), + }); + } + if column_point.len() != layout.num_column_vars { + return Err(SpartanMatrixError::InvalidColumnPointLength { + expected: layout.num_column_vars, + actual: column_point.len(), + }); + } - let row_weights = poly::eq_table(row_point); - let batched = [ - (matrices[0], &column_chunks[0], F::one()), - (matrices[1], &column_chunks[1], rho), - (matrices[2], &column_chunks[2], rho * rho), - ]; - - // All tasks share one cache-sized `low` table and a per-chunk factor. - // Each task reduces one column chunk of nonzeros to a single field element. - let chunk_len = column_chunks[0].chunk_len; - let (low_point, high_point) = column_point.split_at(chunk_len.ilog2() as usize); - let low_weights = poly::eq_table(low_point); - let high_weights = poly::eq_table(high_point); - debug_assert_eq!(high_weights.len(), column_chunks[0].spans.len()); - - let evaluation = high_weights - .par_iter() - .enumerate() - .map(|(chunk, &chunk_weight)| { - let base = chunk * chunk_len; - let mut chunk_sum = F::zero(); - for (matrix, index, batch_scale) in batched { - let mut matrix_sum = F::zero(); - for span in &index.spans[chunk] { - let entries = &matrix.rows()[span.row].entries()[span.start..span.end]; - let span_sum = entries - .iter() - .fold(F::zero(), |sum, &(column, coefficient)| { - sum + low_weights[column - base] * coefficient + let row_weights = poly::eq_table(row_point); + let batched = self.batched(rho); + + // All tasks share one cache-sized `low` table and a per-chunk factor. + // Each task reduces one column chunk of nonzeros to a single field element. + let chunk_len = layout.column_chunks[0].chunk_len; + let (low_point, high_point) = column_point.split_at(chunk_len.ilog2() as usize); + let low_weights = poly::eq_table(low_point); + let high_weights = poly::eq_table(high_point); + debug_assert_eq!(high_weights.len(), layout.column_chunks[0].spans.len()); + + let evaluation = high_weights + .par_iter() + .enumerate() + .map(|(chunk, &chunk_weight)| { + let base = chunk * chunk_len; + let mut chunk_sum = F::zero(); + for (positions, index, values, batch_scale) in batched { + let mut matrix_sum = F::zero(); + for span in &index.spans[chunk] { + let span_sum = (span.start..span.end).fold(F::zero(), |sum, k| { + sum + low_weights[positions.columns[k] as usize - base] * values[k] }); - matrix_sum += row_weights[span.row] * span_sum; + matrix_sum += row_weights[span.row] * span_sum; + } + chunk_sum += batch_scale * matrix_sum; } - chunk_sum += batch_scale * matrix_sum; - } - chunk_weight * chunk_sum + chunk_weight * chunk_sum + }) + .reduce(|| F::zero(), |left, right| left + right); + + Ok(evaluation) + } + + /// `A`, `B` and `C`, each with its positions, chunking and batching + /// scale: `1`, `rho` and `rho^2`. + fn batched(&self, rho: F) -> [(&Positions, &ColumnChunkIndex, &[F], F); 3] { + let layout = &*self.layout; + let scales = [F::one(), rho, rho * rho]; + std::array::from_fn(|i| { + ( + &layout.positions[i], + &layout.column_chunks[i], + self.values[i].as_slice(), + scales[i], + ) }) - .reduce(|| F::zero(), |left, right| left + right); - - Ok(evaluation) + } } pub(crate) fn r1cs_num_vars( @@ -497,11 +534,12 @@ pub(crate) fn r1cs_num_vars( /// Canonically commits the complete public matrix statement before any /// Fiat--Shamir challenge is sampled: `domain`, the positions of `M`, then -/// `A`, `B` and `C` with every coefficient written by `encode`. +/// `A`, `B` and `C` row by row with every coefficient written by `encode`. fn matrix_digest( domain: &[u8], m: &SparseBoolMatrix, - matrices: [&SparseMatrix; 3], + layout: &Layout, + values: [&[C]; 3], encode: impl Fn(&mut Sha256, &C) -> Result<(), SpartanMatrixError>, ) -> Result<[u8; 32], SpartanMatrixError> { let mut hash = Sha256::new(); @@ -517,15 +555,20 @@ fn matrix_digest( } } - for (label, matrix) in [b"A", b"B", b"C"].into_iter().zip(matrices) { + for ((label, positions), values) in [b"A", b"B", b"C"] + .into_iter() + .zip(&layout.positions) + .zip(values) + { hash.update(label); - hash_usize(&mut hash, matrix.row_count())?; - hash_usize(&mut hash, matrix.column_count())?; - for row in matrix.rows() { - hash_usize(&mut hash, row.entries().len())?; - for (column, coefficient) in row.entries() { - hash_usize(&mut hash, *column)?; - encode(&mut hash, coefficient)?; + hash_usize(&mut hash, layout.row_count)?; + hash_usize(&mut hash, layout.column_count)?; + for row in 0..layout.row_count { + let range = positions.row(row); + hash_usize(&mut hash, range.len())?; + for k in range { + hash_usize(&mut hash, positions.columns[k] as usize)?; + encode(&mut hash, &values[k])?; } } } @@ -539,14 +582,16 @@ fn matrix_digest( /// The digest domain is intentionally field-neutral. A protocol that supports /// more than one field must bind the field choice in its transcript session or /// instance; canonical coefficient encodings need not identify their field. -pub(crate) fn constraint_matrix_digest( +fn constraint_matrix_digest( m: &SparseBoolMatrix, - matrices: [&SparseMatrix; 3], + layout: &Layout, + values: [&[F]; 3], ) -> Result<[u8; 32], SpartanMatrixError> { matrix_digest( b"bitz/spartan/constraint-matrices/v1", m, - matrices, + layout, + values, |hash, coefficient| { let encoding = transcript::Encoding::encode(coefficient); let bytes = encoding.as_ref(); @@ -557,14 +602,16 @@ pub(crate) fn constraint_matrix_digest( ) } -/// The statement digest over integer coefficients, each in its normalized +/// The statement digest over integer coefficients, each in its normalised /// two's-complement words, so it is fixed before any modulus is drawn and /// shared by every projection. Its own domain: it never equals the digest of /// the projected matrices. A protocol that projects must bind the modulus in /// its transcript before Spartan reads this digest, as the e2e does by /// absorbing its parameters right after the draw. -pub(crate) fn integer_matrix_digest( - matrices: &ConstraintMatrices, +fn integer_matrix_digest( + m: &SparseBoolMatrix, + layout: &Layout, + values: [&[R]; 3], ) -> Result<[u8; 32], SpartanMatrixError> where R: ToPrimitive, @@ -572,8 +619,9 @@ where { matrix_digest( b"bitz/spartan/integer-constraint-matrices/v1", - &matrices.m, - [&matrices.a, &matrices.b, &matrices.c], + m, + layout, + values, |hash, coefficient| { // Coefficients are small in practice; spare them the heap. if let Some(value) = coefficient.to_i128() { @@ -623,20 +671,26 @@ pub(crate) fn padded_num_vars(logical_len: usize) -> Result SparseMatrix { + fn random_sparse_matrix( + rng: &mut Pcg64, + mut coefficient: impl FnMut(&mut Pcg64) -> C, + ) -> SparseMatrix { let rows = (0..ROWS) .map(|_| { let mut columns: Vec = (0..ENTRIES_PER_ROW) @@ -646,23 +700,35 @@ mod tests { columns.dedup(); columns .into_iter() - .map(|column| (column, F::from(u128::from(rng.random::())))) + .map(|column| (column, coefficient(rng))) .collect() }) .collect(); SparseMatrix::try_from_rows(COLUMNS, rows).unwrap() } + fn empty_m() -> SparseBoolMatrix { + SparseBoolMatrix::try_from_rows(1, vec![Vec::new(); COLUMNS]).unwrap() + } + fn random_matrices(rng: &mut Pcg64) -> ConstraintMatrices { - let m = SparseBoolMatrix::try_from_rows(1, vec![Vec::new(); COLUMNS]).unwrap(); - let a = random_sparse_matrix(rng); - let b = random_sparse_matrix(rng); - let c = random_sparse_matrix(rng); - ConstraintMatrices { m, a, b, c } + let mut field = |rng: &mut Pcg64| F::from(u128::from(rng.random::())); + ConstraintMatrices { + m: empty_m(), + a: random_sparse_matrix(rng, &mut field), + b: random_sparse_matrix(rng, &mut field), + c: random_sparse_matrix(rng, &mut field), + } } - fn random_prepared_matrices(rng: &mut Pcg64) -> PreparedConstraintMatrices { - PreparedConstraintMatrices::new(random_matrices(rng)).unwrap() + fn random_integer_matrices(rng: &mut Pcg64) -> ConstraintMatrices { + let mut integer = |rng: &mut Pcg64| i128::from(rng.random::()); + ConstraintMatrices { + m: empty_m(), + a: random_sparse_matrix(rng, &mut integer), + b: random_sparse_matrix(rng, &mut integer), + c: random_sparse_matrix(rng, &mut integer), + } } fn random_point(rng: &mut Pcg64, len: usize) -> Vec { @@ -673,7 +739,7 @@ mod tests { /// `D(r_y)` by the direct triple loop over every nonzero. fn reference_evaluation( - matrices: [&SparseMatrix; 3], + matrices: &ConstraintMatrices, row_point: &[F], rho: F, column_point: &[F], @@ -681,7 +747,11 @@ mod tests { let row_weights = poly::eq_table(row_point); let column_weights = poly::eq_table(column_point); let mut evaluation = F::from(0u128); - for (matrix, batch_scale) in matrices.into_iter().zip([F::from(1u128), rho, rho * rho]) { + for (matrix, batch_scale) in [ + (&matrices.a, F::from(1u128)), + (&matrices.b, rho), + (&matrices.c, rho * rho), + ] { for (row, entries) in matrix.rows().iter().enumerate() { for &(column, coefficient) in entries.entries() { evaluation += @@ -692,15 +762,43 @@ mod tests { evaluation } + #[test] + fn the_layout_keeps_every_nonzero_in_row_order() { + let mut rng = Pcg64::seed_from_u64(5); + let matrices = random_matrices(&mut rng); + let prepared = PreparedConstraintMatrices::new(matrices.clone()).unwrap(); + assert_eq!(prepared.row_count(), ROWS); + assert_eq!(prepared.column_count(), COLUMNS); + + for ((matrix, positions), values) in [&matrices.a, &matrices.b, &matrices.c] + .into_iter() + .zip(&prepared.layout.positions) + .zip(&prepared.values) + { + assert_eq!(positions.row_count(), matrix.row_count()); + let mut k = 0; + for (row, entries) in matrix.rows().iter().enumerate() { + assert_eq!(positions.row(row), k..k + entries.entries().len()); + for &(column, coefficient) in entries.entries() { + assert_eq!(positions.columns[k] as usize, column); + assert_eq!(values[k], coefficient); + k += 1; + } + } + assert_eq!(k, values.len()); + } + } + #[test] fn column_chunks_partition_every_row() { let mut rng = Pcg64::seed_from_u64(7); - let prepared = random_prepared_matrices(&mut rng); + let prepared = PreparedConstraintMatrices::new(random_matrices(&mut rng)).unwrap(); - for (matrix, index) in prepared - .matrices() - .into_iter() - .zip(prepared.column_chunks.iter()) + for (positions, index) in prepared + .layout + .positions + .iter() + .zip(&prepared.layout.column_chunks) { let mut spans: Vec<_> = index .spans @@ -711,17 +809,18 @@ mod tests { spans.sort_by_key(|(_, span)| (span.row, span.start)); let mut spans = spans.into_iter().peekable(); - for (row, entries) in matrix.rows().iter().enumerate() { - let mut next_start = 0; + for row in 0..positions.row_count() { + let range = positions.row(row); + let mut next_start = range.start; while let Some((chunk, span)) = spans.next_if(|(_, span)| span.row == row) { assert_eq!(span.start, next_start); assert!(span.end > span.start); - for &(column, _) in &entries.entries()[span.start..span.end] { - assert_eq!(column / index.chunk_len, chunk); + for &column in &positions.columns[span.start..span.end] { + assert_eq!(column as usize / index.chunk_len, chunk); } next_start = span.end; } - assert_eq!(next_start, entries.entries().len()); + assert_eq!(next_start, range.end); } assert!(spans.next().is_none()); } @@ -730,12 +829,13 @@ mod tests { #[test] fn evaluate_batched_matches_reference_and_bound_table() { let mut rng = Pcg64::seed_from_u64(11); - let prepared = random_prepared_matrices(&mut rng); + let matrices = random_matrices(&mut rng); + let prepared = PreparedConstraintMatrices::new(matrices.clone()).unwrap(); let row_point = random_point(&mut rng, prepared.num_row_vars()); let column_point = random_point(&mut rng, prepared.num_column_vars()); let rho = F::from(u128::from(rng.random::())); - let expected = reference_evaluation(prepared.matrices(), &row_point, rho, &column_point); + let expected = reference_evaluation(&matrices, &row_point, rho, &column_point); let evaluation = prepared .evaluate_batched(&row_point, rho, &column_point) .unwrap(); @@ -763,34 +863,18 @@ mod tests { let (words, len) = super::canonical_words(value); assert_eq!( &words[..len], - circuit::matrix_products::StoredInteger::from(&value).words(), + StoredInteger::from(&value).words(), "{value}" ); } } /// Projecting integer matrices prepared once gives what preparing the - /// projected matrices gives, the chunking shared rather than rebuilt; only + /// projected matrices gives, the layout shared rather than rebuilt; only /// the digest differs, each over its own coefficients and domain. #[test] fn projection_matches_direct_preparation_up_to_the_digest_domain() { let mut rng = Pcg64::seed_from_u64(13); - let mut random_integer_matrix = || { - let rows = (0..ROWS) - .map(|_| { - let mut columns: Vec = (0..ENTRIES_PER_ROW) - .map(|_| rng.random_range(0..COLUMNS)) - .collect(); - columns.sort_unstable(); - columns.dedup(); - columns - .into_iter() - .map(|column| (column, i128::from(rng.random::()))) - .collect() - }) - .collect(); - SparseMatrix::try_from_rows(COLUMNS, rows).unwrap() - }; let project = |coefficient: &i128| { let magnitude = F::from(coefficient.unsigned_abs()); if *coefficient < 0 { @@ -800,22 +884,15 @@ mod tests { } }; - let integer_matrices = ConstraintMatrices { - m: SparseBoolMatrix::try_from_rows(1, vec![Vec::new(); COLUMNS]).unwrap(), - a: random_integer_matrix(), - b: random_integer_matrix(), - c: random_integer_matrix(), - }; + let integer_matrices = random_integer_matrices(&mut rng); let prepared = PreparedIntegerMatrices::new(integer_matrices.clone()).unwrap(); let projected = prepared.project(project); let direct = PreparedConstraintMatrices::new( integer_matrices.clone().map_coefficients(|c| project(&c)), ) .unwrap(); - assert_eq!(projected.matrices(), direct.matrices()); - assert_eq!(projected.column_chunks, direct.column_chunks); - assert_eq!(projected.num_row_vars(), direct.num_row_vars()); - assert_eq!(projected.num_column_vars(), direct.num_column_vars()); + assert_eq!(projected.values, direct.values); + assert_eq!(projected.layout, direct.layout); assert_ne!( projected.digest(), direct.digest(), @@ -823,14 +900,11 @@ mod tests { ); assert_eq!(projected.digest(), prepared.digest()); assert!(Arc::ptr_eq( - &projected.column_chunks, - &prepared.project(project).column_chunks + &projected.layout, + &prepared.project(project).layout )); - assert_eq!(prepared.matrices().a.row_count(), projected.row_count()); - assert_eq!( - prepared.matrices().a.column_count(), - projected.column_count() - ); + assert_eq!(prepared.row_count(), projected.row_count()); + assert_eq!(prepared.column_count(), projected.column_count()); // The integer digest is the integer matrices': the same under every // projection, different as soon as a coefficient or a position is. diff --git a/tooling/cli/src/end_to_end.rs b/tooling/cli/src/end_to_end.rs index abcd9fd8..39cc22d5 100644 --- a/tooling/cli/src/end_to_end.rs +++ b/tooling/cli/src/end_to_end.rs @@ -164,14 +164,14 @@ where let inputs = generator.take_inputs(); statement.synthesize(&mut generator, &inputs)?; let map = generator.finish(); - if map.h_len() != matrices.matrices().a.column_count() { + if map.h_len() != matrices.column_count() { return Err(Error::Configuration("map and assignment dimensions differ")); } - if map.f_len() != matrices.matrices().m.column_count() { + if map.f_len() != matrices.m().column_count() { return Err(Error::Configuration("map and witness dimensions differ")); } let claim_shape = shape_for(map.h_len())?; - let opening_path = if is_identity(&matrices.matrices().m) { + let opening_path = if is_identity(matrices.m()) { OpeningPath::Direct } else { OpeningPath::Virtual @@ -201,7 +201,7 @@ where pub fn stats(&self) -> CircuitStats { CircuitStats { opening_path: self.opening_path, - constraints: self.matrices.matrices().a.row_count(), + constraints: self.matrices.row_count(), assignment_bits: self.map.h_len(), committed_bits: match self.opening_path { OpeningPath::Direct => self.map.h_len(), @@ -223,7 +223,7 @@ where if f.bit_len() + 1 != self.map.f_len() || h.bit_len() != self.map.h_len() { return Err(Error::Input("circuit replay changed witness dimensions")); } - if products.a_mw.len() != self.matrices.matrices().a.row_count() { + if products.a_mw.len() != self.matrices.row_count() { return Err(Error::Input("circuit replay changed constraint count")); } // Over the integers, so modulo whichever prime is drawn. From 14374ad09a65866f4d8952d79a5788435e11a761 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Tue, 6 Oct 2026 18:35:28 +0100 Subject: [PATCH 11/11] Slightly tweaked comments --- crates/transcript/src/challenge/prime.rs | 38 +++++++++++++----------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/crates/transcript/src/challenge/prime.rs b/crates/transcript/src/challenge/prime.rs index 24b8f8f2..58b3ad65 100644 --- a/crates/transcript/src/challenge/prime.rs +++ b/crates/transcript/src/challenge/prime.rs @@ -1,33 +1,35 @@ //! Public prime sampling from the challenge stream. //! -//! As in `f2z-pcs`: candidates uniform among the odd integers of the requested -//! width, sieved by the small primes, then Miller–Rabin with bases drawn from -//! the same stream, so a prover grinding the transcript faces fresh bases at -//! every candidate. Everything here is public, so the arithmetic is -//! variable-time. +//! Candidates uniform among the odd integers of the requested width, sieved +//! by the small primes, then Miller–Rabin with bases drawn from the same +//! stream, so a prover grinding the transcript faces fresh bases at every +//! candidate. +//! +//! Arithmetic is variable-time. use field::MAX_MODULUS_BITS; use field::helpers::{self, FieldMetadata}; -/// Miller–Rabin bases per candidate. A composite passes a base with -/// probability at most `1/4`, so it survives with probability at most -/// `2^-160`, and a search of up to `2^16` candidates accepts a composite with -/// probability below `2^-144`: the 128 bits `f2z-pcs` targets and its 16 of -/// slack. A longer search does not happen: at least one odd integer in 44 is -/// prime below `2^126`, so `2^16` composites in a row have probability below -/// `2^-2000`. +/// Miller–Rabin bases per candidate. +/// +/// A composite passes a base with probability at most `1/4`, so it survives +/// with probability at most `2^-160`, and a search of up to `2^16` candidates +/// accepts a composite with probability below `2^-144`: the 128 bits and its +/// 16 bits of slack. +/// +/// A longer search does not happen: at least one odd integer in 44 is prime +/// below `2^126`, so `2^16` composites in a row have probability below `2^-2000`. const MILLER_RABIN_ROUNDS: u32 = 80; /// A prime of exactly `bits` bits, `2 <= bits <= MAX_MODULUS_BITS`, from -/// successive `u128` draws, the same on both sides: the fingerprint prime of -/// 5. "An end-to-end BitZ-based SNARK over any finitely generated ring", -/// Step 2, drawn once per proof after the commitment and the statement are -/// absorbed and before the PIOP. +/// successive `u128` draws, the same on both sides: the fingerprint prime. /// /// Each candidate is one draw, uniform among the odd integers in /// `[2^(bits-1), 2^bits)`; each base is uniform in `[2, candidate - 2]` by -/// rejection. The search stops at the first candidate that passes, which is -/// not exactly uniform over the primes, as in `f2z-pcs`. +/// rejection. The search stops at the first candidate that passes. +/// +/// Should be drawn once per proof after the commitment and the statement +/// are absorbed and before the PIOP. pub fn sample(mut next_u128: impl FnMut() -> u128, bits: u32) -> u128 { assert!( (2..=MAX_MODULUS_BITS).contains(&bits),