Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
9 changes: 8 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 16 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,28 @@ license = "MIT"
name = "custom-constraints"
readme = "README.md"
repository = "https://github.com/autoparallel/custom-constraints"
version = "0.1.1"
version = "0.1.0"

[dependencies]
ark-ff = { version = "0.5", default-features = false, features = ["parallel"] }
ark-ff = { version = "0.5", default-features = false, features = [
"parallel",
"asm",
] }
rayon = { version = "1.10" }

[dev-dependencies]
rstest = { version = "0.24", default-features = false }

ark-std = { version = "0.5", default-features = false, features = ["std"] }
rand = "0.8"
rstest = { version = "0.24", default-features = false }

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
getrandom = { version = "0.2", features = ["js"] }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-test = { version = "0.3" }

[profile.release]
codegen-units = 1
lto = "fat"
opt-level = 3
panic = "abort"
strip = true
96 changes: 96 additions & 0 deletions benches/matrix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#![feature(test)]
extern crate test;

use ark_ff::{Fp256, MontBackend, MontConfig};
use custom_constraints::matrix::SparseMatrix;
use test::Bencher;

// Define a large prime field for testing
#[derive(MontConfig)]
#[modulus = "52435875175126190479447740508185965837690552500527637822603658699938581184513"]
#[generator = "7"]
pub struct FqConfig;
pub type Fq = Fp256<MontBackend<FqConfig, 4>>;

// Helper function to create a random field element
fn random_field_element() -> Fq {
// Create a random field element using ark_ff's random generation
use ark_ff::UniformRand;
let mut rng = ark_std::test_rng();
Fq::rand(&mut rng)
}

// Helper to create a sparse matrix with given density
fn create_sparse_matrix(rows: usize, cols: usize, density: f64) -> SparseMatrix<Fq> {
let mut row_offsets = vec![0];
let mut col_indices = Vec::new();
let mut values = Vec::new();
let mut current_offset = 0;

for _ in 0..rows {
for j in 0..cols {
if rand::random::<f64>() < density {
col_indices.push(j);
values.push(random_field_element());
current_offset += 1;
}
}
row_offsets.push(current_offset);
}

SparseMatrix::new(row_offsets, col_indices, values, cols)
}

const COLS: usize = 100;
const SMALL: usize = 2_usize.pow(10);
const MEDIUM: usize = 2_usize.pow(15);
const LARGE: usize = 2_usize.pow(20);

// Matrix-vector multiplication benchmarks
#[bench]
fn bench_sparse_matrix_vec_mul_small(b: &mut Bencher) {
let matrix = create_sparse_matrix(SMALL, COLS, 0.1);
let vector: Vec<Fq> = (0..COLS).map(|_| random_field_element()).collect();

b.iter(|| &matrix * &vector);
}

#[bench]
fn bench_sparse_matrix_vec_mul_medium(b: &mut Bencher) {
let matrix = create_sparse_matrix(MEDIUM, COLS, 0.01);
let vector: Vec<Fq> = (0..COLS).map(|_| random_field_element()).collect();

b.iter(|| &matrix * &vector);
}

#[bench]
fn bench_sparse_matrix_vec_mul_large(b: &mut Bencher) {
let matrix = create_sparse_matrix(LARGE, LARGE, 0.01);
let vector: Vec<Fq> = (0..LARGE).map(|_| random_field_element()).collect();

b.iter(|| &matrix * &vector);
}

#[bench]
fn bench_sparse_matrix_hadamard_small(b: &mut Bencher) {
let matrix1 = create_sparse_matrix(SMALL, COLS, 0.1);
let matrix2 = create_sparse_matrix(SMALL, COLS, 0.1);

b.iter(|| &matrix1 * &matrix2);
}

#[bench]
fn bench_sparse_matrix_hadamard_medium(b: &mut Bencher) {
let matrix1 = create_sparse_matrix(MEDIUM, COLS, 0.1);
let matrix2 = create_sparse_matrix(MEDIUM, COLS, 0.1);

b.iter(|| &matrix1 * &matrix2);
}

#[bench]
fn bench_sparse_matrix_hadamard_large(b: &mut Bencher) {
let matrix1 = create_sparse_matrix(LARGE, COLS, 0.1);
let matrix2 = create_sparse_matrix(LARGE, COLS, 0.1);

b.iter(|| &matrix1 * &matrix2);
}
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ build-wasm:
# Run tests for native architecture and wasm
test:
@just header "Running native architecture tests"
cargo test --workspace --all-targets --all-features
cargo test --workspace --tests --all-features
@just header "Running wasm tests"
wasm-pack test --node

Expand Down
File renamed without changes.
211 changes: 211 additions & 0 deletions src/ccs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
//! Implements the Customizable Constraint System (CCS) format.
//!
//! A CCS represents arithmetic constraints through a combination of matrices
//! and multisets, allowing efficient verification of arithmetic computations.
//!
//! The system consists of:
//! - A set of sparse matrices representing linear combinations
//! - Multisets defining which matrices participate in each constraint
//! - Constants applied to each constraint term

use matrix::SparseMatrix;

use super::*;

/// A Customizable Constraint System over a field F.
#[derive(Debug, Default)]
pub struct CCS<F: Field> {
/// Constants for each constraint term
pub constants: Vec<F>,
/// Sets of matrix indices for Hadamard products
pub multisets: Vec<Vec<usize>>,
/// Constraint matrices
pub matrices: Vec<SparseMatrix<F>>,
}

impl<F: Field + std::fmt::Debug> CCS<F> {
/// Creates a new empty CCS.
pub fn new() -> Self {
Self::default()
}

/// Checks if a witness and public input satisfy the constraint system.
///
/// Forms vector z = (w, 1, x) and verifies that all constraints are satisfied.
///
/// # Arguments
/// * `w` - The witness vector
/// * `x` - The public input vector
///
/// # Returns
/// `true` if all constraints are satisfied, `false` otherwise
pub fn is_satisfied(&self, w: &[F], x: &[F]) -> bool {
// Construct z = (w, 1, x)
let mut z = Vec::with_capacity(w.len() + 1 + x.len());
z.extend(w.iter().copied());
z.push(F::ONE);
z.extend(x.iter().copied());

// Compute all matrix-vector products
let products: Vec<Vec<F>> = self
.matrices
.iter()
.enumerate()
.map(|(i, matrix)| {
let result = matrix * &z;
println!("M{i} · z = {result:?}");
result
})
.collect();

// For each row in the output...
let m = if let Some(first) = products.first() {
first.len()
} else {
return true; // No constraints
};

// For each output coordinate...
for row in 0..m {
let mut sum = F::ZERO;

// For each constraint...
for (i, multiset) in self.multisets.iter().enumerate() {
let mut term = products[multiset[0]][row];

for &idx in multiset.iter().skip(1) {
term *= products[idx][row];
}

let contribution = self.constants[i] * term;
sum += contribution;
}

if sum != F::ZERO {
return false;
}
}

true
}

/// Creates a new CCS configured for constraints up to the given degree.
///
/// # Arguments
/// * `d` - Maximum degree of constraints
///
/// # Panics
/// If d < 2
pub fn new_degree(d: usize) -> Self {
assert!(d >= 2, "Degree must be positive");

let mut ccs = Self { constants: Vec::new(), multisets: Vec::new(), matrices: Vec::new() };

// We'll create terms starting from highest degree down to degree 1
// For a degree d CCS, we need terms of all degrees from d down to 1
let mut next_matrix_index = 0;

// Handle each degree from d down to 1
for degree in (1..=d).rev() {
// For a term of degree k, we need k matrices Hadamard multiplied
let matrix_indices: Vec<usize> = (0..degree).map(|i| next_matrix_index + i).collect();

// Add this term's multiset and its coefficient
ccs.multisets.push(matrix_indices);
ccs.constants.push(F::ONE);

// Update our tracking of matrix indices
next_matrix_index += degree;
}

// Calculate total number of matrices needed:
// For degree d, we need d + (d-1) + ... + 1 matrices
// This is the triangular number formula: n(n+1)/2
let total_matrices = (d * (d + 1)) / 2;

// Initialize empty matrices - their content will be filled during conversion
for _ in 0..total_matrices {
ccs.matrices.push(SparseMatrix::new_rows_cols(1, 0));
}

ccs
}
}

impl<F: Field + Display> Display for CCS<F> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "Customizable Constraint System:")?;

// First, display all matrices with their indices
writeln!(f, "\nMatrices:")?;
for (i, matrix) in self.matrices.iter().enumerate() {
writeln!(f, "M{i} =")?;
writeln!(f, "{matrix}")?;
}

// Show how constraints are formed from multisets and constants
writeln!(f, "\nConstraints:")?;

// We expect multisets to come in pairs, each pair forming one constraint
for i in 0..self.multisets.len() {
// Write the constant for the first multiset
write!(f, "{}·(", self.constants[i])?;

// Write the Hadamard product for the first multiset
if let Some(first_idx) = self.multisets[i].first() {
write!(f, "M{first_idx}")?;
for &idx in &self.multisets[i][1..] {
write!(f, "∘M{idx}")?;
}
}
write!(f, ")")?;

// Sum up the expressions to the last one
if i < self.multisets.len() - 1 {
write!(f, " + ")?;
}
}
writeln!(f, " = 0")?;
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::mock::F17;

#[test]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
fn test_ccs_satisfaction() {
println!("\nSetting up CCS for constraint x * y = z");

// For z = (y, z, 1, x), create matrices:
let mut m1 = SparseMatrix::new_rows_cols(1, 4);
m1.write(0, 3, F17::ONE); // Select x
let mut m2 = SparseMatrix::new_rows_cols(1, 4);
m2.write(0, 0, F17::ONE); // Select y
let mut m3 = SparseMatrix::new_rows_cols(1, 4);
m3.write(0, 1, F17::ONE); // Select z

println!("Created matrices:");
println!("M1 (selects x): {m1:?}");
println!("M2 (selects y): {m2:?}");
println!("M3 (selects z): {m3:?}");

let mut ccs = CCS::new();
ccs.matrices = vec![m1, m2, m3];
// Encode x * y - z = 0
ccs.multisets = vec![vec![0, 1], vec![2]];
ccs.constants = vec![F17::ONE, F17::from(-1)];

println!("\nTesting valid case: x=2, y=3, z=6");
let x = vec![F17::from(2)]; // public input x = 2
let w = vec![F17::from(3), F17::from(6)]; // witness y = 3, z = 6
assert!(ccs.is_satisfied(&w, &x));

println!("\nTesting invalid case: x=2, y=3, z=7");
let w_invalid = vec![F17::from(3), F17::from(7)]; // witness y = 3, z = 7 (invalid)
assert!(!ccs.is_satisfied(&w_invalid, &x));
}
}
Loading
Loading