From 317d81c7ef74fe41e2b4162fa52397b881cc05fc Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Wed, 23 Sep 2026 23:07:53 +0100 Subject: [PATCH 1/5] Circuit crate made generic over BitzIntRing --- Cargo.lock | 37 ++-- Cargo.toml | 2 +- crates/circuit/src/constraints.rs | 232 ++++++++++++------------- crates/circuit/src/ecdsa_sha256.rs | 30 ++-- crates/circuit/src/lib.rs | 22 +++ crates/circuit/src/matrix_products.rs | 174 +++++++++---------- crates/circuit/src/matrix_sparse.rs | 36 ++-- crates/circuit/src/matrix_transpose.rs | 5 +- crates/circuit/src/matrix_wengert.rs | 50 +++--- crates/circuit/src/p256.rs | 178 ++++++++----------- crates/circuit/src/sha256.rs | 15 +- crates/circuit/src/witgen.rs | 2 + crates/common/src/lib.rs | 12 ++ crates/spartan/Cargo.toml | 10 +- crates/spartan/src/matrix.rs | 9 +- tooling/cli/Cargo.toml | 1 + tooling/cli/tests/circuits.rs | 3 +- 17 files changed, 398 insertions(+), 420 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 082feb8d..64e8f8a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,12 +151,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "base16ct" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" - [[package]] name = "bincode" version = "1.3.3" @@ -225,6 +219,7 @@ dependencies = [ "common", "divan", "field", + "num-bigint 0.4.8", "num-traits", "pcs", "poly", @@ -470,8 +465,6 @@ dependencies = [ "ctutils", "num-traits", "rand_core 0.10.1", - "serdect", - "zeroize", ] [[package]] @@ -496,19 +489,18 @@ dependencies = [ [[package]] name = "crypto-primitives" version = "0.1.0" -source = "git+https://github.com/NethermindEth/crypto-primitives.git?rev=3c4232b#3c4232bf362339041414a73254a3af303538ac81" +source = "git+https://github.com/NethermindEth/crypto-primitives.git?rev=b744c74#b744c74ad827b1e2a37cf7972fb1e1cb4fb0faaf" dependencies = [ "ark-ff", "ark-std", "crypto-bigint", - "crypto-primitives-proc-macros", + "crypto-primitives-proc-macros 0.1.0 (git+https://github.com/NethermindEth/crypto-primitives.git?rev=b744c74)", "num-traits", "pastey", "rand 0.10.2", "serde", "thiserror", "zerocopy", - "zerocopy-derive", "zeroize", ] @@ -522,6 +514,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "crypto-primitives-proc-macros" +version = "0.1.0" +source = "git+https://github.com/NethermindEth/crypto-primitives.git?rev=b744c74#b744c74ad827b1e2a37cf7972fb1e1cb4fb0faaf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -666,7 +668,7 @@ dependencies = [ "binius-field", "bytemuck", "crypto-primitives", - "crypto-primitives-proc-macros", + "crypto-primitives-proc-macros 0.1.0 (git+https://github.com/NethermindEth/crypto-primitives.git?rev=3c4232b)", "divan", "flock-core", "num-traits", @@ -1469,16 +1471,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serdect" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" -dependencies = [ - "base16ct", - "serde", -] - [[package]] name = "sha2" version = "0.10.9" @@ -1547,6 +1539,7 @@ name = "spartan" version = "0.1.0" dependencies = [ "circuit", + "common", "crypto-primitives", "divan", "field", diff --git a/Cargo.toml b/Cargo.toml index 7ff204d4..1c79b57c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ bincode = "1.3.3" blake3 = "1.8.6" bytemuck = "1.25" crypto-bigint = "0.7.5" -crypto-primitives = { git = "https://github.com/NethermindEth/crypto-primitives.git", rev = "3c4232b", features = ["rand"] } +crypto-primitives = { git = "https://github.com/NethermindEth/crypto-primitives.git", rev = "b744c74", features = ["rand"] } crypto-primitives-proc-macros = { git = "https://github.com/NethermindEth/crypto-primitives.git", rev = "3c4232b" } divan = "0.1.21" flock-core = { git = "https://github.com/succinctlabs/flock.git", rev = "879072249e52b8b9054bf0c6a034cec20f8f6fc7" } diff --git a/crates/circuit/src/constraints.rs b/crates/circuit/src/constraints.rs index 61d0b6b9..03083723 100644 --- a/crates/circuit/src/constraints.rs +++ b/crates/circuit/src/constraints.rs @@ -4,10 +4,12 @@ //! witness, prefixed by a constant one, to the integer witness. Its first row //! is the implicit integer constant one. `A`, `B`, and `C` then encode the //! rank-1 constraints `(A z) * (B z) = C z` over that integer witness. Every -//! integer coefficient is an arbitrary-precision signed [`BigInt`]. +//! integer coefficient is an arbitrary-precision signed [`BitzIntRing`]. -use num_bigint::BigInt; -use num_traits::{One, Zero}; +use crate::witgen::PackedWitness; +use crate::{BoolWitness, Circuit, HintResult, PackedBits, ScalarBits, WitnessContext}; +use common::{BitzIntRing, BitzIntSemiring}; +use num_traits::Zero; use rayon::prelude::*; use std::array; use std::cmp::Ordering; @@ -17,9 +19,6 @@ use std::iter::Sum; use std::mem; use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; -use crate::witgen::PackedWitness; -use crate::{BoolWitness, Circuit, HintResult, PackedBits, ScalarBits, WitnessContext}; - /// One row of a sparse matrix, sorted by increasing column index. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SparseRow { @@ -235,15 +234,15 @@ impl SparseBoolMatrix { /// The four sparse matrices generated for a BitZ circuit. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct ConstraintMatrices { +pub struct ConstraintMatrices { /// Boolean-to-integer witness matrix. pub m: SparseBoolMatrix, /// Left R1CS matrix. - pub a: SparseMatrix, + pub a: SparseMatrix, /// Right R1CS matrix. - pub b: SparseMatrix, + pub b: SparseMatrix, /// Output R1CS matrix. - pub c: SparseMatrix, + pub c: SparseMatrix, } /// Why the four constraint matrices cannot describe one R1CS relation. @@ -280,7 +279,7 @@ impl Display for ConstraintMatrixShapeError { impl Error for ConstraintMatrixShapeError {} -impl ConstraintMatrices { +impl ConstraintMatrices { /// Checks that A, B, and C share a shape and consume the assignment /// produced by M. pub fn validate_shape(&self) -> Result<(), ConstraintMatrixShapeError> { @@ -323,7 +322,7 @@ impl ConstraintMatrices { pub fn map_coefficients(self, map: M) -> ConstraintMatrices where D: Send + Sync, - M: Fn(C) -> D + Send + Sync, + M: Fn(R) -> D + Send + Sync, { ConstraintMatrices { m: self.m, @@ -332,49 +331,12 @@ impl ConstraintMatrices { c: self.c.map_values_with(&map), } } -} - -/// Why a Boolean witness does not satisfy a generated constraint system. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum SatisfactionError { - /// The four matrices do not share a compatible R1CS shape. - InvalidShape(ConstraintMatrixShapeError), - /// The packed Boolean witness has the wrong number of entries. - WitnessLength { expected: usize, actual: usize }, - /// The indicated R1CS row does not satisfy `a * b = c`. - Constraint { row: usize }, -} - -impl Display for SatisfactionError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InvalidShape(error) => Display::fmt(error, formatter), - Self::WitnessLength { expected, actual } => write!( - formatter, - "Boolean witness has length {actual}, expected {expected}" - ), - Self::Constraint { row } => write!(formatter, "R1CS row {row} is unsatisfied"), - } - } -} -impl Error for SatisfactionError {} - -impl From for SatisfactionError { - fn from(error: ConstraintMatrixShapeError) -> Self { - Self::InvalidShape(error) - } -} - -impl ConstraintMatrices { /// Applies `M` to a packed Boolean witness. /// /// The returned vector starts with the implicit constant one and is the /// witness consumed by `A`, `B`, and `C`. - pub fn integer_witness( - &self, - witness: &PackedWitness, - ) -> Result, SatisfactionError> { + pub fn integer_witness(&self, witness: &PackedWitness) -> Result, SatisfactionError> { let expected = self.m.column_count().saturating_sub(1); if witness.bit_len() != expected { return Err(SatisfactionError::WitnessLength { @@ -396,7 +358,7 @@ impl ConstraintMatrices { witness.bit(column - 1) } }); - BigInt::from(value) + R::from(value) }) .collect()) } @@ -424,7 +386,39 @@ impl ConstraintMatrices { } } -fn evaluate_integer_row(row: &SparseRow, witness: &[BigInt]) -> BigInt { +/// Why a Boolean witness does not satisfy a generated constraint system. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SatisfactionError { + /// The four matrices do not share a compatible R1CS shape. + InvalidShape(ConstraintMatrixShapeError), + /// The packed Boolean witness has the wrong number of entries. + WitnessLength { expected: usize, actual: usize }, + /// The indicated R1CS row does not satisfy `a * b = c`. + Constraint { row: usize }, +} + +impl Display for SatisfactionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidShape(error) => Display::fmt(error, formatter), + Self::WitnessLength { expected, actual } => write!( + formatter, + "Boolean witness has length {actual}, expected {expected}" + ), + Self::Constraint { row } => write!(formatter, "R1CS row {row} is unsatisfied"), + } + } +} + +impl Error for SatisfactionError {} + +impl From for SatisfactionError { + fn from(error: ConstraintMatrixShapeError) -> Self { + Self::InvalidShape(error) + } +} + +fn evaluate_integer_row(row: &SparseRow, witness: &[R]) -> R { row.entries() .iter() .map(|(column, coefficient)| witness[*column].clone() * coefficient.clone()) @@ -484,32 +478,32 @@ impl BoolWitness for BoolLinearCombination { /// A symbolic integer linear combination. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct LinearCombination { - constant: BigInt, +pub struct LinearCombination { + constant: R, /// Indices of witness elements with nonzero coefficients. /// Sorted and deduplicated by witness index. - witnesses: Vec<(usize, BigInt)>, + witnesses: Vec<(usize, R)>, } -impl LinearCombination { +impl LinearCombination { /// The integer constant term. - pub fn constant(&self) -> &BigInt { + pub fn constant(&self) -> &R { &self.constant } /// Nonzero coefficients sorted by zero-based integer witness index. - pub fn witnesses(&self) -> &[(usize, BigInt)] { + pub fn witnesses(&self) -> &[(usize, R)] { &self.witnesses } fn witness(index: usize) -> Self { Self { - constant: BigInt::zero(), - witnesses: vec![(index, BigInt::one())], + constant: R::zero(), + witnesses: vec![(index, R::one())], } } - fn into_sparse_row(self) -> SparseRow { + fn into_sparse_row(self) -> SparseRow { let Self { constant, mut witnesses, @@ -525,8 +519,8 @@ impl LinearCombination { } } -impl From for LinearCombination { - fn from(constant: BigInt) -> Self { +impl From for LinearCombination { + fn from(constant: R) -> Self { Self { constant, witnesses: Vec::new(), @@ -534,9 +528,9 @@ impl From for LinearCombination { } } -impl Zero for LinearCombination { +impl Zero for LinearCombination { fn zero() -> Self { - Self::from(BigInt::zero()) + Self::from(R::zero()) } fn is_zero(&self) -> bool { @@ -544,7 +538,7 @@ impl Zero for LinearCombination { } } -impl Add for LinearCombination { +impl Add for LinearCombination { type Output = Self; fn add(mut self, rhs: Self) -> Self::Output { @@ -553,7 +547,7 @@ impl Add for LinearCombination { } } -impl AddAssign for LinearCombination { +impl AddAssign for LinearCombination { fn add_assign(&mut self, rhs: Self) { self.constant += rhs.constant; self.witnesses = merge_sorted_vecs( @@ -572,7 +566,7 @@ impl AddAssign for LinearCombination { } } -impl Neg for LinearCombination { +impl Neg for LinearCombination { type Output = Self; fn neg(mut self) -> Self::Output { @@ -584,7 +578,7 @@ impl Neg for LinearCombination { } } -impl Sub for LinearCombination { +impl Sub for LinearCombination { type Output = Self; fn sub(self, rhs: Self) -> Self::Output { @@ -592,16 +586,16 @@ impl Sub for LinearCombination { } } -impl SubAssign for LinearCombination { +impl SubAssign for LinearCombination { fn sub_assign(&mut self, rhs: Self) { *self += -rhs; } } -impl Mul for LinearCombination { +impl Mul for LinearCombination { type Output = Self; - fn mul(mut self, rhs: BigInt) -> Self::Output { + fn mul(mut self, rhs: R) -> Self::Output { if rhs.is_zero() { return Self::zero(); } @@ -613,7 +607,7 @@ impl Mul for LinearCombination { } } -impl Sum for LinearCombination { +impl Sum for LinearCombination { fn sum>(iter: I) -> Self { iter.fold(Self::zero(), Add::add) } @@ -669,14 +663,18 @@ fn merge_sorted_vecs( /// Circuit backend that records sparse M/A/B/C matrices without evaluating hints. #[derive(Clone, Debug)] -pub struct ConstraintGenerator { +pub struct ConstraintGenerator { input_witnesses: usize, next_boolean_witness: usize, m_rows: Vec, - r1cs: Vec<(LinearCombination, LinearCombination, LinearCombination)>, + r1cs: Vec<( + LinearCombination, + LinearCombination, + LinearCombination, + )>, } -impl ConstraintGenerator { +impl ConstraintGenerator { /// Starts a generator with `input_witnesses` preallocated Boolean inputs. pub const fn new(input_witnesses: usize) -> Self { Self { @@ -725,7 +723,7 @@ impl ConstraintGenerator { } /// Finishes generation and materializes the four sparse matrices. - pub fn into_matrices(self) -> ConstraintMatrices { + pub fn into_matrices(self) -> ConstraintMatrices { let Self { input_witnesses: _, next_boolean_witness, @@ -785,10 +783,10 @@ fn bool_sparse_row(value: BoolLinearCombination) -> SparseBoolRow { } } -impl Circuit for ConstraintGenerator { +impl Circuit for ConstraintGenerator { type Bool = BoolLinearCombination; - type Coefficient = BigInt; - type Z = LinearCombination; + type Coefficient = R; + type Z = LinearCombination; fn xor( &mut self, @@ -804,7 +802,7 @@ impl Circuit for ConstraintGenerator { ) -> ScalarBits where H: Fn( - &dyn WitnessContext, + &dyn WitnessContext, Self::Bool, Self::Coefficient>, ) -> HintResult> + Send + Sync @@ -821,27 +819,28 @@ impl Circuit for ConstraintGenerator { })) } - fn bitz(&mut self, value: BoolLinearCombination) -> LinearCombination { + fn bitz(&mut self, value: BoolLinearCombination) -> LinearCombination { LinearCombination::witness(self.record_bitz(value)) } fn bitz_unsigned( &mut self, bits_le: &ScalarBits, - ) -> (LinearCombination, LinearCombination) { + ) -> (LinearCombination, LinearCombination) { assert!(LOW <= N, "low part cannot be wider than the input"); // Same layout as the default, one `bitz` per bit with coefficient // 2^index, but each sum is built once with exact capacity. let mut witnesses = Vec::with_capacity(N); + let two = R::one() + R::one(); for (index, bit) in bits_le.0.iter().enumerate() { - witnesses.push((self.record_bitz(bit.clone()), BigInt::one() << index)); + witnesses.push((self.record_bitz(bit.clone()), two.clone().pow(index as u32))); } let low = LinearCombination { - constant: BigInt::zero(), + constant: R::zero(), witnesses: witnesses[..LOW].to_vec(), }; let full = LinearCombination { - constant: BigInt::zero(), + constant: R::zero(), witnesses, }; (full, low) @@ -849,17 +848,17 @@ impl Circuit for ConstraintGenerator { fn assert_r1c( &mut self, - a: LinearCombination, - b: LinearCombination, - c: LinearCombination, + a: LinearCombination, + b: LinearCombination, + c: LinearCombination, ) { self.r1cs.push((a, b, c)); } fn sign_extend_z( &mut self, - value: LinearCombination, - ) -> LinearCombination { + value: LinearCombination, + ) -> LinearCombination { assert!( TO_LIMBS >= FROM_LIMBS, "cannot sign-extend into fewer limbs" @@ -872,11 +871,14 @@ impl Circuit for ConstraintGenerator { mod tests { use super::*; use crate::witgen::Witgen; + use num_traits::One; - fn terms(pairs: &[(usize, i64)]) -> Vec<(usize, BigInt)> { + type R = num_bigint::BigInt; + + fn terms(pairs: &[(usize, i64)]) -> Vec<(usize, R)> { pairs .iter() - .map(|&(witness, coefficient)| (witness, BigInt::from(coefficient))) + .map(|&(witness, coefficient)| (witness, R::from(coefficient))) .collect() } @@ -888,11 +890,7 @@ mod tests { let z_sum = generator.bitz::<1>(sum); let z_x = generator.bitz::<1>(x); let z_y = generator.bitz::<1>(y); - generator.assert_r1c::<1>( - z_x.clone() * BigInt::from(2), - z_y.clone(), - z_x + z_y - z_sum, - ); + generator.assert_r1c::<1>(z_x.clone() * R::from(2), z_y.clone(), z_x + z_y - z_sum); let mut matrices = generator.into_matrices(); assert_eq!(matrices.m.row_count(), 4); @@ -901,22 +899,18 @@ mod tests { assert_eq!(matrices.a.column_count(), 4); assert_eq!(matrices.m.rows()[0].positions(), &[0]); assert_eq!(matrices.m.rows()[1].positions(), &[1, 2]); - assert_eq!(matrices.a.rows()[0].entries(), &[(2, BigInt::from(2))]); - assert_eq!(matrices.b.rows()[0].entries(), &[(3, BigInt::from(1))]); + assert_eq!(matrices.a.rows()[0].entries(), &[(2, R::from(2))]); + assert_eq!(matrices.b.rows()[0].entries(), &[(3, R::from(1))]); assert_eq!( matrices.c.rows()[0].entries(), - &[ - (1, BigInt::from(-1)), - (2, BigInt::from(1)), - (3, BigInt::from(1)), - ] + &[(1, R::from(-1)), (2, R::from(1)), (3, R::from(1)),] ); let satisfying = Witgen::with_inputs(&[true, false]); // This circuit has no hints, so its packed witness consists of inputs. assert!(matrices.is_satisfied(satisfying.witness())); - matrices.c.rows[0].entries.push((0, BigInt::one())); + matrices.c.rows[0].entries.push((0, R::one())); assert_eq!( matrices.check_witness(satisfying.witness()), Err(SatisfactionError::Constraint { row: 0 }) @@ -925,10 +919,10 @@ mod tests { #[test] fn coefficients_are_arbitrary_precision_integers() { - let huge = BigInt::one() << 512_usize; + let huge = R::one() << 512_usize; let mut generator = ConstraintGenerator::new(0); generator.assert_r1c::<1>( - LinearCombination::from(BigInt::one()), + LinearCombination::from(R::one()), LinearCombination::from(huge.clone()), LinearCombination::from(huge.clone()), ); @@ -942,10 +936,7 @@ mod tests { #[test] fn sparse_constructors_reject_unsorted_and_out_of_bounds_columns() { assert_eq!( - SparseMatrix::::try_from_rows( - 3, - vec![vec![(1, BigInt::one()), (1, BigInt::one())]], - ), + SparseMatrix::::try_from_rows(3, vec![vec![(1, R::one()), (1, R::one())]],), Err(SparseMatrixError::ColumnsNotStrictlyIncreasing { row: 0, previous: 1, @@ -966,9 +957,9 @@ mod tests { fn malformed_constraint_shape_returns_an_error_instead_of_panicking() { let matrices = ConstraintMatrices { m: SparseBoolMatrix::try_from_rows(1, vec![vec![0]]).unwrap(), - a: SparseMatrix::try_from_rows(1, vec![vec![(0, BigInt::one())]]).unwrap(), + a: SparseMatrix::try_from_rows(1, vec![vec![(0, R::one())]]).unwrap(), b: SparseMatrix::try_from_rows(1, Vec::new()).unwrap(), - c: SparseMatrix::try_from_rows(1, vec![vec![(0, BigInt::one())]]).unwrap(), + c: SparseMatrix::try_from_rows(1, vec![vec![(0, R::one())]]).unwrap(), }; assert_eq!( @@ -982,9 +973,10 @@ mod tests { #[test] fn linear_combination_terms_stay_sorted_and_merge_shared_witnesses() { let term = |index: usize, coefficient: i64| { - LinearCombination::witness(index) * BigInt::from(coefficient) + LinearCombination::witness(index) * R::from(coefficient) }; - let exact = |value: &LinearCombination| value.witnesses.capacity() == value.witnesses.len(); + let exact = + |value: &LinearCombination<_>| value.witnesses.capacity() == value.witnesses.len(); // Disjoint ranges append on either side without spare capacity. let ascending = term(1, 1) + term(3, 1); @@ -1003,16 +995,16 @@ mod tests { let cancelled = (term(1, 1) + term(2, 1)) - term(1, 1); assert_eq!(cancelled.witnesses(), terms(&[(2, 1)])); assert!((term(1, 2) - term(1, 2)).is_zero()); - assert!((term(5, 7) * BigInt::zero()).is_zero()); + assert!((term(5, 7) * R::zero()).is_zero()); - let scaled = -(term(1, 2) + LinearCombination::from(BigInt::from(3))) * BigInt::from(5); - assert_eq!(*scaled.constant(), BigInt::from(-15)); + let scaled = -(term(1, 2) + LinearCombination::from(R::from(3))) * R::from(5); + assert_eq!(*scaled.constant(), R::from(-15)); assert_eq!(scaled.witnesses(), terms(&[(1, -10)])); } #[test] fn bitz_unsigned_lifts_bits_with_powers_of_two_into_exact_rows() { - let mut generator = ConstraintGenerator::new(3); + let mut generator = ConstraintGenerator::::new(3); let bits = ScalarBits(generator.inputs::<3>()); let (full, low) = generator.bitz_unsigned::<1, 3, 1, 2>(&bits); diff --git a/crates/circuit/src/ecdsa_sha256.rs b/crates/circuit/src/ecdsa_sha256.rs index b298d283..1c311502 100644 --- a/crates/circuit/src/ecdsa_sha256.rs +++ b/crates/circuit/src/ecdsa_sha256.rs @@ -53,30 +53,32 @@ pub fn verify_2kb_message_circuit( #[cfg(test)] mod tests { - use num_bigint::{BigInt, BigUint, Sign}; - use num_traits::{One, Zero}; - use super::*; use crate::matrix_products::StoredInteger; use crate::stats::{Dummy, LeanStats, Stats}; use crate::witgen::ProductWitgen; + use num_traits::{One, Zero}; + + type S = num_bigint::BigUint; + type R = num_bigint::BigInt; - fn from_hex(value: &[u8]) -> BigUint { - BigUint::parse_bytes(value, 16).unwrap() + fn from_hex(value: &[u8]) -> S { + S::parse_bytes(value, 16).unwrap() } - fn inverse(value: &BigUint, modulus: &BigUint) -> BigUint { - let mut t = BigInt::zero(); - let mut new_t = BigInt::one(); - let mut r = BigInt::from(modulus.clone()); - let mut new_r = BigInt::from(value.clone()); + fn inverse(value: &S, modulus: &S) -> S { + use num_bigint::Sign; + let mut t = R::zero(); + let mut new_t = R::one(); + let mut r = R::from(modulus.clone()); + let mut new_r = R::from(value.clone()); while !new_r.is_zero() { let quotient = &r / &new_r; (t, new_t) = (new_t.clone(), t - "ient * new_t); (r, new_r) = (new_r.clone(), r - quotient * new_r); } - assert_eq!(r, BigInt::one()); - let modulus = BigInt::from(modulus.clone()); + assert_eq!(r, R::one()); + let modulus = R::from(modulus.clone()); let mut t = t % &modulus; if t.sign() == Sign::Minus { t += modulus; @@ -112,13 +114,13 @@ mod tests { bits.try_into().unwrap() } - fn stored_bigint(value: &StoredInteger) -> BigInt { + fn stored_bigint(value: &StoredInteger) -> R { let bytes = value .words() .iter() .flat_map(|word| word.to_le_bytes()) .collect::>(); - BigInt::from_signed_bytes_le(&bytes) + R::from_signed_bytes_le(&bytes) } #[test] diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index aa4ab957..2eab0653 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -546,6 +546,28 @@ pub trait Circuit { ) -> Self::Z; } +pub trait Bits { + /// Determines the fewest bits necessary to express this value + fn bits(&self) -> u64; +} + +pub trait IntoWords { + fn into_words(self) -> [u64; LIMBS]; +} + +impl Bits for num_bigint::BigUint { + fn bits(&self) -> u64 { + num_bigint::BigUint::bits(self) + } +} + +impl IntoWords for num_bigint::BigUint { + fn into_words(self) -> [u64; LIMBS] { + let digits = self.to_u64_digits(); + array::from_fn(|index| digits.get(index).copied().unwrap_or(0)) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/circuit/src/matrix_products.rs b/crates/circuit/src/matrix_products.rs index 9942c9c2..f6043c50 100644 --- a/crates/circuit/src/matrix_products.rs +++ b/crates/circuit/src/matrix_products.rs @@ -6,52 +6,46 @@ //! reduces those vectors modulo a runtime modulus. Large batches use Rayon; //! small batches stay sequential to avoid scheduling overhead. -use std::array; -use std::cmp::Ordering; - -use num_bigint::{BigInt, BigUint, Sign}; +use crate::witgen::Z as Integer; +use crate::{Bits, IntoWords}; use num_traits::{One, Zero}; use rayon::prelude::*; +use std::cmp::Ordering; -use crate::witgen::Z as Integer; +// pub trait RuntimeModulus: Sized { +// type Semiring; +// +// fn new(modulus: Self::Semiring) -> Result; +// +// fn modulus(&self) -> Self::Semiring; +// } /// A runtime modulus with a compile-time limb count. #[derive(Debug, Eq, PartialEq)] pub struct RuntimeModulus { - modulus: [u64; PRIME_LIMBS], + /// The runtime modulus in canonical little-endian limbs. + words: [u64; PRIME_LIMBS], } impl RuntimeModulus { /// Validates and stores a runtime modulus. - pub fn new(modulus: BigUint) -> Result { + pub fn new(modulus: S) -> Result { if PRIME_LIMBS == 0 { return Err("a runtime field needs at least one limb"); } - if modulus <= BigUint::one() { + if modulus <= S::one() { return Err("the modulus must be greater than one"); } if modulus.bits() > (PRIME_LIMBS as u64) * 64 { return Err("the modulus does not fit the selected limb count"); } Ok(Self { - modulus: biguint_words(&modulus), + words: modulus.into_words(), }) } - /// The runtime modulus in canonical little-endian limbs. pub const fn modulus_words(&self) -> &[u64; PRIME_LIMBS] { - &self.modulus - } - - /// Converts the modulus back to a `BigUint`. - pub fn modulus(&self) -> BigUint { - BigUint::from_bytes_le( - &self - .modulus - .iter() - .flat_map(|word| word.to_le_bytes()) - .collect::>(), - ) + &self.words } pub(crate) fn reduce(&self, value: &StoredInteger) -> [u64; PRIME_LIMBS] { @@ -59,8 +53,8 @@ impl RuntimeModulus { return [0; PRIME_LIMBS]; } - if PRIME_LIMBS == 2 && self.modulus[1] >> 63 != 0 { - let modulus = [self.modulus[0], self.modulus[1]]; + if PRIME_LIMBS == 2 && self.words[1] >> 63 != 0 { + let modulus = [self.words[0], self.words[1]]; let reduced = reduce_normalized_2(&value.words, modulus); let reduced = if value.is_negative() { // The stored words are x + 2^(64k), where x is negative and @@ -94,15 +88,15 @@ impl RuntimeModulus { one[0] = 1; for &word in words.iter().rev() { for bit in (0..64).rev() { - reduced = add_mod_words(reduced, &reduced, &self.modulus); + reduced = add_mod_words(reduced, &reduced, &self.words); if word >> bit & 1 == 1 { - reduced = add_mod_words(reduced, &one, &self.modulus); + reduced = add_mod_words(reduced, &one, &self.words); } } } if negative && reduced.iter().any(|word| *word != 0) { - subtract_words(self.modulus, &reduced).0 + subtract_words(self.words, &reduced).0 } else { reduced } @@ -200,11 +194,6 @@ fn twos_complement_magnitude(words: &[u64]) -> Vec { magnitude } -fn biguint_words(value: &BigUint) -> [u64; LIMBS] { - let digits = value.to_u64_digits(); - array::from_fn(|index| digits.get(index).copied().unwrap_or(0)) -} - fn compare_words(left: &[u64; LIMBS], right: &[u64; LIMBS]) -> Ordering { for index in (0..LIMBS).rev() { match left[index].cmp(&right[index]) { @@ -301,8 +290,26 @@ pub struct StoredInteger { } impl StoredInteger { + /// Normalized little-endian two's-complement words; zero is empty. + pub fn words(&self) -> &[u64] { + &self.words + } + + /// Whether this integer is exactly zero. + pub fn is_zero(&self) -> bool { + self.words.is_empty() + } + + /// Whether this integer is negative. + pub fn is_negative(&self) -> bool { + self.words.last().is_some_and(|word| word >> 63 == 1) + } +} + +impl From<&num_bigint::BigInt> for StoredInteger { /// Stores an arbitrary-precision integer as normalized two's-complement limbs. - pub(crate) fn from_bigint(value: &BigInt) -> Self { + fn from(value: &num_bigint::BigInt) -> Self { + use num_bigint::{BigUint, Sign}; if value.is_zero() { return Self { words: Box::default(), @@ -340,9 +347,11 @@ impl StoredInteger { words: magnitude.into_boxed_slice(), } } +} +impl From<&Integer> for StoredInteger { /// Stores a gadget-local fixed integer without changing its value. - pub fn from_fixed(value: Integer) -> Self { + fn from(value: &Integer) -> Self { if value.is_zero() { return Self { words: Box::default(), @@ -364,21 +373,6 @@ impl StoredInteger { words: words[..len].into(), } } - - /// Normalized little-endian two's-complement words; zero is empty. - pub fn words(&self) -> &[u64] { - &self.words - } - - /// Whether this integer is exactly zero. - pub fn is_zero(&self) -> bool { - self.words.is_empty() - } - - /// Whether this integer is negative. - pub fn is_negative(&self) -> bool { - self.words.last().is_some_and(|word| word >> 63 == 1) - } } /// Dense exact integer `A(Mw)`, `B(Mw)`, and `C(Mw)` vectors. @@ -397,9 +391,9 @@ impl IntegerProducts { b: Integer, c: Integer, ) { - self.a_mw.push(StoredInteger::from_fixed(a)); - self.b_mw.push(StoredInteger::from_fixed(b)); - self.c_mw.push(StoredInteger::from_fixed(c)); + self.a_mw.push(StoredInteger::from(&a)); + self.b_mw.push(StoredInteger::from(&b)); + self.c_mw.push(StoredInteger::from(&c)); } /// Reduces every materialized element modulo `modulus`. @@ -437,24 +431,25 @@ fn reduce_vector( #[cfg(test)] mod tests { - use num_bigint::BigInt; - use num_traits::Signed; - use super::*; use crate::constraints::{ConstraintGenerator, ConstraintMatrices, SparseRow}; use crate::sha256::{SHA256_2KB_MESSAGE_BITS, SHA256_2KB_WITNESS_BITS, sha256_2kb_circuit}; use crate::witgen::{PackedWitness, ProductWitgen}; + use num_traits::Signed; + + type S = num_bigint::BigUint; + type R = num_bigint::BigInt; - fn stored_bigint(value: &StoredInteger) -> BigInt { + fn stored_ring(value: &StoredInteger) -> R { let bytes = value .words() .iter() .flat_map(|word| word.to_le_bytes()) .collect::>(); - BigInt::from_signed_bytes_le(&bytes) + R::from_signed_bytes_le(&bytes) } - fn direct_row(row: &SparseRow, integer_witness: &PackedWitness) -> BigInt { + fn direct_row(row: &SparseRow, integer_witness: &PackedWitness) -> R { row.entries() .iter() .filter(|(column, _)| integer_witness.bit(*column)) @@ -462,21 +457,21 @@ mod tests { .sum() } - fn reduce_bigint(value: BigInt, modulus: &BigUint) -> [u64; P] { - let modulus = BigInt::from(modulus.clone()); + fn reduce_ring(value: R, modulus: &S) -> [u64; P] { + let modulus = R::from(modulus.clone()); let mut reduced = value % &modulus; if reduced.is_negative() { reduced += &modulus; } - biguint_words(&reduced.to_biguint().unwrap()) + reduced.to_biguint().unwrap().into_words() } fn assert_products_match_direct( exact: &IntegerProducts, products: &MatrixProducts

, - matrices: &ConstraintMatrices, + matrices: &ConstraintMatrices, integer_witness: &PackedWitness, - modulus: &BigUint, + modulus: &S, ) { assert_eq!(exact.a_mw.len(), matrices.a.row_count()); assert_eq!(exact.b_mw.len(), matrices.b.row_count()); @@ -490,33 +485,33 @@ mod tests { let expected_c = direct_row(&matrices.c.rows()[row], integer_witness); assert_eq!( - stored_bigint(&exact.a_mw[row]), + stored_ring(&exact.a_mw[row]), expected_a, "exact A(Mw) differs at row {row}" ); assert_eq!( - stored_bigint(&exact.b_mw[row]), + stored_ring(&exact.b_mw[row]), expected_b, "exact B(Mw) differs at row {row}" ); assert_eq!( - stored_bigint(&exact.c_mw[row]), + stored_ring(&exact.c_mw[row]), expected_c, "exact C(Mw) differs at row {row}" ); assert_eq!( products.a_mw.get(row), - reduce_bigint(expected_a, modulus), + reduce_ring(expected_a, modulus), "A(Mw) differs at row {row}" ); assert_eq!( products.b_mw.get(row), - reduce_bigint(expected_b, modulus), + reduce_ring(expected_b, modulus), "B(Mw) differs at row {row}" ); assert_eq!( products.c_mw.get(row), - reduce_bigint(expected_c, modulus), + reduce_ring(expected_c, modulus), "C(Mw) differs at row {row}" ); } @@ -524,42 +519,42 @@ mod tests { #[test] fn signed_stored_integers_reduce_correctly() { - let modulus = (BigUint::one() << 128_usize) - BigUint::from(159_u64); + let modulus = (S::one() << 128_usize) - S::from(159_u64); let runtime_modulus = RuntimeModulus::<2>::new(modulus.clone()).unwrap(); for value in [0_i128, 1, 7, -7, i128::from(i64::MIN)] { - let stored = StoredInteger::from_fixed(Integer::<2>::from(value)); - let mut expected = BigInt::from(value) % BigInt::from(modulus.clone()); + let stored = StoredInteger::from(&Integer::<2>::from(value)); + let mut expected = R::from(value) % R::from(modulus.clone()); if expected.is_negative() { - expected += BigInt::from(modulus.clone()); + expected += R::from(modulus.clone()); } assert_eq!( runtime_modulus.reduce(&stored), - biguint_words::<2>(&expected.to_biguint().unwrap()) + expected.to_biguint().unwrap().into_words() ); } } #[test] fn arbitrary_bigints_round_trip_through_stored_integers() { - let boundary = BigInt::one() << 128_usize; - let wide = &boundary + BigInt::from(7_u8); + let boundary = R::one() << 128_usize; + let wide = &boundary + R::from(7_u8); for value in [ - BigInt::zero(), - BigInt::one(), - -BigInt::one(), - BigInt::one() << 63_usize, - -(BigInt::one() << 63_usize), + R::zero(), + R::one(), + -R::one(), + R::one() << 63_usize, + -(R::one() << 63_usize), wide.clone(), -wide, ] { - assert_eq!(stored_bigint(&StoredInteger::from_bigint(&value)), value); + assert_eq!(stored_ring(&StoredInteger::from(&value)), value); } } #[test] fn normalized_two_limb_reduction_matches_bigint_for_wide_values() { - let modulus = (BigUint::one() << 128_usize) - BigUint::from(159_u64); + let modulus = (S::one() << 128_usize) - S::from(159_u64); let runtime_modulus = RuntimeModulus::<2>::new(modulus.clone()).unwrap(); let mut state = 0x4d59_5df4_d0f3_3173_u64; for index in 0..1_000 { @@ -578,21 +573,14 @@ mod tests { if index % 2 != 0 { integer = -integer; } - let stored = StoredInteger::from_fixed(integer); + let stored = StoredInteger::from(&integer); assert_eq!( runtime_modulus.reduce(&stored), - reduce_bigint(stored_bigint(&stored), &modulus) + reduce_ring(stored_ring(&stored), &modulus) ); } } - #[test] - fn modulus_round_trips() { - let modulus = (BigUint::one() << 128_usize) - BigUint::from(159_u64); - let runtime_modulus = RuntimeModulus::<2>::new(modulus.clone()).unwrap(); - assert_eq!(runtime_modulus.modulus(), modulus); - } - #[test] fn sha256_recorded_products_match_direct_matrices() { let message: Box<[bool]> = (0..SHA256_2KB_MESSAGE_BITS) @@ -612,7 +600,7 @@ mod tests { let _ = sha256_2kb_circuit(&mut generator, &symbolic_inputs); let matrices = generator.into_matrices(); - let modulus = (BigUint::one() << 128_usize) - BigUint::from(159_u64); + let modulus = (S::one() << 128_usize) - S::from(159_u64); let runtime_modulus = RuntimeModulus::<2>::new(modulus.clone()).unwrap(); let products = witgen.products().reduce_parallel(&runtime_modulus); assert_products_match_direct( diff --git a/crates/circuit/src/matrix_sparse.rs b/crates/circuit/src/matrix_sparse.rs index e543da1a..6912eda4 100644 --- a/crates/circuit/src/matrix_sparse.rs +++ b/crates/circuit/src/matrix_sparse.rs @@ -9,13 +9,13 @@ use std::error::Error; use std::fmt::{self, Display}; use std::mem::{size_of, size_of_val}; -use crypto_bigint::modular::{FixedMontyForm, FixedMontyParams}; -use crypto_bigint::{Odd, U128}; -use rayon::prelude::*; - use crate::constraints::{ConstraintMatrices, SparseMatrix}; use crate::matrix_products::{RuntimeModulus, StoredInteger}; use crate::matrix_wengert::{add_mod_words, montgomery_mul_2, neg_mod_words}; +use common::BitzIntRing; +use crypto_bigint::modular::{FixedMontyForm, FixedMontyParams}; +use crypto_bigint::{Odd, U128}; +use rayon::prelude::*; const PARALLEL_NNZ_THRESHOLD: usize = 1 << 15; const PARALLEL_VECTOR_THRESHOLD: usize = 1 << 14; @@ -74,7 +74,11 @@ pub struct MaterializedAbc { impl MaterializedAbc { /// Transposes and stores the integer matrices without choosing a modulus. - pub fn from_matrices(matrices: &ConstraintMatrices) -> Self { + pub fn from_matrices(matrices: &ConstraintMatrices) -> Self + where + R: BitzIntRing, + StoredInteger: for<'a> From<&'a R>, + { let row_count = matrices.a.row_count(); let column_count = matrices.a.column_count(); assert_eq!(matrices.b.row_count(), row_count); @@ -122,11 +126,7 @@ impl MaterializedAbc { for (matrix, kind) in sources { for (row, sparse_row) in matrix.rows().iter().enumerate() { for (column, coefficient) in sparse_row.entries() { - columns[*column].push(IntegerEntry::new( - row, - kind, - StoredInteger::from_bigint(coefficient), - )); + columns[*column].push(IntegerEntry::new(row, kind, coefficient.into())); } } } @@ -211,7 +211,7 @@ impl MaterializedAbc { } } -fn nonzero_count(matrix: &SparseMatrix) -> usize { +fn nonzero_count(matrix: &SparseMatrix) -> usize { matrix.rows().iter().map(|row| row.entries().len()).sum() } @@ -355,13 +355,14 @@ impl Error for SparseAbcApplyError {} #[cfg(test)] mod tests { - use num_bigint::BigUint; - use num_traits::One; - use super::*; use crate::Circuit; use crate::constraints::ConstraintGenerator; use crate::matrix_wengert::WengertGenerator; + use num_traits::One; + + type S = num_bigint::BigUint; + type R = num_bigint::BigInt; fn example_circuit(circuit: &mut CS, inputs: &[CS::Bool; 3]) { let a = circuit.bitz::<2>(inputs[0].clone()); @@ -382,7 +383,7 @@ mod tests { #[test] fn sparse_integer_runner_matches_wengert_tape() { - let mut matrix_generator = ConstraintGenerator::new(3); + let mut matrix_generator = ConstraintGenerator::::new(3); let matrix_inputs = matrix_generator.inputs(); example_circuit(&mut matrix_generator, &matrix_inputs); let matrices = matrix_generator.into_matrices(); @@ -401,9 +402,8 @@ mod tests { example_circuit(&mut tape_generator, &tape_inputs); let tape = tape_generator.finish(); - let modulus = - RuntimeModulus::<2>::new((BigUint::one() << 128_usize) - BigUint::from(159_u64)) - .unwrap(); + let two = S::one() + S::one(); + let modulus = RuntimeModulus::<2>::new(two.pow(128) - S::from(159_u64)).unwrap(); let challenges = [[23, 0], [29, 0]]; let x = [17, 0]; let mut sparse_evaluator = sparse.prepare(&modulus).unwrap(); diff --git a/crates/circuit/src/matrix_transpose.rs b/crates/circuit/src/matrix_transpose.rs index f3111708..a0030591 100644 --- a/crates/circuit/src/matrix_transpose.rs +++ b/crates/circuit/src/matrix_transpose.rs @@ -527,9 +527,12 @@ mod tests { use crate::sha256::{COMPRESSION_HINT_BITS, COMPRESSION_INPUT_BITS, compression_circuit}; use crate::witgen::Witgen; use crate::{BoolRepresentation, BoolWitness, Circuit}; + use num_bigint::BigInt; use super::*; + type R = BigInt; + fn example_circuit(circuit: &mut CS, inputs: &[CS::Bool; 3]) { let xy = circuit.xor(inputs[0].clone(), inputs[1].clone()); let not_xy = circuit.xor(xy.clone(), CS::Bool::from(true)); @@ -650,7 +653,7 @@ mod tests { example_circuit(&mut materializer, &inputs); let transpose = materializer.finish(); - let mut generator = ConstraintGenerator::new(3); + let mut generator = ConstraintGenerator::::new(3); let symbolic = generator.inputs(); example_circuit(&mut generator, &symbolic); let matrices = generator.into_matrices(); diff --git a/crates/circuit/src/matrix_wengert.rs b/crates/circuit/src/matrix_wengert.rs index 0713f62c..64d79561 100644 --- a/crates/circuit/src/matrix_wengert.rs +++ b/crates/circuit/src/matrix_wengert.rs @@ -50,10 +50,10 @@ struct RawTerm { } impl RawTerm { - fn new(node: u32, coefficient: Z) -> Self { + fn new(node: u32, coefficient: &Z) -> Self { Self { node, - coefficient: StoredInteger::from_fixed(coefficient), + coefficient: StoredInteger::from(coefficient), } } } @@ -175,7 +175,7 @@ impl WengertValue { ValueLocation::Constant => 0, ValueLocation::Node { node, .. } => node, }; - RawTerm::new(node, self.coefficient) + RawTerm::new(node, &self.coefficient) } fn same_recorder(left: &Arc, right: &Arc) { @@ -465,8 +465,8 @@ impl WengertTape { } let mut coefficient_map = HashMap::new(); - let one = StoredInteger::from_fixed(Z::<1>::one()); - let negative_one = StoredInteger::from_fixed(-Z::<1>::one()); + let one = StoredInteger::from(&Z::<1>::one()); + let negative_one = StoredInteger::from(&-Z::<1>::one()); coefficient_map.insert(one.clone(), 0_u32); coefficient_map.insert(negative_one.clone(), 1_u32); let mut coefficients = vec![one, negative_one]; @@ -1362,12 +1362,13 @@ impl Circuit for WengertGenerator { #[cfg(test)] mod tests { - use num_bigint::{BigInt, BigUint}; - use num_traits::Signed; - use super::*; use crate::constraints::{ConstraintGenerator, ConstraintMatrices}; use crate::sha256::{COMPRESSION_INPUT_BITS, compression_circuit}; + use num_traits::Signed; + + type S = num_bigint::BigUint; + type R = num_bigint::BigInt; fn example_circuit(circuit: &mut CS, inputs: &[CS::Bool; 3]) { let a = circuit.bitz::<2>(inputs[0].clone()); @@ -1396,18 +1397,17 @@ mod tests { } fn direct_product( - matrices: &ConstraintMatrices, + matrices: &ConstraintMatrices, challenges: &[[u64; 2]], x: [u64; 2], - modulus: &BigUint, + modulus: &S, ) -> Vec<[u64; 2]> { - let modulus_int = BigInt::from(modulus.clone()); - let as_bigint = |words: [u64; 2]| { - BigInt::from(BigUint::from(words[0]) + (BigUint::from(words[1]) << 64_usize)) - }; + let modulus_int = R::from(modulus.clone()); + let as_bigint = + |words: [u64; 2]| R::from(S::from(words[0]) + (S::from(words[1]) << 64_usize)); let x = as_bigint(x); let x_squared = &x * &x; - let mut output = vec![BigInt::zero(); matrices.a.column_count()]; + let mut output = vec![R::zero(); matrices.a.column_count()]; for (row, challenge) in challenges.iter().enumerate() { let challenge = as_bigint(*challenge); for (column, coefficient) in matrices.a.rows()[row].entries() { @@ -1447,8 +1447,8 @@ mod tests { let x = [17, 0]; for modulus in [ - (BigUint::one() << 127_usize) - BigUint::one(), - (BigUint::one() << 128_usize) - BigUint::from(159_u64), + (S::one() << 127_usize) - S::one(), + (S::one() << 128_usize) - S::from(159_u64), ] { let runtime = RuntimeModulus::<2>::new(modulus.clone()).unwrap(); assert_eq!( @@ -1461,9 +1461,7 @@ mod tests { #[test] fn parallel_and_sequential_reverse_batches_agree() { let tape = build_tape(); - let modulus = - RuntimeModulus::<2>::new((BigUint::one() << 128_usize) - BigUint::from(159_u64)) - .unwrap(); + let modulus = RuntimeModulus::<2>::new((S::one() << 128_usize) - S::from(159_u64)).unwrap(); let challenges = [[0x1234_5678_9abc_def0, 7], [0x0fed_cba9_8765_4321, 11]]; let mut sequential = Vec::new(); let mut parallel = Vec::new(); @@ -1477,9 +1475,7 @@ mod tests { #[test] fn prepared_evaluator_reuses_storage_and_matches_one_shot_apply() { let tape = build_tape(); - let modulus = - RuntimeModulus::<2>::new((BigUint::one() << 128_usize) - BigUint::from(159_u64)) - .unwrap(); + let modulus = RuntimeModulus::<2>::new((S::one() << 128_usize) - S::from(159_u64)).unwrap(); let challenges = [[0x1234_5678_9abc_def0, 7], [0x0fed_cba9_8765_4321, 11]]; let x = [31, 3]; let expected = tape.apply(&challenges, x, &modulus).unwrap(); @@ -1554,7 +1550,7 @@ mod tests { .map(|row| [(17 * row + 3) as u64, (5 * row + 1) as u64]) .collect(); let x = [0x1234_5678_9abc_def0, 0x0123_4567_89ab_cdef]; - let prime = (BigUint::one() << 128_usize) - BigUint::from(159_u64); + let prime = (S::one() << 128_usize) - S::from(159_u64); let modulus = RuntimeModulus::<2>::new(prime.clone()).unwrap(); assert_eq!(tape.row_count(), matrices.a.row_count()); @@ -1570,7 +1566,7 @@ mod tests { #[test] fn rejects_wrong_challenge_length_and_even_modulus() { let tape = build_tape(); - let odd = RuntimeModulus::<2>::new(BigUint::from(101_u64)).unwrap(); + let odd = RuntimeModulus::<2>::new(S::from(101_u64)).unwrap(); assert_eq!( tape.apply(&[[1, 0]], [2, 0], &odd), Err(WengertApplyError::ChallengeLength { @@ -1578,7 +1574,7 @@ mod tests { actual: 1, }) ); - let even = RuntimeModulus::<2>::new(BigUint::from(100_u64)).unwrap(); + let even = RuntimeModulus::<2>::new(S::from(100_u64)).unwrap(); assert_eq!( tape.apply(&[[1, 0], [2, 0]], [3, 0], &even), Err(WengertApplyError::EvenModulus) @@ -1594,7 +1590,7 @@ mod tests { let _dead = unused.clone() + unused; generator.assert_r1c(used, WengertValue::zero(), WengertValue::zero()); let tape = generator.finish(); - let modulus = RuntimeModulus::<2>::new(BigUint::from(101_u64)).unwrap(); + let modulus = RuntimeModulus::<2>::new(S::from(101_u64)).unwrap(); assert_eq!( tape.apply(&[[7, 0]], [3, 0], &modulus).unwrap(), [[0, 0], [7, 0], [0, 0]] diff --git a/crates/circuit/src/p256.rs b/crates/circuit/src/p256.rs index 3246c085..5174f47a 100644 --- a/crates/circuit/src/p256.rs +++ b/crates/circuit/src/p256.rs @@ -6,10 +6,9 @@ use std::array; use std::cmp::Ordering; -use std::sync::OnceLock; +use std::sync::{LazyLock, OnceLock}; use crypto_bigint::{Odd, U256 as CryptoU256}; -use num_bigint::BigUint; use num_traits::{One, Zero}; use crate::{ @@ -38,61 +37,30 @@ const WORD_LIMBS: usize = 4; type P256Z = ::Z; type P256Coefficient = ::Coefficient; +type S = num_bigint::BigUint; -fn base_modulus() -> &'static BigUint { - static VALUE: OnceLock = OnceLock::new(); - VALUE.get_or_init(|| { - BigUint::parse_bytes( - b"ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", - 16, - ) - .unwrap() - }) +fn parse_hex(hex: &[u8]) -> S { + S::parse_bytes(hex, 16).unwrap() } +static BASE_MODULUS: LazyLock = LazyLock::new(|| { + parse_hex(b"ffffffff00000001000000000000000000000000ffffffffffffffffffffffff") +}); -fn scalar_modulus() -> &'static BigUint { - static VALUE: OnceLock = OnceLock::new(); - VALUE.get_or_init(|| { - BigUint::parse_bytes( - b"ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", - 16, - ) - .unwrap() - }) -} +static SCALAR_MODULUS: LazyLock = LazyLock::new(|| { + parse_hex(b"ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551") +}); -fn curve_b() -> &'static BigUint { - static VALUE: OnceLock = OnceLock::new(); - VALUE.get_or_init(|| { - BigUint::parse_bytes( - b"5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", - 16, - ) - .unwrap() - }) -} +static CURVE_B: LazyLock = LazyLock::new(|| { + parse_hex(b"5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b") +}); -fn generator_x() -> &'static BigUint { - static VALUE: OnceLock = OnceLock::new(); - VALUE.get_or_init(|| { - BigUint::parse_bytes( - b"6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", - 16, - ) - .unwrap() - }) -} +static GENERATOR_X: LazyLock = LazyLock::new(|| { + parse_hex(b"6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296") +}); -fn generator_y() -> &'static BigUint { - static VALUE: OnceLock = OnceLock::new(); - VALUE.get_or_init(|| { - BigUint::parse_bytes( - b"4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", - 16, - ) - .unwrap() - }) -} +static GENERATOR_Y: LazyLock = LazyLock::new(|| { + parse_hex(b"4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5") +}); #[derive(Clone, Copy)] enum Modulus { @@ -101,10 +69,10 @@ enum Modulus { } impl Modulus { - fn value(self) -> &'static BigUint { + fn value(self) -> &'static S { match self { - Self::Base => base_modulus(), - Self::Scalar => scalar_modulus(), + Self::Base => &BASE_MODULUS, + Self::Scalar => &SCALAR_MODULUS, } } @@ -162,7 +130,7 @@ impl Lc { Self { z: self.z - rhs.z } } - fn scale(self, coefficient: &BigUint) -> Self { + fn scale(self, coefficient: &S) -> Self { self.scale_coefficient(coefficient_for::(coefficient)) } @@ -231,7 +199,7 @@ impl Clone for Point { } } -fn coefficient_for(value: &BigUint) -> P256Coefficient { +fn coefficient_for(value: &S) -> P256Coefficient { let mut words = [0; P256_Z_LIMBS]; for (output, word) in words.iter_mut().zip(value.iter_u64_digits()) { *output = word; @@ -239,7 +207,7 @@ fn coefficient_for(value: &BigUint) -> P256Coefficient { CS::coefficient_from_le_words(&words) } -fn lc_constant(value: &BigUint) -> Lc { +fn lc_constant(value: &S) -> Lc { Lc { z: P256Z::::from(coefficient_for::(value)), } @@ -348,7 +316,7 @@ fn of_elem(value: &Elem) -> Rep { } } -fn rep_constant(value: &BigUint) -> Rep { +fn rep_constant(value: &S) -> Rep { Rep { value: lc_constant(value), bound: 2, @@ -684,24 +652,6 @@ struct U256([u64; 4]); impl U256 { const ZERO: Self = Self([0; 4]); - fn from_biguint(value: &BigUint) -> Option { - let digits = value.to_u64_digits(); - if digits.len() > 4 { - return None; - } - let mut words = [0; 4]; - words[..digits.len()].copy_from_slice(&digits); - Some(Self(words)) - } - - fn into_biguint(self) -> BigUint { - let digits = self - .0 - .into_iter() - .flat_map(|word| [word as u32, (word >> 32) as u32]); - BigUint::new(digits.collect()) - } - fn is_zero(self) -> bool { self == Self::ZERO } @@ -721,6 +671,29 @@ impl U256 { } } +impl<'a> TryFrom<&'a S> for U256 { + type Error = (); + + fn try_from(value: &'a S) -> Result { + let digits = value.to_u64_digits(); + if digits.len() > 4 { + return Err(()); + } + let mut words = [0; 4]; + words[..digits.len()].copy_from_slice(&digits); + Ok(Self(words)) + } +} + +impl From for S { + fn from(v: U256) -> Self { + let digits = + v.0.into_iter() + .flat_map(|word| [word as u32, (word >> 32) as u32]); + Self::new(digits.collect()) + } +} + /// A little-endian unsigned integer for P-256 hint arithmetic. Nine limbs /// match the circuit's signed Z capacity; eighteen hold a full product. #[derive(Clone, Copy)] @@ -932,7 +905,7 @@ fn modular_inverse_u256(u: U256, v: U256) -> Option { inverse.map(|inverse| U256(inverse.to_words())) } -fn modular_inverse(value: &BigUint, modulus: &BigUint) -> Option { +fn modular_inverse(value: &S, modulus: &S) -> Option { let reduced; let value = if value < modulus { value @@ -940,8 +913,7 @@ fn modular_inverse(value: &BigUint, modulus: &BigUint) -> Option { reduced = value % modulus; &reduced }; - modular_inverse_u256(U256::from_biguint(value)?, U256::from_biguint(modulus)?) - .map(U256::into_biguint) + modular_inverse_u256(U256::try_from(value).ok()?, U256::try_from(modulus).ok()?).map(S::from) } fn point_from_elems(x: &Elem, y: &Elem) -> Point { @@ -1314,20 +1286,16 @@ fn lookup_point(circuit: &mut CS, digit: Lc, table: &[Point } } -fn generator_table() -> &'static Vec<(BigUint, BigUint)> { - static TABLE: OnceLock> = OnceLock::new(); +fn generator_table() -> &'static Vec<(S, S)> { + static TABLE: OnceLock> = OnceLock::new(); TABLE.get_or_init(|| { - let modulus = base_modulus(); + let modulus = &*BASE_MODULUS; let mut table = Vec::with_capacity(256); - table.push((BigUint::zero(), BigUint::zero())); - let mut point = (generator_x().clone(), generator_y().clone()); + table.push((S::zero(), S::zero())); + let mut point = (GENERATOR_X.clone(), GENERATOR_Y.clone()); table.push(point.clone()); for _ in 2..256 { - point = affine_add( - &point, - &(generator_x().clone(), generator_y().clone()), - modulus, - ); + point = affine_add(&point, &(GENERATOR_X.clone(), GENERATOR_Y.clone()), modulus); table.push(point.clone()); } table @@ -1341,18 +1309,14 @@ pub fn prepare() { let _ = generator_table(); } -fn affine_add( - left: &(BigUint, BigUint), - right: &(BigUint, BigUint), - modulus: &BigUint, -) -> (BigUint, BigUint) { +fn affine_add(left: &(S, S), right: &(S, S), modulus: &S) -> (S, S) { let numerator = if left == right { - (BigUint::from(3_u64) * &left.0 * &left.0 + modulus - BigUint::from(3_u64)) % modulus + (S::from(3_u64) * &left.0 * &left.0 + modulus - S::from(3_u64)) % modulus } else { (&right.1 + modulus - &left.1) % modulus }; let denominator = if left == right { - (BigUint::from(2_u64) * &left.1) % modulus + (S::from(2_u64) * &left.1) % modulus } else { (&right.0 + modulus - &left.0) % modulus }; @@ -1481,7 +1445,7 @@ fn assert_on_curve(circuit: &mut CS, x: &Elem, y: &Elem) { let x3 = lazy_mul(circuit, Modulus::Base, x2, x.clone()); let rhs = rep_sub( Modulus::Base, - rep_add(x3, rep_constant(curve_b())), + rep_add(x3, rep_constant(&CURVE_B)), rep_scale(3, x), ); lazy_assert_mul_eq(circuit, Modulus::Base, y.clone(), y, rhs); @@ -1569,8 +1533,8 @@ mod tests { BigInt::from_signed_bytes_le(&bytes) } - fn words_biguint(words: &[u64]) -> BigUint { - BigUint::new( + fn words_biguint(words: &[u64]) -> S { + S::new( words .iter() .flat_map(|word| [*word as u32, (*word >> 32) as u32]) @@ -1579,16 +1543,16 @@ mod tests { } fn valid_input() -> Box<[bool; VERIFY_DIGEST_INPUT_BITS]> { - let r = generator_x().clone(); - let s = &r + BigUint::one(); + let r = GENERATOR_X.clone(); + let s = &r + S::one(); let values = [ - BigUint::one(), - generator_x().clone(), - generator_y().clone(), + S::one(), + GENERATOR_X.clone(), + GENERATOR_Y.clone(), r.clone(), s.clone(), - modular_inverse(&r, scalar_modulus()).unwrap(), - modular_inverse(&s, scalar_modulus()).unwrap(), + modular_inverse(&r, &SCALAR_MODULUS).unwrap(), + modular_inverse(&s, &SCALAR_MODULUS).unwrap(), ]; let bits: Box<[bool]> = (0..VERIFY_DIGEST_INPUT_BITS) .map(|index| values[index / WIDTH].bit((index % WIDTH) as u64)) @@ -1628,7 +1592,7 @@ mod tests { let expected_quotient = &value / modulus.value(); let expected_remainder = value % modulus.value(); assert_eq!(words_biguint("ient.0), expected_quotient); - assert_eq!(remainder.into_biguint(), expected_remainder); + assert_eq!(S::from(remainder), expected_remainder); } } } diff --git a/crates/circuit/src/sha256.rs b/crates/circuit/src/sha256.rs index f270a9df..c1a13181 100644 --- a/crates/circuit/src/sha256.rs +++ b/crates/circuit/src/sha256.rs @@ -554,17 +554,16 @@ where #[cfg(test)] mod tests { - use std::iter::Sum; - use std::ops::{Add, AddAssign}; - - use num_bigint::BigInt; - use num_traits::Zero; - use super::*; use crate::HintError; use crate::constraints::{ConstraintGenerator, ConstraintMatrices}; use crate::stats::{Dummy, LeanStats, Stats}; use crate::witgen::{Witgen, Z}; + use num_traits::Zero; + use std::iter::Sum; + use std::ops::{Add, AddAssign}; + + type R = num_bigint::BigInt; #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct Bit(bool); @@ -687,7 +686,7 @@ mod tests { circuit.sign_extend_z::(small) } - fn assert_m_w_matches_witgen(matrices: &ConstraintMatrices, witgen: &Witgen) { + fn assert_m_w_matches_witgen(matrices: &ConstraintMatrices, witgen: &Witgen) { let from_m = matrices .integer_witness(witgen.witness()) .expect("Boolean witness should have the matrix width"); @@ -696,7 +695,7 @@ mod tests { for (row, value) in from_m.iter().enumerate() { assert_eq!( value, - &BigInt::from(recorded.bit(row)), + &R::from(recorded.bit(row)), "integer witness differs at M row {row}" ); } diff --git a/crates/circuit/src/witgen.rs b/crates/circuit/src/witgen.rs index 31e3e16b..2f04d25a 100644 --- a/crates/circuit/src/witgen.rs +++ b/crates/circuit/src/witgen.rs @@ -8,6 +8,8 @@ use num_traits::{One, Zero}; use crate::matrix_products::IntegerProducts; use crate::{BoolWitness, Circuit, HintResult, PackedBits, WitnessContext}; +// TODO(alex): Make into a proper ring + /// A signed two's-complement integer with a compile-time capacity. /// /// Arithmetic wraps modulo `2^(64 * LIMBS)`. A witness generator must be diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 78fb71b7..5e798dec 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -23,3 +23,15 @@ pub use table::{BitTable, TableError, TransposeError, TransposedBitTable}; pub use virtual_map::{ TransposedWeights, VirtualMap, VirtualMapError, VirtualStatement, VirtualStatementError, }; + +use crypto_primitives::Semiring; +use std::ops::Neg; + +pub trait BitzIntSemiring: Semiring + From {} + +impl BitzIntSemiring for T where T: Semiring + From {} + +// Since BigInt does not support CheckedNeg and CheckedRem, we use this as a workaround +pub trait BitzIntRing: BitzIntSemiring + Neg {} + +impl BitzIntRing for T where T: BitzIntSemiring + Neg {} diff --git a/crates/spartan/Cargo.toml b/crates/spartan/Cargo.toml index f482123d..cd68ad22 100644 --- a/crates/spartan/Cargo.toml +++ b/crates/spartan/Cargo.toml @@ -6,15 +6,17 @@ rust-version.workspace = true license.workspace = true [dependencies] +circuit = { workspace = true } +common = { workspace = true } +field = { workspace = true, features = ["spongefish"] } +poly = { workspace = true } +transcript = { workspace = true } + crypto-primitives = { workspace = true } -circuit = { path = "../circuit" } -field = { path = "../field", features = ["spongefish"] } num-bigint.workspace = true num-traits.workspace = true -poly = { path = "../poly" } rayon = { workspace = true } sha2 = "0.10" -transcript = { path = "../transcript" } tracing = { workspace = true } [dev-dependencies] diff --git a/crates/spartan/src/matrix.rs b/crates/spartan/src/matrix.rs index 8215b6bc..c78bf7dd 100644 --- a/crates/spartan/src/matrix.rs +++ b/crates/spartan/src/matrix.rs @@ -3,6 +3,7 @@ use circuit::constraints::{ConstraintMatrices, SparseMatrix}; use circuit::matrix_products::{IntegerProducts, ModularVector, RuntimeModulus}; use circuit::witgen::PackedWitness; +use common::BitzIntRing; use crypto_primitives::ConstField; use field::{FqDefault, Q100}; use num_bigint::{BigInt, BigUint}; @@ -38,8 +39,8 @@ pub enum SpartanMatrixError { /// the column chunking are performed once during construction rather than /// inside the prover or verifier. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct PreparedConstraintMatrices { - matrices: ConstraintMatrices, +pub struct PreparedConstraintMatrices { + matrices: ConstraintMatrices, /// Nonzeros of `a`, `b` and `c` grouped by column chunk. column_chunks: [ColumnChunkIndex; 3], digest: [u8; 32], @@ -400,8 +401,8 @@ where Ok(evaluation) } -pub(crate) fn r1cs_num_vars( - matrices: &ConstraintMatrices, +pub(crate) fn r1cs_num_vars( + matrices: &ConstraintMatrices, ) -> Result<(usize, usize), SpartanMatrixError> { matrices .validate_shape() diff --git a/tooling/cli/Cargo.toml b/tooling/cli/Cargo.toml index a60dc2d4..a1090a2c 100644 --- a/tooling/cli/Cargo.toml +++ b/tooling/cli/Cargo.toml @@ -28,6 +28,7 @@ verifier.workspace = true [dev-dependencies] divan.workspace = true +num-bigint.workspace = true [[bench]] name = "circuits" diff --git a/tooling/cli/tests/circuits.rs b/tooling/cli/tests/circuits.rs index e5581ca5..4f46dcd6 100644 --- a/tooling/cli/tests/circuits.rs +++ b/tooling/cli/tests/circuits.rs @@ -6,13 +6,14 @@ use circuit::{ constraints::ConstraintGenerator, sha256::{ABC_BLOCK, ABC_DIGEST, INITIAL_STATE}, }; +use num_bigint::BigInt; use num_traits::{Signed, ToPrimitive}; #[test] fn sha_constraint_residuals_cannot_wrap_modulo_q100() { for circuit in BuiltinCircuit::ALL { let statement = CircuitInstance::random(circuit, None, None).unwrap(); - let mut generator = ConstraintGenerator::new(statement.input_bits()); + let mut generator = ConstraintGenerator::::new(statement.input_bits()); let inputs: Vec<_> = (0..statement.input_bits()) .map(|i| generator.input(i)) .collect(); From 8476de1710ee8c3a6410978a2647ee4e16067322 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Thu, 24 Sep 2026 13:56:39 +0100 Subject: [PATCH 2/5] Renames --- crates/circuit/src/constraints.rs | 30 ++++++++++++++--------------- crates/circuit/src/matrix_sparse.rs | 4 ++-- crates/common/src/lib.rs | 10 +++++----- crates/spartan/src/matrix.rs | 4 ++-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/crates/circuit/src/constraints.rs b/crates/circuit/src/constraints.rs index 03083723..018ab1de 100644 --- a/crates/circuit/src/constraints.rs +++ b/crates/circuit/src/constraints.rs @@ -4,11 +4,11 @@ //! witness, prefixed by a constant one, to the integer witness. Its first row //! is the implicit integer constant one. `A`, `B`, and `C` then encode the //! rank-1 constraints `(A z) * (B z) = C z` over that integer witness. Every -//! integer coefficient is an arbitrary-precision signed [`BitzIntRing`]. +//! integer coefficient is an arbitrary-precision signed [`BitzRing`]. use crate::witgen::PackedWitness; use crate::{BoolWitness, Circuit, HintResult, PackedBits, ScalarBits, WitnessContext}; -use common::{BitzIntRing, BitzIntSemiring}; +use common::{BitzRing, BitzSemiring}; use num_traits::Zero; use rayon::prelude::*; use std::array; @@ -279,7 +279,7 @@ impl Display for ConstraintMatrixShapeError { impl Error for ConstraintMatrixShapeError {} -impl ConstraintMatrices { +impl ConstraintMatrices { /// Checks that A, B, and C share a shape and consume the assignment /// produced by M. pub fn validate_shape(&self) -> Result<(), ConstraintMatrixShapeError> { @@ -418,7 +418,7 @@ impl From for SatisfactionError { } } -fn evaluate_integer_row(row: &SparseRow, witness: &[R]) -> R { +fn evaluate_integer_row(row: &SparseRow, witness: &[R]) -> R { row.entries() .iter() .map(|(column, coefficient)| witness[*column].clone() * coefficient.clone()) @@ -485,7 +485,7 @@ pub struct LinearCombination { witnesses: Vec<(usize, R)>, } -impl LinearCombination { +impl LinearCombination { /// The integer constant term. pub fn constant(&self) -> &R { &self.constant @@ -528,7 +528,7 @@ impl From for LinearCombination { } } -impl Zero for LinearCombination { +impl Zero for LinearCombination { fn zero() -> Self { Self::from(R::zero()) } @@ -538,7 +538,7 @@ impl Zero for LinearCombination { } } -impl Add for LinearCombination { +impl Add for LinearCombination { type Output = Self; fn add(mut self, rhs: Self) -> Self::Output { @@ -547,7 +547,7 @@ impl Add for LinearCombination { } } -impl AddAssign for LinearCombination { +impl AddAssign for LinearCombination { fn add_assign(&mut self, rhs: Self) { self.constant += rhs.constant; self.witnesses = merge_sorted_vecs( @@ -566,7 +566,7 @@ impl AddAssign for LinearCombination { } } -impl Neg for LinearCombination { +impl Neg for LinearCombination { type Output = Self; fn neg(mut self) -> Self::Output { @@ -578,7 +578,7 @@ impl Neg for LinearCombination { } } -impl Sub for LinearCombination { +impl Sub for LinearCombination { type Output = Self; fn sub(self, rhs: Self) -> Self::Output { @@ -586,13 +586,13 @@ impl Sub for LinearCombination { } } -impl SubAssign for LinearCombination { +impl SubAssign for LinearCombination { fn sub_assign(&mut self, rhs: Self) { *self += -rhs; } } -impl Mul for LinearCombination { +impl Mul for LinearCombination { type Output = Self; fn mul(mut self, rhs: R) -> Self::Output { @@ -607,7 +607,7 @@ impl Mul for LinearCombination { } } -impl Sum for LinearCombination { +impl Sum for LinearCombination { fn sum>(iter: I) -> Self { iter.fold(Self::zero(), Add::add) } @@ -674,7 +674,7 @@ pub struct ConstraintGenerator { )>, } -impl ConstraintGenerator { +impl ConstraintGenerator { /// Starts a generator with `input_witnesses` preallocated Boolean inputs. pub const fn new(input_witnesses: usize) -> Self { Self { @@ -783,7 +783,7 @@ fn bool_sparse_row(value: BoolLinearCombination) -> SparseBoolRow { } } -impl Circuit for ConstraintGenerator { +impl Circuit for ConstraintGenerator { type Bool = BoolLinearCombination; type Coefficient = R; type Z = LinearCombination; diff --git a/crates/circuit/src/matrix_sparse.rs b/crates/circuit/src/matrix_sparse.rs index 6912eda4..e457df69 100644 --- a/crates/circuit/src/matrix_sparse.rs +++ b/crates/circuit/src/matrix_sparse.rs @@ -12,7 +12,7 @@ use std::mem::{size_of, size_of_val}; use crate::constraints::{ConstraintMatrices, SparseMatrix}; use crate::matrix_products::{RuntimeModulus, StoredInteger}; use crate::matrix_wengert::{add_mod_words, montgomery_mul_2, neg_mod_words}; -use common::BitzIntRing; +use common::BitzRing; use crypto_bigint::modular::{FixedMontyForm, FixedMontyParams}; use crypto_bigint::{Odd, U128}; use rayon::prelude::*; @@ -76,7 +76,7 @@ impl MaterializedAbc { /// Transposes and stores the integer matrices without choosing a modulus. pub fn from_matrices(matrices: &ConstraintMatrices) -> Self where - R: BitzIntRing, + R: BitzRing, StoredInteger: for<'a> From<&'a R>, { let row_count = matrices.a.row_count(); diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 5e798dec..1897a00b 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -27,11 +27,11 @@ pub use virtual_map::{ use crypto_primitives::Semiring; use std::ops::Neg; -pub trait BitzIntSemiring: Semiring + From {} +pub trait BitzSemiring: Semiring + From {} -impl BitzIntSemiring for T where T: Semiring + From {} +impl BitzSemiring for T where T: Semiring + From {} -// Since BigInt does not support CheckedNeg and CheckedRem, we use this as a workaround -pub trait BitzIntRing: BitzIntSemiring + Neg {} +// Since BigInt does not support CheckedNeg and CheckedRem, we can't use Ring here +pub trait BitzRing: BitzSemiring + Neg {} -impl BitzIntRing for T where T: BitzIntSemiring + Neg {} +impl BitzRing for T where T: BitzSemiring + Neg {} diff --git a/crates/spartan/src/matrix.rs b/crates/spartan/src/matrix.rs index c78bf7dd..1ed7aeea 100644 --- a/crates/spartan/src/matrix.rs +++ b/crates/spartan/src/matrix.rs @@ -3,7 +3,7 @@ use circuit::constraints::{ConstraintMatrices, SparseMatrix}; use circuit::matrix_products::{IntegerProducts, ModularVector, RuntimeModulus}; use circuit::witgen::PackedWitness; -use common::BitzIntRing; +use common::BitzRing; use crypto_primitives::ConstField; use field::{FqDefault, Q100}; use num_bigint::{BigInt, BigUint}; @@ -401,7 +401,7 @@ where Ok(evaluation) } -pub(crate) fn r1cs_num_vars( +pub(crate) fn r1cs_num_vars( matrices: &ConstraintMatrices, ) -> Result<(usize, usize), SpartanMatrixError> { matrices From 3d86485799f1cb82491c309fa3b1cd167f6fc630 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Thu, 24 Sep 2026 14:09:59 +0100 Subject: [PATCH 3/5] Introduce thiserror --- Cargo.lock | 1 + crates/circuit/Cargo.toml | 1 + crates/circuit/src/constraints.rs | 94 ++++---------------------- crates/circuit/src/lib.rs | 14 +--- crates/circuit/src/matrix_sparse.rs | 24 ++----- crates/circuit/src/matrix_transpose.rs | 23 ++----- crates/circuit/src/matrix_wengert.rs | 28 ++------ 7 files changed, 35 insertions(+), 150 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 64e8f8a1..f1809a99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,6 +342,7 @@ dependencies = [ "num-traits", "poly", "rayon", + "thiserror", ] [[package]] diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 05a6d132..b6057bd6 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -14,6 +14,7 @@ poly.workspace = true num-traits.workspace = true crypto-bigint.workspace = true rayon.workspace = true +thiserror.workspace = true [dev-dependencies] divan = "0.1.21" diff --git a/crates/circuit/src/constraints.rs b/crates/circuit/src/constraints.rs index 018ab1de..732b6baa 100644 --- a/crates/circuit/src/constraints.rs +++ b/crates/circuit/src/constraints.rs @@ -13,11 +13,10 @@ use num_traits::Zero; use rayon::prelude::*; use std::array; use std::cmp::Ordering; -use std::error::Error; -use std::fmt::{self, Display}; use std::iter::Sum; use std::mem; use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; +use thiserror::Error; /// One row of a sparse matrix, sorted by increasing column index. #[derive(Clone, Debug, Eq, PartialEq)] @@ -120,13 +119,15 @@ impl SparseMatrix { } /// Why a sparse row representation is malformed. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Error)] pub enum SparseMatrixError { + #[error("column {column} in row {row} is outside a {columns}-column matrix")] ColumnOutOfBounds { row: usize, column: usize, columns: usize, }, + #[error("columns in row {row} are not strictly increasing: {previous}, {column}")] ColumnsNotStrictlyIncreasing { row: usize, previous: usize, @@ -134,31 +135,6 @@ pub enum SparseMatrixError { }, } -impl Display for SparseMatrixError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::ColumnOutOfBounds { - row, - column, - columns, - } => write!( - formatter, - "column {column} in row {row} is outside a {columns}-column matrix" - ), - Self::ColumnsNotStrictlyIncreasing { - row, - previous, - column, - } => write!( - formatter, - "columns in row {row} are not strictly increasing: {previous}, {column}" - ), - } - } -} - -impl Error for SparseMatrixError {} - /// One sparse F2 row, represented solely by its nonzero column positions. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SparseBoolRow { @@ -246,39 +222,16 @@ pub struct ConstraintMatrices { } /// Why the four constraint matrices cannot describe one R1CS relation. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Error)] pub enum ConstraintMatrixShapeError { + #[error("A, B, and C have different row counts: {a}, {b}, {c}")] R1csRowCountMismatch { a: usize, b: usize, c: usize }, + #[error("A, B, and C have different column counts: {a}, {b}, {c}")] R1csColumnCountMismatch { a: usize, b: usize, c: usize }, + #[error("M produces {m_rows} assignment entries but A, B, and C expect {r1cs_columns}")] AssignmentLengthMismatch { m_rows: usize, r1cs_columns: usize }, } -impl Display for ConstraintMatrixShapeError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::R1csRowCountMismatch { a, b, c } => { - write!( - formatter, - "A, B, and C have different row counts: {a}, {b}, {c}" - ) - } - Self::R1csColumnCountMismatch { a, b, c } => write!( - formatter, - "A, B, and C have different column counts: {a}, {b}, {c}" - ), - Self::AssignmentLengthMismatch { - m_rows, - r1cs_columns, - } => write!( - formatter, - "M produces {m_rows} assignment entries but A, B, and C expect {r1cs_columns}" - ), - } - } -} - -impl Error for ConstraintMatrixShapeError {} - impl ConstraintMatrices { /// Checks that A, B, and C share a shape and consume the assignment /// produced by M. @@ -387,37 +340,16 @@ impl ConstraintMatrices { } /// Why a Boolean witness does not satisfy a generated constraint system. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Error)] pub enum SatisfactionError { - /// The four matrices do not share a compatible R1CS shape. - InvalidShape(ConstraintMatrixShapeError), - /// The packed Boolean witness has the wrong number of entries. + #[error("The four matrices do not share a compatible R1CS shape: {0}")] + InvalidShape(#[from] ConstraintMatrixShapeError), + #[error("The packed Boolean witness has length {actual}, expected {expected}")] WitnessLength { expected: usize, actual: usize }, - /// The indicated R1CS row does not satisfy `a * b = c`. + #[error("The indicated R1CS row {row} does not satisfy `a * b = c`")] Constraint { row: usize }, } -impl Display for SatisfactionError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InvalidShape(error) => Display::fmt(error, formatter), - Self::WitnessLength { expected, actual } => write!( - formatter, - "Boolean witness has length {actual}, expected {expected}" - ), - Self::Constraint { row } => write!(formatter, "R1CS row {row} is unsatisfied"), - } - } -} - -impl Error for SatisfactionError {} - -impl From for SatisfactionError { - fn from(error: ConstraintMatrixShapeError) -> Self { - Self::InvalidShape(error) - } -} - fn evaluate_integer_row(row: &SparseRow, witness: &[R]) -> R { row.entries() .iter() diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 2eab0653..f81c9c27 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -6,12 +6,11 @@ //! symbolic witnesses when witness generation is run. use std::array; -use std::error::Error; -use std::fmt::{self, Display}; use std::iter::Sum; use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; use num_traits::{One, Zero}; +use thiserror::Error; pub mod constraints; pub mod ecdsa_sha256; @@ -32,7 +31,8 @@ mod projection_tests; /// Hints are fallible because their inputs may not be in the domain expected by /// a gadget (for example, an unsigned decomposition hint may receive a negative /// value). -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Error)] +#[error("{message}")] pub struct HintError { message: String, } @@ -51,14 +51,6 @@ impl HintError { } } -impl Display for HintError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(&self.message) - } -} - -impl Error for HintError {} - impl From for HintError { fn from(message: String) -> Self { Self::new(message) diff --git a/crates/circuit/src/matrix_sparse.rs b/crates/circuit/src/matrix_sparse.rs index e457df69..680d6e93 100644 --- a/crates/circuit/src/matrix_sparse.rs +++ b/crates/circuit/src/matrix_sparse.rs @@ -5,10 +5,6 @@ //! together in column-major order so a prepared evaluator can gather each //! output column independently and in parallel. -use std::error::Error; -use std::fmt::{self, Display}; -use std::mem::{size_of, size_of_val}; - use crate::constraints::{ConstraintMatrices, SparseMatrix}; use crate::matrix_products::{RuntimeModulus, StoredInteger}; use crate::matrix_wengert::{add_mod_words, montgomery_mul_2, neg_mod_words}; @@ -16,6 +12,8 @@ use common::BitzRing; use crypto_bigint::modular::{FixedMontyForm, FixedMontyParams}; use crypto_bigint::{Odd, U128}; use rayon::prelude::*; +use std::mem::{size_of, size_of_val}; +use thiserror::Error; const PARALLEL_NNZ_THRESHOLD: usize = 1 << 15; const PARALLEL_VECTOR_THRESHOLD: usize = 1 << 14; @@ -333,26 +331,14 @@ impl PreparedMaterializedAbc<'_> { } /// Failure to prepare or apply a sparse `A/B/C` materialization. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Error)] pub enum SparseAbcApplyError { + #[error("challenge vector has length {actual}, expected {expected}")] ChallengeLength { expected: usize, actual: usize }, + #[error("Montgomery evaluation needs an odd modulus")] EvenModulus, } -impl Display for SparseAbcApplyError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::ChallengeLength { expected, actual } => write!( - formatter, - "challenge vector has length {actual}, expected {expected}" - ), - Self::EvenModulus => write!(formatter, "Montgomery evaluation needs an odd modulus"), - } - } -} - -impl Error for SparseAbcApplyError {} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/circuit/src/matrix_transpose.rs b/crates/circuit/src/matrix_transpose.rs index a0030591..986fb4e3 100644 --- a/crates/circuit/src/matrix_transpose.rs +++ b/crates/circuit/src/matrix_transpose.rs @@ -4,17 +4,15 @@ //! from circuit structure alone. [`MaterializedMTranspose`] computes `r * M` //! as parallel, disjoint column gathers over the GHASH field. -use std::error::Error; -use std::fmt::{self, Display}; use std::mem::size_of; use std::sync::atomic::{AtomicU64, Ordering}; +use crate::witgen::Z; +use crate::{BoolWitness, Circuit, HintResult, PackedBits, ScalarBits, WitnessContext}; use common::{TransposedWeights, VirtualMap, VirtualMapError}; use field::F128; use rayon::prelude::*; - -use crate::witgen::Z; -use crate::{BoolWitness, Circuit, HintResult, PackedBits, ScalarBits, WitnessContext}; +use thiserror::Error; const PARALLEL_MATRIX_NNZ_THRESHOLD: usize = 1 << 15; @@ -413,24 +411,13 @@ impl VirtualMap for MaterializedMTranspose { } /// A challenge-vector dimension mismatch while applying materialized `M^T`. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Error)] +#[error("challenge vector has length {actual}, expected {expected}")] pub struct MatrixApplyError { pub expected: usize, pub actual: usize, } -impl Display for MatrixApplyError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - formatter, - "challenge vector has length {}, expected {}", - self.actual, self.expected - ) - } -} - -impl Error for MatrixApplyError {} - /// Materializes compact `M^T` from a circuit's static Boolean structure. #[derive(Debug)] pub struct MTransposeGenerator { diff --git a/crates/circuit/src/matrix_wengert.rs b/crates/circuit/src/matrix_wengert.rs index 64d79561..21e3f1eb 100644 --- a/crates/circuit/src/matrix_wengert.rs +++ b/crates/circuit/src/matrix_wengert.rs @@ -8,21 +8,19 @@ //! sufficiently wide depths are evaluated in parallel without atomics. use std::collections::HashMap; -use std::error::Error; -use std::fmt::{self, Display}; use std::iter::Sum; use std::mem::{size_of, size_of_val}; use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; use std::sync::{Arc, Mutex}; +use crate::matrix_products::{RuntimeModulus, StoredInteger}; +use crate::witgen::Z; +use crate::{BoolWitness, Circuit, HintResult, PackedBits, ScalarBits, WitnessContext}; use crypto_bigint::modular::{FixedMontyForm, FixedMontyParams}; use crypto_bigint::{Odd, U128}; use num_traits::{One, Zero}; use rayon::prelude::*; - -use crate::matrix_products::{RuntimeModulus, StoredInteger}; -use crate::witgen::Z; -use crate::{BoolWitness, Circuit, HintResult, PackedBits, ScalarBits, WitnessContext}; +use thiserror::Error; const PARALLEL_LEVEL_THRESHOLD: usize = 1 << 16; const PARALLEL_VECTOR_THRESHOLD: usize = 1 << 14; @@ -1143,26 +1141,14 @@ pub(crate) fn neg_mod_words(value: [u64; 2], modulus: [u64; 2]) -> [u64; 2] { } /// Failure to apply a Wengert tape with the supplied runtime data. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Error)] pub enum WengertApplyError { + #[error("challenge vector has length {actual}, expected {expected}")] ChallengeLength { expected: usize, actual: usize }, + #[error("Wengert evaluation requires an odd modulus")] EvenModulus, } -impl Display for WengertApplyError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::ChallengeLength { expected, actual } => write!( - formatter, - "challenge vector has length {actual}, expected {expected}" - ), - Self::EvenModulus => formatter.write_str("Wengert evaluation requires an odd modulus"), - } - } -} - -impl Error for WengertApplyError {} - /// Records and preprocesses the circuit's Z-side linear arithmetic. #[derive(Debug)] pub struct WengertGenerator { From a737e12d75fd6a109d684f1b5f0121cbfd35684f Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Thu, 24 Sep 2026 15:48:05 +0100 Subject: [PATCH 4/5] Remove dead code comment --- crates/circuit/src/matrix_products.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/circuit/src/matrix_products.rs b/crates/circuit/src/matrix_products.rs index f6043c50..30e2ca46 100644 --- a/crates/circuit/src/matrix_products.rs +++ b/crates/circuit/src/matrix_products.rs @@ -12,14 +12,6 @@ use num_traits::{One, Zero}; use rayon::prelude::*; use std::cmp::Ordering; -// pub trait RuntimeModulus: Sized { -// type Semiring; -// -// fn new(modulus: Self::Semiring) -> Result; -// -// fn modulus(&self) -> Self::Semiring; -// } - /// A runtime modulus with a compile-time limb count. #[derive(Debug, Eq, PartialEq)] pub struct RuntimeModulus { From f2000fba4aad2666860891e7fca060e30ce5e207 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Mon, 28 Sep 2026 14:48:11 +0100 Subject: [PATCH 5/5] Rework BigUint::into_words to not allocate temporary vector --- crates/circuit/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index f81c9c27..59024b4d 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -555,8 +555,8 @@ impl Bits for num_bigint::BigUint { impl IntoWords for num_bigint::BigUint { fn into_words(self) -> [u64; LIMBS] { - let digits = self.to_u64_digits(); - array::from_fn(|index| digits.get(index).copied().unwrap_or(0)) + let mut digits = self.iter_u64_digits(); + array::from_fn(|_| digits.next().unwrap_or(0)) } }