Skip to content
Closed
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- **Consolidated error types.** Reduced from 6 error enums to 4. Folded `WARPSumcheckProverError` into `ProverError`, inlined `WARPSumcheckVerifierError` into `VerifierError`, dropped `WARP` prefix. Moved errors from `utils/errs.rs` to `error.rs`.
- **Made `chunk_size` compile-time.** Replaced runtime serialization with a `const fn` computed from `PrimeField::MODULUS_BIT_SIZE`.

### Optimized

- **Reduced inner product sumcheck communication by 1/3.** Updated to `efficient-sumcheck` 2-coefficient round messages: prover sends `(a, b)` instead of `(s(0), s(1), s(1/2))`. Verifier derives the third coefficient as `c = claim - 2a - b`. Updated verifier transcript to read `[F; 2]` instead of `[F; 3]` per round.
- **Adopted `RoundPolyEvaluator` trait for twin constraint sumcheck.** Replaced closure-based `twin_constraint_round_poly` with `TwinConstraintEvaluator` struct implementing the new `RoundPolyEvaluator` trait from `efficient-sumcheck`. The library now handles pair iteration, parallel summation, and SIMD-accelerated reduce.
- **Reduced twin constraint sumcheck communication.** Prover now sends `d` coefficients per round instead of `d+1`; verifier derives the leading coefficient from the sumcheck constraint. Updated `derive_randomness` to read `1 + max(log_n+1, log_m+2)` instead of `2 + max(log_n+1, log_m+2)`.
- **Twin constraint sumcheck 64% faster.** Combined effect of `RoundPolyEvaluator` refactor, library-side optimizations (zero-allocation `poly_ops`, optimized `protogalaxy::fold`), and SIMD-accelerated pairwise reduce. End-to-end prover time improved ~23%.
- **Pinned `ark-ff`/`ark-poly`/`ark-serialize` to rev `285dac2`.** Resolves spongefish BigInt mismatch with upstream `ark-ff` HEAD.

### Fixed

- **BLS test desync.** Fixed `WARPConfig` in the BLS test from `l=4` to `l=8`, matching `l2=4` accumulated instances. The prover absorbed all accumulators unconditionally while the verifier only read `l2` of them, causing a sponge state mismatch.
Expand Down
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ serde_json = "1.0"
spongefish = { version = "0.7.0", features = ["ark-ff"] }
effsc = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git" }
thiserror = "2.0.16"
tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] }
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi", "env-filter", "json", "registry"], optional = true }
libc = { version = "0.2", optional = true }
ark-codes = { git = "https://github.com/z-tech/ark-codes.git", branch = "z-tech/arkworks-0.6.0" }

[dev-dependencies]
Expand All @@ -36,6 +39,12 @@ default = ["asm"]

asm = ["ark-ff/asm"]

# Opt-in profiling: brings in tracing-subscriber + libc so we can emit
# structured per-phase records (wall_ns, cpu_ns, peak_rss_delta, op counters).
# Release builds with `profile` off pull in neither tracing-subscriber nor
# libc as direct deps of `warp`.
profile = ["dep:tracing-subscriber", "dep:libc"]

[[bench]]
name = "warp_rs"
harness = false
62 changes: 4 additions & 58 deletions benches/warp_rs.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use ark_bls12_381::Fr as BLS12_381;
use ark_codes::reed_solomon::config::ReedSolomonConfig;
use ark_codes::reed_solomon::ReedSolomon;
use ark_codes::traits::LinearCode;

use ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig;
use ark_std::rand::thread_rng;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use utils::domainsep::init_prover_state;
use utils::hash_chain::{get_hashchain_instance_witness_pairs, get_hashchain_r1cs};
use warp::config::WARPConfig;
use warp::crypto::merkle::blake3::Blake3MerkleTreeParams;
use warp::traits::AccumulationScheme;
use warp::WARP;

Expand All @@ -19,59 +18,6 @@ use warp::utils::fields::Goldilocks;

const HASHCHAIN_SIZE: usize = 800;

pub fn bench_rs_warp(c: &mut Criterion) {
let mut rng = thread_rng();
let poseidon_config = poseidon::initialize_poseidon_config::<BLS12_381>();
let r1cs = get_hashchain_r1cs(&poseidon_config, HASHCHAIN_SIZE);

let code_config = ReedSolomonConfig::<BLS12_381>::default(r1cs.k, r1cs.k.next_power_of_two());
let code = ReedSolomon::new(code_config.clone());
let s = 8;
let t = 7;

for l in [32, 64, 128, 256, 512] {
let warp_config = WARPConfig::new(l, l, s, t, r1cs.config(), code.code_len());

let hash_chain_warp = WARP::<_, _, _, Blake3MerkleTreeParams<_>>::new(
warp_config.clone(),
code.clone(),
r1cs.clone(),
(),
(),
);

let instances_witnesses =
get_hashchain_instance_witness_pairs(l, &poseidon_config, HASHCHAIN_SIZE, &mut rng);

let mut group = c.benchmark_group("warp_rs_bls12_381_hash_chain");
group.sample_size(10);
group.bench_with_input(
BenchmarkId::from_parameter(l),
&instances_witnesses,
|b, instance_witnesses| {
b.iter_with_setup(
|| {
let prover_state = init_prover_state();
(prover_state, instance_witnesses.clone())
},
|(mut prover_state, _x_w)| {
let _ = hash_chain_warp
.prove(
(r1cs.clone(), r1cs.m, r1cs.n, r1cs.k),
&mut prover_state,
instances_witnesses.1.clone(),
instances_witnesses.0.clone(),
(vec![], vec![], vec![], (vec![], vec![]), vec![]),
(vec![], vec![], vec![]),
)
.unwrap();
},
);
},
);
}
}

pub fn bench_rs_warp_fields(c: &mut Criterion) {
pub type F = Goldilocks;
let mut rng = thread_rng();
Expand All @@ -86,7 +32,7 @@ pub fn bench_rs_warp_fields(c: &mut Criterion) {
for l in [32, 64, 128, 256, 512] {
let warp_config = WARPConfig::new(l, l, s, t, r1cs.config(), code.code_len());

let hash_chain_warp = WARP::<_, _, _, Blake3MerkleTreeParams<_>>::new(
let hash_chain_warp = WARP::<_, _, _, Blake3MerkleConfig<_>>::new(
warp_config.clone(),
code.clone(),
r1cs.clone(),
Expand Down Expand Up @@ -115,8 +61,8 @@ pub fn bench_rs_warp_fields(c: &mut Criterion) {
&mut prover_state,
instances_witnesses.1.clone(),
instances_witnesses.0.clone(),
(vec![], vec![], vec![], (vec![], vec![]), vec![]),
(vec![], vec![], vec![]),
warp::types::AccumulatorInstance::empty(),
warp::types::AccumulatorWitness::empty(),
)
.unwrap();
},
Expand Down
89 changes: 89 additions & 0 deletions docs/audits/fiat_shamir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Fiat–Shamir audit

Status: **v1 manual audit**, current as of commit `9bf4f43`. Automated
runtime-ordering enforcement is deferred (see bottom).

## What this document is

An ordered, line-for-line mapping of every `prover_message(…)` /
`prover_messages(…)` call on the prover side to its matching
`prover_message()` / `prover_messages_vec(…)` call on the verifier
side, and similarly for every `verifier_message(…)` /
`verifier_messages_vec(…)` challenge squeeze.

A Fiat–Shamir soundness flaw typically has a simple shape: a challenge
is squeezed **before** some prover message that should have influenced
it. This document lets a reviewer walk both sides in order and satisfy
themselves that every squeeze happens after every absorb that should
determine it. It is the compensating control for us not yet having a
runtime harness that asserts this automatically.

## Transcript ordering

| Step | Prover call | File:line | Verifier call | File:line |
|------|-------------|-----------|---------------|-----------|
| 1. Index — public params | `public_message(description)` | [src/lib.rs:113](../../src/lib.rs#L113) | (via `public_message` domain-sep, no explicit read) | — |
| 2. Index — `m` | `prover_message(m)` | [src/lib.rs:114](../../src/lib.rs#L114) | `prover_messages_vec` (absorbed as part of `vk`) | via `index()` |
| 3. Index — `n` | `prover_message(n)` | [src/lib.rs:115](../../src/lib.rs#L115) | " | " |
| 4. Index — `k` | `prover_message(k)` | [src/lib.rs:116](../../src/lib.rs#L116) | " | " |
| 5. Fresh instances `x_i` | `absorb_instances` → `prover_message` loop | [src/protocol/transcript/prover.rs:14](../../src/protocol/transcript/prover.rs#L14) | `prover_messages_vec(instance_len)` loop | [src/protocol/transcript/verifier.rs:25](../../src/protocol/transcript/verifier.rs#L25) |
| 6. Accumulator `rt[i]` | `prover_message(bytes)` | [src/protocol/transcript/prover.rs:28](../../src/protocol/transcript/prover.rs#L28) | `prover_message() -> [u8;32]` loop | [src/protocol/transcript/verifier.rs:49](../../src/protocol/transcript/verifier.rs#L49) |
| 7. Accumulator `α[i]` | `prover_message` loop | [src/protocol/transcript/prover.rs:33](../../src/protocol/transcript/prover.rs#L33) | `prover_messages_vec(log_n)` loop | [src/protocol/transcript/verifier.rs:55](../../src/protocol/transcript/verifier.rs#L55) |
| 8. Accumulator `μ[i]` | `prover_message` | [src/protocol/transcript/prover.rs:38](../../src/protocol/transcript/prover.rs#L38) | `prover_messages_vec(l2)` | [src/protocol/transcript/verifier.rs:58](../../src/protocol/transcript/verifier.rs#L58) |
| 9. Accumulator `τ[i]` | `prover_message` loop | [src/protocol/transcript/prover.rs:43](../../src/protocol/transcript/prover.rs#L43) | `prover_messages_vec(log_m)` loop | [src/protocol/transcript/verifier.rs:61](../../src/protocol/transcript/verifier.rs#L61) |
| 10. Accumulator `x[i]` | `prover_message` loop | [src/protocol/transcript/prover.rs:49](../../src/protocol/transcript/prover.rs#L49) | `prover_messages_vec(instance_len)` loop | [src/protocol/transcript/verifier.rs:65](../../src/protocol/transcript/verifier.rs#L65) |
| 11. Accumulator `η[i]` | `prover_message` | [src/protocol/transcript/prover.rs:54](../../src/protocol/transcript/prover.rs#L54) | `prover_messages_vec(l2)` | [src/protocol/transcript/verifier.rs:68](../../src/protocol/transcript/verifier.rs#L68) |
| 12. PESAT — `rt₀` | `prover_message(root_bytes)` | [src/protocol/phases/pesat.rs:78](../../src/protocol/phases/pesat.rs#L78) | `prover_message() -> [u8;32]` | [src/protocol/transcript/verifier.rs:111](../../src/protocol/transcript/verifier.rs#L111) |
| 13. PESAT — `μ_i` | `prover_messages(&mus)` | [src/protocol/phases/pesat.rs:79](../../src/protocol/phases/pesat.rs#L79) | `prover_messages_vec(l1)` | [src/protocol/transcript/verifier.rs:115](../../src/protocol/transcript/verifier.rs#L115) |
| 14. PESAT — τ squeeze | `verifier_messages_vec::<F>(log_m)` × l1 | [src/protocol/phases/pesat.rs:82](../../src/protocol/phases/pesat.rs#L82) | `verifier_message::<F>()` × (l1 · log_m) | [src/protocol/transcript/verifier.rs:121](../../src/protocol/transcript/verifier.rs#L121) |
| 15. Twin-constraint — ω | `verifier_message()` | [src/protocol/phases/twin_constraint.rs:160](../../src/protocol/phases/twin_constraint.rs#L160) | `verifier_message::<F>()` | [src/protocol/transcript/verifier.rs:126](../../src/protocol/transcript/verifier.rs#L126) |
| 16. Twin-constraint — τ | `verifier_messages_vec::<F>(log_l)` | [src/protocol/phases/twin_constraint.rs:161](../../src/protocol/phases/twin_constraint.rs#L161) | `verifier_message::<F>()` × log_l | [src/protocol/transcript/verifier.rs:128](../../src/protocol/transcript/verifier.rs#L128) |
| 17. Twin-constraint — sumcheck | per round: coeffs absorbed, γ squeezed (inside `coefficient_sumcheck`) | [src/protocol/phases/twin_constraint.rs:202](../../src/protocol/phases/twin_constraint.rs#L202) | per round: `prover_messages_vec` coeffs, `verifier_message` γ | [src/protocol/transcript/verifier.rs:136](../../src/protocol/transcript/verifier.rs#L136) |
| 18. Post-TC — new root | `prover_message(td_root_bytes)` | [src/lib.rs:213](../../src/lib.rs#L213) | `prover_message() -> [u8;32]` | [src/protocol/transcript/verifier.rs:143](../../src/protocol/transcript/verifier.rs#L143) |
| 19. Post-TC — η | `prover_message(&eta)` | [src/lib.rs:214](../../src/lib.rs#L214) | `prover_message() -> F` | [src/protocol/transcript/verifier.rs:147](../../src/protocol/transcript/verifier.rs#L147) |
| 20. Post-TC — ν₀ | `prover_message(&nu_0)` | [src/lib.rs:215](../../src/lib.rs#L215) | `prover_message() -> F` | [src/protocol/transcript/verifier.rs:148](../../src/protocol/transcript/verifier.rs#L148) |
| 21. OOD — sample points | `verifier_messages_vec::<F>(s·log_n)` | [src/protocol/phases/ood.rs:35](../../src/protocol/phases/ood.rs#L35) | `verifier_message::<F>()` × (s · log_n) | [src/protocol/transcript/verifier.rs:154](../../src/protocol/transcript/verifier.rs#L154) |
| 22. OOD — answers | `prover_messages(&answers)` | [src/protocol/phases/ood.rs:41](../../src/protocol/phases/ood.rs#L41) | `prover_messages_vec(s)` | [src/protocol/transcript/verifier.rs:158](../../src/protocol/transcript/verifier.rs#L158) |
| 23. Proximity — query bytes | `verifier_messages_vec::<[u8;1]>(num_bytes)` via `QueryIndices::sample` | [src/protocol/query.rs:18](../../src/protocol/query.rs#L18) | `verifier_message::<[u8;1]>()` × num_bytes | [src/protocol/transcript/verifier.rs:166](../../src/protocol/transcript/verifier.rs#L166) |
| 24. Batching — ξ | `verifier_messages_vec::<F>(log_r)` | [src/protocol/phases/batching.rs:61](../../src/protocol/phases/batching.rs#L61) | `verifier_message::<F>()` × log_r | [src/protocol/transcript/verifier.rs:169](../../src/protocol/transcript/verifier.rs#L169) |
| 25. Batching — sumcheck | per round: `[a, b]` absorbed, α squeezed (inside `inner_product_sumcheck`) | [src/protocol/phases/batching.rs:93](../../src/protocol/phases/batching.rs#L93) | per round: `prover_messages() -> [F;2]`, `verifier_message()` α | [src/protocol/transcript/verifier.rs:176](../../src/protocol/transcript/verifier.rs#L176) |

## What every reviewer should spot-check

For each challenge squeeze, confirm that every prover message that
**defines** that challenge's semantic purpose has already been absorbed
above it. Examples:

- **τ challenges (step 14).** Squeezed after the PESAT root and fresh
μ_i (steps 12–13). ✓ Both values determine which witness the prover
committed to; binding τ to them is required so the prover can't pick
τ after learning which witness is rejected.
- **ω challenge (step 15).** Squeezed after all prior PESAT state and
after τ. ✓ ω linearly combines two claims; if the prover could pick
ω *before* τ, they could cancel the two terms against a dishonest
witness.
- **Shift-query bytes (step 23).** Squeezed after the new commitment
`td_root_bytes`, η, ν₀, and the OOD answers (steps 18–22). ✓ The
query index must be unpredictable relative to the committed oracle.
- **Batching-sumcheck α challenges (step 25).** Squeezed per round
after each `[a, b]` is absorbed (inside the sumcheck). ✓ Standard.

## What is **not** in this table

- `domain_separator!("…")` invocations: those are audited via the
domain-sep macro's own per-call string matching; if the same
domainsep string is used on prover and verifier, the transcripts
align. Rotate the string if the layout changes.
- `AccumulatorInstance::empty()` / `AccumulatorWitness::empty()`
paths: these do nothing to the transcript, so they are
unaudited here.

## Deferred — a runtime Fiat–Shamir harness

Plan T originally proposed a test that captures the ordered list of
`prover_message` / `verifier_message` calls during a prove, and
asserts it matches a golden sequence. That would make drift between
prover and verifier impossible to land undetected. Implementing it
requires instrumenting `spongefish::ProverState` (external crate), so
it's deferred. The current table is the compensating control; it is
*manually* regenerated when any file above changes.
40 changes: 40 additions & 0 deletions docs/paper-mods/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Warp paper-mods

Living spec of notation and structural modifications we make to the Warp paper
so that the paper's IOR decomposition and the Rust implementation agree
exactly. This is **not** a paper in its own right — it is a working document
tracking deltas from the published construction, each paired with the code
module that realises it.

## Scope

Warp is framed as an **IOP** (Ben-Sasson–Chiesa–Spooner 2016), **not** as an
AHP. Oracles are functions `f: [n] → F` queried by index under the BCS
compiler, with multilinear-extension semantics layered on top for point
queries. See `notation.tex` for the shared preamble and rationale.

## Convention

- One `.tex` file per modification, paired with one Rust module.
- Shared preamble lives in `notation.tex`; every `.tex` file `\input`s it.
- **Authoring order**: the `.tex` file is written *before* the code module.
Code doc comments cite the `.tex` filename; `.tex` files cite the Rust
module path. Drift is caught in code review.
- No CI compilation. Build locally with `latexmk -pdf mod1_oracle.tex`.

## Modification numbering (reserved)

| File | Modification | Owning plan |
|-----------------------------------|------------------------------------------|-------------|
| `mod1_oracle.tex` | Oracle as first-class IOR output | Plan 0 |
| `mod2_structured_sumcheck.tex` | Structured-sumcheck primitive | Plan B' |
| `mod3_accumulator_state.tex` | Accumulator as explicit IOR state | Plan C |
| `mod4_parameter_selection.tex` | Soundness-driven parameter selection | Plan P |

Future modifications take the next free number in sequence.

## Cross-reference lint

Each phase module's doc comment must name a `.tex` file in this folder. Each
`.tex` must name at least one Rust module path it pairs with. A manual check
is in the Plan 0 verification list; automation is Plan T's job.
Loading
Loading