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
583 changes: 568 additions & 15 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[workspace]
members = ["multi-plonk"]

[package]
name = "multi-stark"
version = "0.1.0"
Expand Down
61 changes: 61 additions & 0 deletions multi-plonk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[package]
name = "multi-plonk"
version = "0.1.0"
edition = "2024"
authors = ["Argument Engineering <engineering@argument.xyz>"]
license = "MIT OR Apache-2.0"
rust-version = "1.88"

[dependencies]
ark-bls12-381 = { version = "0.5", features = ["curve"] }
ark-ec = "0.5"
ark-ff = "0.5"
ark-poly = "0.5"
ark-std = "0.5"
ark-crypto-primitives = { version = "0.5", features = ["sponge"] }
ark-poly-commit = "0.5"
ark-serialize = "0.5"

[lints.clippy]
cast_lossless = "warn"
cast_possible_truncation = "warn"
cast_precision_loss = "warn"
cast_sign_loss = "warn"
cast_possible_wrap = "warn"
ptr_as_ptr = "warn"
checked_conversions = "warn"
dbg_macro = "warn"
derive_partial_eq_without_eq = "warn"
enum_glob_use = "warn"
explicit_into_iter_loop = "warn"
fallible_impl_from = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
from_iter_instead_of_collect = "warn"
implicit_clone = "warn"
inefficient_to_string = "warn"
large_stack_arrays = "warn"
large_types_passed_by_value = "warn"
macro_use_imports = "warn"
manual_assert = "warn"
map_err_ignore = "warn"
map_unwrap_or = "warn"
match_same_arms = "warn"
match_wild_err_arm = "warn"
needless_continue = "warn"
needless_for_each = "warn"
needless_pass_by_value = "warn"
option_option = "warn"
same_functions_in_if_condition = "warn"
trait_duplication_in_bounds = "warn"
unnecessary_wraps = "warn"
unnested_or_patterns = "warn"
use_self = "warn"

[lints.rust]
nonstandard_style = "warn"
rust_2024_compatibility = "warn"
trivial_numeric_casts = "warn"
unused_lifetimes = "warn"
unused_qualifications = "warn"
unreachable_pub = "warn"
166 changes: 166 additions & 0 deletions multi-plonk/examples/lookup_proof.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//! Multi-circuit PLONK example with lookup arguments.
//!
//! Mirrors the multi-stark `lookup_proof` example. Two circuits (Even, Odd)
//! compute parity via a recursive lookup: Even(n) → Odd(n-1), Odd(n) → Even(n-1).
//! The claim encodes the initial query: is_even(4) == 1.
//!
//! Run with:
//! ```sh
//! cargo run --example lookup_proof --release
//! ```

use ark_ff::{Field, One, Zero};
use ark_serialize::CanonicalSerialize;
use ark_std::test_rng;

use multi_plonk::air::{Air, AirBuilder, BaseAir};
use multi_plonk::builder::symbolic::{SymbolicExpression, var};
use multi_plonk::lookup::{Lookup, LookupAir};
use multi_plonk::matrix::Matrix;
use multi_plonk::system::{System, SystemWitness};
use multi_plonk::types::{PlonkConfig, Val};

/// Width: [multiplicity, input, input_inverse, input_is_zero, input_not_zero, recursion_output]
enum ParityAir {
Even,
Odd,
}

impl ParityAir {
fn lookups(&self) -> Vec<Lookup<SymbolicExpression>> {
let multiplicity = var(0);
let input = var(1);
let input_is_zero = var(3);
let input_not_zero = var(4);
let recursion_output = var(5);
let even_index: SymbolicExpression = Val::zero().into();
let odd_index: SymbolicExpression = Val::one().into();
let one: SymbolicExpression = Val::one().into();
match self {
Self::Even => vec![
Lookup::pull(
multiplicity,
vec![
even_index,
input.clone(),
input_not_zero.clone() * recursion_output.clone() + input_is_zero,
],
),
Lookup::push(
input_not_zero,
vec![odd_index, input - one, recursion_output],
),
],
Self::Odd => vec![
Lookup::pull(
multiplicity,
vec![
odd_index,
input.clone(),
input_not_zero.clone() * recursion_output.clone(),
],
),
Lookup::push(
input_not_zero,
vec![even_index, input - one, recursion_output],
),
],
}
}
}

impl BaseAir for ParityAir {
fn width(&self) -> usize {
6
}
}

impl<AB> Air<AB> for ParityAir
where
AB: AirBuilder,
AB::Var: Copy,
{
fn eval(&self, builder: &mut AB) {
let local = builder.main_local();
let multiplicity = local[0];
let input = local[1];
let input_inverse = local[2];
let input_is_zero = local[3];
let input_not_zero = local[4];
builder.assert_bool(input_is_zero);
builder.assert_bool(input_not_zero);
builder
.when(multiplicity)
.assert_one(input_is_zero.into() + input_not_zero.into());
builder.when(input_is_zero).assert_zero(input);
builder
.when(input_not_zero)
.assert_one(input.into() * input_inverse.into());
}
}

fn main() {
let mut rng = test_rng();
// max_constraint_degree = 3 (not_zero * input * input_inverse - 1)
// trace size = 4, q_factor = next_pow2(3) = 4, quotient_degree ≤ 3*4 = 12
let config = PlonkConfig::setup(64, &mut rng);

let even = LookupAir::new(ParityAir::Even, ParityAir::Even.lookups());
let odd = LookupAir::new(ParityAir::Odd, ParityAir::Odd.lookups());
let (system, key) = System::new(&config, [even, odd]);

let f = |x: u64| Val::from(x);
// When input = 0 the constraint is gated by input_not_zero = 0, so the
// inverse column is left as 0 (any value is valid).
let inv = |x: u64| -> Val {
if x == 0 {
Val::zero()
} else {
Val::from(x).inverse().unwrap()
}
};

#[rustfmt::skip]
let witness = SystemWitness::from_stage_1(
vec![
// Even circuit trace (4 rows × 6 cols):
// [multiplicity, input, input_inverse, input_is_zero, input_not_zero, recursion_output]
Matrix::new(
vec![
f(1), f(4), inv(4), f(0), f(1), f(1),
f(1), f(2), inv(2), f(0), f(1), f(1),
f(1), f(0), f(0), f(1), f(0), f(0),
f(0), f(0), f(0), f(0), f(0), f(0),
],
6,
),
// Odd circuit trace (4 rows × 6 cols)
Matrix::new(
vec![
f(1), f(3), inv(3), f(0), f(1), f(1),
f(1), f(1), inv(1), f(0), f(1), f(1),
f(0), f(0), f(0), f(0), f(0), f(0),
f(0), f(0), f(0), f(0), f(0), f(0),
],
6,
),
],
&system,
);

// claim: [circuit_index=0, input=4, output=1] — is_even(4) should be 1
let claim = &[f(0), f(4), f(1)];
let proof = system.prove(&config, &key, claim, &witness);
system
.verify(&config, claim, &proof)
.expect("verify failed");
println!("Lookup proof verified successfully!");

let mut bytes = vec![];
proof.serialize_uncompressed(&mut bytes).expect("serialize");
println!(
"Proof size: {} bytes (uncompressed), {} bytes (compressed)",
bytes.len(),
proof.compressed_size()
);
}
130 changes: 130 additions & 0 deletions multi-plonk/examples/preprocessed_proof.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Example with preprocessed trace and lookups, ported to multi-plonk.
//!
//! Two circuits:
//! - **RangeTable**: read-only bytes 0..256 committed as a preprocessed trace.
//! Each row pulls a lookup weighted by its multiplicity column.
//! - **Squares**: computes x * x and range-checks both bytes of the result via
//! lookup pushes into RangeTable.
//!
//! Run with:
//! ```sh
//! cargo run --example preprocessed_proof --release
//! ```

use ark_ff::{One, Zero};
use ark_serialize::CanonicalSerialize;
use ark_std::test_rng;

use multi_plonk::air::{Air, AirBuilder, BaseAir};
use multi_plonk::builder::symbolic::{SymbolicExpression, preprocessed_var, var};
use multi_plonk::lookup::{Lookup, LookupAir};
use multi_plonk::matrix::Matrix;
use multi_plonk::system::{System, SystemWitness};
use multi_plonk::types::{PlonkConfig, Val};

enum SquaresCS {
/// Preprocessed column: bytes 0..256. Main column: multiplicity.
RangeTable,
/// Columns: [x, x², low_byte, high_byte, multiplicity].
Squares,
}

impl BaseAir for SquaresCS {
fn width(&self) -> usize {
match self {
Self::RangeTable => 1,
Self::Squares => 5,
}
}

fn preprocessed_trace(&self) -> Option<Matrix<Val>> {
match self {
Self::RangeTable => Some(Matrix::new((0u64..256).map(Val::from).collect(), 1)),
Self::Squares => None,
}
}
}

impl<AB> Air<AB> for SquaresCS
where
AB: AirBuilder,
AB::Var: Copy,
{
fn eval(&self, builder: &mut AB) {
match self {
Self::RangeTable => {}
Self::Squares => {
let local = builder.main_local();
let x = local[0];
let x_sq = local[1];
let low = local[2];
let high = local[3];
// x² == x * x
builder.assert_eq(x_sq, x.into() * x.into());
// x² == low + 256 * high (byte decomposition)
let c256: AB::Expr = Val::from(256u64).into();
builder.assert_eq(x_sq, low.into() + high.into() * c256);
}
}
}
}

impl SquaresCS {
fn lookups(&self) -> Vec<Lookup<SymbolicExpression>> {
match self {
// RangeTable pulls: multiplicity × (preprocessed byte value)
Self::RangeTable => vec![Lookup::pull(var(0), vec![preprocessed_var(0)])],
// Squares pushes each byte of x² into the range table
Self::Squares => vec![
Lookup::push(var(4), vec![var(2)]), // low byte
Lookup::push(var(4), vec![var(3)]), // high byte
],
}
}
}

fn main() {
let mut rng = test_rng();
// max_constraint_degree = 2 (x² = x*x), trace size up to 256 rows,
// q_factor = 2, quotient_degree ≤ 256, preprocessed polys degree ≤ 255
let config = PlonkConfig::setup(512, &mut rng);

let range_table = LookupAir::new(SquaresCS::RangeTable, SquaresCS::RangeTable.lookups());
let squares = LookupAir::new(SquaresCS::Squares, SquaresCS::Squares.lookups());
let (system, key) = System::new(&config, [range_table, squares]);

let n = 16usize;
let f = |x: usize| Val::from(x as u64);

// Range-table main trace: multiplicity per byte value (256 rows × 1 col)
let mut range_mults = vec![Val::zero(); 256];
// Squares trace: 16 rows × 5 cols
let mut sq_values = Vec::with_capacity(5 * n);
for x in 0..n {
let sq = x * x;
let low = sq & 0xFF;
let high = (sq >> 8) & 0xFF;
sq_values.extend([f(x), f(sq), f(low), f(high), Val::one()]);
range_mults[low] += Val::one();
range_mults[high] += Val::one();
}

let range_trace = Matrix::new(range_mults, 1);
let squares_trace = Matrix::new(sq_values, 5);
let witness = SystemWitness::from_stage_1(vec![range_trace, squares_trace], &system);

let no_claims: &[&[Val]] = &[];
let proof = system.prove_multiple_claims(&config, &key, no_claims, &witness);
system
.verify_multiple_claims(&config, no_claims, &proof)
.expect("verify failed");
println!("Preprocessed proof verified successfully!");

let mut bytes = vec![];
proof.serialize_uncompressed(&mut bytes).expect("serialize");
println!(
"Proof size: {} bytes (uncompressed), {} bytes (compressed)",
bytes.len(),
proof.compressed_size()
);
}
Loading
Loading