diff --git a/crates/circuit/src/constraints.rs b/crates/circuit/src/constraints.rs index bea93a0..94a5caa 100644 --- a/crates/circuit/src/constraints.rs +++ b/crates/circuit/src/constraints.rs @@ -4,11 +4,13 @@ //! 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 [`BitzConstraintRing`]. +//! integer coefficient lives in the caller's signed [`BitzConstraintRing`], +//! which must hold the circuit's widest coefficient. use crate::witgen::PackedWitness; use crate::{BoolWitness, Circuit, HintResult, PackedBits, ScalarBits, WitnessContext}; use common::{BitzConstraintRing, BitzSemiring}; +use num_bigint::BigInt; use num_traits::Zero; use rayon::prelude::*; use std::array; @@ -359,7 +361,8 @@ impl ConstraintMatrices { let a = evaluate_integer_row(&self.a.rows[row], &integer_witness); let b = evaluate_integer_row(&self.b.rows[row], &integer_witness); let c = evaluate_integer_row(&self.c.rows[row], &integer_witness); - if a * b != c { + // A product overflowing `R` differs from real `c`. + if a.checked_mul(&b) != Some(c) { return Err(SatisfactionError::Constraint { row }); } } @@ -627,6 +630,49 @@ fn merge_sorted_vecs( merged } +/// The capacity of a constraint ring, in the 64-bit limbs of a gadget's signed +/// two's-complement bound. +/// +/// [`ConstraintGenerator`] checks it against every gadget's `LIMBS` when the +/// gadget is monomorphized, so a gadget wider than the ring fails to build +/// (`cargo check` does not monomorphize): +/// +/// ```compile_fail,E0080 +/// use circuit::Circuit; +/// use circuit::constraints::ConstraintGenerator; +/// +/// let mut generator = ConstraintGenerator::::new(1); +/// let bit = generator.input(0); +/// let _ = generator.bitz::<2>(bit); +/// ``` +pub trait SignedLimbCapacity { + /// The largest `LIMBS` such that the ring holds every signed integer in + /// `[-2^(64 * LIMBS - 1), 2^(64 * LIMBS - 1))`, the sign taking one of the + /// `64 * LIMBS` bits; `usize::MAX` for an unbounded ring. + const MAX_SIGNED_LIMBS: usize; +} + +/// Fails const evaluation unless `R` holds the signed integers of a +/// `limbs`-limb gadget. +const fn assert_signed_limb_capacity(limbs: usize) { + assert!( + limbs <= R::MAX_SIGNED_LIMBS, + "the constraint ring is narrower than the gadget's limb bound" + ); +} + +impl SignedLimbCapacity for i64 { + const MAX_SIGNED_LIMBS: usize = 1; +} + +impl SignedLimbCapacity for i128 { + const MAX_SIGNED_LIMBS: usize = 2; +} + +impl SignedLimbCapacity for BigInt { + const MAX_SIGNED_LIMBS: usize = usize::MAX; +} + /// Circuit backend that records sparse M/A/B/C matrices without evaluating hints. #[derive(Clone, Debug)] pub struct ConstraintGenerator { @@ -749,7 +795,10 @@ fn bool_sparse_row(value: BoolLinearCombination) -> SparseBoolRow { } } -impl Circuit for ConstraintGenerator { +impl Circuit for ConstraintGenerator +where + R: BitzConstraintRing + SignedLimbCapacity, +{ type Bool = BoolLinearCombination; type Coefficient = R; type Z = LinearCombination; @@ -774,6 +823,7 @@ impl Circuit for ConstraintGenerator { + Sync + 'static, { + const { assert_signed_limb_capacity::(LIMBS) }; assert_eq!(M, N.div_ceil(64), "incorrect packed limb count"); let first = self.next_boolean_witness; self.next_boolean_witness = self @@ -786,6 +836,7 @@ impl Circuit for ConstraintGenerator { } fn bitz(&mut self, value: BoolLinearCombination) -> LinearCombination { + const { assert_signed_limb_capacity::(LIMBS) }; LinearCombination::witness(self.record_bitz(value)) } @@ -793,6 +844,7 @@ impl Circuit for ConstraintGenerator { &mut self, bits_le: &ScalarBits, ) -> (LinearCombination, LinearCombination) { + const { assert_signed_limb_capacity::(LIMBS) }; 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. @@ -818,6 +870,7 @@ impl Circuit for ConstraintGenerator { b: LinearCombination, c: LinearCombination, ) { + const { assert_signed_limb_capacity::(LIMBS) }; self.r1cs.push((a, b, c)); } @@ -825,6 +878,7 @@ impl Circuit for ConstraintGenerator { &mut self, value: LinearCombination, ) -> LinearCombination { + const { assert_signed_limb_capacity::(TO_LIMBS) }; assert!( TO_LIMBS >= FROM_LIMBS, "cannot sign-extend into fewer limbs" @@ -899,6 +953,48 @@ mod tests { assert!(matrices.is_satisfied(Witgen::with_inputs(&[]).witness())); } + #[test] + fn narrow_rings_keep_small_coefficients_exact() { + let mut generator = ConstraintGenerator::::new(40); + let bits = ScalarBits(generator.inputs::<40>()); + let (full, low) = generator.bitz_unsigned::<1, 40, 1, 32>(&bits); + generator.assert_r1c::<1>(Zero::zero(), Zero::zero(), full - low * 3); + let matrices = generator.into_matrices(); + let entries = matrices.c.rows()[0].entries(); + assert_eq!(entries.len(), 40); + for (bit, (column, coefficient)) in entries.iter().enumerate() { + assert_eq!(*column, bit + 1); + let expected = if bit < 32 { -2 } else { 1 }; + assert_eq!(*coefficient, expected * (1_i64 << bit)); + } + } + + /// The guard on a gadget exceeding its own limb bound: the dev profile's + /// overflow checks. Release wraps instead. + #[test] + #[cfg(debug_assertions)] + #[should_panic(expected = "overflow")] + fn narrow_ring_overflow_panics_in_debug() { + let mut generator = ConstraintGenerator::::new(64); + let bits = ScalarBits(generator.inputs::<64>()); + // The top coefficient is 2^63. + let _ = generator.bitz_unsigned::<1, 64, 1, 64>(&bits); + } + + #[test] + fn default_coefficient_from_le_words_keeps_every_bit() { + let words = [0x0123_4567_89ab_cdef, 0, u64::MAX, 1 << 63]; + let expected = words.iter().rev().fold(R::zero(), |value, word| { + (value << 64_usize) + R::from(*word) + }); + + assert_eq!( + ConstraintGenerator::::coefficient_from_le_words::<4>(&words), + expected + ); + assert!(ConstraintGenerator::::coefficient_from_le_words::<1>(&[]).is_zero()); + } + #[test] fn sparse_constructors_reject_unsorted_and_out_of_bounds_columns() { assert_eq!( diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 2ccc503..421f627 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -96,7 +96,7 @@ pub trait WitnessContext { /// witness type so backends are not forced to use integers for coefficients. pub trait Coefficient: Clone - + From + + From + Zero + One + Add @@ -111,7 +111,7 @@ pub trait Coefficient: impl Coefficient for T where T: Clone - + From + + From + Zero + One + Add @@ -454,15 +454,17 @@ pub trait Circuit { /// The default only relies on ring operations. Fixed-integer backends /// should override this to copy limbs directly. fn coefficient_from_le_words(words: &[u64]) -> Self::Coefficient { + // 32-bit halves: `Coefficient` only promises `From`. let mut radix = Self::Coefficient::::one(); - for _ in 0..64 { + for _ in 0..32 { radix += radix.clone(); } words .iter() .rev() - .fold(Self::Coefficient::::zero(), |value, word| { - value * radix.clone() + Self::Coefficient::::from(*word) + .flat_map(|word| [(word >> 32) as u32, *word as u32]) + .fold(Self::Coefficient::::zero(), |value, half| { + value * radix.clone() + Self::Coefficient::::from(half) }) } @@ -592,8 +594,8 @@ mod tests { } } - impl From for Mod7 { - fn from(value: u64) -> Self { + impl From for Mod7 { + fn from(value: u32) -> Self { Self::new((value % 7) as i32) } } diff --git a/crates/circuit/src/matrix_products.rs b/crates/circuit/src/matrix_products.rs index 19fc7e2..f0af0d5 100644 --- a/crates/circuit/src/matrix_products.rs +++ b/crates/circuit/src/matrix_products.rs @@ -415,6 +415,25 @@ impl From<&i128> for StoredInteger { } } +impl From<&i64> for StoredInteger { + fn from(value: &i64) -> Self { + if *value == 0 { + return Self::ZERO; + } + Self::from_normalized([*value as u64]) + } +} + +impl From<&StoredInteger> for i64 { + /// Panics on a value wider than one word. + fn from(value: &StoredInteger) -> Self { + match value.words { + Words::Inline(word) => word as i64, + Words::Heap(_) => panic!("stored integer does not fit an i64"), + } + } +} + impl From<&Integer> for StoredInteger { /// Stores a gadget-local fixed integer without changing its value. fn from(value: &Integer) -> Self { @@ -464,6 +483,7 @@ impl IntegerProducts { where R: BitzConstraintRing + for<'a> From<&'a StoredInteger>, { + // A product overflowing `R` differs from real `C(Mw)`. self.a_mw.len() == self.b_mw.len() && self.a_mw.len() == self.c_mw.len() && self @@ -471,7 +491,7 @@ impl IntegerProducts { .par_iter() .zip(&self.b_mw) .zip(&self.c_mw) - .all(|((a, b), c)| R::from(a) * R::from(b) == R::from(c)) + .all(|((a, b), c)| R::from(a).checked_mul(&R::from(b)) == Some(R::from(c))) } /// Reduces every materialized element modulo `modulus`. @@ -672,6 +692,33 @@ mod tests { } } + #[test] + fn one_word_integers_round_trip_through_stored_integers() { + for value in [0_i64, 1, -1, 7, -7, 1 << 35, i64::MAX, i64::MIN] { + let stored = StoredInteger::from(&value); + assert_eq!(stored, StoredInteger::from(&R::from(value)), "{value}"); + assert_eq!(i64::from(&stored), value); + } + } + + #[test] + #[should_panic(expected = "does not fit an i64")] + fn wide_stored_integers_do_not_truncate_into_one_word() { + let _ = i64::from(&StoredInteger::from(&(1_i128 << 63))); + } + + #[test] + fn products_overflowing_the_ring_are_unsatisfied() { + let mut products = IntegerProducts::default(); + let z = |value: i128| Integer::<1>::from(value); + products.push(z(-3), z(5), z(-15)); + assert!(products.is_satisfied::()); + // 2^32 * 2^32 wraps to zero in an i64. + products.push(z(1 << 32), z(1 << 32), z(0)); + assert!(!products.is_satisfied::()); + assert!(!products.is_satisfied::()); + } + #[test] fn arbitrary_bigints_round_trip_through_stored_integers() { let boundary = R::one() << 128_usize; diff --git a/crates/circuit/src/matrix_transpose.rs b/crates/circuit/src/matrix_transpose.rs index e4fe513..1393cc7 100644 --- a/crates/circuit/src/matrix_transpose.rs +++ b/crates/circuit/src/matrix_transpose.rs @@ -534,7 +534,7 @@ impl Circuit for MTransposeGenerator { fn bitz(&mut self, value: MatrixBit) -> Z { self.recorder.push_row(&value); - Z::from(u64::from(value.constant_term())) + Z::from(u32::from(value.constant_term())) } fn bitz_unsigned( diff --git a/crates/circuit/src/p256.rs b/crates/circuit/src/p256.rs index 5174f47..6cc1ce7 100644 --- a/crates/circuit/src/p256.rs +++ b/crates/circuit/src/p256.rs @@ -219,7 +219,7 @@ fn lc_words(words: &[u64]) -> Lc { } } -fn lc_u64(value: u64) -> Lc { +fn lc_u32(value: u32) -> Lc { Lc { z: P256Z::::from(P256Coefficient::::from(value)), } @@ -323,9 +323,9 @@ fn rep_constant(value: &S) -> Rep { } } -fn rep_u64(value: u64) -> Rep { +fn rep_u32(value: u32) -> Rep { Rep { - value: lc_u64(value), + value: lc_u32(value), bound: 2, } } @@ -345,12 +345,12 @@ fn rep_sub(modulus: Modulus, left: Rep, right: Rep) -> Rep< } } -fn rep_scale(coefficient: usize, value: Rep) -> Rep { +fn rep_scale(coefficient: u32, value: Rep) -> Rep { Rep { value: value .value - .scale_coefficient(P256Coefficient::::from(coefficient as u64)), - bound: coefficient * value.bound, + .scale_coefficient(P256Coefficient::::from(coefficient)), + bound: coefficient as usize * value.bound, } } @@ -920,15 +920,15 @@ fn point_from_elems(x: &Elem, y: &Elem) -> Point { Point { x: of_elem(x), y: of_elem(y), - infinity: lc_u64(0), + infinity: lc_u32(0), } } fn infinity() -> Point { Point { - x: rep_u64(0), - y: rep_u64(0), - infinity: lc_u64(1), + x: rep_u32(0), + y: rep_u32(0), + infinity: lc_u32(1), } } @@ -970,7 +970,7 @@ fn lazy_zero_test(circuit: &mut CS, modulus: Modulus, x: Rep) - x.clone(), of_elem(&Elem { value: inverse }), Rep { - value: lc_u64::(1).sub(z.clone()), + value: lc_u32::(1).sub(z.clone()), bound: 1, }, ); @@ -1043,12 +1043,12 @@ fn select_formula( fn double_complete(circuit: &mut CS, point: Point) -> Point { let x2 = lazy_mul(circuit, Modulus::Base, point.x.clone(), point.x.clone()); let numerator = rep_add( - rep_sub(Modulus::Base, rep_scale(3, x2), rep_u64::(3)), + rep_sub(Modulus::Base, rep_scale(3, x2), rep_u32::(3)), Rep { value: point .infinity .clone() - .scale_coefficient(P256Coefficient::::from(3_u64)), + .scale_coefficient(P256Coefficient::::from(3_u32)), bound: 1, }, ); @@ -1097,16 +1097,16 @@ fn add_complete(circuit: &mut CS, p: Point, q: Point) -> Po let opposite_y = lazy_zero_test(circuit, Modulus::Base, y_sum); let finite = and_bit( circuit, - lc_u64::(1).sub(p.infinity.clone()), - lc_u64::(1).sub(q.infinity.clone()), + lc_u32::(1).sub(p.infinity.clone()), + lc_u32::(1).sub(q.infinity.clone()), ); let double_kind = and_bit( circuit, same_x.clone(), - lc_u64::(1).sub(opposite_y.clone()), + lc_u32::(1).sub(opposite_y.clone()), ); let double_case = and_bit(circuit, finite.clone(), double_kind); - let generic_case = and_bit(circuit, finite.clone(), lc_u64::(1).sub(same_x.clone())); + let generic_case = and_bit(circuit, finite.clone(), lc_u32::(1).sub(same_x.clone())); let control = AddControl { same_x, opposite_y, @@ -1117,7 +1117,7 @@ fn add_complete(circuit: &mut CS, p: Point, q: Point) -> Po let dy = rep_sub(Modulus::Base, q.y.clone(), p.y.clone()); let x2 = lazy_mul(circuit, Modulus::Base, p.x.clone(), p.x.clone()); - let double_numerator = rep_sub(Modulus::Base, rep_scale(3, x2), rep_u64::(3)); + let double_numerator = rep_sub(Modulus::Base, rep_scale(3, x2), rep_u32::(3)); let double_denominator = rep_scale(2, p.y.clone()); let selected_numerator = select_formula(circuit, control.double_case.clone(), double_numerator, dy); @@ -1127,13 +1127,13 @@ fn add_complete(circuit: &mut CS, p: Point, q: Point) -> Po circuit, control.active.clone(), selected_numerator, - rep_u64(0), + rep_u32(0), ); let denominator = select_formula( circuit, control.active.clone(), selected_denominator, - rep_u64(1), + rep_u32(1), ); let slope = lazy_divide(circuit, Modulus::Base, denominator, numerator); let candidate_x = lazy_mul_sub_to_elem( @@ -1153,8 +1153,8 @@ fn add_complete(circuit: &mut CS, p: Point, q: Point) -> Po ); let candidate_y = of_elem(&candidate_y); - let inactive_x0 = select_canonical(circuit, q.infinity.clone(), p.x.clone(), rep_u64(0)); - let inactive_y0 = select_canonical(circuit, q.infinity.clone(), p.y.clone(), rep_u64(0)); + let inactive_x0 = select_canonical(circuit, q.infinity.clone(), p.x.clone(), rep_u32(0)); + let inactive_y0 = select_canonical(circuit, q.infinity.clone(), p.y.clone(), rep_u32(0)); let inactive_x = select_canonical(circuit, p.infinity.clone(), q.x.clone(), inactive_x0); let inactive_y = select_canonical(circuit, p.infinity.clone(), q.y.clone(), inactive_y0); let x = select_canonical(circuit, control.active.clone(), candidate_x, inactive_x); @@ -1204,14 +1204,14 @@ fn indicators_impl( bits, value: Lc { z: full }, }; - let sum = lifted.iter().cloned().fold(lc_u64::(0), Lc::add); - assert_zero(circuit, sum.sub(lc_u64(1))); + let sum = lifted.iter().cloned().fold(lc_u32::(0), Lc::add); + assert_zero(circuit, sum.sub(lc_u32(1))); let weighted = lifted .iter() .cloned() - .enumerate() - .fold(lc_u64::(0), |sum, (i, bit)| { - sum.add(bit.scale_coefficient(P256Coefficient::::from(i as u64))) + .zip(0_u32..) + .fold(lc_u32::(0), |sum, (bit, i)| { + sum.add(bit.scale_coefficient(P256Coefficient::::from(i))) }); assert_zero(circuit, weighted.sub(digit)); (out, lifted) @@ -1342,13 +1342,13 @@ fn lookup_generator_byte( let x = indicators .iter() .zip(table) - .fold(lc_u64::(0), |sum, (bit, (x, _))| { + .fold(lc_u32::(0), |sum, (bit, (x, _))| { sum.add(bit.clone().scale_coefficient(x.clone())) }); let y = indicators .iter() .zip(table) - .fold(lc_u64::(0), |sum, (bit, (_, y))| { + .fold(lc_u32::(0), |sum, (bit, (_, y))| { sum.add(bit.clone().scale_coefficient(y.clone())) }); Point { @@ -1404,8 +1404,8 @@ fn scalar_window(scalar: &ScalarElem, start: usize, width: usiz .iter() .cloned() .enumerate() - .fold(lc_u64::(0), |sum, (bit, value)| { - sum.add(value.scale_coefficient(P256Coefficient::::from(1_u64 << bit))) + .fold(lc_u32::(0), |sum, (bit, value)| { + sum.add(value.scale_coefficient(P256Coefficient::::from(1_u32 << bit))) }) } @@ -1492,14 +1492,14 @@ pub fn verify_digest_circuit( Modulus::Scalar, of_elem(&r), of_elem(&r_inverse), - rep_u64(1), + rep_u32(1), ); lazy_assert_mul_eq( circuit, Modulus::Scalar, of_elem(&s), of_elem(&s_inverse), - rep_u64(1), + rep_u32(1), ); let z = relaxed_reduce_small(circuit, Modulus::Scalar, digest.elem.value.value); diff --git a/crates/circuit/src/sha256.rs b/crates/circuit/src/sha256.rs index c1a1318..51927a3 100644 --- a/crates/circuit/src/sha256.rs +++ b/crates/circuit/src/sha256.rs @@ -168,11 +168,11 @@ where UInt { word, z_values } } -fn uint_constant(value: u64) -> UInt32 +fn uint_constant(value: u32) -> UInt32 where CS: Circuit, { - let word = Word::constant(value); + let word = Word::constant(u64::from(value)); let full = ShaZ::::from(ShaCoefficient::::from(value)); let z_values = ZValues { low_32: full.clone(), @@ -410,7 +410,7 @@ where let old_f = f; let old_g = g; let old_h = h; - let round_constant = uint_constant::(u64::from(ROUND_CONSTANTS[i])); + let round_constant = uint_constant::(ROUND_CONSTANTS[i]); h = old_g.clone(); g = old_f.clone(); @@ -563,7 +563,7 @@ mod tests { use std::iter::Sum; use std::ops::{Add, AddAssign}; - type R = num_bigint::BigInt; + type R = i64; #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct Bit(bool); diff --git a/crates/circuit/src/stats.rs b/crates/circuit/src/stats.rs index 1d76691..cb55c10 100644 --- a/crates/circuit/src/stats.rs +++ b/crates/circuit/src/stats.rs @@ -21,8 +21,8 @@ impl From for Dummy { } } -impl From for Dummy { - fn from(_: u64) -> Self { +impl From for Dummy { + fn from(_: u32) -> Self { Self } } diff --git a/crates/circuit/src/witgen.rs b/crates/circuit/src/witgen.rs index 759cdfd..1f21346 100644 --- a/crates/circuit/src/witgen.rs +++ b/crates/circuit/src/witgen.rs @@ -108,11 +108,11 @@ impl From for Z { } } -impl From for Z { - fn from(value: u64) -> Self { - let mut words = [0; LIMBS]; +impl From for Z { + fn from(value: u32) -> Self { + let mut words = [0_u64; LIMBS]; if LIMBS != 0 { - words[0] = value; + words[0] = u64::from(value); } Self { words } } @@ -130,7 +130,7 @@ impl Zero for Z { impl One for Z { fn one() -> Self { - Self::from(1_u64) + Self::from(1_u32) } } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 82a6cd9..9134d0f 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -61,7 +61,7 @@ define_blanket_trait! { Semiring + BitAnd + ShrAssign - + From + + From + ToPrimitive } diff --git a/tooling/cli/benches/circuits.rs b/tooling/cli/benches/circuits.rs index fcd030b..8a60a4d 100644 --- a/tooling/cli/benches/circuits.rs +++ b/tooling/cli/benches/circuits.rs @@ -1,7 +1,7 @@ //! End-to-end and per-stage benchmarks; instance generation is outside timing. use bitz_cli::{ - ProjectBigIntToField, + ProjectI64ToField, benchmark::run, circuits::{BuiltinCircuit, CircuitInstance}, end_to_end::CircuitProofSystem, @@ -9,9 +9,9 @@ use bitz_cli::{ use divan::Bencher; use pcs::SecurityLevel; -type R = num_bigint::BigInt; +type R = i64; type F = field::DynField; -type Proj = ProjectBigIntToField; +type Proj = ProjectI64ToField; fn main() { divan::main(); diff --git a/tooling/cli/benches/sha256_spartan.rs b/tooling/cli/benches/sha256_spartan.rs index 47736ed..e8eb81d 100644 --- a/tooling/cli/benches/sha256_spartan.rs +++ b/tooling/cli/benches/sha256_spartan.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex}; -use bitz_cli::{ProjectBigIntToField, ProjectConstraint}; +use bitz_cli::{ProjectConstraint, ProjectI64ToField}; use circuit::constraints::{ConstraintGenerator, ConstraintMatrices}; use circuit::sha256::sha256_block_aligned_circuit; use circuit::witgen::ProductWitgen; @@ -28,9 +28,9 @@ const INSTANCE: &[u8] = b"bench"; /// 2^22-bit committed shape. const BLOCKS: &[usize] = &[608]; -type R = num_bigint::BigInt; +type R = i64; type F = field::FqDefault; -type Proj = ProjectBigIntToField; +type Proj = ProjectI64ToField; /// An R1CS instance with a witness satisfying it. #[derive(Debug, Clone)] diff --git a/tooling/cli/src/benchmark.rs b/tooling/cli/src/benchmark.rs index f9c3af5..70d2bca 100644 --- a/tooling/cli/src/benchmark.rs +++ b/tooling/cli/src/benchmark.rs @@ -2,6 +2,7 @@ use crate::ProjectConstraint; use crate::end_to_end::{CircuitProofSystem, CircuitStatement, CircuitStats, Error}; +use circuit::constraints::SignedLimbCapacity; use circuit::matrix_products::StoredInteger; use circuit::{BitWidth, IntoWords}; use common::{BitzClaimField, BitzConstraintRing}; @@ -33,7 +34,7 @@ where S: CircuitStatement, F: BitzClaimField + FieldWithDynamicModulus, F::Integer: BitWidth + IntoWords, - R: BitzConstraintRing + for<'a> From<&'a StoredInteger>, + R: BitzConstraintRing + SignedLimbCapacity + for<'a> From<&'a StoredInteger>, for<'a> StoredInteger: From<&'a R>, Proj: ProjectConstraint, { diff --git a/tooling/cli/src/circuits.rs b/tooling/cli/src/circuits.rs index 40ddb6a..bbfce6d 100644 --- a/tooling/cli/src/circuits.rs +++ b/tooling/cli/src/circuits.rs @@ -203,8 +203,8 @@ impl CircuitStatement for CircuitInstance { fn constrain_bit(cs: &mut C, bit: C::Bool, expected: bool) { let value = cs.bitz::<1>(bit); - let one = C::Z::<1>::from(C::Coefficient::<1>::from(1u64)); - let expected = C::Z::<1>::from(C::Coefficient::<1>::from(u64::from(expected))); + let one = C::Z::<1>::from(C::Coefficient::<1>::from(1u32)); + let expected = C::Z::<1>::from(C::Coefficient::<1>::from(u32::from(expected))); cs.assert_r1c::<1>(one, value, expected); } diff --git a/tooling/cli/src/cmd/circuit_e2e.rs b/tooling/cli/src/cmd/circuit_e2e.rs index 67a3991..3f0f67f 100644 --- a/tooling/cli/src/cmd/circuit_e2e.rs +++ b/tooling/cli/src/cmd/circuit_e2e.rs @@ -11,7 +11,8 @@ use { }; type F = field::DynField; -type Proj = bitz_cli::ProjectBigIntToField; +// Every builtin circuit is SHA-256, whose coefficients fit an `i64`. +type Proj = bitz_cli::ProjectI64ToField; /// Prove generated circuit constraints over a transcript-drawn prime and verify the proof. #[derive(FromArgs, PartialEq, Debug)] diff --git a/tooling/cli/src/end_to_end.rs b/tooling/cli/src/end_to_end.rs index d10d089..f6c9c47 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, ConstraintMatrices, SignedLimbCapacity, SparseBoolMatrix}, matrix_products::IntegerProducts, matrix_transpose::{MTransposeGenerator, MaterializedMTranspose}, witgen::{PackedWitness, ProductWitgen}, @@ -152,7 +152,7 @@ where S: CircuitStatement, F: BitzClaimField + FieldWithDynamicModulus, F::Integer: BitWidth + IntoWords, - R: BitzConstraintRing + for<'a> From<&'a StoredInteger>, + R: BitzConstraintRing + SignedLimbCapacity + for<'a> From<&'a StoredInteger>, for<'a> StoredInteger: From<&'a R>, Proj: ProjectConstraint, { @@ -570,7 +570,7 @@ mod tests { } fn synthesize(&self, cs: &mut C, inputs: &[C::Bool]) -> Result<(), Error> { let bit = cs.bitz::<1>(inputs[0].clone()); - let one = C::Z::<1>::from(C::Coefficient::<1>::from(1u64)); + let one = C::Z::<1>::from(C::Coefficient::<1>::from(1u32)); cs.assert_r1c::<1>(one.clone(), bit, one); Ok(()) } diff --git a/tooling/cli/src/lib.rs b/tooling/cli/src/lib.rs index 9aae2a9..fef8160 100644 --- a/tooling/cli/src/lib.rs +++ b/tooling/cli/src/lib.rs @@ -37,17 +37,7 @@ impl ProjectConstraint for ProjectBigIntToField { fn project(&self, constraint: &BigInt) -> F { // 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 - }); + return F::from(canonical_residue(value, self.modulus)); } let mut reduced = constraint % &self.wide_modulus; if reduced.is_negative() { @@ -61,6 +51,40 @@ impl ProjectConstraint for ProjectBigIntToField { } } +/// Projects [`i64`] constraints onto a prime field by reducing them +/// canonically modulo its modulus, read when prepared. +#[derive(Debug, Clone)] +pub struct ProjectI64ToField { + modulus: u128, +} + +impl ProjectConstraint for ProjectI64ToField { + fn prepare() -> Self { + Self { + modulus: F::modulus().to_u128().expect("the modulus fits a u128"), + } + } + + fn project(&self, constraint: &i64) -> F { + F::from(canonical_residue(i128::from(*constraint), self.modulus)) + } +} + +/// The canonical residue of `value` modulo `modulus`. +fn canonical_residue(value: i128, modulus: u128) -> u128 { + let magnitude = value.unsigned_abs(); + let magnitude = if magnitude < modulus { + magnitude + } else { + magnitude % modulus + }; + if value < 0 && magnitude != 0 { + modulus - magnitude + } else { + magnitude + } +} + #[cfg(test)] mod tests { use super::*; @@ -107,4 +131,14 @@ mod tests { assert_eq!(projected, reference(value), "{value}"); } } + + #[test] + fn narrow_projection_matches_the_bigint_one() { + let wide = >::prepare(); + let narrow = >::prepare(); + for value in [0_i64, 1, -1, 7, -7, 1 << 35, i64::MAX, i64::MIN] { + let projected: F = narrow.project(&value); + assert_eq!(projected, wide.project(&BigInt::from(value)), "{value}"); + } + } } diff --git a/tooling/cli/tests/circuits.rs b/tooling/cli/tests/circuits.rs index 1e113cf..e29d0f2 100644 --- a/tooling/cli/tests/circuits.rs +++ b/tooling/cli/tests/circuits.rs @@ -1,18 +1,69 @@ use bitz_cli::{ - ProjectBigIntToField, + ProjectI64ToField, circuits::{BuiltinCircuit, CircuitInstance}, end_to_end::{CircuitProofSystem, CircuitStatement, Error}, }; use circuit::{ - constraints::ConstraintGenerator, + constraints::{ConstraintGenerator, ConstraintMatrices, SignedLimbCapacity}, sha256::{ABC_BLOCK, ABC_DIGEST, INITIAL_STATE}, }; -use num_traits::{Signed, ToPrimitive}; +use common::BitzConstraintRing; +use num_bigint::BigInt; +use num_traits::Signed; use pcs::SecurityLevel; +use spartan::PreparedIntegerMatrices; -type R = num_bigint::BigInt; +type R = i64; type F = field::DynField; -type Proj = ProjectBigIntToField; +type Proj = ProjectI64ToField; + +fn synthesize(statement: &CircuitInstance) -> ConstraintMatrices +where + C: BitzConstraintRing + SignedLimbCapacity, +{ + let mut generator = ConstraintGenerator::::new(statement.input_bits()); + let inputs: Vec<_> = (0..statement.input_bits()) + .map(|i| generator.input(i)) + .collect(); + statement.synthesize(&mut generator, &inputs).unwrap(); + generator.into_matrices() +} + +/// Synthesis over `i64` never wraps: it yields the matrices exact arithmetic +/// does, under the same statement digest, and no Boolean assignment overflows +/// a row evaluated in `i64`. +#[test] +fn sha_constraints_over_i64_match_exact_arithmetic() { + for circuit in BuiltinCircuit::ALL { + let statement = CircuitInstance::random(circuit, None, None).unwrap(); + let exact = synthesize::(&statement); + let narrow = synthesize::(&statement); + assert_eq!( + PreparedIntegerMatrices::new(narrow.clone()) + .unwrap() + .digest(), + PreparedIntegerMatrices::new(exact.clone()) + .unwrap() + .digest(), + "{circuit}: the ring changed the statement digest" + ); + let narrow = narrow.map_coefficients(BigInt::from); + assert!(narrow == exact, "{circuit}: i64 coefficients wrapped"); + for matrix in [&exact.a, &exact.b, &exact.c] { + for row in matrix.rows() { + let bound: BigInt = row + .entries() + .iter() + .map(|(_, coefficient)| coefficient.abs()) + .sum(); + assert!( + bound <= BigInt::from(i64::MAX), + "{circuit}: a row may overflow" + ); + } + } + } +} /// Every residual is below the narrowest prime a proof may draw, so no /// Boolean assignment satisfies a row modulo the prime without satisfying it @@ -21,14 +72,8 @@ type Proj = ProjectBigIntToField; 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()); - let inputs: Vec<_> = (0..statement.input_bits()) - .map(|i| generator.input(i)) - .collect(); - statement.synthesize(&mut generator, &inputs).unwrap(); - let matrices = generator - .into_matrices() - .map_coefficients(|coefficient| coefficient.abs().to_u128().unwrap()); + let matrices = synthesize::(&statement) + .map_coefficients(|coefficient| u128::from(coefficient.unsigned_abs())); for ((a, b), c) in matrices .a .rows() diff --git a/tooling/cli/tests/end_to_end.rs b/tooling/cli/tests/end_to_end.rs index 9faefbc..7cd98b1 100644 --- a/tooling/cli/tests/end_to_end.rs +++ b/tooling/cli/tests/end_to_end.rs @@ -43,8 +43,8 @@ impl CircuitStatement for PublicBit { } fn synthesize(&self, cs: &mut CS, inputs: &[CS::Bool]) -> Result<(), Error> { let bit = cs.bitz::<1>(inputs[0].clone()); - let one = CS::Z::<1>::from(CS::Coefficient::<1>::from(1u64)); - let expected = CS::Z::<1>::from(CS::Coefficient::<1>::from(u64::from(self.0))); + let one = CS::Z::<1>::from(CS::Coefficient::<1>::from(1u32)); + let expected = CS::Z::<1>::from(CS::Coefficient::<1>::from(u32::from(self.0))); cs.assert_r1c::<1>(one, bit, expected); Ok(()) } @@ -118,7 +118,7 @@ impl CircuitStatement for PublicXor { let first = cs.bitz::<1>(inputs[0].clone()); let xor = cs.xor(inputs[0].clone(), inputs[1].clone()); let output = cs.bitz::<1>(xor); - let one = C::Z::<1>::from(C::Coefficient::<1>::from(1u64)); + let one = C::Z::<1>::from(C::Coefficient::<1>::from(1u32)); cs.assert_r1c::<1>(one.clone(), first.clone(), first); cs.assert_r1c::<1>(one.clone(), output, one); Ok(()) diff --git a/tooling/cli/tests/sha256_spartan.rs b/tooling/cli/tests/sha256_spartan.rs index 2256273..214e6db 100644 --- a/tooling/cli/tests/sha256_spartan.rs +++ b/tooling/cli/tests/sha256_spartan.rs @@ -1,4 +1,4 @@ -use bitz_cli::{ProjectBigIntToField, ProjectConstraint}; +use bitz_cli::{ProjectConstraint, ProjectI64ToField}; use circuit::constraints::ConstraintMatrices; use circuit::{ constraints::ConstraintGenerator, @@ -15,9 +15,9 @@ use spartan::{ use std::array; use transcript::{build_prover, build_verifier}; -type R = num_bigint::BigInt; +type R = i64; type F = field::FqDefault; -type Proj = ProjectBigIntToField; +type Proj = ProjectI64ToField; const SESSION: &[u8] = b"spartan/piop/sha256-compression/v1"; const INSTANCE: &[u8] = b"abc-single-compression";