Skip to content
Merged
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
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Changelog

All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- **Base/Extension field support**: `multilinear_sumcheck` and `inner_product_sumcheck` now take two type parameters `<BF, EF>` — base field for evaluations, extension field for challenges. Set `EF = BF` when no extension is needed.
- `pairwise::cross_field_reduce` — parallel helper for folding `BF` evaluations with an `EF` challenge.

## [0.0.2] - 2026-02-11

### Added
- **High-level API**: `multilinear_sumcheck()` and `inner_product_sumcheck()` free functions for simple one-call sumcheck.
- **Fiat–Shamir support**: `Transcript` trait with `SpongefishTranscript` (real Fiat-Shamir via [SpongeFish](https://github.com/arkworks-rs/spongefish)) and `SanityTranscript` (random challenges for testing).
- `batched_constraint_poly` — merges dense and sparse polynomials into a single constraint polynomial.
- **Pairwise reduce for `TimeProductProver`** ([#87](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/87)).
- **Improved order strategies** ([#86](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/86)).
- **Pairwise compression for `TimeProver`** ([#83](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/83)).
- `BasicProver` for testing ([#84](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/84)).

### Changed
- Removed `claim` from the `Prover` trait ([#85](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/85)).
- Updated to criterion 0.8 ([#88](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/88)).

## [0.0.1] - 2025-08-26

### Added
- **Rayon parallelization** for `TimeProver` and `TimeProductProver` ([#80](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/80)).
- **Stream iterator** for sequential evaluation access ([#74](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/74)).
- **Benchmark improvements** ([#75](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/75), [#76](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/76)).
- `FileStream` for memory-mapped file-backed evaluations ([#67](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/67)).
- **Blendy product prover** — `BlendyProductProver` ([#64](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/64)).
- Refactored module structure ([#63](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/63)).

### Changed
- Bumped arkworks dependencies from 0.4 → 0.5.
- Avoid dynamic dispatch (`dyn`) and heap allocation (`Box`) ([#53](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/53)).
- Gray code ordering for sequential Lagrange polynomial iterator ([#55](https://github.com/compsec-epfl/space-efficient-sumcheck/pull/55)).

## [0.0.0] - 2024-02-09

### Added
- Initial release with three proving algorithms:
- **`SpaceProver`** — quasi-linear time, logarithmic space [[CTY11](https://arxiv.org/pdf/1109.6882.pdf)].
- **`TimeProver`** — linear time, linear space [[VSBW13](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=6547112)].
- **`BlendyProver`** — linear time, sublinear space [[CFFZ24](https://eprint.iacr.org/2024/524.pdf)].
- Product sumcheck variants (`SpaceProductProver`, `TimeProductProver`, `BlendyProductProver`).
- `MemoryStream` for in-memory evaluation access.
- Hypercube utilities and Lagrange polynomial interpolation.
- Configurable reduction modes (pairwise, variablewise).
- Benchmark suite using criterion.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This library exposes two high-level functions:
1) [`multilinear_sumcheck`](multilinear_sumcheck) and
2) [`inner_product_sumcheck`](inner_product_sumcheck).

Both are parameterized by two field types: `BF` (base field, of the evaluations) and `EF` (extension field, of the challenges). When no extension field is needed, set `EF = BF`.

Using [SpongeFish](https://github.com/arkworks-rs/spongefish) (or similar Fiat-Shamir interface) simply call the functions with the prover state:

### Multilinear Sumcheck
Expand All @@ -18,9 +20,9 @@ $claim = \sum_{x \in \{0,1\}^n} p(x)$
use efficient_sumcheck::{multilinear_sumcheck, Sumcheck};
use efficient_sumcheck::transcript::SanityTranscript;

let mut evals_p_01n: Vec<F> = /* ... */;
let mut evals_p_01n: Vec<BF> = /* ... */;
let mut prover_state = SanityTranscript::new(&mut rng);
let sumcheck_transcript: Sumcheck<F> = multilinear_sumcheck(
let sumcheck_transcript: Sumcheck<EF> = multilinear_sumcheck::<BF, EF>(
&mut evals_p_01n,
&mut prover_state
);
Expand All @@ -33,10 +35,10 @@ $claim = \sum_{x \in \{0,1\}^n} f(x) \cdot g(x)$
use efficient_sumcheck::{inner_product_sumcheck, ProductSumcheck};
use efficient_sumcheck::transcript::SanityTranscript;

let mut evals_f_01n: Vec<F> = /* ... */;
let mut evals_g_01n: Vec<F> = /* ... */;
let mut evals_f_01n: Vec<BF> = /* ... */;
let mut evals_g_01n: Vec<BF> = /* ... */;
let mut prover_state = SanityTranscript::new(&mut rng);
let sumcheck_transcript: ProductSumcheck<F> = inner_product_sumcheck(
let sumcheck_transcript: ProductSumcheck<EF> = inner_product_sumcheck::<BF, EF>(
&mut evals_f_01n,
&mut evals_g_01n,
&mut prover_state
Expand All @@ -54,7 +56,7 @@ Using Efficient Sumcheck this reduces to six lines of code and brings paralleliz
```rust
use efficient_sumcheck::{inner_product_sumcheck, batched_constraint_poly};

let alpha = inner_product_sumcheck(
let alpha = inner_product_sumcheck::<BF, EF>(
&mut f,
&mut batched_constraint_poly(&dense_evals, &sparse_evals),
&mut transcript,
Expand Down
74 changes: 54 additions & 20 deletions src/inner_product_sumcheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
//! `n` rounds of the product sumcheck protocol computing `∑_x f(x)·g(x)`, and returns
//! the resulting [`ProductSumcheck`] transcript.
//!
//! The function is parameterized by two field types:
//! - `BF` (base field): the field the evaluations live in
//! - `EF` (extension field): the field challenges are sampled from
//!
//! When no extension field is needed, set `EF = BF`.
//!
//! # Example
//!
//! ```ignore
//! ```text
//! use efficient_sumcheck::{inner_product_sumcheck, ProductSumcheck};
//! use efficient_sumcheck::transcript::SanityTranscript;
//!
//! // No extension field (BF = EF):
//! let mut f = vec![F::from(1), F::from(2), F::from(3), F::from(4)];
//! let mut g = vec![F::from(5), F::from(6), F::from(7), F::from(8)];
//! let mut transcript = SanityTranscript::new(&mut rng);
Expand Down Expand Up @@ -61,49 +68,76 @@ pub fn batched_constraint_poly<F: Field>(
/// Run the inner product sumcheck protocol over two evaluation vectors,
/// using a generic [`Transcript`] for Fiat-Shamir (or sanity/random challenges).
///
/// `BF` is the base field of the evaluations, `EF` is the extension field for challenges.
/// When `BF = EF`, this is the standard single-field inner product sumcheck.
/// When `BF ≠ EF`, round 0 evaluates in `BF` and lifts to `EF`, then subsequent
/// rounds work entirely in `EF`.
///
/// Each round:
/// 1. Computes the round polynomial evaluations `(s(0), s(1), s(2))` via the product prover.
/// 2. Writes them to the transcript (3 field elements).
/// 3. Reads the verifier's challenge from the transcript (1 field element).
/// 4. Reduces both evaluation vectors by folding with the challenge.
pub fn inner_product_sumcheck<F: Field>(
f: &mut Vec<F>,
g: &mut Vec<F>,
transcript: &mut impl Transcript<F>,
) -> ProductSumcheck<F> {
pub fn inner_product_sumcheck<BF: Field, EF: Field + From<BF>>(
f: &mut [BF],
g: &mut [BF],
transcript: &mut impl Transcript<EF>,
) -> ProductSumcheck<EF> {
// checks
assert_eq!(f.len(), g.len());
assert!(f.len().count_ones() == 1);

// initialize
let num_rounds = f.len().trailing_zeros() as usize;
let mut prover_messages: Vec<(F, F, F)> = vec![];
let mut verifier_messages: Vec<F> = vec![];
let mut prover_messages: Vec<(EF, EF, EF)> = vec![];
let mut verifier_messages: Vec<EF> = vec![];

// all rounds
for _ in 0..num_rounds {
// ── Round 0: evaluate in BF, lift to EF, cross-field reduce ──
if num_rounds > 0 {
let mut prover = TimeProductProver::new(TimeProductProverConfig::new(
f.len().trailing_zeros() as usize,
vec![MemoryStream::new(f.to_vec()), MemoryStream::new(g.to_vec())],
ReduceMode::Pairwise,
));

// call the prover
let msg = prover.next_message(None).unwrap();
let msg_bf = prover.next_message(None).unwrap();
let msg = (EF::from(msg_bf.0), EF::from(msg_bf.1), EF::from(msg_bf.2));

// write transcript
prover_messages.push(msg);
transcript.write(msg.0);
transcript.write(msg.1);
transcript.write(msg.2);

// read the transcript
let chg = transcript.read();
verifier_messages.push(chg);

// reduce
pairwise::reduce_evaluations(f, chg);
pairwise::reduce_evaluations(g, chg);
// Cross-field reduce: BF evaluations + EF challenge → Vec<EF>
let mut ef_f = pairwise::cross_field_reduce(f, chg);
let mut ef_g = pairwise::cross_field_reduce(g, chg);

// Remaining rounds work in EF
for _ in 1..num_rounds {
let mut prover = TimeProductProver::new(TimeProductProverConfig::new(
ef_f.len().trailing_zeros() as usize,
vec![
MemoryStream::new(ef_f.to_vec()),
MemoryStream::new(ef_g.to_vec()),
],
ReduceMode::Pairwise,
));

let msg = prover.next_message(None).unwrap();

prover_messages.push(msg);
transcript.write(msg.0);
transcript.write(msg.1);
transcript.write(msg.2);

let chg = transcript.read();
verifier_messages.push(chg);

pairwise::reduce_evaluations(&mut ef_f, chg);
pairwise::reduce_evaluations(&mut ef_g, chg);
}
}

ProductSumcheck {
Expand Down Expand Up @@ -133,7 +167,7 @@ mod tests {
let mut g: Vec<F64> = (0..n).map(|_| F64::rand(&mut rng)).collect();

let mut transcript = SanityTranscript::new(&mut rng);
let result = inner_product_sumcheck(&mut f, &mut g, &mut transcript);
let result = inner_product_sumcheck::<F64, F64>(&mut f, &mut g, &mut transcript);

assert_eq!(result.prover_messages.len(), NUM_VARS);
assert_eq!(result.verifier_messages.len(), NUM_VARS);
Expand Down Expand Up @@ -163,7 +197,7 @@ mod tests {

let prover_state = domsep.to_prover_state();
let mut transcript = SpongefishTranscript::new(prover_state);
let result = inner_product_sumcheck(&mut f, &mut g, &mut transcript);
let result = inner_product_sumcheck::<F64, F64>(&mut f, &mut g, &mut transcript);

assert_eq!(result.prover_messages.len(), NUM_VARS);
assert_eq!(result.verifier_messages.len(), NUM_VARS);
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//!
//! For most use cases, you need just two functions and a transcript:
//!
//! ```ignore
//! ```text
//! use efficient_sumcheck::{multilinear_sumcheck, inner_product_sumcheck};
//! use efficient_sumcheck::transcript::{Transcript, SpongefishTranscript, SanityTranscript};
//! ```
Expand Down
13 changes: 13 additions & 0 deletions src/multilinear/provers/time/reductions/pairwise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,16 @@ pub fn reduce_evaluations_from_stream<F: Field, S: Stream<F>>(
.collect();
*dst = out;
}

/// Cross-field reduce: fold `BF` evaluations with an `EF` challenge, producing `Vec<EF>`.
///
/// For each adjacent pair `(a, b)` in `src`: `EF::from(a) + challenge * (EF::from(b) - EF::from(a))`.
pub fn cross_field_reduce<BF: Field, EF: Field + From<BF>>(src: &[BF], challenge: EF) -> Vec<EF> {
cfg_chunks!(src, 2)
.map(|chunk| {
let a = EF::from(chunk[0]);
let b = EF::from(chunk[1]);
a + challenge * (b - a)
})
.collect()
}
61 changes: 41 additions & 20 deletions src/multilinear_sumcheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@
//! on the boolean hypercube `{0,1}^n`, the [`multilinear_sumcheck`] function executes `n`
//! rounds of the sumcheck protocol and returns the resulting [`Sumcheck`] transcript.
//!
//! The function is parameterized by two field types:
//! - `BF` (base field): the field the evaluations live in
//! - `EF` (extension field): the field challenges are sampled from
//!
//! When no extension field is needed, set `EF = BF`.
//!
//! # Example
//!
//! ```ignore
//! ```text
//! use efficient_sumcheck::{multilinear_sumcheck, Sumcheck};
//! use efficient_sumcheck::transcript::SanityTranscript;
//!
//! // No extension field (BF = EF):
//! let mut evals = vec![F::from(1), F::from(2), F::from(3), F::from(4)];
//! let mut transcript = SanityTranscript::new(&mut rng);
//! let result: Sumcheck<F> = multilinear_sumcheck(&mut evals, &mut transcript);
Expand All @@ -25,43 +32,59 @@ pub use crate::multilinear::Sumcheck;
/// Run the standard multilinear sumcheck protocol over an evaluation vector,
/// using a generic [`Transcript`] for Fiat-Shamir (or sanity/random challenges).
///
/// `BF` is the base field of the evaluations, `EF` is the extension field for challenges.
/// When `BF = EF`, this is the standard single-field sumcheck.
/// When `BF ≠ EF`, round 0 evaluates in `BF` and lifts to `EF`, then subsequent
/// rounds work entirely in `EF`.
///
/// Each round:
/// 1. Computes the round polynomial evaluations `(s(0), s(1))` via pairwise reduction.
/// 2. Writes them to the transcript (2 field elements).
/// 3. Reads the verifier's challenge from the transcript (1 field element).
/// 4. Reduces the evaluation vector by folding with the challenge.
pub fn multilinear_sumcheck<F: Field>(
evaluations: &mut Vec<F>,
transcript: &mut impl Transcript<F>,
) -> Sumcheck<F> {
pub fn multilinear_sumcheck<BF: Field, EF: Field + From<BF>>(
evaluations: &mut [BF],
transcript: &mut impl Transcript<EF>,
) -> Sumcheck<EF> {
// checks
assert!(
evaluations.len().count_ones() == 1,
"length must be a power of 2"
);
assert!(evaluations.len() >= 2, "need at least 1 variable");

// initialize
let num_rounds = evaluations.len().trailing_zeros() as usize;
let mut prover_messages: Vec<(F, F)> = vec![];
let mut verifier_messages: Vec<F> = vec![];
let mut prover_messages: Vec<(EF, EF)> = vec![];
let mut verifier_messages: Vec<EF> = vec![];

// all rounds
for _ in 0..num_rounds {
// evaluate: compute s(0) and s(1)
let msg = pairwise::evaluate(evaluations);
// ── Round 0: evaluate in BF, lift to EF, cross-field reduce ──
if num_rounds > 0 {
let msg_bf = pairwise::evaluate(evaluations);
let msg = (EF::from(msg_bf.0), EF::from(msg_bf.1));

// write transcript
prover_messages.push(msg);
transcript.write(msg.0);
transcript.write(msg.1);

// read the transcript
let chg = transcript.read();
verifier_messages.push(chg);

// reduce
pairwise::reduce_evaluations(evaluations, chg);
// Cross-field reduce: BF evaluations + EF challenge → Vec<EF>
let mut ef_evals = pairwise::cross_field_reduce(evaluations, chg);

// Remaining rounds work in EF
for _ in 1..num_rounds {
let msg = pairwise::evaluate(&ef_evals);

prover_messages.push(msg);
transcript.write(msg.0);
transcript.write(msg.1);

let chg = transcript.read();
verifier_messages.push(chg);

pairwise::reduce_evaluations(&mut ef_evals, chg);
}
}

Sumcheck {
Expand Down Expand Up @@ -90,11 +113,10 @@ mod tests {
let mut evaluations: Vec<F64> = (0..n).map(|_| F64::rand(&mut rng)).collect();

let mut transcript = SanityTranscript::new(&mut rng);
let result = multilinear_sumcheck(&mut evaluations, &mut transcript);
let result = multilinear_sumcheck::<F64, F64>(&mut evaluations, &mut transcript);

assert_eq!(result.prover_messages.len(), NUM_VARS);
assert_eq!(result.verifier_messages.len(), NUM_VARS);
assert_eq!(evaluations.len(), 1);
}

#[test]
Expand All @@ -120,10 +142,9 @@ mod tests {

let prover_state = domsep.to_prover_state();
let mut transcript = SpongefishTranscript::new(prover_state);
let result = multilinear_sumcheck(&mut evaluations, &mut transcript);
let result = multilinear_sumcheck::<F64, F64>(&mut evaluations, &mut transcript);

assert_eq!(result.prover_messages.len(), NUM_VARS);
assert_eq!(result.verifier_messages.len(), NUM_VARS);
assert_eq!(evaluations.len(), 1);
}
}
Loading