Skip to content

feat: basic constraint setup #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Feb 10, 2025
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