Skip to content

Commit 3785d96

Browse files
authored
feat: circuits + CCS (#2)
1 parent d0bfacb commit 3785d96

File tree

11 files changed

+1851
-17
lines changed

11 files changed

+1851
-17
lines changed

Cargo.lock

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,28 @@ license = "MIT"
77
name = "custom-constraints"
88
readme = "README.md"
99
repository = "https://github.com/autoparallel/custom-constraints"
10-
version = "0.1.1"
10+
version = "0.1.0"
1111

1212
[dependencies]
13-
ark-ff = { version = "0.5", default-features = false, features = ["parallel"] }
13+
ark-ff = { version = "0.5", default-features = false, features = [
14+
"parallel",
15+
"asm",
16+
] }
17+
rayon = { version = "1.10" }
1418

1519
[dev-dependencies]
16-
rstest = { version = "0.24", default-features = false }
17-
20+
ark-std = { version = "0.5", default-features = false, features = ["std"] }
21+
rand = "0.8"
22+
rstest = { version = "0.24", default-features = false }
1823

1924
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
2025
getrandom = { version = "0.2", features = ["js"] }
2126
wasm-bindgen = { version = "0.2" }
2227
wasm-bindgen-test = { version = "0.3" }
28+
29+
[profile.release]
30+
codegen-units = 1
31+
lto = "fat"
32+
opt-level = 3
33+
panic = "abort"
34+
strip = true

benches/matrix.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#![feature(test)]
2+
extern crate test;
3+
4+
use ark_ff::{Fp256, MontBackend, MontConfig};
5+
use custom_constraints::matrix::SparseMatrix;
6+
use test::Bencher;
7+
8+
// Define a large prime field for testing
9+
#[derive(MontConfig)]
10+
#[modulus = "52435875175126190479447740508185965837690552500527637822603658699938581184513"]
11+
#[generator = "7"]
12+
pub struct FqConfig;
13+
pub type Fq = Fp256<MontBackend<FqConfig, 4>>;
14+
15+
// Helper function to create a random field element
16+
fn random_field_element() -> Fq {
17+
// Create a random field element using ark_ff's random generation
18+
use ark_ff::UniformRand;
19+
let mut rng = ark_std::test_rng();
20+
Fq::rand(&mut rng)
21+
}
22+
23+
// Helper to create a sparse matrix with given density
24+
fn create_sparse_matrix(rows: usize, cols: usize, density: f64) -> SparseMatrix<Fq> {
25+
let mut row_offsets = vec![0];
26+
let mut col_indices = Vec::new();
27+
let mut values = Vec::new();
28+
let mut current_offset = 0;
29+
30+
for _ in 0..rows {
31+
for j in 0..cols {
32+
if rand::random::<f64>() < density {
33+
col_indices.push(j);
34+
values.push(random_field_element());
35+
current_offset += 1;
36+
}
37+
}
38+
row_offsets.push(current_offset);
39+
}
40+
41+
SparseMatrix::new(row_offsets, col_indices, values, cols)
42+
}
43+
44+
const COLS: usize = 100;
45+
const SMALL: usize = 2_usize.pow(10);
46+
const MEDIUM: usize = 2_usize.pow(15);
47+
const LARGE: usize = 2_usize.pow(20);
48+
49+
// Matrix-vector multiplication benchmarks
50+
#[bench]
51+
fn bench_sparse_matrix_vec_mul_small(b: &mut Bencher) {
52+
let matrix = create_sparse_matrix(SMALL, COLS, 0.1);
53+
let vector: Vec<Fq> = (0..COLS).map(|_| random_field_element()).collect();
54+
55+
b.iter(|| &matrix * &vector);
56+
}
57+
58+
#[bench]
59+
fn bench_sparse_matrix_vec_mul_medium(b: &mut Bencher) {
60+
let matrix = create_sparse_matrix(MEDIUM, COLS, 0.01);
61+
let vector: Vec<Fq> = (0..COLS).map(|_| random_field_element()).collect();
62+
63+
b.iter(|| &matrix * &vector);
64+
}
65+
66+
#[bench]
67+
fn bench_sparse_matrix_vec_mul_large(b: &mut Bencher) {
68+
let matrix = create_sparse_matrix(LARGE, LARGE, 0.01);
69+
let vector: Vec<Fq> = (0..LARGE).map(|_| random_field_element()).collect();
70+
71+
b.iter(|| &matrix * &vector);
72+
}
73+
74+
#[bench]
75+
fn bench_sparse_matrix_hadamard_small(b: &mut Bencher) {
76+
let matrix1 = create_sparse_matrix(SMALL, COLS, 0.1);
77+
let matrix2 = create_sparse_matrix(SMALL, COLS, 0.1);
78+
79+
b.iter(|| &matrix1 * &matrix2);
80+
}
81+
82+
#[bench]
83+
fn bench_sparse_matrix_hadamard_medium(b: &mut Bencher) {
84+
let matrix1 = create_sparse_matrix(MEDIUM, COLS, 0.1);
85+
let matrix2 = create_sparse_matrix(MEDIUM, COLS, 0.1);
86+
87+
b.iter(|| &matrix1 * &matrix2);
88+
}
89+
90+
#[bench]
91+
fn bench_sparse_matrix_hadamard_large(b: &mut Bencher) {
92+
let matrix1 = create_sparse_matrix(LARGE, COLS, 0.1);
93+
let matrix2 = create_sparse_matrix(LARGE, COLS, 0.1);
94+
95+
b.iter(|| &matrix1 * &matrix2);
96+
}

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ build-wasm:
123123
# Run tests for native architecture and wasm
124124
test:
125125
@just header "Running native architecture tests"
126-
cargo test --workspace --all-targets --all-features
126+
cargo test --workspace --tests --all-features
127127
@just header "Running wasm tests"
128128
wasm-pack test --node
129129

File renamed without changes.

src/ccs.rs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
//! Implements the Customizable Constraint System (CCS) format.
2+
//!
3+
//! A CCS represents arithmetic constraints through a combination of matrices
4+
//! and multisets, allowing efficient verification of arithmetic computations.
5+
//!
6+
//! The system consists of:
7+
//! - A set of sparse matrices representing linear combinations
8+
//! - Multisets defining which matrices participate in each constraint
9+
//! - Constants applied to each constraint term
10+
11+
use matrix::SparseMatrix;
12+
13+
use super::*;
14+
15+
/// A Customizable Constraint System over a field F.
16+
#[derive(Debug, Default)]
17+
pub struct CCS<F: Field> {
18+
/// Constants for each constraint term
19+
pub constants: Vec<F>,
20+
/// Sets of matrix indices for Hadamard products
21+
pub multisets: Vec<Vec<usize>>,
22+
/// Constraint matrices
23+
pub matrices: Vec<SparseMatrix<F>>,
24+
}
25+
26+
impl<F: Field + std::fmt::Debug> CCS<F> {
27+
/// Creates a new empty CCS.
28+
pub fn new() -> Self {
29+
Self::default()
30+
}
31+
32+
/// Checks if a witness and public input satisfy the constraint system.
33+
///
34+
/// Forms vector z = (w, 1, x) and verifies that all constraints are satisfied.
35+
///
36+
/// # Arguments
37+
/// * `w` - The witness vector
38+
/// * `x` - The public input vector
39+
///
40+
/// # Returns
41+
/// `true` if all constraints are satisfied, `false` otherwise
42+
pub fn is_satisfied(&self, w: &[F], x: &[F]) -> bool {
43+
// Construct z = (w, 1, x)
44+
let mut z = Vec::with_capacity(w.len() + 1 + x.len());
45+
z.extend(w.iter().copied());
46+
z.push(F::ONE);
47+
z.extend(x.iter().copied());
48+
49+
// Compute all matrix-vector products
50+
let products: Vec<Vec<F>> = self
51+
.matrices
52+
.iter()
53+
.enumerate()
54+
.map(|(i, matrix)| {
55+
let result = matrix * &z;
56+
println!("M{i} · z = {result:?}");
57+
result
58+
})
59+
.collect();
60+
61+
// For each row in the output...
62+
let m = if let Some(first) = products.first() {
63+
first.len()
64+
} else {
65+
return true; // No constraints
66+
};
67+
68+
// For each output coordinate...
69+
for row in 0..m {
70+
let mut sum = F::ZERO;
71+
72+
// For each constraint...
73+
for (i, multiset) in self.multisets.iter().enumerate() {
74+
let mut term = products[multiset[0]][row];
75+
76+
for &idx in multiset.iter().skip(1) {
77+
term *= products[idx][row];
78+
}
79+
80+
let contribution = self.constants[i] * term;
81+
sum += contribution;
82+
}
83+
84+
if sum != F::ZERO {
85+
return false;
86+
}
87+
}
88+
89+
true
90+
}
91+
92+
/// Creates a new CCS configured for constraints up to the given degree.
93+
///
94+
/// # Arguments
95+
/// * `d` - Maximum degree of constraints
96+
///
97+
/// # Panics
98+
/// If d < 2
99+
pub fn new_degree(d: usize) -> Self {
100+
assert!(d >= 2, "Degree must be positive");
101+
102+
let mut ccs = Self { constants: Vec::new(), multisets: Vec::new(), matrices: Vec::new() };
103+
104+
// We'll create terms starting from highest degree down to degree 1
105+
// For a degree d CCS, we need terms of all degrees from d down to 1
106+
let mut next_matrix_index = 0;
107+
108+
// Handle each degree from d down to 1
109+
for degree in (1..=d).rev() {
110+
// For a term of degree k, we need k matrices Hadamard multiplied
111+
let matrix_indices: Vec<usize> = (0..degree).map(|i| next_matrix_index + i).collect();
112+
113+
// Add this term's multiset and its coefficient
114+
ccs.multisets.push(matrix_indices);
115+
ccs.constants.push(F::ONE);
116+
117+
// Update our tracking of matrix indices
118+
next_matrix_index += degree;
119+
}
120+
121+
// Calculate total number of matrices needed:
122+
// For degree d, we need d + (d-1) + ... + 1 matrices
123+
// This is the triangular number formula: n(n+1)/2
124+
let total_matrices = (d * (d + 1)) / 2;
125+
126+
// Initialize empty matrices - their content will be filled during conversion
127+
for _ in 0..total_matrices {
128+
ccs.matrices.push(SparseMatrix::new_rows_cols(1, 0));
129+
}
130+
131+
ccs
132+
}
133+
}
134+
135+
impl<F: Field + Display> Display for CCS<F> {
136+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
137+
writeln!(f, "Customizable Constraint System:")?;
138+
139+
// First, display all matrices with their indices
140+
writeln!(f, "\nMatrices:")?;
141+
for (i, matrix) in self.matrices.iter().enumerate() {
142+
writeln!(f, "M{i} =")?;
143+
writeln!(f, "{matrix}")?;
144+
}
145+
146+
// Show how constraints are formed from multisets and constants
147+
writeln!(f, "\nConstraints:")?;
148+
149+
// We expect multisets to come in pairs, each pair forming one constraint
150+
for i in 0..self.multisets.len() {
151+
// Write the constant for the first multiset
152+
write!(f, "{}·(", self.constants[i])?;
153+
154+
// Write the Hadamard product for the first multiset
155+
if let Some(first_idx) = self.multisets[i].first() {
156+
write!(f, "M{first_idx}")?;
157+
for &idx in &self.multisets[i][1..] {
158+
write!(f, "∘M{idx}")?;
159+
}
160+
}
161+
write!(f, ")")?;
162+
163+
// Sum up the expressions to the last one
164+
if i < self.multisets.len() - 1 {
165+
write!(f, " + ")?;
166+
}
167+
}
168+
writeln!(f, " = 0")?;
169+
Ok(())
170+
}
171+
}
172+
173+
#[cfg(test)]
174+
mod tests {
175+
use super::*;
176+
use crate::mock::F17;
177+
178+
#[test]
179+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
180+
fn test_ccs_satisfaction() {
181+
println!("\nSetting up CCS for constraint x * y = z");
182+
183+
// For z = (y, z, 1, x), create matrices:
184+
let mut m1 = SparseMatrix::new_rows_cols(1, 4);
185+
m1.write(0, 3, F17::ONE); // Select x
186+
let mut m2 = SparseMatrix::new_rows_cols(1, 4);
187+
m2.write(0, 0, F17::ONE); // Select y
188+
let mut m3 = SparseMatrix::new_rows_cols(1, 4);
189+
m3.write(0, 1, F17::ONE); // Select z
190+
191+
println!("Created matrices:");
192+
println!("M1 (selects x): {m1:?}");
193+
println!("M2 (selects y): {m2:?}");
194+
println!("M3 (selects z): {m3:?}");
195+
196+
let mut ccs = CCS::new();
197+
ccs.matrices = vec![m1, m2, m3];
198+
// Encode x * y - z = 0
199+
ccs.multisets = vec![vec![0, 1], vec![2]];
200+
ccs.constants = vec![F17::ONE, F17::from(-1)];
201+
202+
println!("\nTesting valid case: x=2, y=3, z=6");
203+
let x = vec![F17::from(2)]; // public input x = 2
204+
let w = vec![F17::from(3), F17::from(6)]; // witness y = 3, z = 6
205+
assert!(ccs.is_satisfied(&w, &x));
206+
207+
println!("\nTesting invalid case: x=2, y=3, z=7");
208+
let w_invalid = vec![F17::from(3), F17::from(7)]; // witness y = 3, z = 7 (invalid)
209+
assert!(!ccs.is_satisfied(&w_invalid, &x));
210+
}
211+
}

0 commit comments

Comments
 (0)