Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 99 additions & 3 deletions crates/circuit/src/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -359,7 +361,8 @@ impl<R: BitzSemiring> ConstraintMatrices<R> {
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 });
}
}
Expand Down Expand Up @@ -627,6 +630,49 @@ fn merge_sorted_vecs<T>(
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::<i64>::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<R: SignedLimbCapacity>(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<R> {
Expand Down Expand Up @@ -749,7 +795,10 @@ fn bool_sparse_row(value: BoolLinearCombination) -> SparseBoolRow {
}
}

impl<R: BitzConstraintRing> Circuit for ConstraintGenerator<R> {
impl<R> Circuit for ConstraintGenerator<R>
where
R: BitzConstraintRing + SignedLimbCapacity,
{
type Bool = BoolLinearCombination;
type Coefficient<const LIMBS: usize> = R;
type Z<const LIMBS: usize> = LinearCombination<R>;
Expand All @@ -774,6 +823,7 @@ impl<R: BitzConstraintRing> Circuit for ConstraintGenerator<R> {
+ Sync
+ 'static,
{
const { assert_signed_limb_capacity::<R>(LIMBS) };
assert_eq!(M, N.div_ceil(64), "incorrect packed limb count");
let first = self.next_boolean_witness;
self.next_boolean_witness = self
Expand All @@ -786,13 +836,15 @@ impl<R: BitzConstraintRing> Circuit for ConstraintGenerator<R> {
}

fn bitz<const LIMBS: usize>(&mut self, value: BoolLinearCombination) -> LinearCombination<R> {
const { assert_signed_limb_capacity::<R>(LIMBS) };
LinearCombination::witness(self.record_bitz(value))
}

fn bitz_unsigned<const LIMBS: usize, const N: usize, const M: usize, const LOW: usize>(
&mut self,
bits_le: &ScalarBits<BoolLinearCombination, N>,
) -> (LinearCombination<R>, LinearCombination<R>) {
const { assert_signed_limb_capacity::<R>(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.
Expand All @@ -818,13 +870,15 @@ impl<R: BitzConstraintRing> Circuit for ConstraintGenerator<R> {
b: LinearCombination<R>,
c: LinearCombination<R>,
) {
const { assert_signed_limb_capacity::<R>(LIMBS) };
self.r1cs.push((a, b, c));
}

fn sign_extend_z<const FROM_LIMBS: usize, const TO_LIMBS: usize>(
&mut self,
value: LinearCombination<R>,
) -> LinearCombination<R> {
const { assert_signed_limb_capacity::<R>(TO_LIMBS) };
assert!(
TO_LIMBS >= FROM_LIMBS,
"cannot sign-extend into fewer limbs"
Expand Down Expand Up @@ -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::<i64>::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::<i64>::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::<R>::coefficient_from_le_words::<4>(&words),
expected
);
assert!(ConstraintGenerator::<R>::coefficient_from_le_words::<1>(&[]).is_zero());
}

#[test]
fn sparse_constructors_reject_unsorted_and_out_of_bounds_columns() {
assert_eq!(
Expand Down
16 changes: 9 additions & 7 deletions crates/circuit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ pub trait WitnessContext<ZW, BW, C> {
/// witness type so backends are not forced to use integers for coefficients.
pub trait Coefficient:
Clone
+ From<u64>
+ From<u32>
+ Zero
+ One
+ Add<Output = Self>
Expand All @@ -111,7 +111,7 @@ pub trait Coefficient:

impl<T> Coefficient for T where
T: Clone
+ From<u64>
+ From<u32>
+ Zero
+ One
+ Add<Output = T>
Expand Down Expand Up @@ -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<const LIMBS: usize>(words: &[u64]) -> Self::Coefficient<LIMBS> {
// 32-bit halves: `Coefficient` only promises `From<u32>`.
let mut radix = Self::Coefficient::<LIMBS>::one();
for _ in 0..64 {
for _ in 0..32 {
radix += radix.clone();
}
words
.iter()
.rev()
.fold(Self::Coefficient::<LIMBS>::zero(), |value, word| {
value * radix.clone() + Self::Coefficient::<LIMBS>::from(*word)
.flat_map(|word| [(word >> 32) as u32, *word as u32])
.fold(Self::Coefficient::<LIMBS>::zero(), |value, half| {
value * radix.clone() + Self::Coefficient::<LIMBS>::from(half)
})
}

Expand Down Expand Up @@ -592,8 +594,8 @@ mod tests {
}
}

impl From<u64> for Mod7 {
fn from(value: u64) -> Self {
impl From<u32> for Mod7 {
fn from(value: u32) -> Self {
Self::new((value % 7) as i32)
}
}
Expand Down
49 changes: 48 additions & 1 deletion crates/circuit/src/matrix_products.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<const LIMBS: usize> From<&Integer<LIMBS>> for StoredInteger {
/// Stores a gadget-local fixed integer without changing its value.
fn from(value: &Integer<LIMBS>) -> Self {
Expand Down Expand Up @@ -464,14 +483,15 @@ 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
.a_mw
.par_iter()
.zip(&self.b_mw)
.zip(&self.c_mw)
.all(|((a, b), c)| R::from(a) * R::from(b) == R::from(c))
.all(|((a, b), c)| R::from(a).checked_mul(&R::from(b)) == Some(R::from(c)))
}

/// Reduces every materialized element modulo `modulus`.
Expand Down Expand Up @@ -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::<i64>());
// 2^32 * 2^32 wraps to zero in an i64.
products.push(z(1 << 32), z(1 << 32), z(0));
assert!(!products.is_satisfied::<i64>());
assert!(!products.is_satisfied::<R>());
}

#[test]
fn arbitrary_bigints_round_trip_through_stored_integers() {
let boundary = R::one() << 128_usize;
Expand Down
2 changes: 1 addition & 1 deletion crates/circuit/src/matrix_transpose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ impl Circuit for MTransposeGenerator {

fn bitz<const LIMBS: usize>(&mut self, value: MatrixBit) -> Z<LIMBS> {
self.recorder.push_row(&value);
Z::from(u64::from(value.constant_term()))
Z::from(u32::from(value.constant_term()))
}

fn bitz_unsigned<const LIMBS: usize, const N: usize, const M: usize, const LOW: usize>(
Expand Down
Loading
Loading