diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..c91c3f3 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[net] +git-fetch-with-cli = true diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 491d77a..d4d6374 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -23,6 +23,14 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Load ark-vc deploy key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.ARK_VC_DEPLOY_KEY }} + + - name: Rewrite ark-vc HTTPS URL to SSH + run: git config --global url."ssh://git@github.com/arkworks-rs/ark-vc.git".insteadOf "https://github.com/arkworks-rs/ark-vc.git" + - name: Install stable toolchain with clippy uses: actions-rs/toolchain@v1 with: @@ -47,6 +55,14 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Load ark-vc deploy key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.ARK_VC_DEPLOY_KEY }} + + - name: Rewrite ark-vc HTTPS URL to SSH + run: git config --global url."ssh://git@github.com/arkworks-rs/ark-vc.git".insteadOf "https://github.com/arkworks-rs/ark-vc.git" + - name: Run tests run: cargo test --verbose @@ -55,5 +71,13 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Load ark-vc deploy key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.ARK_VC_DEPLOY_KEY }} + + - name: Rewrite ark-vc HTTPS URL to SSH + run: git config --global url."ssh://git@github.com/arkworks-rs/ark-vc.git".insteadOf "https://github.com/arkworks-rs/ark-vc.git" + - name: Run tests run: cargo test --verbose --no-default-features diff --git a/CHANGELOG.md b/CHANGELOG.md index 23dbfa2..e9569dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Cargo.toml b/Cargo.toml index c79b099..ee42e44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,20 @@ ark-relations = "0.6.0" ark-serialize = "0.6.0" ark-std = "0.6.0" blake3 = "1.5.0" +rayon = "1" serde = { version = "1.0.0", features = ["derive"] } serde_json = "1.0" spongefish = { version = "0.7.0", features = ["ark-ff"] } +rand_core = { version = "0.6", default-features = false } 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", "env-filter", "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" } +ark-iop = { git = "https://github.com/arkworks-rs/ark-vc.git", branch = "z-tech/ark-iop" } +ark-vc = { git = "https://github.com/arkworks-rs/ark-vc.git", branch = "z-tech/hash-regions" } +ark-mt = { git = "https://github.com/arkworks-rs/ark-vc.git", branch = "z-tech/hash-regions", features = ["blake3", "blake3-field", "vc"] } [dev-dependencies] ark-bls12-381 = "0.6.0" @@ -36,6 +44,8 @@ default = ["asm"] asm = ["ark-ff/asm"] +profile = ["dep:tracing-subscriber", "dep:libc"] + [[bench]] name = "warp_rs" harness = false diff --git a/README.md b/README.md index 23a13b4..616b813 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,93 @@ 🌀 WARP 🌀 -Implementation repo for [WARP](https://eprint.iacr.org/2025/753). +Implementation repo for [WARP](https://eprint.iacr.org/2025/753) — an +accumulation scheme with code-based commitments. +## Status +Ongoing research. + +## Quick start + +```rust +use warp::{WARPConfig, AccumulatorInstance, AccumulatorWitness, WARPProverKey, WARP}; +use warp::utils::poseidon; +use warp::relations::{r1cs::{R1CS, hashchain::HashChainRelation}, BundledPESAT, ToPolySystem}; +use ark_codes::{reed_solomon::{ReedSolomon, config::ReedSolomonConfig}, traits::LinearCode}; +use ark_crypto_primitives::crh::poseidon::{CRH, constraints::CRHGadget}; +use ark_mt::blake3::Blake3FieldHasher; +use ark_bls12_381::Fr as F; + +// 1. Build the relation (here: a hash chain of length 10). +let poseidon = poseidon::initialize_poseidon_config::(); +let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&(poseidon, 10))?; + +// 2. Pick a code and a hasher. +let code = ReedSolomon::new(ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two())); +let hasher = Blake3FieldHasher::::new(); + +// 3. Configure WARP and instantiate. +// `l1` = fresh-instance batch size, `l` = total accumulator capacity, `s`/`t` = OOD/shift queries. +let cfg = WARPConfig::new(/*l1*/ 4, /*l*/ 4, /*s*/ 8, /*t*/ 7, r1cs.config(), code.code_len()); +let warp = WARP::new(cfg, code, r1cs.clone(), hasher); + +// 4. Fold a stream of fresh (instance, witness) batches into the accumulator. +let mut acc_x = AccumulatorInstance::empty(); +let mut acc_w = AccumulatorWitness::empty(); +let pk = WARPProverKey { index: r1cs.clone(), m: r1cs.m, n: r1cs.n, k: r1cs.k }; +for batch in batches { + let mut prover_state = /* spongefish prover state */; + let ((new_x, new_w), _proof) = warp.prove( + pk.clone(), &mut prover_state, batch.witnesses, batch.instances, + AccumulatorInstance::empty(), AccumulatorWitness::empty(), + )?; + acc_x = acc_x.extend(new_x); + acc_w = acc_w.extend(new_w); +} + +// 5. Final decide (the only non-succinct step — wrap in a SNARK if you need succinctness). +warp.decide(acc_w, acc_x)?; +``` + +## Picking `(s, t)` for a target security level + +The `warp-params` binary picks and validates soundness parameters per +`docs/paper-mods/mod4_parameter_selection.tex`: + +```sh +# Pick (s, t) for λ bits of security at a given code rate and field size. +cargo run --release --bin warp-params -- select \ + --lambda 128 --rate 1/2 --field-bits 64 --regime conjectured + +# Check that a specific (s, t) actually hits λ bits. +cargo run --release --bin warp-params -- validate \ + --s 8 --t 128 --lambda 128 --rate 1/2 --field-bits 64 --regime conjectured + +# Dump the attested presets as TSV. +cargo run --release --bin warp-params -- table +``` + +`--regime` picks between `provable` and `conjectured` proximity bounds. +Exit codes: 0 ok, 1 derivation failed / target not met, 2 bad args. + +## Layout + +- `src/warp/` — `WARP::{prove, verify, decide}` choreography over IORs +- `src/protocol/ior.rs` — the `IOR` trait +- `src/protocol/iors/` — concrete IORs (`pesat`, `twin_constraint`, `bridge`, `ood`, `sample_queries`, `batching`, `proximity`) +- `src/protocol/oracles/` — oracle vocabulary used by IORs +- `src/protocol/transcript/` — transcript absorb / parse helpers +- `src/relations/` — `R1CS`, `BundledPESAT`, `HashChainRelation` +- `src/params/` — soundness-driven `(s, t)` selection backing `warp-params` +- `src/bin/warp-params.rs` — CLI front-end for `src/params/` +- `src/crypto/`, `src/utils/` — Merkle wrapper, field / poly helpers +- `src/profile/` — opt-in tracing layer (gated behind the `profile` feature) +- `tests/integration_warp.rs` — end-to-end on BLS12-381 and Goldilocks +- `tests/verifier_negative.rs` — single-tamper rejection tests + +## Running tests / benches + +```sh +cargo test --release +cargo bench +``` diff --git a/benches/utils/hash_chain.rs b/benches/utils/hash_chain.rs index 5c5517e..ffb2cde 100644 --- a/benches/utils/hash_chain.rs +++ b/benches/utils/hash_chain.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use ark_crypto_primitives::{ crh::poseidon::{constraints::CRHGadget, CRH}, sponge::{poseidon::PoseidonConfig, Absorb}, @@ -11,7 +9,7 @@ use warp::relations::{ hashchain::{compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness}, R1CS, }, - Relation, ToPolySystem, + Arithmetize, Relation, }; // utilities for the hashchain benchmark @@ -19,7 +17,7 @@ pub fn get_hashchain_r1cs( poseidon_config: &PoseidonConfig, hashchain_size: usize, ) -> R1CS { - HashChainRelation::, CRHGadget<_>>::into_r1cs(&( + HashChainRelation::, CRHGadget<_>>::arithmetize(&( poseidon_config.clone(), hashchain_size, )) @@ -37,10 +35,7 @@ pub fn get_hashchain_instance_witness_pairs( let instance = HashChainInstance { digest: compute_hash_chain::>(poseidon_config, &preimage, hashchain_size), }; - let witness = HashChainWitness { - preimage, - _crhs_scheme: PhantomData::>, - }; + let witness = HashChainWitness::>::new(preimage); let relation = HashChainRelation::, CRHGadget<_>>::new( instance, witness, diff --git a/benches/warp_rs.rs b/benches/warp_rs.rs index 1943dfd..c8f9d56 100644 --- a/benches/warp_rs.rs +++ b/benches/warp_rs.rs @@ -1,97 +1,56 @@ -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_mt::{ + blake3::Blake3FieldHasher, hash_region::HashRegion, scheme::MerkleCommitment, + shape::PerfectBinary, +}; use ark_std::rand::thread_rng; +use ark_vc::mvc::MultiVectorCommitment; 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; +use warp::config::WarpConfig; +use warp::warp::WarpProverKey; +use warp::WarpAccumulationScheme; mod utils; use utils::poseidon; -use warp::relations::BundledPESAT; +use warp::relations::PolyPredicate; 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::(); - let r1cs = get_hashchain_r1cs(&poseidon_config, HASHCHAIN_SIZE); - - let code_config = ReedSolomonConfig::::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(); let poseidon_config = poseidon::initialize_poseidon_config::(); let r1cs = get_hashchain_r1cs(&poseidon_config, HASHCHAIN_SIZE); - let code_config = ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); + let code_config = ReedSolomonConfig::::default( + r1cs.k_num_witness_vars, + r1cs.k_num_witness_vars.next_power_of_two(), + ); let code = ReedSolomon::new(code_config.clone()); let s = 2; let t = 125; + type V = MerkleCommitment>, PerfectBinary>; + for l in [32, 64, 128, 256, 512] { - let warp_config = WARPConfig::new(l, l, s, t, r1cs.config(), code.code_len()); + let warp_config = WarpConfig::new(l, 0, s, t, r1cs.config(), code.code_len()); - let hash_chain_warp = WARP::<_, _, _, Blake3MerkleTreeParams<_>>::new( + let pp = ::setup_multiple(0, code.code_len(), t, &mut rng) + .expect("setup_multiple"); + let (ck, vk) = ::trim_multiple(&pp, 0, code.code_len(), t) + .expect("trim_multiple"); + let hash_chain_warp = WarpAccumulationScheme::<_, _, _, V>::new( warp_config.clone(), code.clone(), r1cs.clone(), - (), - (), + ck, + vk, ); let instances_witnesses = @@ -111,12 +70,17 @@ pub fn bench_rs_warp_fields(c: &mut Criterion) { |(mut prover_state, _x_w)| { let _ = hash_chain_warp .prove( - (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), + WarpProverKey { + index: r1cs.clone(), + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, &mut prover_state, instances_witnesses.1.clone(), instances_witnesses.0.clone(), - (vec![], vec![], vec![], (vec![], vec![]), vec![]), - (vec![], vec![], vec![]), + warp::warp::AccumulatorInstance::empty(), + warp::warp::AccumulatorWitness::empty(), ) .unwrap(); }, diff --git a/docs/audits/fiat_shamir.md b/docs/audits/fiat_shamir.md new file mode 100644 index 0000000..ecd1944 --- /dev/null +++ b/docs/audits/fiat_shamir.md @@ -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::(log_m)` × l1 | [src/protocol/phases/pesat.rs:82](../../src/protocol/phases/pesat.rs#L82) | `verifier_message::()` × (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::()` | [src/protocol/transcript/verifier.rs:126](../../src/protocol/transcript/verifier.rs#L126) | +| 16. Twin-constraint — τ | `verifier_messages_vec::(log_l)` | [src/protocol/phases/twin_constraint.rs:161](../../src/protocol/phases/twin_constraint.rs#L161) | `verifier_message::()` × 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::(s·log_n)` | [src/protocol/phases/ood.rs:35](../../src/protocol/phases/ood.rs#L35) | `verifier_message::()` × (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::(log_r)` | [src/protocol/phases/batching.rs:61](../../src/protocol/phases/batching.rs#L61) | `verifier_message::()` × 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. diff --git a/docs/paper-mods/README.md b/docs/paper-mods/README.md new file mode 100644 index 0000000..7346f59 --- /dev/null +++ b/docs/paper-mods/README.md @@ -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. diff --git a/docs/paper-mods/mod1_oracle.tex b/docs/paper-mods/mod1_oracle.tex new file mode 100644 index 0000000..8101507 --- /dev/null +++ b/docs/paper-mods/mod1_oracle.tex @@ -0,0 +1,129 @@ +\documentclass{article} +\input{notation.tex} + +\title{Modification 1: \\ Oracle as first-class IOR output} +\author{Warp paper-mods} +\date{} + +\begin{document} +\maketitle + +\paragraph{Paired Rust module.} \codemod{src/protocol/oracle.rs}, consumed +throughout \codemod{src/protocol/phases/}. + +\section{Motivation} + +The Warp paper decomposes its construction as a sequence of interactive +oracle reductions (IORs). In the pure IOR formalism, each IOR's prover is an +abstract Turing machine that recomputes anything it needs; there is no notion +of ``shared state'' across IORs beyond the transcript. This abstraction is +sound but imposes an unpleasant cost on any implementation: several distinct +IORs in Warp --- the twin-constraint sumcheck, OOD sampling, the batching +sumcheck, and the proximity-query phase --- all operate on \emph{the same +codeword} \(f\), its multilinear extension \(\MLE{f}\), and its Merkle +commitment. A literal reading of the paper forces the code either to +rematerialise these objects phase by phase (wasteful) or to smuggle them +across phase boundaries as ambient state (unprincipled). + +The BCS formalism already provides the right object to legitimise the +sharing: an \emph{oracle message}. BCS treats prover oracles as typed, +persistent objects that can be queried in any subsequent round. What the +paper presently leaves implicit is that \emph{composed} IORs can export and +import these oracles as typed inputs and outputs. + +This modification promotes Oracle to a first-class input/output type of every +Warp IOR. No new soundness argument is required; the BCS compiler already +handles the Merkle-commitment bookkeeping. What changes is the presentation, +which in turn legitimises a direct Rust implementation in +\codemod{src/protocol/oracle.rs}. + +\section{Definition} + +An \emph{oracle} \(\Oracle{f}\) carries the following views of a single +object: +\begin{itemize} + \item \(f : \idx{n} \to \F\) is an evaluation table (the \emph{BCS-native} + view); + \item \(\MLE{f} : \F^{\log n} \to \F\) is the unique multilinear polynomial + over \(\bool{\log n}\) agreeing with \(f\) after identifying + \(\idx{n} \leftrightarrow \bool{\log n}\) by binary expansion; + \item under BCS compilation, \(f\) is committed via a Merkle root + \(\Commit{f}\); this commitment is produced by a separate + \emph{commit} operation and is not a field of \(\Oracle{f}\). +\end{itemize} + +\(\MLE{f}\) is \emph{implied} by \(f\); the prover materialises it lazily and +caches. See \codemod{src/protocol/oracle.rs}, method \texttt{query\_at\_point}. + +\paragraph{Implementation note: commitments and oracles are not 1:1.} Warp's +\Pesat{} phase interleaves \(l_1\) codewords into a single Merkle tree +(\codemod{src/crypto/merkle/mod.rs}, \texttt{build\_codeword\_leaves}), so one +commitment covers many oracles. The Rust \codemod{Oracle} struct therefore +stores only \(f\) and the lazy \(\MLE{f}\); the Merkle tree implementing +\(\Commit{f}\) is tracked by the enclosing data structure +(\codemod{src/types.rs} --- \texttt{PesatOutput}, \texttt{AccumulatorWitness}). +This is an orthogonal implementation choice; the IOR composition rule in +\S4 is unchanged. + +\section{Query interfaces} + +Two legal query operations on \(\Oracle{f}\): + +\begin{description} + \item[Index query.] \(f[i]\) for \(i \in \idx{n}\). BCS-native. + The verifier receives \(f[i]\) alongside a Merkle opening against + \(\Commit{f}\); the BCS compiler makes this non-interactive via + Fiat--Shamir. Corresponds to + \codemod{src/protocol/oracle.rs}, method \texttt{query\_at\_leaf}. + \item[Point query.] \(\MLE{f}(\zeta)\) for \(\zeta \in \F^{\log n}\). + The prover answers directly; the verifier's check that the answer is + consistent with \(\Commit{f}\) is deferred to a downstream IOR (in + Warp, the batching sumcheck). Corresponds to + \codemod{src/protocol/oracle.rs}, method \texttt{query\_at\_point}. +\end{description} + +Both operations act on \emph{the same} oracle; a downstream IOR is free to +mix query kinds. + +\section{Composition rule} + +If IOR \(A\) has signature +\(\; (\text{stmt}_A, \text{wit}_A) \;\mapsto\; (\text{stmt}_A', \Oracle{f})\) +and IOR \(B\) has signature +\(\; (\text{stmt}_B, \Oracle{f}) \;\mapsto\; \text{stmt}_B'\), +then the sequential composition \(A; B\) has signature +\(\; (\text{stmt}_A, \text{stmt}_B, \text{wit}_A) \;\mapsto\; (\text{stmt}_A', \text{stmt}_B') \). +Soundness follows from the BCS composition theorem for oracle messages: the +Merkle commitment \(\Commit{f}\) is produced once by \(A\) and reused by any +number of downstream IORs. + +\section{Application to Warp} + +Every Warp phase can be restated in terms of oracles: + +\begin{center} +\begin{tabular}{l l} +\toprule +Phase & Oracle role \\ +\midrule +Encode-and-commit & emits \(\Oracle{f}\) per fresh witness \\ +\Pesat & emits \(\Oracle{u}\) for accumulated + fresh codewords \\ +\TwinConstraint & consumes \(\Oracle{u}\), emits reduced claim \\ +\OOD & point queries on \(\Oracle{f}\) \\ +\Batching & consumes \(\Oracle{f}\); emits new \(\Oracle{f}'\) via fold \\ +\Proximity & index queries on \(\Oracle{f}\) with auth paths \\ +\bottomrule +\end{tabular} +\end{center} + +The same object carries the codeword, the multilinear extension, and the +Merkle tree; downstream phases access whichever view the operation requires. +In Rust this is literally one struct (\codemod{src/protocol/oracle.rs}). + +\section{What this does not change} + +Soundness proofs, parameter choices, and the transcript layout are unchanged. +Modification 1 is presentational: it makes the existing data flow type-check +in a paper that stays inside BCS rather than retreating to AHP. + +\end{document} diff --git a/docs/paper-mods/mod2_structured_sumcheck.tex b/docs/paper-mods/mod2_structured_sumcheck.tex new file mode 100644 index 0000000..c2128c7 --- /dev/null +++ b/docs/paper-mods/mod2_structured_sumcheck.tex @@ -0,0 +1,34 @@ +\documentclass{article} +\input{notation.tex} + +\title{Modification 2: \\ Structured-sumcheck primitive (stub)} +\author{Warp paper-mods} +\date{} + +\begin{document} +\maketitle + +\paragraph{Status.} Stub. Full spec deferred to Plan B' in the multi-plan +roadmap. Paired code: refactor of +\codemod{src/protocol/phases/twin_constraint.rs} once authored. + +\section*{Abstract} + +Warp's twin-constraint sumcheck reduces the claim +\(\sum_i \tau(i) \cdot (f(i) + \omega \cdot p(i)) = 0\) +where \(f\) and \(p\) are themselves defined as protogalaxy folds over +\(\alpha\) and \(\beta\), respectively. At the paper level this is presented +as a single \Sumcheck{} IOR; at the code level, the prover fuses the folds +across rounds to share passes over tables, which is what lets the phase +dominate only \(\sim 55\%\) of prover time rather than \(\sim 4\times\) +that under the naive sequencing. + +Modification 2 promotes the structure \emph{sum of products of linear +folds} to a first-class IOR primitive in the paper, in the style of +HyperPlonk's \emph{ZeroCheck}. Both Warp's twin-constraint sumcheck and +the batching sumcheck become instances. The primitive justifies the +fusion at the paper level; the code then exposes a +\texttt{StructuredSumcheck} trait whose instances correspond to the +paper's variants. See Plan B' for the full treatment. + +\end{document} diff --git a/docs/paper-mods/mod3_accumulator_state.tex b/docs/paper-mods/mod3_accumulator_state.tex new file mode 100644 index 0000000..dddc6d4 --- /dev/null +++ b/docs/paper-mods/mod3_accumulator_state.tex @@ -0,0 +1,30 @@ +\documentclass{article} +\input{notation.tex} + +\title{Modification 3: \\ Accumulator as explicit IOR state (stub)} +\author{Warp paper-mods} +\date{} + +\begin{document} +\maketitle + +\paragraph{Status.} Stub. Full spec deferred to Plan C in the multi-plan +roadmap. Paired code: signature-level refactor across +\codemod{src/protocol/phases/} once authored. + +\section*{Abstract} + +The paper presently treats the accumulator \((\AccX, \AccW)\) partly as an +object the protocol operates on and partly as ambient state read and written +by each phase. Modification 3 makes the accumulator an explicit pre- and +post-condition of every IOR: +\[ + \IOR{Phase}\ :\ (\AccState, \text{input}) \;\longmapsto\; (\AccState', \text{output}). +\] +This matches Nova-style folding-scheme presentation and lets the composition +theorem be stated mechanically. At the code level it gives every phase +function the same \texttt{AccState} parameter and return and makes prover +and verifier phases exact structural duals. See Plan C for the full +treatment. + +\end{document} diff --git a/docs/paper-mods/mod4_parameter_selection.tex b/docs/paper-mods/mod4_parameter_selection.tex new file mode 100644 index 0000000..1ec203b --- /dev/null +++ b/docs/paper-mods/mod4_parameter_selection.tex @@ -0,0 +1,90 @@ +\documentclass{article} +\input{notation.tex} + +\title{Modification 4: \\ Soundness-driven parameter selection} +\author{Warp paper-mods} +\date{} + +\begin{document} +\maketitle + +\paragraph{Paired Rust module.} \codemod{src/params/}, with a CLI front-end +at \codemod{src/bin/warp-params.rs}. + +\section{Motivation} + +The paper states the soundness bound of Warp symbolically; the implementation +has so far hard-coded parameter values (for instance, \(s = 8\), \(t = 7\) +in the regression tests). There is no record of which security target those +choices correspond to, nor is there a way to ask the code for the minimum +parameter tuple that hits a chosen target \(\lambda\). + +Modification 4 adds, on both sides, an \emph{inverse} to the soundness +analysis. Given +\(\lambda \in \mathbb{N}\), field size \(\size{\F}\), Reed--Solomon code rate +\(\rho = k/n\), and a choice of \emph{list-decoding regime}, produce the +smallest parameter tuple \((s,\ t)\) that provably (or conjecturally) gives +\(\lambda\) bits of soundness. The workload parameters \((l,\ l_1)\) remain +caller-chosen; Modification 4 does not select them. + +\section{Regimes} + +\begin{description} + \item[\IOR{Provable}.] Each proximity query rejects a word at distance + \(e\) from the nearest codeword with probability at least \(e\). + Taking the Johnson bound \(e = 1 - \sqrt{\rho}\) as the largest + value supported by a proof for Reed--Solomon codes, the soundness + error of \(t\) independent proximity queries is at most + \(\bigl(1 - e\bigr)^{t} = \rho^{t/2}\). Therefore + \[ + t \;\geq\; \frac{2\lambda}{\log_2(1/\rho)}. + \] + \item[\IOR{Conjectured}.] The list-decodability conjecture for + Reed--Solomon codes extends the argument up to radius + \(e = 1 - \rho\). The same calculation gives + \[ + t \;\geq\; \frac{\lambda}{\log_2(1/\rho)}. + \] +\end{description} + +For \(s\) (OOD samples) we use the conservative constant \(s = 8\), +which dominates the round-complexity of batching without materially +affecting soundness at the scales we run. A tighter bound is deferred to +a follow-up; the current code documents this choice explicitly. + +\section{Field-size admissibility} + +Several other soundness terms are each at most +\(\mathrm{polylog}(\cdot) / \size{\F}\) (sumcheck rounds, zero-check +randomness absorbs, transcript-squeezed challenges). They are negligible +provided \(\log_2 \size{\F} \geq \lambda + \epsilon\) for a small constant +\(\epsilon\); the code rejects any choice that violates this admissibility. + +\section{References used, and scope limits} + +The formulas above are the standard proximity-gap / list-decoding bounds +from the Reed--Solomon STARK lineage, in the precise formulation used in +STIR~\cite{STIR} and WHIR~\cite{WHIR}. They are \emph{not} a full +re-derivation of the Warp paper's soundness proof; they capture the +dominant term (proximity queries) and ignore polylogarithmic noise +controlled by the field-size check. The Rust module +\codemod{src/params/} uses the same formulas and marks every constant +it uses, so refining against the paper's exact proof reduces to editing +one table. + +\paragraph{Deferred.} Calibration of \(s\) against the batching-sumcheck +soundness, derivation for non-Reed--Solomon codes, and a reference +table of attested parameter tuples with matching proofs. + +\begin{thebibliography}{9} +\bibitem{STIR} + Arnon, Chiesa, Fenzi, Yogev. + \emph{STIR: Reed--Solomon Proximity Testing with Fewer Queries.} + CRYPTO 2024. +\bibitem{WHIR} + Arnon, Chiesa, Fenzi, Yogev. + \emph{WHIR: Reed--Solomon Proximity Testing with Super-Fast Verification.} + 2024. +\end{thebibliography} + +\end{document} diff --git a/docs/paper-mods/notation.tex b/docs/paper-mods/notation.tex new file mode 100644 index 0000000..de81c81 --- /dev/null +++ b/docs/paper-mods/notation.tex @@ -0,0 +1,58 @@ +% notation.tex --- shared preamble for Warp paper-mods. +% +% Every mod*.tex file begins with: +% +% \input{notation.tex} +% +% Conventions +% ----------- +% * Framing: IOP in the sense of Ben-Sasson--Chiesa--Spooner (BCS, TCC 2016), +% NOT Algebraic Holographic Proofs. Oracles are functions $f:[n] \to \F$, +% queried by index; multilinear-extension semantics layer on top for point +% queries. +% * Every modification file cites the Rust module path it pairs with. + +\usepackage{amsmath,amssymb,amsthm} +\usepackage{mathtools} +\usepackage{hyperref} + +% ---- Fields, indexing, domains ------------------------------------------- +\newcommand{\F}{\mathbb{F}} +\newcommand{\idx}[1]{\ensuremath{[#1]}} % index set [n] +\newcommand{\bool}[1]{\ensuremath{\{0,1\}^{#1}}} % boolean hypercube +\newcommand{\size}[1]{\ensuremath{\left\lvert#1\right\rvert}} + +% ---- Oracles (Modification 1) -------------------------------------------- +% An Oracle carries three views of the same object: +% (i) an evaluation table f : [n] -> F (BCS-native) +% (ii) a multilinear extension \hat f : F^{log n} -> F +% (iii) a commitment handle (Merkle root under BCS compilation) +% See mod1_oracle.tex. +\newcommand{\Oracle}[1]{\ensuremath{\mathsf{O}[#1]}} +\newcommand{\MLE}[1]{\ensuremath{\widehat{#1}}} +\newcommand{\Commit}[1]{\ensuremath{\mathsf{com}(#1)}} +\newcommand{\query}{\ensuremath{\mathsf{query}}} % generic query operator + +% ---- Accumulator state (Modification 3, reserved) ------------------------- +\newcommand{\AccX}{\ensuremath{\mathsf{acc}_x}} % accumulator instance +\newcommand{\AccW}{\ensuremath{\mathsf{acc}_w}} % accumulator witness +\newcommand{\AccState}{\ensuremath{(\AccX, \AccW)}} + +% ---- Common structural IORs ---------------------------------------------- +\newcommand{\IOR}[1]{\textsc{#1}} +\newcommand{\Zerocheck}{\IOR{Zerocheck}} +\newcommand{\Sumcheck}{\IOR{Sumcheck}} +\newcommand{\Pesat}{\IOR{Pesat}} +\newcommand{\TwinConstraint}{\IOR{TwinConstraint}} +\newcommand{\Proximity}{\IOR{Proximity}} +\newcommand{\Batching}{\IOR{Batching}} +\newcommand{\OOD}{\IOR{OOD}} + +% ---- Transcript notation -------------------------------------------------- +\newcommand{\absorb}{\ensuremath{\lhd}} % absorb into transcript +\newcommand{\squeeze}{\ensuremath{\rhd}} % derive from transcript + +% ---- Code / module cross-reference --------------------------------------- +% Use \codemod{src/protocol/oracle.rs} in .tex files to point at a Rust +% module. Renders as inline typewriter with a URL when compiled with hyperref. +\newcommand{\codemod}[1]{\href{../../#1}{\texttt{#1}}} diff --git a/docs/paper-mods/slides/iors.pdf b/docs/paper-mods/slides/iors.pdf new file mode 100644 index 0000000..254f809 Binary files /dev/null and b/docs/paper-mods/slides/iors.pdf differ diff --git a/docs/paper-mods/slides/iors.tex b/docs/paper-mods/slides/iors.tex new file mode 100644 index 0000000..4508c77 --- /dev/null +++ b/docs/paper-mods/slides/iors.tex @@ -0,0 +1,311 @@ +\documentclass[10pt,aspectratio=169]{beamer} + +\usepackage{listings} +\usepackage{xcolor} +\usepackage{booktabs} +\usepackage{array} + +\usetheme{metropolis} +\usepackage[scale=0.85]{FiraSans} + +\definecolor{warpaccent}{HTML}{2C3E50} +\definecolor{warpcode}{HTML}{1B5E20} +\definecolor{warpgray}{HTML}{555555} + +\setbeamercolor{frametitle}{bg=warpaccent} +\setbeamercolor{title separator}{fg=warpaccent} +\setbeamertemplate{frame numbering}[fraction] + +\lstset{ + basicstyle=\ttfamily\footnotesize, + keywordstyle=\color{warpaccent}\bfseries, + commentstyle=\color{warpgray}\itshape, + stringstyle=\color{warpcode}, + showstringspaces=false, + morekeywords={Self, dyn, impl, async, await, fn, type, trait, where, Result, Vec, Box, Option, mut, pub, struct, let, return, use}, + keepspaces=true, + columns=fullflexible, + frame=none, + breaklines=true, +} + +\title{Warp Modularity} +\subtitle{From paper IORs to a code-level interface} +\author{Internal note} +\date{} + +\begin{document} + +\maketitle + +% -------------------------------------------------------------------- +\begin{frame}{What we want} +A single interface every Warp phase satisfies, that is visibly the same +shape as the paper's IOR signature. + +\vspace{0.5em} +\begin{itemize} + \item \texttt{lib.rs::prove} reads as a chain of typed transitions. + \item Each transition: $(\text{stmt}, \text{wit}, \text{or}_\text{in}) \mapsto (\text{stmt}', \text{or}_\text{out})$. + \item Adding a phase: a struct + a trait impl. No bespoke plumbing. +\end{itemize} + +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{Why Warp wasn't easy modularly} +The paper IOR formalism doesn't share state across IORs. Real Warp does. + +\vspace{0.4em} +\textbf{Three concrete frictions:} +\begin{enumerate} + \item \textbf{Shared oracle $f$.} TwinConstraint, OOD, Batching, Proximity all consume the same codeword. A literal IOR-per-phase would rematerialise. + \item \textbf{Transcript ordering coupling.} Shift-query indices are squeezed once and consumed by both Batching and Proximity. No phase ``owns'' the squeeze. + \item \textbf{Statement / witness / oracle conflation.} The pre-refactor function had $\sim$10 mixed-purpose args; the paper's tripartite split was invisible. +\end{enumerate} +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{What an IOR signature is} +The Warp paper structures the protocol as a sequence of Interactive Oracle Reductions, each one having the composition rule: +\[ A: (\mathrm{stmt}_A, \mathrm{wit}_A) \mapsto (\mathrm{stmt}_A', \mathsf{O}[f]) \] +\[ B: (\mathrm{stmt}_B, \mathsf{O}[f]) \mapsto \mathrm{stmt}_B' \] +\[ A;B: (\mathrm{stmt}_A, \mathrm{stmt}_B, \mathrm{wit}_A) \mapsto (\mathrm{stmt}_A', \mathrm{stmt}_B') \] + +\vspace{0.4em} +Five typed components per IOR: + +\begin{center}\small +\begin{tabular}{lcc} +\toprule +& \textbf{Prover} & \textbf{Verifier} \\ +\midrule +\texttt{Statement} & \checkmark & \checkmark \\ +\texttt{Witness} & \checkmark & --- \\ +\texttt{InputOracles} & by data & by handle \\ +\texttt{ReducedStatement} & \checkmark & \checkmark \\ +\texttt{OutputOracles} & by data & by handle \\ +\bottomrule +\end{tabular} +\end{center} +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}[fragile]{The IOR trait we built} +\small +Current shape (post-PR\#22 review) mirrors the paper and routes both sides through one reduction function: +\begin{lstlisting} +pub trait IOR { + type Statement; type Witness; + type ProverInputs; type VerifierInputs; + type ReductionInputs; + type ReducedStatement; + type ProofString; type ReducedWitness; + type VerifierOutputs; + + fn reduce_statement(&self, &Stmt, &ReductionInputs) -> Reduced; + fn prove_inner(...) -> (ReductionInputs, ProofString, ReducedWitness); + fn verify_inner(...) -> (ReductionInputs, VerifierOutputs); + // default `prove` / `verify` chain *_inner -> reduce_statement +} +\end{lstlisting} +Implementor writes the three; defaults wire them. \texttt{Statement} / \texttt{ReducedStatement} shared; oracle types role-split; reduction is single-source-of-truth. 33/33 tests green. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{Per-phase mapping} +\scriptsize +\begin{center} +\begin{tabular}{lp{1.2cm}p{1.0cm}p{1.0cm}p{1.4cm}p{0.9cm}p{1.0cm}} +\toprule +& \textbf{Stmt} & \textbf{Wit} & \textbf{Pr.\,In} & \textbf{Reduced} & \textbf{Proof} & \textbf{Red.\,Wit} \\ +\midrule +\texttt{Pesat} & $(l_1, \log m)$ & wits & --- & $(\mu, \tau)$ & --- & cw + tree \\ +\texttt{TwinC.} & acc + $\mu$/$\tau$ & acc-w + ins & cw + acc-cw & $\gamma, \zeta_0, \beta_\tau,$ defer & --- & $\mathsf{O}[f] + z$ \\ +\texttt{Ood} & $(s, \log n)$ & --- & $\mathsf{O}[f]$ & samples + ans & --- & --- \\ +\texttt{Batching} & $\zeta$\,+\,$s,t,\log n$ & --- & $\mathsf{O}[f]$ & $\alpha$ & --- & $\mu$ \\ +\texttt{Proximity} & queries\,+\,$l_2,t$ & --- & trees + cw & $()$ & paths + ans & --- \\ +\bottomrule +\end{tabular} +\end{center} + +\vspace{0.4em} +\textbf{Asymmetries are now structural:} +\begin{itemize}\scriptsize + \item \texttt{Pesat} \,---\, empty \texttt{ProverInputs} (it's the source). + \item \texttt{Proximity} \,---\, empty \texttt{Reduced} and \texttt{Red.\,Wit} (it's a check; pure \texttt{ProofString}). + \item \texttt{Ood} / \texttt{Batching} \,---\, empty \texttt{Witness}. + \item \texttt{ProofString} is non-empty only for \texttt{Proximity}; every other phase's prover output is reduced-witness-shaped. +\end{itemize} +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{Single-protocol view: a candid review} +\small +\textbf{What's good.} +\begin{itemize}\small + \item Paper-aligned types \,---\, the \texttt{IOR} trait and paper composition rule line up directly. + \item Verifier symmetry \,---\, \texttt{*::verify} are first-class trait impls. + \item Honest oracle role split \,---\, prover sees data, verifier sees \emph{handles} (see oracle slide). + \item Empty-type asymmetries visible (\texttt{ProofString = ()} for reductions; \texttt{Reduced = ()} for checks). + \item Statement reduction is single-source-of-truth \,---\, no prover/verifier drift. +\end{itemize} + +\vspace{0.4em} +\textbf{What's not.} +\begin{itemize}\small + \item Nine associated types per phase \,---\, real cognitive load. + \item Lifetimes \& \texttt{PhantomData} proliferate; structs carry \texttt{<'a, F, \dots>}. + \item No type-level ordering enforcement; transcript order is implicit. + \item Orchestrator got longer, not shorter \,---\, $\sim$10 lines per phase, plus oracle-handle construction. +\end{itemize} +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{Reframing: a family of protocols} +The single-protocol view above was honest \,---\, but the actual goal isn't ``a clean WARP.'' + +\vspace{0.4em} +We design protocols like \textbf{STIR}, \textbf{WHIR}, \textbf{WARP} all the time. They share: +\begin{itemize}\small + \item Encode-and-commit (codeword \,---\, Reed-Solomon, or otherwise). + \item Sumcheck reductions (different flavours: degree-1, multilinear, fused twin-constraint, $\dots$). + \item Out-of-domain (OOD) sampling for proximity boost. + \item Shift-query opening with auth paths. + \item Batching sumchecks that fold many claims into one. + \item Accumulator updates / final consistency. +\end{itemize} + +\vspace{0.4em} +\textbf{The bar for the IOR trait then shifts:} not ``does this clean up WARP?'' but ``is this the right backbone for a family?'' +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{The toolkit, audited} +What practitioners writing STIR / WHIR / WARP \emph{already} have: +\begin{itemize}\small + \item \textbf{Transcripts.} \texttt{spongefish} 0.7 \,---\, de facto in this corner of the ecosystem. Practitioners use it directly. No abstraction needed. + \item \textbf{Sumchecks.} \texttt{effsc} \,---\, \texttt{SumcheckProver}, \texttt{RoundPolyEvaluator}, \texttt{InnerProductProver}, \texttt{CoefficientProverLSB}, \texttt{MultilinearProver}, hypercube + protogalaxy utilities. WARP uses 9 of 10 effsc primitives already. + \item \textbf{Linear codes.} \texttt{ark-codes::LinearCode} \,---\, encode, code\_len. + \item \textbf{Vector commitments.} \texttt{ark-vc} (abstract trait) + \texttt{ark-mt} (Merkle backend, multi-vector + single-shot openings, Blake3 / Poseidon hashers). + \item \textbf{Oracle struct.} \texttt{Vec} codeword + lazy MLE cache. Same storage shape across STIR / WHIR / WARP. +\end{itemize} + +\vspace{0.4em} +The primitive layer is mostly done. The discussion is really about \emph{integration} \,---\, how primitives compose, and what shape protocol authors copy. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{What practitioner experience actually needs} +Designing a STIR / WHIR / WARP-style protocol is hard. The friction is rarely ``I need a new sumcheck primitive.'' It's: +\begin{itemize}\small + \item \textbf{Tying primitives together correctly.} LinearCode + Merkle is two crates; integration is per-protocol boilerplate. + \item \textbf{Knowing the right phase decomposition.} What's a phase? What's its statement? Where's the witness? When does the verifier act? + \item \textbf{Threading transcript ordering without bugs.} Squeeze before write, derive challenges in the right segments. + \item \textbf{Maintaining paper-code alignment.} The paper's IOR composition is the contract; reviewers expect to see it in code. + \item \textbf{Debugging when soundness fails.} ``Where in this 600-line \texttt{verify} did the rejection happen?'' +\end{itemize} + +\vspace{0.4em} +The fellow: \emph{practitioners benefit from patterns + examples + shared primitives}, not from cross-protocol trait contracts. That's the lever. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}[fragile]{What WARP migrated onto: \texttt{ark-vc} / \texttt{ark-mt}} +\small +Replaced \texttt{ark-crypto-primitives::merkle\_tree} with a multi-vector commitment scheme. Two structural changes: + +\vspace{0.3em} +\textbf{1. Per-codeword $\to$ one tree.} PESAT commits $l_1$ fresh codewords; leaves are column-tuples $(c_0[i], \dots, c_{l_1-1}[i])$, hashed as \texttt{Vec}. One root authenticates all codewords. + +\vspace{0.3em} +\textbf{2. Per-query $\to$ one proof.} Was $t$ separate \texttt{Path}, each carrying $\log n$ siblings. Now a single path-pruned \texttt{OpeningProof} for all $t$ positions (union of copaths minus union of paths, book §20). + +\vspace{0.3em} +At $n{=}2^{15}$, $t{=}100$: $1500 \to \approx 730$ sibling hashes. Savings grow with $t$. + +\vspace{0.3em} +\textbf{Type diff:} \texttt{MerkleTree} $\to$ \texttt{MultiVectorCommitted}; \texttt{Path} $\to$ \texttt{OpeningProof>}; \texttt{H: MerkleHasher>} replaces \texttt{MT: Config} across all phases. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{What we're for, positively} +\textbf{Keep \texttt{IOR} as a structural template, not a cross-protocol contract.} +\begin{itemize}\small + \item It's the shape WARP-style protocols copy. The paper section on accumulator-as-state makes it legible. + \item Other protocols follow as a \emph{pattern}, get shared mental model + idioms, without being forced into one trait. +\end{itemize} + +\textbf{Lean on the existing primitive layer.} +\begin{itemize}\small + \item \texttt{effsc}, \texttt{spongefish}, \texttt{ark-codes}, \texttt{ark-vc} + \texttt{ark-mt}. + \item Already strong. The discipline is to use them, not reinvent. +\end{itemize} + +\textbf{WARP runs on \texttt{ark-vc}.} The missing integration trait was filled by \texttt{ark-vc}/\texttt{ark-mt}; WARP migrated onto its multi-vector commitment scheme. STIR-lite plugs into the same surface. + +\textbf{Make WARP the reference implementation.} A new protocol designer reads \texttt{src/protocol/phases/}, copies the IOR-shaped pattern, plugs in their primitives. + +\textbf{Document the pattern.} Short ``how to design a Warp-style protocol'' guide \,---\, narrative, not a trait. The actual lever for practitioner ergonomics. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}[fragile]{\texttt{reduce\_statement} \,---\, eliminating prover/verifier drift (1 of 2)} +\small +\textbf{Problem.}\; +Prover and verifier each compute \texttt{ReducedStatement} via parallel-but-different code; they produce the same value by sumcheck-fold semantics, nothing structural keeps them in sync. Concrete site: TwinConstraint's $\zeta_0$ / $\beta_\tau$ \,---\, prover pulls from sumcheck CC's internal \texttt{tablewise()}, verifier recomputes via \texttt{scale\_and\_sum}. Patch one, forget the other, ship divergence. + +\vspace{0.3em} +\textbf{Change.} +\begin{lstlisting} +fn reduce_statement(&self, &Stmt, &ReductionInputs) -> ReducedStatement; +fn prove_inner(...) -> (ReductionInputs, ProofString, ReducedWitness); +fn verify_inner(...) -> (ReductionInputs, VerifierOutputs); +// default `prove`/`verify` chain *_inner -> reduce_statement. +\end{lstlisting} +Both sides land at \texttt{reduce\_statement} with the same \texttt{ReductionInputs}; the math runs in one place. + +\vspace{0.3em} +\textbf{Outcome.} Drift is structurally impossible: prover pulls only $f$, $z$ from CC; verifier stops parallel \texttt{scale\_and\_sum}. Cost: 1 associated type + 2 methods. 33/33 tests green. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}[fragile]{\texttt{IndexedOracle} \,---\, BCS-agnostic verifier-side oracles (2 of 2)} +\small +\textbf{Problem.}\; +\texttt{ProximityVerifierInputs} carried \texttt{\{ rt\_0, l2\_roots, auth\_0, auth\_j, shift\_query\_answers \}} \,---\, a fully materialised BCS instantiation glued onto the IOR-level type. Slide 4 promises ``by handle''; the code delivered ``by precomputed table plus auth artifacts.'' Phase verifier knew about Merkle paths. + +\vspace{0.3em} +\textbf{Change.} +\begin{lstlisting} +trait IndexedOracle { fn query(&self, i: usize) -> Option; ... } +struct MerkleIndexedOracle<'a, F, H> { /* root, proof, idx+vals, OnceCell */ } +impl IndexedOracle> for MerkleIndexedOracle<...> { ... } +\end{lstlisting} +\texttt{ProximityVerifierInputs} is now generic over \texttt{O: IndexedOracle>} and holds \texttt{\&O} / \texttt{\&[O]} (static dispatch, no trait objects). BCS handle construction moves to the orchestrator. + +\vspace{0.3em} +\textbf{Outcome.} Phase code is BCS-agnostic \,---\, two \texttt{validate()} calls in \texttt{verify\_inner}. iBCS / ARG composition slots in via a different impl. Lazy validation \texttt{OnceCell}-memoised, so perf matches the prior eager check. + +\vspace{0.3em} +\textbf{Caveat.} Prover-side \texttt{Oracle} (OOD / Batching) is still concrete; sibling \texttt{EvalOracle} for point queries is a follow-up. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{Where this leaves us \,---\, the next move} +\small +\textbf{Already landed.}\; +\texttt{ark-vc}/\texttt{ark-mt} integration ($\sim$16\% smaller proof, prover matches with \texttt{parallel}). Four PR-\#22 follow-ups: \texttt{\&}-borrow, \texttt{ProofString}/\texttt{ReducedWitness} split, \texttt{IndexedOracle} handle, \texttt{reduce\_statement} split. Tests + benches green. + +\vspace{0.5em} +\textbf{Next.} +\begin{itemize}\small + \item Paper section \,---\, accumulator as typed state: IOR trait as paper-aligned \emph{template} for WARP-family protocols. + \item Paper section \,---\, structured sumcheck primitive: TwinConstraint's fused $\alpha/\beta/\tau$-fold extracted into \texttt{effsc} as a citable \texttt{RoundPolyEvaluator} impl. + \item Sibling \texttt{EvalOracle} \,---\, point-query analogue of \texttt{IndexedOracle} so prover-side OOD / Batching is BCS-agnostic too. + \item Second protocol \,---\, STIR-lite or WHIR-lite against the same primitives. Friction it surfaces is the next iteration's guide. + \item Practitioner narrative \,---\, ``how to design a Warp-style protocol,'' \texttt{docs/}. +\end{itemize} +\end{frame} + +\end{document} diff --git a/src/accumulation_scheme/accumulator.rs b/src/accumulation_scheme/accumulator.rs new file mode 100644 index 0000000..ecba80d --- /dev/null +++ b/src/accumulation_scheme/accumulator.rs @@ -0,0 +1,113 @@ +use ark_ff::Field; +use ark_vc::mvc::MultiVectorCommitment; + +use crate::crypto::vc::CommittedCodewords; + +/// Per-instance β coordinates absorbed into the accumulator: the `(τ, x)` +/// twin pair. Replaces the old parallel-`Vec` tuple shape which let callers +/// accidentally swap or desync the two halves. +#[derive(Clone, Debug)] +pub struct BetaTwinPair { + pub tau: Vec, + pub x: Vec, +} + +/// Public part of an accumulated claim: `(rt, α, μ, (τ, x), η)` in the paper. +pub struct AccumulatorInstance +where + F: Field, + V: MultiVectorCommitment, +{ + pub rt_commitments: Vec, + pub alpha_fold_vectors: Vec>, + pub mu_claimed_evals: Vec, + pub beta_twin_pairs: Vec>, + pub eta_predicate_evals: Vec, +} + +impl Clone for AccumulatorInstance +where + F: Field, + V: MultiVectorCommitment, + V::Commitment: Clone, +{ + fn clone(&self) -> Self { + Self { + rt_commitments: self.rt_commitments.clone(), + alpha_fold_vectors: self.alpha_fold_vectors.clone(), + mu_claimed_evals: self.mu_claimed_evals.clone(), + beta_twin_pairs: self.beta_twin_pairs.clone(), + eta_predicate_evals: self.eta_predicate_evals.clone(), + } + } +} + +impl AccumulatorInstance +where + F: Field, + V: MultiVectorCommitment, +{ + pub fn empty() -> Self { + Self { + rt_commitments: vec![], + alpha_fold_vectors: vec![], + mu_claimed_evals: vec![], + beta_twin_pairs: vec![], + eta_predicate_evals: vec![], + } + } + + pub fn extend(mut self, other: Self) -> Self { + self.rt_commitments.extend(other.rt_commitments); + self.alpha_fold_vectors.extend(other.alpha_fold_vectors); + self.mu_claimed_evals.extend(other.mu_claimed_evals); + self.beta_twin_pairs.extend(other.beta_twin_pairs); + self.eta_predicate_evals.extend(other.eta_predicate_evals); + self + } +} + +/// Private part of an accumulated claim: `(td, w)` in the paper. +pub struct AccumulatorWitness +where + F: Field, + V: MultiVectorCommitment, +{ + pub td_committed_codewords: Vec>, + pub w_witnesses: Vec>, +} + +impl Clone for AccumulatorWitness +where + F: Field, + V: MultiVectorCommitment, + V::Commitment: Clone, + V::CommitmentState: Clone, +{ + fn clone(&self) -> Self { + Self { + td_committed_codewords: self.td_committed_codewords.clone(), + w_witnesses: self.w_witnesses.clone(), + } + } +} + +impl AccumulatorWitness +where + F: Field, + V: MultiVectorCommitment, +{ + pub fn empty() -> Self { + Self { + td_committed_codewords: vec![], + w_witnesses: vec![], + } + } + + pub fn extend(mut self, other: Self) -> Self { + self.td_committed_codewords + .extend(other.td_committed_codewords); + self.w_witnesses.extend(other.w_witnesses); + self + } +} diff --git a/src/accumulation_scheme/iop.rs b/src/accumulation_scheme/iop.rs new file mode 100644 index 0000000..611ad1d --- /dev/null +++ b/src/accumulation_scheme/iop.rs @@ -0,0 +1,117 @@ +//! WarpAccumulationScheme's trait impls: `IOP` (per-round protocol identity) and +//! `AccumulationScheme` (split-accumulation wrapper). Both are +//! implemented directly on `WarpAccumulationScheme` — no phantom markers. +//! The same value that holds params (code, ck, predicate) is also +//! the FS-prologue absorber, the IOR-list owner, and the decider. + +use ark_codes::traits::LinearCode; +use ark_ff::{Field, PrimeField}; +use ark_iop::{IOP, IOR}; +use ark_poly::{DenseMultilinearExtension, Polynomial}; +use ark_std::log2; +use ark_vc::mvc::MultiVectorCommitment; +use ark_vc::vc::VectorCommitment; +use effsc::hypercube::compute_hypercube_eq_evals; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize}; + +use crate::accumulation_scheme::AccumulationScheme; +use crate::accumulation_scheme::{ + accumulator::{AccumulatorInstance, AccumulatorWitness}, + proof::WarpProof, + scheme::WarpAccumulationScheme, +}; +use crate::error::DeciderError; +use crate::iop::iors::{ + batching::Batching, bridge::Bridge, ood::Ood, pesat::Pesat, sample_queries::SampleQueries, + twin_constraint::TwinConstraint, +}; +use crate::relations::PolyPredicate; + +impl IOP for WarpAccumulationScheme +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + P: Clone + PolyPredicate, + C: LinearCode + Clone, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargSerialize + NargDeserialize + Clone, +{ + const NAME: &'static str = "WARP"; + + fn ior_names() -> Vec<&'static str> { + vec![ + as IOR>::NAME, + as IOR>::NAME, + as IOR>::NAME, + as IOR>::NAME, + as IOR>::NAME, + as IOR>::NAME, + ] + } + + type Statement = Vec>; + type Witness = Vec>; + type Proof = WarpProof; +} + +impl AccumulationScheme for WarpAccumulationScheme +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + P: Clone + PolyPredicate, + C: LinearCode + Clone, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargSerialize + NargDeserialize + Clone + Eq, +{ + const NAME: &'static str = "WARP-AccScheme"; + type Iop = Self; + type FreshInstance = Vec>; + type FreshWitness = Vec>; + type AccumulatorInstance = AccumulatorInstance; + type AccumulatorWitness = AccumulatorWitness; + type AccumulationProof = WarpProof; + + fn decide( + &self, + acc_instance: &Self::AccumulatorInstance, + acc_witness: &Self::AccumulatorWitness, + ) -> Result<(), DeciderError> { + let acc_codeword = &acc_witness.td_committed_codewords[0].codewords[0]; + + let computed_f = self.params.code.encode(&acc_witness.w_witnesses[0]); + (acc_codeword == &computed_f) + .then_some(()) + .ok_or(DeciderError::EncodedWitness)?; + + let (recomputed_commitment, _state) = + ::commit(&self.params.ck, computed_f.iter()) + .map_err(|_| DeciderError::MerkleRoot)?; + (acc_instance.rt_commitments[0] == recomputed_commitment) + .then_some(()) + .ok_or(DeciderError::MerkleRoot)?; + (acc_witness.td_committed_codewords[0].commitment == recomputed_commitment) + .then_some(()) + .ok_or(DeciderError::MerkleTrapDoor)?; + + let f_hat = DenseMultilinearExtension::from_evaluations_slice( + log2(self.params.code.code_len()) as usize, + acc_codeword, + ); + (f_hat.evaluate(&acc_instance.alpha_fold_vectors[0]) == acc_instance.mu_claimed_evals[0]) + .then_some(()) + .ok_or(DeciderError::MLExtensionEvaluation)?; + + let tau = &acc_instance.beta_twin_pairs[0].tau; + let tau_zero_evader = compute_hypercube_eq_evals(tau.len(), tau); + let mut z = acc_instance.beta_twin_pairs[0].x.clone(); + z.extend(acc_witness.w_witnesses[0].clone()); + let computed_eta = self + .params + .predicate + .evaluate_bundled(&tau_zero_evader, &z) + .map_err(|_| DeciderError::BundledEvaluation)?; + (computed_eta == acc_instance.eta_predicate_evals[0]) + .then_some(()) + .ok_or(DeciderError::BundledEvaluation)?; + + Ok(()) + } +} diff --git a/src/accumulation_scheme/keys.rs b/src/accumulation_scheme/keys.rs new file mode 100644 index 0000000..aad984a --- /dev/null +++ b/src/accumulation_scheme/keys.rs @@ -0,0 +1,16 @@ +/// Prover key — relation index plus dimensions `(M, N, k)`. +#[derive(Clone)] +pub struct WarpProverKey

{ + pub index: P, + pub m_num_constraints: usize, + pub n_num_variables: usize, + pub k_num_witness_vars: usize, +} + +/// Verifier key — dimensions only `(M, N, k)`. +#[derive(Clone, Copy)] +pub struct WarpVerifierKey { + pub m_num_constraints: usize, + pub n_num_variables: usize, + pub k_num_witness_vars: usize, +} diff --git a/src/accumulation_scheme/mod.rs b/src/accumulation_scheme/mod.rs new file mode 100644 index 0000000..8f1fd33 --- /dev/null +++ b/src/accumulation_scheme/mod.rs @@ -0,0 +1,22 @@ +//! WarpAccumulationScheme accumulation scheme: the `AccumulationScheme` trait (in [`trait_def`]) +//! and WarpAccumulationScheme's implementation of it (orchestration in [`prove`] / [`verify`], +//! decider + IOP/AccumulationScheme trait impls in [`iop`], plus types +//! [`scheme`], [`accumulator`], [`keys`], [`params`], [`proof`]). + +pub mod accumulator; +pub mod iop; +pub mod keys; +pub mod params; +pub mod proof; +pub mod prove; +pub mod scheme; +pub mod trait_def; +pub mod transcript; +pub mod verify; + +pub use accumulator::{AccumulatorInstance, AccumulatorWitness}; +pub use keys::{WarpProverKey, WarpVerifierKey}; +pub use params::WarpParams; +pub use proof::{ProveResult, WarpProof}; +pub use scheme::WarpAccumulationScheme; +pub use trait_def::AccumulationScheme; diff --git a/src/accumulation_scheme/params.rs b/src/accumulation_scheme/params.rs new file mode 100644 index 0000000..2d897e8 --- /dev/null +++ b/src/accumulation_scheme/params.rs @@ -0,0 +1,23 @@ +use ark_codes::traits::LinearCode; +use ark_ff::Field; +use ark_vc::mvc::MultiVectorCommitment; +use std::marker::PhantomData; + +use crate::config::WarpConfig; +use crate::relations::PolyPredicate; + +/// Shared configuration used by all IORs. +pub struct WarpParams +where + F: Field, + P: PolyPredicate, + C: LinearCode + Clone, + V: MultiVectorCommitment, +{ + pub(crate) _phantom_f: PhantomData, + pub config: WarpConfig, + pub code: C, + pub predicate: P, + pub ck: V::CommitterKey, + pub vk: V::VerifierKey, +} diff --git a/src/accumulation_scheme/proof.rs b/src/accumulation_scheme/proof.rs new file mode 100644 index 0000000..9aab8a9 --- /dev/null +++ b/src/accumulation_scheme/proof.rs @@ -0,0 +1,46 @@ +use ark_ff::Field; +use ark_vc::mvc::MultiVectorCommitment; + +use crate::accumulation_scheme::accumulator::{AccumulatorInstance, AccumulatorWitness}; +use crate::error::ProverError; + +/// Proof produced by the WarpAccumulationScheme accumulation prover. Auth paths and +/// sibling digests now live in the spongefish transcript (the trait's +/// `open_multiple` writes them via `prover_state.prover_message`), so +/// they no longer appear here as separate fields. +pub struct WarpProof +where + F: Field, + V: MultiVectorCommitment, +{ + pub rt_0_fresh_commitment: V::Commitment, + pub mu_i_first_codeword_coords: Vec, + pub nu_0_oracle_eval: F, + pub nu_i_oracle_evals: Vec, + pub shift_query_answers: Vec>, +} + +impl Clone for WarpProof +where + F: Field, + V: MultiVectorCommitment, + V::Commitment: Clone, +{ + fn clone(&self) -> Self { + Self { + rt_0_fresh_commitment: self.rt_0_fresh_commitment.clone(), + mu_i_first_codeword_coords: self.mu_i_first_codeword_coords.clone(), + nu_0_oracle_eval: self.nu_0_oracle_eval, + nu_i_oracle_evals: self.nu_i_oracle_evals.clone(), + shift_query_answers: self.shift_query_answers.clone(), + } + } +} + +pub type ProveResult = Result< + ( + (AccumulatorInstance, AccumulatorWitness), + WarpProof, + ), + ProverError, +>; diff --git a/src/accumulation_scheme/prove.rs b/src/accumulation_scheme/prove.rs new file mode 100644 index 0000000..c643fc6 --- /dev/null +++ b/src/accumulation_scheme/prove.rs @@ -0,0 +1,345 @@ +use ark_codes::traits::LinearCode; +use ark_ff::{Field, PrimeField}; +use ark_std::log2; +use ark_vc::mvc::MultiVectorCommitment; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; +use std::marker::PhantomData; + +use crate::accumulation_scheme::accumulator::{ + AccumulatorInstance, AccumulatorWitness, BetaTwinPair, +}; +use crate::accumulation_scheme::keys::WarpProverKey; +use crate::accumulation_scheme::proof::{ProveResult, WarpProof}; +use crate::accumulation_scheme::scheme::WarpAccumulationScheme; +use crate::accumulation_scheme::transcript::absorb_instances; +use crate::accumulation_scheme::AccumulationScheme; +use crate::error::ProverError; +use crate::iop::iors::{ + batching::{ + Batching, BatchingProverInputs, BatchingReducedStatement, BatchingReducedWitness, + BatchingStatement, + }, + bridge::{ + Bridge, BridgeProverInputs, BridgeReducedStatement, BridgeReducedWitness, BridgeStatement, + BridgeWitness, + }, + ood::{Ood, OodProverInputs, OodReducedStatement, OodStatement}, + pesat::{Pesat, PesatReducedStatement, PesatReducedWitness, PesatStatement, PesatWitness}, + proximity::{Proximity, ProximityProofString, ProximityProverInputs, ProximityStatement}, + sample_queries::{SampleQueries, SampleQueriesReducedStatement, SampleQueriesStatement}, + twin_constraint::{ + TwinConstraint, TwinConstraintProverInputs, TwinConstraintReducedStatement, + TwinConstraintReducedWitness, TwinConstraintStatement, TwinConstraintWitness, + }, +}; +use crate::relations::PolyPredicate; + +impl WarpAccumulationScheme +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + P: Clone + PolyPredicate, + C: LinearCode + Clone, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargSerialize + NargDeserialize + Clone + Eq, +{ + fn validate_prover_inputs( + &self, + pk: &WarpProverKey

, + instances: &[Vec], + witnesses: &[Vec], + acc_instance: &AccumulatorInstance, + acc_witness: &AccumulatorWitness, + ) -> Result<(), ProverError> { + if instances.len() < 2 { + return Err(ProverError::InsufficientInstances { + got: instances.len(), + }); + } + if witnesses.len() != instances.len() { + return Err(ProverError::InstanceWitnessLengthMismatch { + instances: instances.len(), + witnesses: witnesses.len(), + }); + } + if acc_witness.td_committed_codewords.len() != acc_instance.rt_commitments.len() { + return Err(ProverError::AccumulatorShapeMismatch { + instances: acc_instance.rt_commitments.len(), + roots: acc_witness.td_committed_codewords.len(), + }); + } + let l = self.params.config.l_total_fold_factor(); + if !l.is_power_of_two() { + return Err(ProverError::ConfigParameterInvalid { + reason: format!( + "l1_first_fold_factor + l2_second_fold_factor = {l} is not a power of two" + ), + }); + } + if pk.m_num_constraints == 0 || pk.n_num_variables == 0 { + return Err(ProverError::ConfigParameterInvalid { + reason: format!( + "pk.m_num_constraints = {} and pk.n_num_variables = {} must both be > 0", + pk.m_num_constraints, pk.n_num_variables + ), + }); + } + if pk.n_num_variables < pk.k_num_witness_vars { + return Err(ProverError::ConfigParameterInvalid { + reason: format!( + "pk.n_num_variables ({}) < pk.k_num_witness_vars ({})", + pk.n_num_variables, pk.k_num_witness_vars + ), + }); + } + let expected_instance_len = pk.n_num_variables - pk.k_num_witness_vars; + if instances[0].len() != expected_instance_len { + return Err(ProverError::InstanceLengthMismatch { + expected: expected_instance_len, + got: instances[0].len(), + }); + } + Ok(()) + } + + #[tracing::instrument(name = "warp.prove", skip_all)] + pub fn prove( + &self, + pk: WarpProverKey

, + prover_state: &mut ProverState, + witnesses: Vec>, + instances: Vec>, + acc_instance: AccumulatorInstance, + acc_witness: AccumulatorWitness, + ) -> ProveResult { + self.validate_prover_inputs(&pk, &instances, &witnesses, &acc_instance, &acc_witness)?; + + let log_l = log2(self.params.config.l_total_fold_factor()) as usize; + let log_m = log2(pk.m_num_constraints) as usize; + let n_code_len = self.params.code.code_len(); + let log_n = log2(n_code_len) as usize; + let n_minus_k = pk.n_num_variables - pk.k_num_witness_vars; + + self.absorb_scheme_prologue_prover(prover_state); + + absorb_instances(prover_state, &instances); + acc_instance.absorb_into(prover_state); + + let AccumulatorWitness { + td_committed_codewords: acc_tds, + w_witnesses: acc_ws, + } = acc_witness; + let acc_fs: Vec> = acc_tds.iter().map(|td| td.codewords[0].clone()).collect(); + + let pesat_ior = Pesat:: { + code: &self.params.code, + ck: &self.params.ck, + _phantom: PhantomData, + }; + let twin_constraint_ior = TwinConstraint:: { + r1cs: self.params.predicate.constraints(), + _phantom: PhantomData, + }; + let bridge_ior = Bridge::::default(); + let ood_ior = Ood::::default(); + let sample_queries_ior = SampleQueries::::default(); + let batching_ior = Batching::::default(); + let proximity_ior = Proximity:: { + ck: &self.params.ck, + _phantom: PhantomData, + }; + + let ark_iop::IorProveResult { + reduced: + PesatReducedStatement { + mus_codeword_first_coords, + taus_zero_check_challenges, + }, + proof: _, + witness: + PesatReducedWitness { + codewords, + td_0_committed_codeword, + }, + } = ark_iop::prove_ior!( + pesat_ior, + prover_state, + statement: PesatStatement { + l1_first_fold_factor: self.params.config.l1_first_fold_factor, + log_m, + }, + witness: PesatWitness { witnesses: &witnesses }, + inputs: (), + )?; + + let ark_iop::IorProveResult { + reduced: + TwinConstraintReducedStatement { + gamma_sumcheck_challenges: _, + zeta_0, + beta_tau, + deferred: _, + }, + proof: _, + witness: + TwinConstraintReducedWitness { + f_oracle, + z_witness_assignment, + }, + } = ark_iop::prove_ior!( + twin_constraint_ior, + prover_state, + statement: TwinConstraintStatement { + acc_instance, + l1_mus_codeword_first_coords: mus_codeword_first_coords.clone(), + l1_taus_zero_check_challenges: taus_zero_check_challenges, + log_l, + log_m, + log_n, + }, + witness: TwinConstraintWitness { + acc_witness_w: &acc_ws, + instances: &instances, + witnesses: &witnesses, + }, + inputs: TwinConstraintProverInputs { + fresh_codewords: &codewords, + acc_codewords: &acc_fs, + }, + )?; + + let ark_iop::IorProveResult { + reduced: + BridgeReducedStatement { + eta_predicate_eval, + nu_0_oracle_eval, + td_new_commitment: _, + }, + proof: _, + witness: + BridgeReducedWitness { + td_new, + new_x, + new_w, + }, + } = ark_iop::prove_ior!( + bridge_ior, + prover_state, + statement: BridgeStatement { + zeta_0: zeta_0.clone(), + beta_tau: beta_tau.clone(), + log_m, + n_minus_k, + }, + witness: BridgeWitness { + z_witness_assignment: &z_witness_assignment, + f_oracle: &f_oracle, + }, + inputs: BridgeProverInputs { + predicate: &self.params.predicate, + ck: &self.params.ck, + _f: PhantomData, + }, + )?; + + let ark_iop::IorProveResult { + reduced: + OodReducedStatement { + samples_flat, + answers, + }, + proof: _, + witness: _, + } = ark_iop::prove_ior!( + ood_ior, + prover_state, + statement: OodStatement { s_num_ood_samples: self.params.config.s_num_ood_samples, log_n }, + witness: (), + inputs: OodProverInputs { oracle: &f_oracle }, + )?; + + let ark_iop::IorProveResult { + reduced: SampleQueriesReducedStatement { queries }, + proof: _, + witness: _, + } = ark_iop::prove_ior!( + sample_queries_ior, + prover_state, + statement: SampleQueriesStatement { log_n, t_num_queries: self.params.config.t_num_queries }, + witness: (), + inputs: (), + )?; + + let ark_iop::IorProveResult { + reduced: + BatchingReducedStatement { + alpha_sumcheck_challenges, + }, + proof: _, + witness: BatchingReducedWitness { mu_claimed_eval }, + } = ark_iop::prove_ior!( + batching_ior, + prover_state, + statement: BatchingStatement::from_ior_outputs( + zeta_0.clone(), + &samples_flat, + &queries.evaluation_points, + self.params.config.s_num_ood_samples, + self.params.config.t_num_queries, + log_n, + ), + witness: (), + inputs: BatchingProverInputs { oracle: &f_oracle }, + )?; + + let ark_iop::IorProveResult { + reduced: _, + proof: ProximityProofString { + shift_query_answers, + }, + witness: _, + } = ark_iop::prove_ior!( + proximity_ior, + prover_state, + statement: ProximityStatement { + queries: queries.clone(), + l2_second_fold_factor: self.params.config.l2_second_fold_factor, + t_num_queries: self.params.config.t_num_queries, + n_code_len, + }, + witness: (), + inputs: ProximityProverInputs { + ck: &self.params.ck, + td_0_committed_codeword: &td_0_committed_codeword, + acc_td_committed_codewords: &acc_tds, + }, + )?; + + let mut nu_i_oracle_evals = Vec::with_capacity(1 + self.params.config.s_num_ood_samples); + nu_i_oracle_evals.push(nu_0_oracle_eval); + nu_i_oracle_evals.extend(answers); + + let new_acc_instance = AccumulatorInstance { + rt_commitments: vec![td_new.commitment.clone()], + alpha_fold_vectors: vec![alpha_sumcheck_challenges], + mu_claimed_evals: vec![mu_claimed_eval], + beta_twin_pairs: vec![BetaTwinPair { + tau: beta_tau, + x: new_x, + }], + eta_predicate_evals: vec![eta_predicate_eval], + }; + let new_acc_witness = AccumulatorWitness { + td_committed_codewords: vec![td_new], + w_witnesses: vec![new_w], + }; + let proof = WarpProof { + rt_0_fresh_commitment: td_0_committed_codeword.commitment.clone(), + mu_i_first_codeword_coords: mus_codeword_first_coords, + nu_0_oracle_eval, + nu_i_oracle_evals, + shift_query_answers, + }; + + Ok(((new_acc_instance, new_acc_witness), proof)) + } +} diff --git a/src/accumulation_scheme/scheme.rs b/src/accumulation_scheme/scheme.rs new file mode 100644 index 0000000..86c12b3 --- /dev/null +++ b/src/accumulation_scheme/scheme.rs @@ -0,0 +1,81 @@ +use ark_codes::traits::LinearCode; +use ark_ff::{Field, PrimeField}; +use ark_vc::mvc::MultiVectorCommitment; +use spongefish::{ + Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerificationResult, +}; +use std::marker::PhantomData; + +use crate::accumulation_scheme::keys::{WarpProverKey, WarpVerifierKey}; +use crate::accumulation_scheme::params::WarpParams; +use crate::config::WarpConfig; +use crate::relations::PolyPredicate; + +pub struct WarpAccumulationScheme +where + F: Field, + P: PolyPredicate, + C: LinearCode + Clone, + V: MultiVectorCommitment, +{ + pub params: WarpParams, +} + +impl WarpAccumulationScheme +where + F: Field, + P: Clone + PolyPredicate, + C: LinearCode + Clone, + V: MultiVectorCommitment, +{ + pub fn new( + config: WarpConfig, + code: C, + predicate: P, + ck: V::CommitterKey, + vk: V::VerifierKey, + ) -> WarpAccumulationScheme { + Self { + params: WarpParams { + _phantom_f: PhantomData, + config, + code, + predicate, + ck, + vk, + }, + } + } +} + +impl WarpAccumulationScheme +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + P: Clone + PolyPredicate, + C: LinearCode + Clone, + V: MultiVectorCommitment, +{ + pub fn index( + prover_state: &mut ProverState, + index: P, + ) -> VerificationResult<(WarpProverKey

, WarpVerifierKey)> { + let (m, n, k) = index.config(); + prover_state.public_message(&index.description()); + prover_state.prover_message(&F::from(m as u32)); + prover_state.prover_message(&F::from(n as u32)); + prover_state.prover_message(&F::from(k as u32)); + Ok(( + WarpProverKey { + index, + m_num_constraints: m, + n_num_variables: n, + k_num_witness_vars: k, + }, + WarpVerifierKey { + m_num_constraints: m, + n_num_variables: n, + k_num_witness_vars: k, + }, + )) + } +} diff --git a/src/accumulation_scheme/trait_def.rs b/src/accumulation_scheme/trait_def.rs new file mode 100644 index 0000000..a990da1 --- /dev/null +++ b/src/accumulation_scheme/trait_def.rs @@ -0,0 +1,77 @@ +//! Accumulation-scheme trait — warp-local, slated for upstream to +//! `ark-iop` once a second accumulating consumer appears. +//! +//! Mirrors the BCLMS (Bünz–Chen–Lin–Mishra–Vesely) split-accumulation +//! shape: an accumulator splits into a public `Instance` and a private +//! `Witness`; the scheme accumulates fresh claims via an inner IOP run +//! per round; the decider closes the accumulator. Atomic accumulation +//! falls out as `AccumulatorWitness = ()` and `AccumulationProof = ()`. +//! +//! Identity-shaped trait (no method signatures). The scheme is the FS +//! root once it implements this — its prologue absorbs `AS:NAME` plus +//! the inner IOP's name and IOR list, so callers no longer also need +//! to invoke `IOP::absorb_protocol_map_*`. + +use ark_iop::{ProverTranscript, VerifierTranscript, IOP}; + +use crate::error::DeciderError; + +pub trait AccumulationScheme { + const NAME: &'static str; + + /// The IOP whose transcripts this scheme accumulates per round. + type Iop: IOP; + + /// One fresh claim's public part. Often equal to `::Statement`; kept separate to allow batching or + /// transcript-derived forms. + type FreshInstance; + /// Witness counterpart of `FreshInstance`. + type FreshWitness; + + /// Public part of the accumulator (BCLMS "instance"). + type AccumulatorInstance; + /// Private part of the accumulator (BCLMS "witness"). Atomic + /// schemes set this to `()`. + type AccumulatorWitness; + + /// Per-round proof of correct accumulation. For atomic schemes + /// this is typically `()` (fold happens in-line). + type AccumulationProof; + + /// Absorb the scheme-level FS prologue: `AS:` tag + scheme NAME + + /// inner IOP NAME + inner IOP ior_names. Subsumes the inner + /// `IOP::absorb_protocol_map_prover` — callers invoke one or the + /// other, not both. + fn absorb_scheme_prologue_prover(&self, transcript: &mut P) { + transcript.public_message(b"AS:"); + transcript.public_message(Self::NAME.as_bytes()); + transcript.public_message(b"|"); + transcript.public_message(::NAME.as_bytes()); + transcript.public_message(b"|"); + for name in ::ior_names() { + transcript.public_message(name.as_bytes()); + transcript.public_message(b"|"); + } + } + + fn absorb_scheme_prologue_verifier(&self, transcript: &mut V) { + transcript.public_message(b"AS:"); + transcript.public_message(Self::NAME.as_bytes()); + transcript.public_message(b"|"); + transcript.public_message(::NAME.as_bytes()); + transcript.public_message(b"|"); + for name in ::ior_names() { + transcript.public_message(name.as_bytes()); + transcript.public_message(b"|"); + } + } + + /// Decider — closes the accumulator. The single BCLMS-uniform check + /// across split-accumulation schemes: `(instance, witness) → ok/err`. + fn decide( + &self, + acc_instance: &Self::AccumulatorInstance, + acc_witness: &Self::AccumulatorWitness, + ) -> Result<(), DeciderError>; +} diff --git a/src/accumulation_scheme/transcript.rs b/src/accumulation_scheme/transcript.rs new file mode 100644 index 0000000..7be4d2c --- /dev/null +++ b/src/accumulation_scheme/transcript.rs @@ -0,0 +1,138 @@ +//! Warp-specific transcript helpers: `AccumulatorInstance` (de)serialization +//! plus the plain-instance absorber. + +use ark_ff::Field; +use ark_vc::mvc::MultiVectorCommitment; +use spongefish::{ + Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerificationResult, + VerifierState, +}; + +use crate::accumulation_scheme::{accumulator::BetaTwinPair, AccumulatorInstance}; + +pub type ParsedStatement = (Vec>, AccumulatorInstance); + +pub fn absorb_instances>( + prover_state: &mut ProverState, + instances: &[Vec], +) { + for instance in instances { + for f in instance { + prover_state.prover_message(f); + } + } +} + +pub fn parse_statement( + verifier_state: &mut VerifierState<'_>, + l1: usize, + l2: usize, + instance_len: usize, + log_n: usize, + log_m: usize, +) -> VerificationResult> +where + F: Field + NargDeserialize + Encoding<[u8]> + Decoding<[u8]>, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargDeserialize, +{ + let l1_xs: Vec> = (0..l1) + .map(|_| verifier_state.prover_messages_vec(instance_len)) + .collect::>()?; + + let acc = + AccumulatorInstance::::parse_from(verifier_state, l2, log_n, log_m, instance_len)?; + + Ok((l1_xs, acc)) +} + +impl AccumulatorInstance +where + F: Field + Encoding<[u8]>, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargSerialize, +{ + pub fn absorb_into(&self, prover_state: &mut ProverState) { + for commitment in &self.rt_commitments { + prover_state.prover_message(commitment); + } + + for alpha in &self.alpha_fold_vectors { + for f in alpha { + prover_state.prover_message(f); + } + } + + for f in &self.mu_claimed_evals { + prover_state.prover_message(f); + } + + // Layout: all τ vectors first (l2 of them), then all x vectors. + // Verifier reads in the same order. Keeping the τ/x halves in + // separate passes lets the verifier reconstruct the pair list + // without needing length-prefixes per pair. + for pair in &self.beta_twin_pairs { + for f in &pair.tau { + prover_state.prover_message(f); + } + } + for pair in &self.beta_twin_pairs { + for f in &pair.x { + prover_state.prover_message(f); + } + } + + for f in &self.eta_predicate_evals { + prover_state.prover_message(f); + } + } +} + +impl AccumulatorInstance +where + F: Field + NargDeserialize + Encoding<[u8]> + Decoding<[u8]>, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargDeserialize, +{ + pub fn parse_from( + verifier_state: &mut VerifierState<'_>, + l2: usize, + log_n: usize, + log_m: usize, + instance_len: usize, + ) -> VerificationResult { + let rt: Vec = (0..l2) + .map(|_| verifier_state.prover_message::()) + .collect::>()?; + + let alpha: Vec> = (0..l2) + .map(|_| verifier_state.prover_messages_vec(log_n)) + .collect::>()?; + + let mu: Vec = verifier_state.prover_messages_vec(l2)?; + + let taus: Vec> = (0..l2) + .map(|_| verifier_state.prover_messages_vec(log_m)) + .collect::>()?; + + let xs: Vec> = (0..l2) + .map(|_| verifier_state.prover_messages_vec(instance_len)) + .collect::>()?; + + let beta_twin_pairs: Vec> = taus + .into_iter() + .zip(xs) + .map(|(tau, x)| BetaTwinPair { tau, x }) + .collect(); + + let eta: Vec = verifier_state.prover_messages_vec(l2)?; + + Ok(Self { + rt_commitments: rt, + alpha_fold_vectors: alpha, + mu_claimed_evals: mu, + beta_twin_pairs, + eta_predicate_evals: eta, + }) + } +} diff --git a/src/accumulation_scheme/verify.rs b/src/accumulation_scheme/verify.rs new file mode 100644 index 0000000..1140423 --- /dev/null +++ b/src/accumulation_scheme/verify.rs @@ -0,0 +1,326 @@ +use ark_codes::traits::LinearCode; +use ark_ff::{Field, PrimeField}; +use ark_std::log2; +use ark_vc::mvc::MultiVectorCommitment; +use effsc::hypercube::compute_hypercube_eq_evals; +use rand_core::OsRng; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, VerifierState}; +use std::marker::PhantomData; + +use crate::accumulation_scheme::accumulator::AccumulatorInstance; +use crate::accumulation_scheme::keys::WarpVerifierKey; +use crate::accumulation_scheme::proof::WarpProof; +use crate::accumulation_scheme::scheme::WarpAccumulationScheme; +use crate::accumulation_scheme::transcript::parse_statement; +use crate::accumulation_scheme::AccumulationScheme; +use crate::error::VerifierError; +use crate::iop::iors::{ + batching::{Batching, BatchingReducedStatement, BatchingStatement, BatchingVerifierInputs}, + bridge::{Bridge, BridgeReducedStatement, BridgeStatement, BridgeVerifierInputs}, + ood::{Ood, OodReducedStatement, OodStatement}, + pesat::{Pesat, PesatReducedStatement, PesatStatement, PesatVerifierOutputs}, + proximity::{Proximity, ProximityStatement, ProximityVerifierInputs}, + sample_queries::{SampleQueries, SampleQueriesReducedStatement, SampleQueriesStatement}, + twin_constraint::{TwinConstraint, TwinConstraintReducedStatement, TwinConstraintStatement}, +}; +use crate::iop::oracles::indexed::ValidatedOracle; +use crate::relations::PolyPredicate; +use crate::utils::{concat_slices, scale_and_sum}; + +impl WarpAccumulationScheme +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + P: Clone + PolyPredicate, + C: LinearCode + Clone, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargSerialize + NargDeserialize + Clone + Eq, +{ + pub fn verify<'a>( + &self, + vk: WarpVerifierKey, + verifier_state: &mut VerifierState<'a>, + acc_instance: AccumulatorInstance, + proof: WarpProof, + ) -> Result<(), VerifierError> { + let log_l = log2(self.params.config.l_total_fold_factor()) as usize; + let log_m = log2(vk.m_num_constraints) as usize; + let n_code_len = self.params.code.code_len(); + let log_n = log2(n_code_len) as usize; + let n_minus_k = vk.n_num_variables - vk.k_num_witness_vars; + + self.absorb_scheme_prologue_verifier(verifier_state); + + let (l1_xs, parsed_acc) = parse_statement::( + verifier_state, + self.params.config.l1_first_fold_factor, + self.params.config.l2_second_fold_factor, + n_minus_k, + log_n, + log_m, + )?; + + let acc_alpha_first = acc_instance.alpha_fold_vectors[0].clone(); + let acc_beta_0_first = acc_instance.beta_twin_pairs[0].tau.clone(); + let acc_beta_1_first = acc_instance.beta_twin_pairs[0].x.clone(); + let acc_mu_first = acc_instance.mu_claimed_evals[0]; + let l2_taus: Vec> = parsed_acc + .beta_twin_pairs + .iter() + .map(|p| p.tau.clone()) + .collect(); + let l2_xs: Vec> = parsed_acc + .beta_twin_pairs + .iter() + .map(|p| p.x.clone()) + .collect(); + let l2_commitments = parsed_acc.rt_commitments.clone(); + + let pesat_ior = Pesat:: { + code: &self.params.code, + ck: &self.params.ck, + _phantom: PhantomData, + }; + let twin_constraint_ior = TwinConstraint:: { + r1cs: self.params.predicate.constraints(), + _phantom: PhantomData, + }; + let bridge_ior = Bridge::::default(); + let ood_ior = Ood::::default(); + let sample_queries_ior = SampleQueries::::default(); + let batching_ior = Batching::::default(); + let proximity_ior = Proximity:: { + ck: &self.params.ck, + _phantom: PhantomData, + }; + + let ark_iop::IorVerifyResult { + reduced: + PesatReducedStatement { + mus_codeword_first_coords, + taus_zero_check_challenges, + }, + outputs: PesatVerifierOutputs { + rt_0_fresh_commitment, + }, + } = ark_iop::verify_ior!( + pesat_ior, + verifier_state, + statement: PesatStatement { l1_first_fold_factor: self.params.config.l1_first_fold_factor, log_m }, + inputs: (), + )?; + + let ark_iop::IorVerifyResult { + reduced: + TwinConstraintReducedStatement { + gamma_sumcheck_challenges, + zeta_0, + beta_tau, + deferred, + }, + outputs: _, + } = ark_iop::verify_ior!( + twin_constraint_ior, + verifier_state, + statement: TwinConstraintStatement { + acc_instance: parsed_acc, + l1_mus_codeword_first_coords: mus_codeword_first_coords.clone(), + l1_taus_zero_check_challenges: taus_zero_check_challenges.clone(), + log_l, + log_m, + log_n, + }, + inputs: (), + )?; + + let ark_iop::IorVerifyResult { + reduced: + BridgeReducedStatement { + eta_predicate_eval: _, + nu_0_oracle_eval, + td_new_commitment: _, + }, + outputs: _, + } = ark_iop::verify_ior!( + bridge_ior, + verifier_state, + statement: BridgeStatement { + zeta_0: zeta_0.clone(), + beta_tau, + log_m, + n_minus_k, + }, + inputs: BridgeVerifierInputs { + deferred: &deferred, + gamma_twin_constraint_challenges: &gamma_sumcheck_challenges, + }, + )?; + + if !deferred.is_discharged() { + return Err(VerifierError::Target); + } + + let ark_iop::IorVerifyResult { + reduced: + OodReducedStatement { + samples_flat, + answers, + }, + outputs: _, + } = ark_iop::verify_ior!( + ood_ior, + verifier_state, + statement: OodStatement { s_num_ood_samples: self.params.config.s_num_ood_samples, log_n }, + inputs: (), + )?; + + let ark_iop::IorVerifyResult { + reduced: SampleQueriesReducedStatement { queries }, + outputs: _, + } = ark_iop::verify_ior!( + sample_queries_ior, + verifier_state, + statement: SampleQueriesStatement { log_n, t_num_queries: self.params.config.t_num_queries }, + inputs: (), + )?; + + (proof.shift_query_answers.len() == self.params.config.t_num_queries) + .then_some(()) + .ok_or(VerifierError::NumShiftQueries)?; + + let mut indexed: Vec<(usize, usize)> = queries + .leaf_positions + .iter() + .copied() + .enumerate() + .map(|(row, pos)| (pos, row)) + .collect(); + indexed.sort_by_key(|&(pos, _)| pos); + indexed.dedup_by_key(|&mut (pos, _)| pos); + let sorted_unique: Vec = indexed.iter().map(|&(p, _)| p).collect(); + let row_indices: Vec = indexed.iter().map(|&(_, r)| r).collect(); + + // Column tuples for the fresh PESAT commitment: per query, the + // l1 values from `shift_query_answers[row][l2..]`. + let fresh_values: Vec> = row_indices + .iter() + .map(|&r| { + proof.shift_query_answers[r][self.params.config.l2_second_fold_factor..].to_vec() + }) + .collect(); + + // Per-acc column tuples (each acc has m=1 so each tuple is length 1). + let acc_values: Vec>> = (0..self.params.config.l2_second_fold_factor) + .map(|j| { + row_indices + .iter() + .map(|&r| vec![proof.shift_query_answers[r][j]]) + .collect() + }) + .collect(); + + // Batching consumes its transcript bytes BEFORE the proximity + // opens (matches the prover's order: ...→Batching→Proximity opens). + let gamma_eq_evals = compute_hypercube_eq_evals(log_l, &gamma_sumcheck_challenges); + let mut nu_i_oracle_evals = Vec::with_capacity( + 1 + self.params.config.s_num_ood_samples + self.params.config.t_num_queries, + ); + nu_i_oracle_evals.push(nu_0_oracle_eval); + nu_i_oracle_evals.extend(answers); + for v_jk in proof.shift_query_answers.iter() { + let nu_st = v_jk + .iter() + .zip(&gamma_eq_evals) + .fold(F::zero(), |acc, (v, eq)| acc + *eq * *v); + nu_i_oracle_evals.push(nu_st); + } + + let ark_iop::IorVerifyResult { + reduced: + BatchingReducedStatement { + alpha_sumcheck_challenges, + }, + outputs: _, + } = ark_iop::verify_ior!( + batching_ior, + verifier_state, + statement: BatchingStatement::from_ior_outputs( + zeta_0.clone(), + &samples_flat, + &queries.evaluation_points, + self.params.config.s_num_ood_samples, + self.params.config.t_num_queries, + log_n, + ), + inputs: BatchingVerifierInputs { + nus_claimed_evals: nu_i_oracle_evals, + acc_mu: acc_mu_first, + }, + )?; + + // Now consume opening proofs from the transcript (in the same + // order the prover wrote them: fresh first, then each acc). + let mut rng = OsRng; + V::check_multiple( + &self.params.vk, + &rt_0_fresh_commitment, + sorted_unique.iter().copied(), + fresh_values.iter().cloned(), + &mut rng, + verifier_state, + ) + .map_err(|_| VerifierError::ShiftQuery)?; + + for (j, commitment) in l2_commitments.iter().enumerate() { + V::check_multiple( + &self.params.vk, + commitment, + sorted_unique.iter().copied(), + acc_values[j].iter().cloned(), + &mut rng, + verifier_state, + ) + .map_err(|_| VerifierError::ShiftQuery)?; + } + + // Hand pre-validated lookup handles to the proximity IOR. + let fresh_handle = ValidatedOracle::new(sorted_unique.clone(), fresh_values); + let acc_handles: Vec> = acc_values + .into_iter() + .map(|vals| ValidatedOracle::new(sorted_unique.clone(), vals)) + .collect(); + + ark_iop::verify_ior!( + proximity_ior, + verifier_state, + statement: ProximityStatement { + queries: queries.clone(), + l2_second_fold_factor: self.params.config.l2_second_fold_factor, + t_num_queries: self.params.config.t_num_queries, + n_code_len, + }, + inputs: ProximityVerifierInputs { + fresh: &fresh_handle, + acc: &acc_handles, + _f: PhantomData, + }, + )?; + + (acc_alpha_first == alpha_sumcheck_challenges) + .then_some(()) + .ok_or(VerifierError::CodeEvaluationPoint)?; + + let betas = l2_taus + .into_iter() + .chain(taus_zero_check_challenges) + .zip(l2_xs.into_iter().chain(l1_xs)) + .map(|(tau_i, x)| concat_slices(&tau_i, &x)) + .collect::>>(); + let beta = scale_and_sum(&betas, &gamma_eq_evals); + let expected_beta = concat_slices(&acc_beta_0_first, &acc_beta_1_first); + (expected_beta == beta) + .then_some(()) + .ok_or(VerifierError::CircuitEvaluationPoint)?; + + Ok(()) + } +} diff --git a/src/bin/warp-params.rs b/src/bin/warp-params.rs new file mode 100644 index 0000000..9479882 --- /dev/null +++ b/src/bin/warp-params.rs @@ -0,0 +1,260 @@ +//! `warp-params` — CLI for soundness-driven parameter selection. +//! Paired spec: `docs/paper-mods/mod4_parameter_selection.tex`. + +use std::process::ExitCode; + +use warp::params::{inspect, lookup, select, ParamError, Params, Regime, SecurityLevel, PRESETS}; + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + match args.first().map(String::as_str) { + Some("select") => cmd_select(&args[1..]), + Some("validate") => cmd_validate(&args[1..]), + Some("table") => cmd_table(), + Some("-h") | Some("--help") | Some("help") | None => { + print_usage(); + ExitCode::SUCCESS + } + Some(other) => { + eprintln!("warp-params: unknown subcommand `{other}`"); + print_usage(); + ExitCode::from(2) + } + } +} + +fn print_usage() { + eprintln!( + "warp-params — pick soundness-driven WarpAccumulationScheme parameters.\n\ + \n\ + Usage:\n\ + warp-params select --lambda N --rate NUM/DEN|FLOAT --field-bits N --regime provable|conjectured\n\ + warp-params validate --s N --t N --lambda N --rate ... --field-bits N --regime ...\n\ + warp-params table\n\ + \n\ + See docs/paper-mods/mod4_parameter_selection.tex for the derivation." + ); +} + +fn cmd_select(args: &[String]) -> ExitCode { + let flags = match parse_flags(args) { + Ok(f) => f, + Err(e) => { + eprintln!("warp-params select: {e}"); + return ExitCode::from(2); + } + }; + let (lambda, rate, field_bits, regime) = + match (flags.lambda, flags.rate, flags.field_bits, flags.regime) { + (Some(l), Some(r), Some(fb), Some(rg)) => (SecurityLevel(l), r, fb, rg), + _ => { + eprintln!("warp-params select: need --lambda, --rate, --field-bits, --regime"); + return ExitCode::from(2); + } + }; + match select(lambda, field_bits, rate.as_f64(), regime) { + Ok(p) => { + // Prefer a preset if we have an exact-rational match on file — + // lets users see the canonical attested row, not just a + // recomputed tuple. (Equal by `presets_match_select_output`.) + let preset = rate.ratio().and_then(|(n, d)| lookup(lambda, n, d, regime)); + if let Some(preset) = preset { + println!( + "λ={} rate={}/{} regime={:?} → s={} t={} (preset)", + preset.lambda.bits(), + preset.code_rate_num, + preset.code_rate_den, + preset.regime, + preset.params.s, + preset.params.t + ); + } else { + println!( + "λ={} rate={:.4} regime={:?} → s={} t={}", + lambda.bits(), + rate.as_f64(), + regime, + p.s, + p.t + ); + } + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("warp-params select: {}", format_err(e)); + ExitCode::from(1) + } + } +} + +fn cmd_validate(args: &[String]) -> ExitCode { + let flags = match parse_flags(args) { + Ok(f) => f, + Err(e) => { + eprintln!("warp-params validate: {e}"); + return ExitCode::from(2); + } + }; + let (s, t, lambda, rate, field_bits, regime) = match ( + flags.s, + flags.t, + flags.lambda, + flags.rate, + flags.field_bits, + flags.regime, + ) { + (Some(s), Some(t), Some(l), Some(r), Some(fb), Some(rg)) => { + (s, t, SecurityLevel(l), r, fb, rg) + } + _ => { + eprintln!( + "warp-params validate: need --s, --t, --lambda, --rate, --field-bits, --regime" + ); + return ExitCode::from(2); + } + }; + let params = Params { s, t }; + match inspect(¶ms, field_bits, rate.as_f64(), regime, lambda) { + Ok(bound) => { + println!( + "proximity_bits={:.2} field_admissible={} ood_admissible={} meets_target={}", + bound.proximity_bits, + bound.field_admissible, + bound.ood_admissible, + bound.meets(lambda), + ); + if bound.meets(lambda) { + ExitCode::SUCCESS + } else { + ExitCode::from(1) + } + } + Err(e) => { + eprintln!("warp-params validate: {}", format_err(e)); + ExitCode::from(1) + } + } +} + +fn cmd_table() -> ExitCode { + println!("lambda\trate\tregime\ts\tt"); + for preset in PRESETS { + println!( + "{}\t{}/{}\t{:?}\t{}\t{}", + preset.lambda.bits(), + preset.code_rate_num, + preset.code_rate_den, + preset.regime, + preset.params.s, + preset.params.t, + ); + } + ExitCode::SUCCESS +} + +#[derive(Default)] +struct Flags { + lambda: Option, + rate: Option, + field_bits: Option, + regime: Option, + s: Option, + t: Option, +} + +/// Parsed rate that remembers whether it was written as `num/den` or as +/// a decimal. Exact-rational form enables preset lookup. +#[derive(Clone, Copy)] +enum Rate { + Ratio { num: u32, den: u32 }, + Float(f64), +} + +impl Rate { + fn as_f64(self) -> f64 { + match self { + Rate::Ratio { num, den } => num as f64 / den as f64, + Rate::Float(x) => x, + } + } + fn ratio(self) -> Option<(u32, u32)> { + match self { + Rate::Ratio { num, den } => Some((num, den)), + Rate::Float(_) => None, + } + } +} + +fn parse_flags(args: &[String]) -> Result { + let mut flags = Flags::default(); + let mut it = args.iter(); + while let Some(flag) = it.next() { + let value = it + .next() + .ok_or_else(|| format!("missing value for {flag}"))?; + match flag.as_str() { + "--lambda" => flags.lambda = Some(parse_u32(value)?), + "--rate" => flags.rate = Some(parse_rate(value)?), + "--field-bits" => flags.field_bits = Some(parse_u32(value)?), + "--regime" => flags.regime = Some(parse_regime(value)?), + "--s" => flags.s = Some(parse_u32(value)? as usize), + "--t" => flags.t = Some(parse_u32(value)? as usize), + other => return Err(format!("unknown flag: {other}")), + } + } + Ok(flags) +} + +fn parse_u32(s: &str) -> Result { + s.parse() + .map_err(|e| format!("expected u32, got `{s}`: {e}")) +} + +fn parse_rate(s: &str) -> Result { + if let Some((num, den)) = s.split_once('/') { + let n: u32 = num.parse().map_err(|e| format!("rate num `{num}`: {e}"))?; + let d: u32 = den.parse().map_err(|e| format!("rate den `{den}`: {e}"))?; + if d == 0 { + return Err("rate denominator must be non-zero".into()); + } + Ok(Rate::Ratio { num: n, den: d }) + } else { + s.parse::() + .map(Rate::Float) + .map_err(|e| format!("rate `{s}`: {e}")) + } +} + +fn parse_regime(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "provable" | "prov" => Ok(Regime::Provable), + "conjectured" | "conj" => Ok(Regime::Conjectured), + other => Err(format!( + "unknown regime `{other}`, want `provable` or `conjectured`" + )), + } +} + +fn format_err(e: ParamError) -> String { + match e { + ParamError::InvalidRate => "rate must be in (0, 1)".to_string(), + ParamError::FieldTooSmall { field_bits, lambda } => { + format!( + "field is only {field_bits} bits; \ + a target of {lambda} bits requires at least {} bits", + lambda + warp::params::select::FIELD_EPSILON + ) + } + ParamError::OodSamplesTooFew { s, min } => { + format!("OOD samples s = {s} is below minimum {min}") + } + ParamError::ProximitySoundnessBelowTarget { + proximity_bits, + target_bits, + } => { + format!( + "proximity soundness {proximity_bits:.2} bits is below target {target_bits} bits" + ) + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 45be824..e1c6e8a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,26 +1,59 @@ use ark_ff::Field; -use crate::relations::BundledPESAT; +use crate::params::{validate, ParamError, Params, Regime, SecurityLevel, SoundnessBound}; +use crate::relations::PolyPredicate; #[derive(Clone)] -pub struct WARPConfig> { - pub l: usize, - pub l1: usize, - pub s: usize, - pub t: usize, - pub p_conf: P::Config, - pub n: usize, +pub struct WarpConfig> { + pub l1_first_fold_factor: usize, + pub l2_second_fold_factor: usize, + pub s_num_ood_samples: usize, + pub t_num_queries: usize, + pub predicate_config: P::Config, + pub n_code_len: usize, } -impl> WARPConfig { - pub fn new(l: usize, l1: usize, s: usize, t: usize, p_conf: P::Config, n: usize) -> Self { +impl> WarpConfig { + pub fn new( + l1_first_fold_factor: usize, + l2_second_fold_factor: usize, + s_num_ood_samples: usize, + t_num_queries: usize, + predicate_config: P::Config, + n_code_len: usize, + ) -> Self { Self { - l, - l1, - s, - t, - p_conf, - n, + l1_first_fold_factor, + l2_second_fold_factor, + s_num_ood_samples, + t_num_queries, + predicate_config, + n_code_len, } } + + /// Total fold factor `l = l1 + l2`. + pub fn l_total_fold_factor(&self) -> usize { + self.l1_first_fold_factor + self.l2_second_fold_factor + } + + /// Fail-closed soundness check on `(s, t)`. Returns `Ok(bound)` only when + /// every admissibility flag passes and proximity soundness meets the + /// target. `WarpConfig::new` itself does not call this (it cannot, the + /// security target / field-size / rate / regime live outside the config); + /// callers building a config for production use **must** call this before + /// invoking the prover. See `docs/paper-mods/mod4_parameter_selection.tex`. + pub fn validate_security( + &self, + field_bits: u32, + code_rate: f64, + regime: Regime, + target: SecurityLevel, + ) -> Result { + let params = Params { + s: self.s_num_ood_samples, + t: self.t_num_queries, + }; + validate(¶ms, field_bits, code_rate, regime, target) + } } diff --git a/src/crypto/merkle/blake3.rs b/src/crypto/merkle/blake3.rs deleted file mode 100644 index d2ef41b..0000000 --- a/src/crypto/merkle/blake3.rs +++ /dev/null @@ -1,27 +0,0 @@ -use super::parameters::MerkleTreeParams; -use ark_crypto_primitives::crh::blake3::{Blake3CRH, Blake3TwoToOneCRH}; -use ark_crypto_primitives::crh::ByteDigest; -use ark_crypto_primitives::{ - crh::{CRHScheme, TwoToOneCRHScheme}, - merkle_tree::{Config as MerkleConfig, IdentityDigestConverter}, - sponge::Absorb, -}; -use ark_ff::PrimeField; -use ark_std::marker::PhantomData; - -#[derive(Clone)] -pub struct Blake3MerkleConfig { - _field: PhantomData, -} - -pub type Blake3MerkleTreeParams = - MerkleTreeParams, Blake3TwoToOneCRH, ByteDigest<32>>; - -impl MerkleConfig for Blake3MerkleConfig { - type Leaf = [F]; - type LeafDigest = ::Output; - type LeafInnerDigestConverter = IdentityDigestConverter; - type InnerDigest = ::Output; - type LeafHash = Blake3CRH; - type TwoToOneHash = Blake3TwoToOneCRH; -} diff --git a/src/crypto/merkle/mod.rs b/src/crypto/merkle/mod.rs deleted file mode 100644 index 43b2474..0000000 --- a/src/crypto/merkle/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -use ark_codes::traits::LinearCode; -use ark_crypto_primitives::{ - merkle_tree::{Config, MerkleTree, Path}, - Error, -}; -use ark_ff::Field; - -pub mod blake3; -pub mod parameters; -pub mod poseidon; - -pub fn build_codeword_leaves>( - code: &C, - witnesses: &[Vec], - l1: usize, -) -> (Vec>, Vec) { - let mut leaves = vec![F::default(); l1 * code.code_len()]; - let mut codewords = vec![vec![F::default(); code.code_len()]; l1]; - for (i, w) in witnesses.iter().enumerate() { - let f_i = code.encode(w); - // stacking codewords in flat array, which we chunk below - // [[w_0[0], .., w_{N-1}[0]], .., [w_0[N-1], .., w_{N-1}[N-1]]] // L * N elements - for (j, value) in f_i.iter().enumerate() { - leaves[(j * l1) + i] = *value; - } - codewords[i] = f_i; - } - (codewords, leaves) -} - -pub fn compute_auth_paths( - td: &MerkleTree

, - indexes: &[usize], -) -> Result>, Error> { - let paths = indexes - .iter() - .map(|x_t| td.generate_proof(*x_t)) - .collect::>, Error>>()?; - Ok(paths) -} diff --git a/src/crypto/merkle/parameters.rs b/src/crypto/merkle/parameters.rs deleted file mode 100644 index 19667d7..0000000 --- a/src/crypto/merkle/parameters.rs +++ /dev/null @@ -1,76 +0,0 @@ -use ark_crypto_primitives::{ - crh::{CRHScheme, TwoToOneCRHScheme}, - merkle_tree::{Config, IdentityDigestConverter}, - sponge::Absorb, -}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::rand::RngCore; -use serde::Deserialize; -use serde::Serialize; -use std::{hash::Hash, marker::PhantomData}; - -/// A generic Merkle tree config usable across hash types (e.g., Blake3, Keccak). -/// -/// # Type Parameters: -/// - `F`: Field element used in the leaves -/// - `LeafH`: Leaf hash function -/// - `CompressH`: Internal node hasher -/// - `Digest`: Digest type -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct MerkleTreeParams { - #[serde(skip)] - _marker: PhantomData<(F, LeafH, CompressH, Digest)>, -} - -impl Config for MerkleTreeParams -where - F: CanonicalSerialize + Send, - LeafH: CRHScheme, - CompressH: TwoToOneCRHScheme, - Digest: Clone - + std::fmt::Debug - + Default - + CanonicalSerialize - + CanonicalDeserialize - + Eq - + PartialEq - + Hash - + Send - + Absorb, -{ - type Leaf = [F]; - - type LeafDigest = Digest; - type LeafInnerDigestConverter = IdentityDigestConverter; - type InnerDigest = Digest; - - type LeafHash = LeafH; - type TwoToOneHash = CompressH; -} - -/// Returns the `(leaf_hash_params, two_to_one_hash_params)` for any compatible Merkle tree. -/// -/// # Type Parameters -/// - `F`: The leaf field element type -/// - `LeafH`: The leaf hash function -/// - `CompressH`: The two-to-one internal hash function -/// -/// # Panics -/// Panics if `setup()` fails (which should not happen for deterministic hashers). -pub fn default_config( - rng: &mut impl RngCore, -) -> ( - ::Parameters, - ::Parameters, -) -where - F: CanonicalSerialize + Send, - LeafH: CRHScheme + Send, - CompressH: TwoToOneCRHScheme + Send, -{ - ( - LeafH::setup(rng).expect("Failed to setup Leaf hash"), - CompressH::setup(rng).expect("Failed to setup Compress hash"), - ) -} diff --git a/src/crypto/merkle/poseidon.rs b/src/crypto/merkle/poseidon.rs deleted file mode 100644 index f5a989d..0000000 --- a/src/crypto/merkle/poseidon.rs +++ /dev/null @@ -1,52 +0,0 @@ -use ark_crypto_primitives::{ - crh::{ - poseidon::{ - constraints::{ - CRHGadget as PoseidonCRHGadget, TwoToOneCRHGadget as PoseidonTwoToOneCRHGadget, - }, - TwoToOneCRH as PoseidonTwoToOneCRH, CRH as PoseidonCRH, - }, - CRHScheme, CRHSchemeGadget, TwoToOneCRHScheme, TwoToOneCRHSchemeGadget, - }, - merkle_tree::{ - constraints::ConfigGadget as MerkleConfigGadget, Config as MerkleConfig, - IdentityDigestConverter, - }, - sponge::Absorb, -}; -use ark_ff::PrimeField; -use ark_r1cs_std::fields::fp::FpVar; -use ark_std::marker::PhantomData; - -#[derive(Clone)] -pub struct PoseidonMerkleConfig { - _field: PhantomData, -} - -impl MerkleConfig for PoseidonMerkleConfig { - type Leaf = [F]; - type LeafDigest = ::Output; - type LeafInnerDigestConverter = IdentityDigestConverter; - type InnerDigest = ::Output; - type LeafHash = PoseidonCRH; - type TwoToOneHash = PoseidonTwoToOneCRH; -} - -#[derive(Clone)] -pub struct PoseidonMerkleConfigGadget { - _field: PhantomData, -} - -impl MerkleConfigGadget, F> - for PoseidonMerkleConfigGadget -{ - type Leaf = [FpVar]; - type LeafDigest = as CRHSchemeGadget, F>>::OutputVar; - type LeafHash = PoseidonCRHGadget; - type LeafInnerConverter = IdentityDigestConverter; - type InnerDigest = as TwoToOneCRHSchemeGadget< - PoseidonTwoToOneCRH, - F, - >>::OutputVar; - type TwoToOneHash = PoseidonTwoToOneCRHGadget; -} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index bdf9eb8..22ea2ae 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1 +1 @@ -pub mod merkle; +pub mod vc; diff --git a/src/crypto/vc.rs b/src/crypto/vc.rs new file mode 100644 index 0000000..6d4ade8 --- /dev/null +++ b/src/crypto/vc.rs @@ -0,0 +1,33 @@ +//! Vector-commitment plumbing for warp. +//! +//! Protocol code is generic over `ark_vc::mvc::MultiVectorCommitment`. +//! This module's only job is the [`CommittedCodewords`] helper that +//! pairs `(Commitment, CommitmentState)` from the trait with the +//! original column-codewords — `V::CommitmentState` is opaque per the +//! trait, so warp keeps codewords beside it for the proximity opens +//! and the decider's recompute check. + +use ark_ff::Field; +use ark_vc::mvc::MultiVectorCommitment; + +pub struct CommittedCodewords> { + pub commitment: V::Commitment, + pub state: V::CommitmentState, + pub codewords: Vec>, +} + +impl Clone for CommittedCodewords +where + F: Field, + V: MultiVectorCommitment, + V::Commitment: Clone, + V::CommitmentState: Clone, +{ + fn clone(&self) -> Self { + Self { + commitment: self.commitment.clone(), + state: self.state.clone(), + codewords: self.codewords.clone(), + } + } +} diff --git a/src/error.rs b/src/error.rs index fb6aa08..59d7387 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,7 +2,7 @@ use ark_crypto_primitives::Error; use thiserror::Error; #[derive(Error, Debug)] -pub enum WARPError { +pub enum WarpError { #[error(transparent)] ProverError(#[from] ProverError), #[error(transparent)] @@ -17,12 +17,8 @@ pub enum WARPError { ZeroEvaderSize(usize, usize), #[error("LC does not exist")] R1CSNonExistingLC, - #[error("Error decoding codeword")] - DecodeFailed, - #[error("Bundled PESAT eval returned {0}, multilinear evals returned {1}")] - UnsatisfiedMultiConstraints(bool, bool), - #[error("f.len() is {0}, but tried accessing at {1}")] - CodewordSize(usize, usize), + #[error("R1CS construction: {reason}")] + R1CSConstruction { reason: &'static str }, } #[derive(Error, Debug)] @@ -31,8 +27,22 @@ pub enum ProverError { ArkError(#[from] Error), #[error("Spongefish verification error")] SpongeFish, - #[error("Expected eval, got None")] - EmptyEval, + #[error("instance batch must contain at least 2 instances; got {got}")] + InsufficientInstances { got: usize }, + #[error("instances.len() ({instances}) != witnesses.len() ({witnesses})")] + InstanceWitnessLengthMismatch { instances: usize, witnesses: usize }, + #[error("acc_witness.td_committed_codewords.len() ({roots}) != acc_instance.rt_merkle_roots.len() ({instances})")] + AccumulatorShapeMismatch { instances: usize, roots: usize }, + #[error("config parameter invalid: {reason}")] + ConfigParameterInvalid { reason: String }, + #[error("instance length mismatch: expected {expected}, got {got}")] + InstanceLengthMismatch { expected: usize, got: usize }, + #[error("statement layout mismatch: {what} expected {expected}, got {got}")] + StatementShape { + what: &'static str, + expected: usize, + got: usize, + }, } impl From for ProverError { @@ -41,6 +51,24 @@ impl From for ProverError { } } +impl From for ProverError { + fn from(e: ark_iop::IorProverError) -> Self { + match e { + ark_iop::IorProverError::StatementShape { + what, + expected, + got, + } => Self::StatementShape { + what, + expected, + got, + }, + ark_iop::IorProverError::Transcript(_) => Self::SpongeFish, + ark_iop::IorProverError::Custom(msg) => Self::ConfigParameterInvalid { reason: msg }, + } + } +} + #[derive(Error, Debug)] pub enum VerifierError { #[error(transparent)] @@ -65,6 +93,12 @@ pub enum VerifierError { SumcheckRound, #[error("Incorrect target")] Target, + #[error("statement layout mismatch: {what} expected {expected}, got {got}")] + StatementShape { + what: &'static str, + expected: usize, + got: usize, + }, } impl From for VerifierError { @@ -73,21 +107,40 @@ impl From for VerifierError { } } +impl From for VerifierError { + fn from(e: ark_iop::IorVerifierError) -> Self { + match e { + ark_iop::IorVerifierError::StatementShape { + what, + expected, + got, + } => Self::StatementShape { + what, + expected, + got, + }, + ark_iop::IorVerifierError::Target => Self::Target, + ark_iop::IorVerifierError::Transcript(_) => Self::SpongeFish, + ark_iop::IorVerifierError::Custom(_) => Self::Target, + } + } +} + impl From for VerifierError { fn from(err: effsc::proof::SumcheckError) -> Self { use effsc::proof::SumcheckError; match err { - // Round consistency or degree mismatch detected by the library. + // Round consistency or degree checks failed inside the library. SumcheckError::ConsistencyCheck { .. } | SumcheckError::DegreeMismatch { .. } => { Self::SumcheckRound } - // Not raised by `sumcheck_verify` any more (the library dropped - // the internal oracle check); warp does the final-claim check - // itself. Retained for enum completeness. + // The caller-supplied oracle_check (the final-claim equality) + // rejected — surfaces as warp's `Target` variant for backwards + // compatibility with the existing negative tests. SumcheckError::FinalEvaluation => Self::Target, // Ran out of transcript or malformed bytes. SumcheckError::TranscriptError { .. } => Self::SpongeFish, - // Unreachable: warp always passes a noop hook. + // Not reachable: warp passes `noop_hook_verify`. SumcheckError::HookError { .. } => Self::SumcheckRound, } } diff --git a/src/iop/iors/batching.rs b/src/iop/iors/batching.rs new file mode 100644 index 0000000..21d1a7c --- /dev/null +++ b/src/iop/iors/batching.rs @@ -0,0 +1,333 @@ +//! Batching sumcheck IOR. Reduces `Σ_i ξ(i)·f̂(ζ_i) = σ₂` to a single +//! claim `μ = f̂(α)` via inner-product sumcheck with the CBBZ23 / HyperPlonk +//! sparse-evaluation optimization. + +use ark_ff::{Field, PrimeField}; +use ark_iop::{ + IorProveResult, IorProverError, IorVerifierError, IorVerifyResult, ProverTriple, IOR, +}; +use ark_std::log2; +use effsc::{ + noop_hook, provers::inner_product::InnerProductProver, runner::sumcheck, + verifier::sumcheck_verify, +}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::collections::HashMap; +use std::marker::PhantomData; + +use crate::count_ops; +use crate::iop::oracles::evaluation::Oracle; +use crate::utils::poly::{eq_poly, eq_poly_non_binary}; + +/// Sparse-eval optimization (CBBZ23 / HyperPlonk): shift-query zetas at +/// indices `1+s..r` are 0/1 vectors picking a single hypercube point. +fn accumulate_sparse_evaluations( + zetas_evaluation_points: &[Vec], + xi_eq_evals: &[F], + s_num_ood_samples: usize, + r_total_points: usize, +) -> HashMap { + let mut result: HashMap = HashMap::new(); + for i in 1 + s_num_ood_samples..r_total_points { + let index = zetas_evaluation_points[i] + .iter() + .enumerate() + .filter_map(|(j, bit)| bit.is_one().then_some(1 << j)) + .sum::(); + *result.entry(index).or_insert_with(F::zero) += xi_eq_evals[i]; + } + result +} + +fn batched_constraint_poly( + dense_polys: &[Vec], + sparse_polys: &HashMap, +) -> Vec { + if dense_polys.is_empty() { + return Vec::new(); + } + let mut result = vec![F::ZERO; dense_polys[0].len()]; + for row in dense_polys { + for (i, val) in row.iter().enumerate() { + result[i] += *val; + } + } + for (k, v) in sparse_polys.iter() { + result[*k] += *v; + } + result +} + +pub struct BatchingStatement { + /// `1 + s + t` evaluation points: `[ζ_0, ood_chunk_0…, query_k…]`. + pub zetas_prefix: Vec>, + pub s_num_ood_samples: usize, + pub t_num_queries: usize, + pub log_n: usize, + pub _phantom: std::marker::PhantomData, +} + +impl BatchingStatement { + /// Single source of truth for `zetas_prefix` — both prover and verifier + /// build the statement through this so layout drift is impossible. + pub fn from_ior_outputs( + zeta_0: Vec, + ood_samples_flat: &[F], + query_eval_points: &[Vec], + s_num_ood_samples: usize, + t_num_queries: usize, + log_n: usize, + ) -> Self { + let mut zetas: Vec> = Vec::with_capacity(1 + s_num_ood_samples + t_num_queries); + zetas.push(zeta_0); + for chunk in ood_samples_flat.chunks(log_n) { + zetas.push(chunk.to_vec()); + } + for q in query_eval_points { + zetas.push(q.clone()); + } + Self { + zetas_prefix: zetas, + s_num_ood_samples, + t_num_queries, + log_n, + _phantom: std::marker::PhantomData, + } + } +} + +pub struct BatchingProverInputs<'a, F: Field> { + pub oracle: &'a Oracle, +} + +pub struct BatchingVerifierInputs { + pub nus_claimed_evals: Vec, + pub acc_mu: F, +} + +pub struct BatchingReductionInputs { + pub alpha_sumcheck_challenges: Vec, +} + +pub struct BatchingReducedStatement { + pub alpha_sumcheck_challenges: Vec, +} + +pub struct BatchingReducedWitness { + pub mu_claimed_eval: F, +} + +#[derive(Default)] +pub struct Batching(PhantomData); + +impl IOR for Batching +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, +{ + const NAME: &'static str = "Batching"; + const MESSAGE_TAGS: &'static [&'static str] = &["squeeze:xis", "delegate:effsc.sumcheck"]; + + type Statement<'b> + = BatchingStatement + where + Self: 'b; + type Witness<'b> + = () + where + Self: 'b; + type ProverInputs<'b> + = BatchingProverInputs<'b, F> + where + Self: 'b; + type VerifierInputs<'b> + = BatchingVerifierInputs + where + Self: 'b; + type ReductionInputs = BatchingReductionInputs; + type ReducedStatement = BatchingReducedStatement; + type ProofString = (); + type ReducedWitness = BatchingReducedWitness; + type VerifierOutputs = (); + + fn reduce_statement<'b>( + &self, + _statement: &Self::Statement<'b>, + inputs: &Self::ReductionInputs, + ) -> Self::ReducedStatement + where + Self: 'b, + { + BatchingReducedStatement { + alpha_sumcheck_challenges: inputs.alpha_sumcheck_challenges.clone(), + } + } +} + +impl Batching +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, +{ + #[tracing::instrument( + name = "batching", + skip_all, + fields(s = statement.s_num_ood_samples, t = statement.t_num_queries, log_n = statement.log_n) + )] + fn prove_inner( + &self, + prover_state: &mut ProverState, + statement: &BatchingStatement, + inputs: &BatchingProverInputs<'_, F>, + ) -> ProverTriple, (), BatchingReducedWitness> { + let n = inputs.oracle.len(); + let r = 1 + statement.s_num_ood_samples + statement.t_num_queries; + let log_r = log2(r) as usize; + if statement.zetas_prefix.len() != r { + return Err(IorProverError::StatementShape { + what: "zetas_prefix", + expected: r, + got: statement.zetas_prefix.len(), + }); + } + + let xis = prover_state.verifier_messages_vec::(log_r); + + let (xi_eq_evals, ood_evals_vec) = { + let _s = tracing::info_span!("batching.eq_evals").entered(); + let xi_eq_evals = (0..r).map(|i| eq_poly(&xis, i)).collect::>(); + let ood_evals_vec = (0..1 + statement.s_num_ood_samples) + .map(|i| { + (0..n) + .map(|a| eq_poly(&statement.zetas_prefix[i], a) * xi_eq_evals[i]) + .collect::>() + }) + .collect::>(); + (xi_eq_evals, ood_evals_vec) + }; + + let id_non_0_eval_sums = { + let _s = tracing::info_span!("batching.accumulate_sparse").entered(); + accumulate_sparse_evaluations( + &statement.zetas_prefix, + &xi_eq_evals, + statement.s_num_ood_samples, + r, + ) + }; + + // Inner-product sumcheck. MSB half-split → reverse once. + let alpha = { + let _s = tracing::info_span!("batching.sumcheck").entered(); + let log_n_bits = ark_std::log2(n) as u64; + count_ops!(BatchingRounds, log_n_bits); + let mut ip = InnerProductProver::new( + inputs.oracle.evals().to_vec(), + batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), + ); + let mut challenges = + sumcheck(&mut ip, log_n_bits as usize, prover_state, noop_hook).challenges; + challenges.reverse(); + challenges + }; + + let mu = inputs.oracle.query_at_point(&alpha); + + Ok(( + BatchingReductionInputs { + alpha_sumcheck_challenges: alpha.clone(), + }, + (), + BatchingReducedWitness { + mu_claimed_eval: mu, + }, + )) + } + + #[tracing::instrument( + name = "batching.verify", + skip_all, + fields(s = statement.s_num_ood_samples, t = statement.t_num_queries, log_n = statement.log_n) + )] + fn verify_inner( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &BatchingStatement, + inputs: &BatchingVerifierInputs, + ) -> Result<(BatchingReductionInputs, ()), IorVerifierError> { + let r = 1 + statement.s_num_ood_samples + statement.t_num_queries; + let log_r = log2(r) as usize; + if statement.zetas_prefix.len() != r { + return Err(IorVerifierError::StatementShape { + what: "zetas_prefix", + expected: r, + got: statement.zetas_prefix.len(), + }); + } + if inputs.nus_claimed_evals.len() != r { + return Err(IorVerifierError::StatementShape { + what: "nus_claimed_evals", + expected: r, + got: inputs.nus_claimed_evals.len(), + }); + } + + let xis: Vec = (0..log_r) + .map(|_| verifier_state.verifier_message::()) + .collect(); + let xi_eq_evals = (0..r).map(|i| eq_poly(&xis, i)).collect::>(); + + let sigma_2 = xi_eq_evals + .iter() + .zip(&inputs.nus_claimed_evals) + .fold(F::zero(), |acc, (xi_eq, nu)| acc + *xi_eq * nu); + + let res = sumcheck_verify(sigma_2, 2, statement.log_n, verifier_state, |_, _| Ok(())) + .map_err(|e| IorVerifierError::Transcript(format!("sumcheck: {e:?}")))?; + let alpha_lsb: Vec = res.challenges.iter().rev().copied().collect(); + + let mut zeta_eqs = Vec::with_capacity(r); + for zeta in &statement.zetas_prefix { + zeta_eqs.push(eq_poly_non_binary(zeta, &alpha_lsb)); + } + let expected = inputs.acc_mu + * zeta_eqs + .into_iter() + .zip(&xi_eq_evals) + .fold(F::zero(), |acc, (a, b)| acc + a * *b); + (expected == res.final_claim) + .then_some(()) + .ok_or(IorVerifierError::Target)?; + + Ok(( + BatchingReductionInputs { + alpha_sumcheck_challenges: alpha_lsb, + }, + (), + )) + } + + pub fn prove( + &self, + prover_state: &mut ProverState, + statement: &BatchingStatement, + _witness: &(), + inputs: &BatchingProverInputs<'_, F>, + ) -> Result< + IorProveResult, (), BatchingReducedWitness>, + IorProverError, + > { + self.compose_prove(prover_state, statement, |t| { + self.prove_inner(t, statement, inputs) + }) + } + + pub fn verify( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &BatchingStatement, + inputs: &BatchingVerifierInputs, + ) -> Result, ()>, IorVerifierError> { + self.compose_verify(verifier_state, statement, |t| { + self.verify_inner(t, statement, inputs) + }) + } +} diff --git a/src/iop/iors/bridge.rs b/src/iop/iors/bridge.rs new file mode 100644 index 0000000..b41086e --- /dev/null +++ b/src/iop/iors/bridge.rs @@ -0,0 +1,258 @@ +//! TC → OOD bridge IOR. Publishes `(td_new, η, ν₀)` and discharges +//! TwinConstraint's deferred oracle check `eq(τ,γ)·(ν₀ + ω·η) ≟ final_claim`. + +use ark_ff::Field; +use ark_iop::{ + IorProveResult, IorProverError, IorVerifierError, IorVerifyResult, ProverTriple, IOR, +}; +use ark_vc::mvc::MultiVectorCommitment; +use ark_vc::vc::VectorCommitment; +use effsc::hypercube::compute_hypercube_eq_evals; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::marker::PhantomData; + +use crate::count_ops; +use crate::crypto::vc::CommittedCodewords; +use crate::iop::iors::twin_constraint::DeferredOracleCheck; +use crate::iop::oracles::evaluation::Oracle; +use crate::relations::PolyPredicate; + +pub struct BridgeStatement { + pub zeta_0: Vec, + pub beta_tau: Vec, + pub log_m: usize, + pub n_minus_k: usize, +} + +pub struct BridgeWitness<'a, F: Field> { + pub z_witness_assignment: &'a [F], + pub f_oracle: &'a Oracle, +} + +pub struct BridgeProverInputs<'a, F, P, V> +where + F: Field, + P: PolyPredicate, + V: MultiVectorCommitment, +{ + pub predicate: &'a P, + pub ck: &'a V::CommitterKey, + pub _f: PhantomData, +} + +pub struct BridgeVerifierInputs<'a, F: Field> { + pub deferred: &'a DeferredOracleCheck, + pub gamma_twin_constraint_challenges: &'a [F], +} + +pub struct BridgeReductionInputs +where + F: Field, + V: MultiVectorCommitment, +{ + pub eta_predicate_eval: F, + pub nu_0_oracle_eval: F, + pub td_new_commitment: V::Commitment, +} + +pub struct BridgeReducedStatement +where + F: Field, + V: MultiVectorCommitment, +{ + pub eta_predicate_eval: F, + pub nu_0_oracle_eval: F, + pub td_new_commitment: V::Commitment, +} + +pub struct BridgeReducedWitness +where + F: Field, + V: MultiVectorCommitment, +{ + pub td_new: CommittedCodewords, + pub new_x: Vec, + pub new_w: Vec, +} + +pub struct Bridge(PhantomData, PhantomData

, PhantomData); + +impl Default for Bridge { + fn default() -> Self { + Self(PhantomData, PhantomData, PhantomData) + } +} + +impl IOR for Bridge +where + F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + P: PolyPredicate, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargSerialize + NargDeserialize + Clone, +{ + const NAME: &'static str = "Bridge"; + const MESSAGE_TAGS: &'static [&'static str] = + &["send:td_new_commitment", "send:eta", "send:nu_0"]; + + type Statement<'b> + = BridgeStatement + where + Self: 'b; + type Witness<'b> + = BridgeWitness<'b, F> + where + Self: 'b; + type ProverInputs<'b> + = BridgeProverInputs<'b, F, P, V> + where + Self: 'b; + type VerifierInputs<'b> + = BridgeVerifierInputs<'b, F> + where + Self: 'b; + type ReductionInputs = BridgeReductionInputs; + type ReducedStatement = BridgeReducedStatement; + type ProofString = (); + type ReducedWitness = BridgeReducedWitness; + type VerifierOutputs = (); + + fn reduce_statement<'a>( + &self, + _statement: &Self::Statement<'a>, + inputs: &Self::ReductionInputs, + ) -> Self::ReducedStatement + where + Self: 'a, + { + BridgeReducedStatement { + eta_predicate_eval: inputs.eta_predicate_eval, + nu_0_oracle_eval: inputs.nu_0_oracle_eval, + td_new_commitment: inputs.td_new_commitment.clone(), + } + } +} + +impl Bridge +where + F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + P: PolyPredicate, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargSerialize + NargDeserialize + Clone, +{ + #[tracing::instrument( + name = "bridge", + skip_all, + fields(log_m = statement.log_m, n_minus_k = statement.n_minus_k) + )] + fn prove_inner( + &self, + prover_state: &mut ProverState, + statement: &BridgeStatement, + witness: &BridgeWitness<'_, F>, + inputs: &BridgeProverInputs<'_, F, P, V>, + ) -> ProverTriple, (), BridgeReducedWitness> { + let beta_eq_evals = compute_hypercube_eq_evals(statement.log_m, &statement.beta_tau); + let eta = inputs + .predicate + .evaluate_bundled(&beta_eq_evals, witness.z_witness_assignment) + .map_err(|e| IorProverError::Custom(format!("predicate eval: {e}")))?; + + let nu_0 = witness.f_oracle.query_at_point(&statement.zeta_0); + + let (new_x_slice, new_w_slice) = witness.z_witness_assignment.split_at(statement.n_minus_k); + let new_x = new_x_slice.to_vec(); + let new_w = new_w_slice.to_vec(); + + let td_new = { + let _s = tracing::info_span!("bridge.commit_new_oracle").entered(); + count_ops!(MerkleTreeBuilds); + let codeword = witness.f_oracle.evals().to_vec(); + let (commitment, state) = + ::commit(inputs.ck, codeword.iter()) + .map_err(|e| IorProverError::Custom(format!("commit: {e}")))?; + CommittedCodewords:: { + commitment, + state, + codewords: vec![codeword], + } + }; + let td_new_commitment: V::Commitment = td_new.commitment.clone(); + + prover_state.prover_message(&td_new_commitment); + prover_state.prover_message(&eta); + prover_state.prover_message(&nu_0); + + Ok(( + BridgeReductionInputs { + eta_predicate_eval: eta, + nu_0_oracle_eval: nu_0, + td_new_commitment, + }, + (), + BridgeReducedWitness { + td_new, + new_x, + new_w, + }, + )) + } + + #[tracing::instrument(name = "bridge.verify", skip_all)] + fn verify_inner( + &self, + verifier_state: &mut VerifierState<'_>, + _statement: &BridgeStatement, + inputs: &BridgeVerifierInputs<'_, F>, + ) -> Result<(BridgeReductionInputs, ()), IorVerifierError> { + let td_new_commitment: V::Commitment = verifier_state + .prover_message() + .map_err(|e| IorVerifierError::Transcript(e.to_string()))?; + let eta: F = verifier_state + .prover_message() + .map_err(|e| IorVerifierError::Transcript(e.to_string()))?; + let nu_0: F = verifier_state + .prover_message() + .map_err(|e| IorVerifierError::Transcript(e.to_string()))?; + + inputs + .deferred + .discharge(inputs.gamma_twin_constraint_challenges, nu_0, eta) + .map_err(|_| IorVerifierError::Target)?; + + Ok(( + BridgeReductionInputs { + eta_predicate_eval: eta, + nu_0_oracle_eval: nu_0, + td_new_commitment, + }, + (), + )) + } + + #[allow(clippy::type_complexity)] + pub fn prove( + &self, + prover_state: &mut ProverState, + statement: &BridgeStatement, + witness: &BridgeWitness<'_, F>, + inputs: &BridgeProverInputs<'_, F, P, V>, + ) -> Result< + IorProveResult, (), BridgeReducedWitness>, + IorProverError, + > { + self.compose_prove(prover_state, statement, |t| { + self.prove_inner(t, statement, witness, inputs) + }) + } + + pub fn verify( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &BridgeStatement, + inputs: &BridgeVerifierInputs<'_, F>, + ) -> Result, ()>, IorVerifierError> { + self.compose_verify(verifier_state, statement, |t| { + self.verify_inner(t, statement, inputs) + }) + } +} diff --git a/src/iop/iors/mod.rs b/src/iop/iors/mod.rs new file mode 100644 index 0000000..b43c422 --- /dev/null +++ b/src/iop/iors/mod.rs @@ -0,0 +1,9 @@ +//! Concrete Warp IORs, in protocol order. + +pub mod batching; +pub mod bridge; +pub mod ood; +pub mod pesat; +pub mod proximity; +pub mod sample_queries; +pub mod twin_constraint; diff --git a/src/iop/iors/ood.rs b/src/iop/iors/ood.rs new file mode 100644 index 0000000..8b265f4 --- /dev/null +++ b/src/iop/iors/ood.rs @@ -0,0 +1,155 @@ +//! Out-of-domain sampling IOR. + +use ark_ff::{Field, PrimeField}; +use ark_iop::{ + IorProveResult, IorProverError, IorVerifierError, IorVerifyResult, ProverTriple, IOR, +}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::marker::PhantomData; + +use crate::count_ops; +use crate::iop::oracles::evaluation::Oracle; + +pub struct OodStatement { + pub s_num_ood_samples: usize, + pub log_n: usize, +} + +pub struct OodProverInputs<'a, F: Field> { + pub oracle: &'a Oracle, +} + +pub struct OodReductionInputs { + pub samples_flat: Vec, + pub answers: Vec, +} + +pub struct OodReducedStatement { + pub samples_flat: Vec, + pub answers: Vec, +} + +#[derive(Default)] +pub struct Ood(PhantomData); + +impl IOR for Ood +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, +{ + const NAME: &'static str = "OOD"; + const MESSAGE_TAGS: &'static [&'static str] = &["squeeze:samples", "send:answers"]; + + type Statement<'b> + = OodStatement + where + Self: 'b; + type Witness<'b> + = () + where + Self: 'b; + type ProverInputs<'b> + = OodProverInputs<'b, F> + where + Self: 'b; + type VerifierInputs<'b> + = () + where + Self: 'b; + type ReductionInputs = OodReductionInputs; + type ReducedStatement = OodReducedStatement; + type ProofString = (); + type ReducedWitness = (); + type VerifierOutputs = (); + + fn reduce_statement<'b>( + &self, + _statement: &Self::Statement<'b>, + inputs: &Self::ReductionInputs, + ) -> Self::ReducedStatement + where + Self: 'b, + { + OodReducedStatement { + samples_flat: inputs.samples_flat.clone(), + answers: inputs.answers.clone(), + } + } +} + +impl Ood +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, +{ + #[tracing::instrument(name = "ood", skip_all, fields(s = statement.s_num_ood_samples, log_n = statement.log_n))] + fn prove_inner( + &self, + prover_state: &mut ProverState, + statement: &OodStatement, + inputs: &OodProverInputs<'_, F>, + ) -> ProverTriple, (), ()> { + let samples_flat = + prover_state.verifier_messages_vec::(statement.s_num_ood_samples * statement.log_n); + count_ops!(OodPointQueries, statement.s_num_ood_samples as u64); + let answers = samples_flat + .chunks(statement.log_n) + .map(|zeta| inputs.oracle.query_at_point(zeta)) + .collect::>(); + prover_state.prover_messages(&answers); + Ok(( + OodReductionInputs { + samples_flat, + answers, + }, + (), + (), + )) + } + + #[tracing::instrument( + name = "ood.verify", + skip_all, + fields(s = statement.s_num_ood_samples, log_n = statement.log_n) + )] + fn verify_inner( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &OodStatement, + ) -> Result<(OodReductionInputs, ()), IorVerifierError> { + let samples_flat: Vec = (0..statement.s_num_ood_samples * statement.log_n) + .map(|_| verifier_state.verifier_message::()) + .collect(); + let answers: Vec = verifier_state + .prover_messages_vec(statement.s_num_ood_samples) + .map_err(|e| IorVerifierError::Transcript(e.to_string()))?; + Ok(( + OodReductionInputs { + samples_flat, + answers, + }, + (), + )) + } + + pub fn prove( + &self, + prover_state: &mut ProverState, + statement: &OodStatement, + _witness: &(), + inputs: &OodProverInputs<'_, F>, + ) -> Result, (), ()>, IorProverError> { + self.compose_prove(prover_state, statement, |t| { + self.prove_inner(t, statement, inputs) + }) + } + + pub fn verify( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &OodStatement, + _inputs: &(), + ) -> Result, ()>, IorVerifierError> { + self.compose_verify(verifier_state, statement, |t| { + self.verify_inner(t, statement) + }) + } +} diff --git a/src/iop/iors/pesat.rs b/src/iop/iors/pesat.rs new file mode 100644 index 0000000..eedc78b --- /dev/null +++ b/src/iop/iors/pesat.rs @@ -0,0 +1,246 @@ +//! PESAT Reduction IOR. +//! +//! Encodes fresh witnesses into codewords, commits via the trait's +//! joint-commit path (one commitment over all l1 codewords), absorbs +//! that commitment + code evaluations, and derives the τ zero-check +//! challenges. + +use ark_codes::traits::LinearCode; +use ark_ff::{Field, PrimeField}; +use ark_iop::{ + IorProveResult, IorProverError, IorVerifierError, IorVerifyResult, ProverTriple, IOR, +}; +use ark_vc::mvc::MultiVectorCommitment; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::marker::PhantomData; + +use crate::count_ops; +use crate::crypto::vc::CommittedCodewords; + +pub struct PesatStatement { + pub l1_first_fold_factor: usize, + pub log_m: usize, +} + +pub struct PesatWitness<'a, F: Field> { + pub witnesses: &'a [Vec], +} + +pub struct PesatReductionInputs { + pub mus_codeword_first_coords: Vec, + pub taus_zero_check_challenges: Vec>, +} + +pub struct PesatReducedStatement { + pub mus_codeword_first_coords: Vec, + pub taus_zero_check_challenges: Vec>, +} + +pub struct PesatReducedWitness +where + F: Field, + V: MultiVectorCommitment, +{ + pub codewords: Vec>, + pub td_0_committed_codeword: CommittedCodewords, +} + +pub struct PesatVerifierOutputs +where + F: Field, + V: MultiVectorCommitment, +{ + pub rt_0_fresh_commitment: V::Commitment, +} + +/// PESAT IOR configuration. Holds borrowed code + the trait CK; the +/// IOR is generic over any `MultiVectorCommitment` over `F`. +pub struct Pesat<'a, F, C, V> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + C: LinearCode, + V: MultiVectorCommitment, +{ + pub code: &'a C, + pub ck: &'a V::CommitterKey, + pub _phantom: PhantomData, +} + +impl<'a, F, C, V> IOR for Pesat<'a, F, C, V> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + C: LinearCode, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargSerialize + NargDeserialize, +{ + const NAME: &'static str = "PESAT"; + const MESSAGE_TAGS: &'static [&'static str] = + &["send:rt_0_commitment", "send:mus", "squeeze:taus"]; + type Statement<'b> + = PesatStatement + where + Self: 'b; + type Witness<'b> + = PesatWitness<'b, F> + where + Self: 'b; + type ProverInputs<'b> + = () + where + Self: 'b; + type VerifierInputs<'b> + = () + where + Self: 'b; + type ReductionInputs = PesatReductionInputs; + type ReducedStatement = PesatReducedStatement; + type ProofString = (); + type ReducedWitness = PesatReducedWitness; + type VerifierOutputs = PesatVerifierOutputs; + + fn reduce_statement<'b>( + &self, + _statement: &Self::Statement<'b>, + inputs: &Self::ReductionInputs, + ) -> Self::ReducedStatement + where + Self: 'b, + { + PesatReducedStatement { + mus_codeword_first_coords: inputs.mus_codeword_first_coords.clone(), + taus_zero_check_challenges: inputs.taus_zero_check_challenges.clone(), + } + } +} + +impl<'a, F, C, V> Pesat<'a, F, C, V> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + C: LinearCode, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargSerialize + NargDeserialize, +{ + #[tracing::instrument( + name = "pesat", + skip_all, + fields(l1 = statement.l1_first_fold_factor, log_m = statement.log_m, n_witnesses = witness.witnesses.len()) + )] + fn prove_inner( + &self, + prover_state: &mut ProverState, + statement: &PesatStatement, + witness: &PesatWitness<'_, F>, + ) -> ProverTriple, (), PesatReducedWitness> { + let codewords: Vec> = { + let _s = tracing::info_span!("pesat.encode").entered(); + count_ops!(EncodeCalls, witness.witnesses.len() as u64); + witness + .witnesses + .iter() + .map(|w| self.code.encode(w)) + .collect() + }; + + let mus = codewords.iter().map(|f| f[0]).collect::>(); + + let td_0 = { + let _s = tracing::info_span!("pesat.merkle_commit").entered(); + count_ops!(MerkleTreeBuilds); + let (commitment, state) = + V::commit_multiple(self.ck, codewords.iter().map(|c| c.iter())) + .expect("pesat: commit_multiple failed"); + CommittedCodewords { + commitment, + state, + codewords: codewords.clone(), + } + }; + + let taus = { + let _s = tracing::info_span!("pesat.absorb_and_derive").entered(); + prover_state.prover_message(&td_0.commitment); + prover_state.prover_messages(&mus); + + (0..statement.l1_first_fold_factor) + .map(|_| prover_state.verifier_messages_vec::(statement.log_m)) + .collect::>() + }; + + Ok(( + PesatReductionInputs { + mus_codeword_first_coords: mus.clone(), + taus_zero_check_challenges: taus, + }, + (), + PesatReducedWitness { + codewords, + td_0_committed_codeword: td_0, + }, + )) + } + + #[tracing::instrument( + name = "pesat.verify", + skip_all, + fields(l1 = statement.l1_first_fold_factor, log_m = statement.log_m) + )] + fn verify_inner( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &PesatStatement, + ) -> Result<(PesatReductionInputs, PesatVerifierOutputs), IorVerifierError> { + let rt_0: V::Commitment = verifier_state + .prover_message() + .map_err(|e| IorVerifierError::Transcript(e.to_string()))?; + let mus: Vec = verifier_state + .prover_messages_vec(statement.l1_first_fold_factor) + .map_err(|e| IorVerifierError::Transcript(e.to_string()))?; + let taus: Vec> = (0..statement.l1_first_fold_factor) + .map(|_| { + (0..statement.log_m) + .map(|_| verifier_state.verifier_message::()) + .collect() + }) + .collect(); + + Ok(( + PesatReductionInputs { + mus_codeword_first_coords: mus, + taus_zero_check_challenges: taus, + }, + PesatVerifierOutputs { + rt_0_fresh_commitment: rt_0, + }, + )) + } + + #[allow(clippy::type_complexity)] + pub fn prove( + &self, + prover_state: &mut ProverState, + statement: &PesatStatement, + witness: &PesatWitness<'_, F>, + _inputs: &(), + ) -> Result< + IorProveResult, (), PesatReducedWitness>, + IorProverError, + > { + self.compose_prove(prover_state, statement, |t| { + self.prove_inner(t, statement, witness) + }) + } + + pub fn verify( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &PesatStatement, + _inputs: &(), + ) -> Result< + IorVerifyResult, PesatVerifierOutputs>, + IorVerifierError, + > { + self.compose_verify(verifier_state, statement, |t| { + self.verify_inner(t, statement) + }) + } +} diff --git a/src/iop/iors/proximity.rs b/src/iop/iors/proximity.rs new file mode 100644 index 0000000..aa573f7 --- /dev/null +++ b/src/iop/iors/proximity.rs @@ -0,0 +1,279 @@ +//! Proximity / shift-query IOR. +//! +//! Index queries on the committed oracles. Opens both the fresh PESAT +//! commitment and each accumulated commitment at the query positions. +//! Opening proofs (auth paths + sibling digests) are written into the +//! spongefish transcript by `V::open_multiple` rather than carried as +//! separate proof fields. + +use ark_ff::Field; +use ark_iop::{ + IndexedOracle, IorProveResult, IorProverError, IorVerifierError, IorVerifyResult, ProverTriple, + IOR, +}; +use ark_vc::mvc::MultiVectorCommitment; +use spongefish::{Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::marker::PhantomData; + +use crate::count_ops; +use crate::crypto::vc::CommittedCodewords; +use crate::iop::oracles::query_indices::QueryIndices; + +pub struct ProximityStatement { + pub queries: QueryIndices, + pub l2_second_fold_factor: usize, + pub t_num_queries: usize, + pub n_code_len: usize, +} + +pub struct ProximityProverInputs<'a, F, V> +where + F: Field, + V: MultiVectorCommitment, +{ + pub ck: &'a V::CommitterKey, + pub td_0_committed_codeword: &'a CommittedCodewords, + pub acc_td_committed_codewords: &'a [CommittedCodewords], +} + +/// Verifier-side inputs. The IOR sees [`IndexedOracle`] handles, not +/// raw commitments / opening proofs — IORs stay BCS-agnostic. +pub struct ProximityVerifierInputs<'a, F, O> +where + F: Field, + O: IndexedOracle>, +{ + pub fresh: &'a O, + pub acc: &'a [O], + pub _f: PhantomData, +} + +/// Wire-format proof string. Auth paths + sibling digests now live in +/// the spongefish transcript; only the shift-query answers remain +/// out-of-band. +pub struct ProximityProofString { + /// Per-query × per-codeword. Outer length = t (queries). Inner + /// length = (l2 acc + l1 fresh). + pub shift_query_answers: Vec>, +} + +pub struct Proximity<'a, F, V> +where + F: Field, + V: MultiVectorCommitment, +{ + pub ck: &'a V::CommitterKey, + pub _phantom: PhantomData, +} + +impl<'a, F, V> IOR for Proximity<'a, F, V> +where + F: Field, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargSerialize + NargDeserialize, +{ + const NAME: &'static str = "Proximity"; + const MESSAGE_TAGS: &'static [&'static str] = &["delegate:vc.open_multiple"]; + + type Statement<'b> + = ProximityStatement + where + Self: 'b; + type Witness<'b> + = () + where + Self: 'b; + type ProverInputs<'b> + = ProximityProverInputs<'b, F, V> + where + Self: 'b; + type VerifierInputs<'b> + = ProximityVerifierInputs<'b, F, ark_iop::ValidatedOracle> + where + Self: 'b; + type ReductionInputs = (); + type ReducedStatement = (); + type ProofString = ProximityProofString; + type ReducedWitness = (); + type VerifierOutputs = (); + + fn reduce_statement<'b>( + &self, + _statement: &Self::Statement<'b>, + _inputs: &Self::ReductionInputs, + ) -> Self::ReducedStatement + where + Self: 'b, + { + } +} + +impl<'a, F, V> Proximity<'a, F, V> +where + F: Field, + V: MultiVectorCommitment, + V::Commitment: Encoding<[u8]> + NargSerialize + NargDeserialize, +{ + #[tracing::instrument( + name = "proximity", + skip_all, + fields( + n_queries = statement.queries.leaf_positions.len(), + n_accumulators = inputs.acc_td_committed_codewords.len(), + ) + )] + fn prove_inner( + &self, + prover_state: &mut ProverState, + statement: &ProximityStatement, + inputs: &ProximityProverInputs<'_, F, V>, + ) -> ProverTriple<(), ProximityProofString, ()> { + let leaf_positions = &statement.queries.leaf_positions; + + let mut sorted_unique = leaf_positions.clone(); + sorted_unique.sort_unstable(); + sorted_unique.dedup(); + + let column_tuples = |codewords: &[Vec]| -> Vec> { + sorted_unique + .iter() + .map(|&i| codewords.iter().map(|c| c[i]).collect()) + .collect() + }; + + { + let _s = tracing::info_span!("proximity.auth_0").entered(); + count_ops!(MerklePathsGenerated, sorted_unique.len() as u64); + let values = column_tuples(&inputs.td_0_committed_codeword.codewords); + V::open_multiple( + inputs.ck, + inputs + .td_0_committed_codeword + .codewords + .iter() + .map(|c| c.iter()), + &inputs.td_0_committed_codeword.commitment, + sorted_unique.iter().copied(), + values.into_iter(), + &inputs.td_0_committed_codeword.state, + prover_state, + ) + .expect("proximity: open_multiple (fresh) failed"); + } + + { + let _s = tracing::info_span!("proximity.auth_j").entered(); + count_ops!( + MerklePathsGenerated, + (inputs.acc_td_committed_codewords.len() * sorted_unique.len()) as u64 + ); + for td in inputs.acc_td_committed_codewords.iter() { + let values = column_tuples(&td.codewords); + V::open_multiple( + inputs.ck, + td.codewords.iter().map(|c| c.iter()), + &td.commitment, + sorted_unique.iter().copied(), + values.into_iter(), + &td.state, + prover_state, + ) + .expect("proximity: open_multiple (acc) failed"); + } + } + + let shift_query_answers = { + let _s = tracing::info_span!("proximity.shift_queries").entered(); + let total_codewords = inputs + .acc_td_committed_codewords + .iter() + .map(|td| td.codewords.len()) + .sum::() + + inputs.td_0_committed_codeword.codewords.len(); + let mut answers = vec![vec![F::default(); total_codewords]; leaf_positions.len()]; + for (qi, idx) in leaf_positions.iter().enumerate() { + let mut col = 0usize; + for td in inputs.acc_td_committed_codewords.iter() { + for cw in &td.codewords { + answers[qi][col] = cw[*idx]; + col += 1; + } + } + for cw in &inputs.td_0_committed_codeword.codewords { + answers[qi][col] = cw[*idx]; + col += 1; + } + } + answers + }; + + Ok(( + (), + ProximityProofString { + shift_query_answers, + }, + (), + )) + } + + #[tracing::instrument( + name = "proximity.verify", + skip_all, + fields(t = statement.t_num_queries, l2 = statement.l2_second_fold_factor) + )] + fn verify_inner( + &self, + _verifier_state: &mut VerifierState<'_>, + statement: &ProximityStatement, + inputs: &ProximityVerifierInputs<'_, F, ark_iop::ValidatedOracle>, + ) -> Result<((), ()), IorVerifierError> { + (inputs.acc.len() == statement.l2_second_fold_factor) + .then_some(()) + .ok_or_else(|| { + IorVerifierError::Custom(format!( + "Proximity: NumL2Instances mismatch (got {}, expected {})", + inputs.acc.len(), + statement.l2_second_fold_factor + )) + })?; + + // Validation by construction: ValidatedOracle's existence + // already attests the orchestrator ran V::check_multiple + // upstream. No runtime validate() call needed. + count_ops!( + MerklePathsVerified, + statement.queries.leaf_positions.len() as u64 + ); + for _ in inputs.acc.iter() { + count_ops!( + MerklePathsVerified, + statement.queries.leaf_positions.len() as u64 + ); + } + + Ok(((), ())) + } + + pub fn prove( + &self, + prover_state: &mut ProverState, + statement: &ProximityStatement, + _witness: &(), + inputs: &ProximityProverInputs<'_, F, V>, + ) -> Result, ()>, IorProverError> { + self.compose_prove(prover_state, statement, |t| { + self.prove_inner(t, statement, inputs) + }) + } + + pub fn verify( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &ProximityStatement, + inputs: &ProximityVerifierInputs<'_, F, ark_iop::ValidatedOracle>, + ) -> Result, IorVerifierError> { + self.compose_verify(verifier_state, statement, |t| { + self.verify_inner(t, statement, inputs) + }) + } +} diff --git a/src/iop/iors/sample_queries.rs b/src/iop/iors/sample_queries.rs new file mode 100644 index 0000000..b371454 --- /dev/null +++ b/src/iop/iors/sample_queries.rs @@ -0,0 +1,118 @@ +//! Shift-query sampling IOR. +//! +//! Between OOD and Batching, the verifier draws `t · log_n` random +//! bits and decodes them into `t` shift-query positions over +//! `{0, 1}^log_n`, plus their corresponding evaluation-point vectors. + +use ark_ff::Field; +use ark_iop::{ + IorProveResult, IorProverError, IorVerifierError, IorVerifyResult, ProverTriple, + VerifierTranscript, IOR, +}; +use spongefish::{ProverState, VerifierState}; +use std::marker::PhantomData; + +use crate::iop::oracles::query_indices::QueryIndices; + +pub struct SampleQueriesStatement { + pub log_n: usize, + pub t_num_queries: usize, +} + +pub struct SampleQueriesReductionInputs { + pub queries: QueryIndices, +} + +pub struct SampleQueriesReducedStatement { + pub queries: QueryIndices, +} + +#[derive(Default)] +pub struct SampleQueries(PhantomData); + +impl IOR for SampleQueries { + const NAME: &'static str = "SampleQueries"; + const MESSAGE_TAGS: &'static [&'static str] = &["squeeze:queries"]; + + type Statement<'b> + = SampleQueriesStatement + where + Self: 'b; + type Witness<'b> + = () + where + Self: 'b; + type ProverInputs<'b> + = () + where + Self: 'b; + type VerifierInputs<'b> + = () + where + Self: 'b; + type ReductionInputs = SampleQueriesReductionInputs; + type ReducedStatement = SampleQueriesReducedStatement; + type ProofString = (); + type ReducedWitness = (); + type VerifierOutputs = (); + + fn reduce_statement<'b>( + &self, + _statement: &Self::Statement<'b>, + inputs: &Self::ReductionInputs, + ) -> Self::ReducedStatement + where + Self: 'b, + { + SampleQueriesReducedStatement { + queries: inputs.queries.clone(), + } + } +} + +impl SampleQueries { + #[tracing::instrument(name = "sample_queries", skip_all, fields(t = statement.t_num_queries, log_n = statement.log_n))] + fn prove_inner( + &self, + prover_state: &mut ProverState, + statement: &SampleQueriesStatement, + ) -> ProverTriple, (), ()> { + let queries = + QueryIndices::::sample(prover_state, statement.log_n, statement.t_num_queries); + Ok((SampleQueriesReductionInputs { queries }, (), ())) + } + + #[tracing::instrument(name = "sample_queries.verify", skip_all)] + fn verify_inner( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &SampleQueriesStatement, + ) -> Result<(SampleQueriesReductionInputs, ()), IorVerifierError> { + let n_bytes = (statement.t_num_queries * statement.log_n).div_ceil(8); + let bytes = verifier_state.squeeze_bytes(n_bytes); + let queries = + QueryIndices::from_squeezed_bytes(&bytes, statement.log_n, statement.t_num_queries); + Ok((SampleQueriesReductionInputs { queries }, ())) + } + + pub fn prove( + &self, + prover_state: &mut ProverState, + statement: &SampleQueriesStatement, + _witness: &(), + _inputs: &(), + ) -> Result, (), ()>, IorProverError> { + self.compose_prove(prover_state, statement, |t| self.prove_inner(t, statement)) + } + + pub fn verify( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &SampleQueriesStatement, + _inputs: &(), + ) -> Result, ()>, IorVerifierError> { + self.compose_verify(verifier_state, statement, |t| { + self.verify_inner(t, statement) + }) + } +} diff --git a/src/iop/iors/twin_constraint.rs b/src/iop/iors/twin_constraint.rs new file mode 100644 index 0000000..6b8f873 --- /dev/null +++ b/src/iop/iors/twin_constraint.rs @@ -0,0 +1,481 @@ +//! Twin-constraint sumcheck IOR. Reduces `Σ_i τ(i) · (f(i) + ω·p(i)) = σ₁` +//! to evaluations at γ via protogalaxy folding. +//! Paired spec: `docs/paper-mods/mod1_oracle.tex`. + +use ark_ff::{Field, PrimeField}; +use ark_iop::{ + IorProveResult, IorProverError, IorVerifierError, IorVerifyResult, ProverTriple, IOR, +}; +use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial}; +use ark_vc::mvc::MultiVectorCommitment; +use effsc::{ + coefficient_sumcheck::RoundPolyEvaluator, + folding::protogalaxy, + hypercube::{compute_hypercube_eq_evals, Ascending}, + noop_hook, + provers::coefficient_lsb::CoefficientProverLSB, + runner::sumcheck, + verifier::sumcheck_verify, +}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::marker::PhantomData; + +use crate::accumulation_scheme::AccumulatorInstance; +use crate::count_ops; +use crate::error::VerifierError; +use crate::iop::oracles::evaluation::Oracle; +use crate::relations::r1cs::R1CSConstraints; +use crate::utils::{ + concat_slices, + poly::{eq_poly, eq_poly_non_binary}, + scale_and_sum, +}; + +/// Degree-1 polynomial interpolating two field elements: `lo + (hi - lo)·X`. +fn linear_poly(lo: F, hi: F) -> DensePolynomial { + DensePolynomial::from_coefficients_vec(vec![lo, hi - lo]) +} + +type R1CSConstraint = (Vec<(F, usize)>, Vec<(F, usize)>, Vec<(F, usize)>); + +fn eval_r1cs_constraint_poly( + (a, b, c): &R1CSConstraint, + z0: &[F], + z1: &[F], +) -> DensePolynomial { + let eval = |lc: &[(F, usize)], z: &[F]| { + if z.is_empty() { + F::ZERO + } else { + lc.iter().map(|(t, i)| z[*i] * t).sum::() + } + }; + let (a0, b0, c0) = (eval(a, z0), eval(b, z0), eval(c, z0)); + let (a1, b1, c1) = (eval(a, z1) - a0, eval(b, z1) - b0, eval(c, z1) - c0); + DensePolynomial::from_coefficients_vec(vec![a0 * b0 - c0, a0 * b1 + a1 * b0 - c1, a1 * b1]) +} + +struct TwinConstraintEvaluator<'a, F: Field> { + r1cs: &'a R1CSConstraints, + omega: F, + degree: usize, +} + +impl<'a, F: Field> RoundPolyEvaluator for TwinConstraintEvaluator<'a, F> { + fn degree(&self) -> usize { + self.degree + } + + fn accumulate_pair(&self, coeffs: &mut [F], tw: &[(&[F], &[F])], pw: &[(F, F)]) { + let (u_even, u_odd) = tw[0]; + let (z_even, z_odd) = tw[1]; + let (a_even, a_odd) = tw[2]; + let (b_even, b_odd) = tw[3]; + let (tau_even, tau_odd) = pw[0]; + + if u_odd.is_empty() { + let f_val = u_even + .iter() + .enumerate() + .map(|(i, &u_i)| u_i * eq_poly(a_even, i)) + .sum::(); + let p_val = self + .r1cs + .iter() + .enumerate() + .map(|(i, (a, b, c))| { + let eq = eq_poly(b_even, i); + let eval = + |lc: &[(F, usize)]| lc.iter().map(|(t, idx)| z_even[*idx] * t).sum::(); + eq * (eval(a) * eval(b) - eval(c)) + }) + .sum::(); + let h_val = (f_val + self.omega * p_val) * tau_even; + coeffs[0] += h_val; + if coeffs.len() > 1 { + coeffs[1] -= h_val; + } + return; + } + + let f = protogalaxy::fold( + a_even.iter().zip(a_odd).map(|(&l, &r)| (l, r - l)), + u_even + .iter() + .zip(u_odd) + .map(|(&l, &r)| linear_poly(l, r)) + .collect(), + ); + + let p = protogalaxy::fold( + b_even.iter().zip(b_odd).map(|(&l, &r)| (l, r - l)), + self.r1cs + .iter() + .map(|c| eval_r1cs_constraint_poly(c, z_even, z_odd)) + .collect(), + ); + + let t0 = tau_even; + let t1 = tau_odd - tau_even; + let f_coeffs = &f.coeffs; + let p_coeffs = &p.coeffs; + let mut q_im1 = F::zero(); + for (i, c) in coeffs.iter_mut().enumerate() { + let f_i = f_coeffs.get(i).copied().unwrap_or(F::zero()); + let p_i = p_coeffs.get(i).copied().unwrap_or(F::zero()); + let q_i = f_i + self.omega * p_i; + *c += q_im1 * t1 + q_i * t0; + q_im1 = q_i; + } + } +} + +pub struct TwinConstraintStatement +where + F: Field, + V: MultiVectorCommitment, +{ + pub acc_instance: AccumulatorInstance, + pub l1_mus_codeword_first_coords: Vec, + pub l1_taus_zero_check_challenges: Vec>, + pub log_l: usize, + pub log_m: usize, + pub log_n: usize, +} + +pub struct TwinConstraintWitness<'a, F: Field> { + pub acc_witness_w: &'a [Vec], + pub instances: &'a [Vec], + pub witnesses: &'a [Vec], +} + +pub struct TwinConstraintProverInputs<'a, F: Field> { + pub fresh_codewords: &'a [Vec], + pub acc_codewords: &'a [Vec], +} + +/// Deferred oracle check: `final_claim ≟ eq(τ,γ)·(ν₀ + ω·η)`. Cannot fire +/// inside `verify` because ν₀, η arrive on the transcript only after Bridge. +#[must_use = "DeferredOracleCheck must be discharged by the downstream IOR; \ + dropping it without calling discharge() leaves the verifier unsound"] +pub struct DeferredOracleCheck { + omega_zero_check_randomness: F, + tau_zero_check_challenges: Vec, + claim: F, + discharged: std::cell::Cell, +} + +impl DeferredOracleCheck { + pub(crate) fn new(omega: F, tau: Vec, claim: F) -> Self { + Self { + omega_zero_check_randomness: omega, + tau_zero_check_challenges: tau, + claim, + discharged: std::cell::Cell::new(false), + } + } + + pub fn discharge(&self, gamma: &[F], nu_0: F, eta: F) -> Result<(), VerifierError> { + let expected = eq_poly_non_binary(&self.tau_zero_check_challenges, gamma) + * (nu_0 + self.omega_zero_check_randomness * eta); + let ok = expected == self.claim; + self.discharged.set(true); + ok.then_some(()).ok_or(VerifierError::Target) + } + + pub fn is_discharged(&self) -> bool { + self.discharged.get() + } +} + +pub struct TwinConstraintReducedStatement { + pub gamma_sumcheck_challenges: Vec, + pub zeta_0: Vec, + pub beta_tau: Vec, + pub deferred: DeferredOracleCheck, +} + +pub struct TwinConstraintReductionInputs { + pub omega_zero_check_randomness: F, + pub tau_zero_check_challenges: Vec, + pub gamma_sumcheck_challenges: Vec, + pub final_claim: F, +} + +pub struct TwinConstraintReducedWitness { + pub f_oracle: Oracle, + pub z_witness_assignment: Vec, +} + +pub struct TwinConstraint<'a, F, V> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + V: MultiVectorCommitment, +{ + pub r1cs: &'a R1CSConstraints, + pub _phantom: PhantomData, +} + +impl<'a, F, V> IOR for TwinConstraint<'a, F, V> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + V: MultiVectorCommitment, +{ + const NAME: &'static str = "TwinConstraint"; + const MESSAGE_TAGS: &'static [&'static str] = + &["squeeze:omega", "squeeze:tau", "delegate:effsc.sumcheck"]; + + type Statement<'b> + = TwinConstraintStatement + where + Self: 'b; + type Witness<'b> + = TwinConstraintWitness<'b, F> + where + Self: 'b; + type ProverInputs<'b> + = TwinConstraintProverInputs<'b, F> + where + Self: 'b; + type VerifierInputs<'b> + = () + where + Self: 'b; + type ReductionInputs = TwinConstraintReductionInputs; + type ReducedStatement = TwinConstraintReducedStatement; + type ProofString = (); + type ReducedWitness = TwinConstraintReducedWitness; + type VerifierOutputs = (); + + fn reduce_statement<'b>( + &self, + statement: &Self::Statement<'b>, + inputs: &Self::ReductionInputs, + ) -> Self::ReducedStatement + where + Self: 'b, + { + let log_l = statement.log_l; + let log_n = statement.log_n; + let l1 = statement.l1_mus_codeword_first_coords.len(); + + let gamma_eq_evals = compute_hypercube_eq_evals(log_l, &inputs.gamma_sumcheck_challenges); + + let alpha_vecs = concat_slices( + &statement.acc_instance.alpha_fold_vectors, + &vec![vec![F::zero(); log_n]; l1], + ); + let zeta_0 = scale_and_sum(&alpha_vecs, &gamma_eq_evals); + + let beta_taus: Vec> = statement + .acc_instance + .beta_twin_pairs + .iter() + .map(|p| p.tau.clone()) + .chain(statement.l1_taus_zero_check_challenges.iter().cloned()) + .collect(); + let beta_tau = scale_and_sum(&beta_taus, &gamma_eq_evals); + + TwinConstraintReducedStatement { + gamma_sumcheck_challenges: inputs.gamma_sumcheck_challenges.clone(), + zeta_0, + beta_tau, + deferred: DeferredOracleCheck::new( + inputs.omega_zero_check_randomness, + inputs.tau_zero_check_challenges.clone(), + inputs.final_claim, + ), + } + } +} + +impl<'a, F, V> TwinConstraint<'a, F, V> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + V: MultiVectorCommitment, +{ + #[tracing::instrument( + name = "twin_constraint", + skip_all, + fields(log_l = statement.log_l, log_m = statement.log_m, log_n = statement.log_n) + )] + fn prove_inner( + &self, + prover_state: &mut ProverState, + statement: &TwinConstraintStatement, + witness: &TwinConstraintWitness<'_, F>, + inputs: &TwinConstraintProverInputs<'_, F>, + ) -> ProverTriple, (), TwinConstraintReducedWitness> { + let l1 = inputs.fresh_codewords.len(); + let log_l = statement.log_l; + let log_m = statement.log_m; + let log_n = statement.log_n; + + let omega: F = prover_state.verifier_message(); + let tau = prover_state.verifier_messages_vec::(log_l); + + let tau_eq_evals = Ascending::new(log_l) + .map(|p| eq_poly(&tau, p.index)) + .collect::>(); + + let alpha_vecs = concat_slices( + &statement.acc_instance.alpha_fold_vectors, + &vec![vec![F::zero(); log_n]; l1], + ); + + let z_vecs: Vec> = statement + .acc_instance + .beta_twin_pairs + .iter() + .map(|p| &p.x) + .zip(witness.acc_witness_w) + .chain(witness.instances.iter().zip(witness.witnesses)) + .map(|(x, w)| concat_slices(x, w)) + .collect(); + + let beta_vecs: Vec> = statement + .acc_instance + .beta_twin_pairs + .iter() + .map(|p| p.tau.clone()) + .chain(statement.l1_taus_zero_check_challenges.iter().cloned()) + .collect(); + + let tablewise = vec![ + concat_slices(inputs.acc_codewords, inputs.fresh_codewords), + z_vecs, + alpha_vecs, + beta_vecs, + ]; + let pw = vec![tau_eq_evals]; + + let degree = 1 + (log_n + 1).max(log_m + 2); + let evaluator = TwinConstraintEvaluator { + r1cs: self.r1cs, + omega, + degree, + }; + + let mut cc = CoefficientProverLSB::new(&evaluator, tablewise, pw); + let proof = { + let _s = tracing::info_span!("twin_constraint.sumcheck").entered(); + count_ops!(TwinConstraintRounds, log_l as u64); + sumcheck(&mut cc, log_l, prover_state, noop_hook) + }; + if proof.challenges.len() != log_l { + return Err(IorProverError::StatementShape { + what: "sumcheck.challenges", + expected: log_l, + got: proof.challenges.len(), + }); + } + + let reduced = cc.tablewise(); + if !reduced.iter().all(|t| t.len() == 1) { + return Err(IorProverError::StatementShape { + what: "sumcheck.tablewise (singleton after full fold)", + expected: 1, + got: reduced.iter().map(|t| t.len()).max().unwrap_or(0), + }); + } + let f = reduced[0][0].clone(); + let z = reduced[1][0].clone(); + + Ok(( + TwinConstraintReductionInputs { + omega_zero_check_randomness: omega, + tau_zero_check_challenges: tau, + gamma_sumcheck_challenges: proof.challenges, + final_claim: proof.final_value, + }, + (), + TwinConstraintReducedWitness { + f_oracle: Oracle::from_evals(f), + z_witness_assignment: z, + }, + )) + } + + #[tracing::instrument( + name = "twin_constraint.verify", + skip_all, + fields(log_l = statement.log_l, log_m = statement.log_m, log_n = statement.log_n) + )] + fn verify_inner( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &TwinConstraintStatement, + ) -> Result<(TwinConstraintReductionInputs, ()), IorVerifierError> { + let log_l = statement.log_l; + let log_n = statement.log_n; + let l1 = statement.l1_mus_codeword_first_coords.len(); + + let omega: F = verifier_state.verifier_message(); + let tau: Vec = (0..log_l) + .map(|_| verifier_state.verifier_message::()) + .collect(); + + let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); + let etas_l2_first = concat_slices( + &statement.acc_instance.eta_predicate_evals, + &vec![F::zero(); l1], + ); + let sigma_1 = tau_eq_evals + .into_iter() + .zip( + statement + .acc_instance + .mu_claimed_evals + .iter() + .copied() + .chain(statement.l1_mus_codeword_first_coords.iter().copied()) + .zip(etas_l2_first), + ) + .fold(F::zero(), |acc, (eq_tau, (mu, eta))| { + acc + eq_tau * (mu + omega * eta) + }); + + let tc_degree = 1 + (log_n + 1).max(statement.log_m + 2); + let (gamma, final_claim) = { + let res = sumcheck_verify(sigma_1, tc_degree, log_l, verifier_state, |_, _| Ok(())) + .map_err(|e| IorVerifierError::Transcript(format!("sumcheck: {e:?}")))?; + (res.challenges, res.final_claim) + }; + + Ok(( + TwinConstraintReductionInputs { + omega_zero_check_randomness: omega, + tau_zero_check_challenges: tau, + gamma_sumcheck_challenges: gamma, + final_claim, + }, + (), + )) + } + + pub fn prove( + &self, + prover_state: &mut ProverState, + statement: &TwinConstraintStatement, + witness: &TwinConstraintWitness<'_, F>, + inputs: &TwinConstraintProverInputs<'_, F>, + ) -> Result< + IorProveResult, (), TwinConstraintReducedWitness>, + IorProverError, + > { + self.compose_prove(prover_state, statement, |t| { + self.prove_inner(t, statement, witness, inputs) + }) + } + + pub fn verify( + &self, + verifier_state: &mut VerifierState<'_>, + statement: &TwinConstraintStatement, + _inputs: &(), + ) -> Result, ()>, IorVerifierError> { + self.compose_verify(verifier_state, statement, |t| { + self.verify_inner(t, statement) + }) + } +} diff --git a/src/iop/mod.rs b/src/iop/mod.rs new file mode 100644 index 0000000..e0e7e93 --- /dev/null +++ b/src/iop/mod.rs @@ -0,0 +1,2 @@ +pub mod iors; +pub mod oracles; diff --git a/src/iop/oracles/evaluation.rs b/src/iop/oracles/evaluation.rs new file mode 100644 index 0000000..3c55b4e --- /dev/null +++ b/src/iop/oracles/evaluation.rs @@ -0,0 +1,63 @@ +//! Codeword oracle: index-queryable evaluation table plus its +//! lazily-materialised multilinear extension. + +use ark_ff::Field; +use ark_poly::{DenseMultilinearExtension, MultilinearExtension}; +use ark_std::log2; +use std::cell::OnceCell; + +use crate::count_ops; + +/// A Warp oracle: a committed codeword together with its lazily-materialised +/// multilinear extension. +pub struct Oracle { + evals: Vec, + mle: OnceCell>, +} + +impl Oracle { + pub fn from_evals(evals: Vec) -> Self { + Self { + evals, + mle: OnceCell::new(), + } + } + + pub fn evals(&self) -> &[F] { + &self.evals + } + + pub fn into_evals(self) -> Vec { + self.evals + } + + pub fn len(&self) -> usize { + self.evals.len() + } + + pub fn is_empty(&self) -> bool { + self.evals.is_empty() + } + + pub fn query_at_leaf(&self, idx: usize) -> F { + count_ops!(OracleLeafQueries); + self.evals[idx] + } + + /// `\hat f(ζ)`. Materialises the MLE on first call, caches afterward. + pub fn query_at_point(&self, point: &[F]) -> F { + count_ops!(OraclePointQueries); + let mle = self.mle.get_or_init(|| { + count_ops!(MleMaterializations); + let log_n = log2(self.evals.len()) as usize; + DenseMultilinearExtension::from_evaluations_slice(log_n, &self.evals) + }); + mle.fix_variables(point)[0] + } +} + +impl From> for Oracle { + fn from(evals: Vec) -> Self { + Self::from_evals(evals) + } +} diff --git a/src/iop/oracles/indexed.rs b/src/iop/oracles/indexed.rs new file mode 100644 index 0000000..edb4a86 --- /dev/null +++ b/src/iop/oracles/indexed.rs @@ -0,0 +1,5 @@ +//! Re-exports of the canonical IndexedOracle types from `ark_iop`. +//! Existed locally during the extraction; canonical definitions now +//! live in the ark-iop crate. + +pub use ark_iop::oracle::indexed::{IndexedOracle, ValidatedOracle}; diff --git a/src/iop/oracles/mod.rs b/src/iop/oracles/mod.rs new file mode 100644 index 0000000..5b5936f --- /dev/null +++ b/src/iop/oracles/mod.rs @@ -0,0 +1,6 @@ +//! Oracle vocabulary used by Warp's IORs: codeword evaluation oracles, +//! indexed-oracle handles, and verifier query positions. + +pub mod evaluation; +pub mod indexed; +pub mod query_indices; diff --git a/src/iop/oracles/query_indices.rs b/src/iop/oracles/query_indices.rs new file mode 100644 index 0000000..8441823 --- /dev/null +++ b/src/iop/oracles/query_indices.rs @@ -0,0 +1,176 @@ +use ark_ff::Field; +use ark_iop::ProverTranscript; + +#[derive(Clone)] +pub struct QueryIndices { + pub leaf_positions: Vec, + pub evaluation_points: Vec>, +} + +impl QueryIndices { + pub fn sample( + transcript: &mut P, + log_codeword_len: usize, + num_queries: usize, + ) -> Self { + let num_bytes = (num_queries * log_codeword_len).div_ceil(8); + let squeezed_bytes = transcript.squeeze_bytes(num_bytes); + Self::from_squeezed_bytes(&squeezed_bytes, log_codeword_len, num_queries) + } + + pub fn from_squeezed_bytes(squeezed_bytes: &[u8], log_n: usize, count: usize) -> Self { + let evaluation_points = + Self::evaluation_points_from_squeezed_bytes(squeezed_bytes, log_n, count); + let leaf_positions = Self::leaf_positions_from_evaluation_points(&evaluation_points); + Self { + leaf_positions, + evaluation_points, + } + } + + fn evaluation_points_from_squeezed_bytes( + squeezed_bytes: &[u8], + log_codeword_len: usize, + num_queries: usize, + ) -> Vec> { + squeezed_bytes + .iter() + .flat_map(|squeezed_byte| (0..8).map(move |i| F::from((squeezed_byte >> i) & 1 == 1))) + .take(num_queries * log_codeword_len) + .collect::>() + .chunks(log_codeword_len) + .map(|chunk| chunk.to_vec()) + .collect() + } + + // Convert each evaluation point (vector of F in {0,1}) to its little-endian leaf index. + fn leaf_positions_from_evaluation_points(evaluation_points: &[Vec]) -> Vec { + let binary_to_leaf_index = |bits: &Vec| -> usize { + bits.iter() + .rev() + .fold(0, |acc, &b| (acc << 1) | b.is_one() as usize) + }; + evaluation_points.iter().map(binary_to_leaf_index).collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::fields::Goldilocks; + use ark_bls12_381::Fr as BLS12_381; + use ark_std::{One, Zero}; + + // check dimensions, binary values, and leaf position range + fn check_query_indices(bytes: &[u8], log_n: usize, num_queries: usize) { + let q = QueryIndices::::from_squeezed_bytes(bytes, log_n, num_queries); + + assert_eq!(q.leaf_positions.len(), num_queries); + assert_eq!(q.evaluation_points.len(), num_queries); + + for (i, eval_pt) in q.evaluation_points.iter().enumerate() { + assert_eq!(eval_pt.len(), log_n); + for &bit in eval_pt { + assert!(bit.is_zero() || bit.is_one()); + } + assert!(q.leaf_positions[i] < (1 << log_n)); + } + } + + // check leaf_positions match manual binary-to-index conversion + fn check_roundtrip(bytes: &[u8], log_n: usize, num_queries: usize) { + let q = QueryIndices::::from_squeezed_bytes(bytes, log_n, num_queries); + for (i, eval_pt) in q.evaluation_points.iter().enumerate() { + let expected = eval_pt + .iter() + .rev() + .fold(0usize, |acc, &b| (acc << 1) | b.is_one() as usize); + assert_eq!(q.leaf_positions[i], expected); + } + } + + // BLS12-381 (multi-limb, 256-bit) + + #[test] + fn bls12_381_basic() { + let bytes = vec![0b10110010, 0b01101001, 0b11110000, 0b00001111]; + check_query_indices::(&bytes, 4, 3); + } + + #[test] + fn bls12_381_roundtrip() { + let bytes: Vec = (0..16).collect(); + check_roundtrip::(&bytes, 8, 10); + } + + #[test] + fn bls12_381_single_bit_queries() { + // log_n = 1 → each query is a single bit + let bytes = vec![0b10101010]; + let q = QueryIndices::::from_squeezed_bytes(&bytes, 1, 8); + assert_eq!(q.leaf_positions.len(), 8); + for &pos in &q.leaf_positions { + assert!(pos <= 1); + } + } + + // Goldilocks (SmallFp, single-limb u128) + + #[test] + fn goldilocks_basic() { + let bytes = vec![0xFF, 0x00, 0xAB, 0xCD]; + check_query_indices::(&bytes, 4, 3); + } + + #[test] + fn goldilocks_roundtrip() { + let bytes: Vec = (0..16).collect(); + check_roundtrip::(&bytes, 8, 10); + } + + #[test] + fn goldilocks_large_log_n() { + // 16-bit queries → range [0, 65536) + let bytes: Vec = (0..=255).cycle().take(64).collect(); + check_query_indices::(&bytes, 16, 4); + } + + // edge cases + + #[test] + fn zero_bytes_produce_zero_indices() { + let bytes = vec![0u8; 8]; + let q = QueryIndices::::from_squeezed_bytes(&bytes, 4, 4); + for &pos in &q.leaf_positions { + assert_eq!(pos, 0); + } + for eval_pt in &q.evaluation_points { + for &bit in eval_pt { + assert!(bit.is_zero()); + } + } + } + + #[test] + fn all_ones_bytes() { + let bytes = vec![0xFF; 8]; + let q = QueryIndices::::from_squeezed_bytes(&bytes, 4, 4); + for &pos in &q.leaf_positions { + assert_eq!(pos, (1 << 4) - 1); + } + for eval_pt in &q.evaluation_points { + for &bit in eval_pt { + assert!(bit.is_one()); + } + } + } + + #[test] + fn deterministic_output() { + let bytes = vec![0x42, 0x13, 0x7F, 0xE0]; + let q1 = QueryIndices::::from_squeezed_bytes(&bytes, 4, 4); + let q2 = QueryIndices::::from_squeezed_bytes(&bytes, 4, 4); + assert_eq!(q1.leaf_positions, q2.leaf_positions); + assert_eq!(q1.evaluation_points, q2.evaluation_points); + } +} diff --git a/src/lib.rs b/src/lib.rs index 30bf5e0..d8fb9be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,1106 +1,65 @@ -#![allow(clippy::assign_op_pattern)] // generated by SmallFpConfig derive macro -use crate::error::WARPError; -use crate::traits::AccumulationScheme; -use ark_codes::traits::LinearCode; -use ark_crypto_primitives::{ - crh::{CRHScheme, TwoToOneCRHScheme}, - merkle_tree::{Config, MerkleTree, Path}, -}; -use ark_ff::{Field, PrimeField}; -use ark_poly::{ - univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial, - MultilinearExtension, Polynomial, -}; -use ark_std::log2; -use crypto::merkle::build_codeword_leaves; -use crypto::merkle::compute_auth_paths; -use effsc::{ - coefficient_sumcheck::RoundPolyEvaluator, - folding::protogalaxy, - hypercube::{compute_hypercube_eq_evals, eq_poly_non_binary}, - noop_hook, - provers::{coefficient_lsb::CoefficientProverLSB, inner_product::InnerProductProver}, - runner::sumcheck, - verifier::sumcheck_verify, -}; -use protocol::domainsep::{derive_between_sumchecks, derive_pre_twin_constraint, parse_statement}; -use protocol::EffscVerifierTranscript; -use relations::{r1cs::R1CSConstraints, BundledPESAT}; -use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; -use std::collections::HashMap; -use std::marker::PhantomData; -use utils::binary_field_elements_to_usize; -use utils::byte_to_binary_field_array; -use utils::concat_slices; -use utils::scale_and_sum; - -use config::WARPConfig; -use protocol::domainsep::{absorb_accumulated_instances, absorb_instances}; - +//! # WarpAccumulationScheme — accumulation scheme with code-based commitments +//! +//! WarpAccumulationScheme folds many R1CS instances into a single, sub-linear-to-check accumulator. +//! Per accumulation round it runs six IORs (Interactive Oracle Reductions) in +//! order through a Fiat-Shamir transcript, producing a new accumulator and a +//! per-round proof. A separate [`WarpAccumulationScheme::decide`] step closes the accumulator. +//! +//! Entry point: [`WarpAccumulationScheme`]. The same type implements [`ark_iop::IOP`] +//! (per-round protocol identity) and [`crate::accumulation_scheme::AccumulationScheme`] +//! (split-accumulation wrapper). One value, two traits. +//! +//! ## End-to-end flow +//! +//! ```text +//! WarpAccumulationScheme::prove ──▶ ((AccumulatorInstance, AccumulatorWitness), WarpProof) +//! WarpAccumulationScheme::verify ──▶ accept the new accumulator instance (or reject) +//! WarpAccumulationScheme::decide ──▶ close the accumulator (final accept / reject) +//! ``` +//! +//! See `examples/hash_chain.rs` for a runnable end-to-end example. +//! +//! ## Where to look +//! +//! - [`WarpAccumulationScheme`] (`src/accumulation_scheme/scheme.rs`) — the scheme value carrying code, ck, predicate. +//! - [`accumulation_scheme::iop`](`crate::accumulation_scheme::iop`) (`src/accumulation_scheme/iop.rs`) — +//! `IOP` + `AccumulationScheme` impls on `WarpAccumulationScheme`. Names the ordered IOR list and provides +//! the FS prologue + decider. +//! - [`accumulation_scheme::trait_def`](`crate::accumulation_scheme::trait_def`) +//! (`src/accumulation_scheme/trait_def.rs`) — the `AccumulationScheme` trait itself, +//! warp-local for now (slated for upstream to `ark-iop` when a second accumulating +//! consumer arrives). +//! - [`iop::iors`](`crate::iop::iors`) (`src/iop/iors/`) — the six IORs run per round, +//! in this order: `Pesat` → `TwinConstraint` → `Bridge` → `Ood` → `SampleQueries` +//! → `Batching`. Plus `Proximity` (FS-transparent). +//! +//! ## Fiat-Shamir hygiene (structural) +//! +//! Two parity invariants are compiled in via [`ark_iop`]: +//! +//! 1. **Reduction parity** — `IOR::compose_prove` / `compose_verify` are +//! the only legal constructors of result types; both funnel through +//! `reduce_statement`. +//! 2. **Message-order parity** — every IOR declares `const MESSAGE_TAGS`; +//! `compose_*` absorbs them as an FS prologue. The outer scheme adds +//! `AccumulationScheme::absorb_scheme_prologue_*` for the protocol-map +//! domain separator. + +pub mod accumulation_scheme; pub mod config; pub mod constraints; pub mod crypto; pub mod error; -pub mod protocol; +pub mod iop; +pub mod params; +pub mod profile; pub mod relations; pub mod serialize; -pub mod traits; pub mod utils; -use ark_crypto_primitives::Error; -use error::{DeciderError, ProverError, VerifierError}; - -/// Degree-1 polynomial interpolating two field elements: `lo + (hi - lo)·X`. -fn linear_poly(lo: F, hi: F) -> DensePolynomial { - DensePolynomial::from_coefficients_vec(vec![lo, hi - lo]) -} - -/// A single R1CS constraint row: sparse representations of A, B, and C. -type R1CSConstraint = (Vec<(F, usize)>, Vec<(F, usize)>, Vec<(F, usize)>); - -/// Evaluate one R1CS constraint `Az·Bz - Cz` as a degree-2 polynomial -/// from two witness vectors `z0`, `z1`. -fn eval_r1cs_constraint_poly( - (a, b, c): &R1CSConstraint, - z0: &[F], - z1: &[F], -) -> DensePolynomial { - let eval = |lc: &[(F, usize)], z: &[F]| lc.iter().map(|(t, i)| z[*i] * t).sum::(); - let (a0, b0, c0) = (eval(a, z0), eval(b, z0), eval(c, z0)); - let (a1, b1, c1) = (eval(a, z1) - a0, eval(b, z1) - b0, eval(c, z1) - c0); - DensePolynomial::from_coefficients_vec(vec![a0 * b0 - c0, a0 * b1 + a1 * b0 - c1, a1 * b1]) -} - -/// `RoundPolyEvaluator` for the twin-constraint sumcheck. Fuses the α-fold, -/// β-fold, and τ-linear multiplication in a single pass over even/odd pairs -/// of the input tables. Replaces the closure `twin_constraint_round_poly` -/// previously handed to `coefficient_sumcheck`. -struct TwinConstraintEvaluator<'a, F: Field> { - r1cs: &'a R1CSConstraints, - omega: F, - degree: usize, -} - -impl<'a, F: Field> RoundPolyEvaluator for TwinConstraintEvaluator<'a, F> { - fn degree(&self) -> usize { - self.degree - } - - fn accumulate_pair(&self, coeffs: &mut [F], tw: &[(&[F], &[F])], pw: &[(F, F)]) { - // tw[0]=(u_even,u_odd), tw[1]=(z_even,z_odd), - // tw[2]=(a_even,a_odd), tw[3]=(b_even,b_odd); pw[0]=(tau_even,tau_odd). - let (u_even, u_odd) = tw[0]; - let (z_even, z_odd) = tw[1]; - let (a_even, a_odd) = tw[2]; - let (b_even, b_odd) = tw[3]; - let (tau_even, tau_odd) = pw[0]; - - // CoefficientProverLSB::final_value() invokes the evaluator on fully - // reduced singleton tables with empty odd slices. Warp ignores - // `SumcheckProof::final_value` (only `.challenges` is consumed), so - // bail out — leaves `coeffs` zero, `final_value` returns zero, no - // one reads it. - if u_odd.is_empty() { - return; - } - - let f = protogalaxy::fold( - a_even.iter().zip(a_odd).map(|(&l, &r)| (l, r - l)), - u_even - .iter() - .zip(u_odd) - .map(|(&l, &r)| linear_poly(l, r)) - .collect(), - ); - - let p = protogalaxy::fold( - b_even.iter().zip(b_odd).map(|(&l, &r)| (l, r - l)), - self.r1cs - .iter() - .map(|c| eval_r1cs_constraint_poly(c, z_even, z_odd)) - .collect(), - ); - - let t = linear_poly(tau_even, tau_odd); - let h = (f + p * self.omega).naive_mul(&t); - - for (c, &hc) in coeffs.iter_mut().zip(h.coeffs.iter()) { - *c += hc; - } - } -} - -/// [CBBZ23] / HyperPlonk sparse-evaluation optimization: for shift-query -/// zetas (indices `1+s..r`), each ζ is a 0/1 vector representing a single -/// hypercube point. Accumulates the corresponding `eq_evals[i]` into a sparse -/// map keyed by that point's index. Reimplemented locally; the equivalent -/// helper in the old effsc API was removed in the rewrite. -fn accumulate_sparse_evaluations( - zetas: Vec<&[F]>, - eq_evals: Vec, - s: usize, - r: usize, -) -> HashMap { - let mut result: HashMap = HashMap::new(); - for i in 1 + s..r { - let index = zetas[i] - .iter() - .enumerate() - .filter_map(|(j, bit)| bit.is_one().then_some(1 << j)) - .sum::(); - *result.entry(index).or_insert_with(F::zero) += eq_evals[i]; - } - result -} - -/// Build the `g` side of the batching inner-product sumcheck: column-sum the -/// dense OOD evaluation matrix and add the sparse shift-query contributions. -fn batched_constraint_poly( - dense_polys: &[Vec], - sparse_polys: &HashMap, -) -> Vec { - if dense_polys.is_empty() { - return Vec::new(); - } - let mut result = vec![F::ZERO; dense_polys[0].len()]; - for row in dense_polys { - for (i, val) in row.iter().enumerate() { - result[i] += *val; - } - } - for (k, v) in sparse_polys.iter() { - result[*k] += *v; - } - result -} - -pub trait BoolResult { - fn ok_or_err(self, err: E) -> Result<(), E>; -} - -impl BoolResult for bool { - #[inline] - fn ok_or_err(self, err: E) -> Result<(), E> { - if self { - Ok(()) - } else { - Err(err) - } - } -} - -pub struct WARP, C: LinearCode + Clone, MT: Config> { - _f: PhantomData, - config: WARPConfig, - code: C, - p: P, - mt_leaf_hash_params: ::Parameters, - mt_two_to_one_hash_params: ::Parameters, -} - -impl< - F: Field, - P: Clone + BundledPESAT, // m, n, k - C: LinearCode + Clone, - MT: Config + From<[u8; 32]>>, - > WARP -{ - pub fn new( - config: WARPConfig, - code: C, - p: P, - mt_leaf_hash_params: ::Parameters, - mt_two_to_one_hash_params: ::Parameters, - ) -> WARP { - Self { - _f: PhantomData, - config, - code, - p, - mt_leaf_hash_params, - mt_two_to_one_hash_params, - } - } -} - -impl< - F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, - P: Clone + BundledPESAT, Config = (usize, usize, usize)>, // m, n, k - C: LinearCode + Clone, - MT: Config + From<[u8; 32]>>, - > AccumulationScheme for WARP -{ - type Index = P; - type ProverKey = (P, usize, usize, usize); - type VerifierKey = (usize, usize, usize); - type Instances = Vec>; - type Witnesses = Vec>; - - type AccumulatorInstances = ( - Vec, - Vec>, - Vec, - (Vec>, Vec>), - Vec, - ); // (rt, \alpha, \mu, \beta (\tau, x), \eta) - - type AccumulatorWitnesses = (Vec>, Vec>, Vec>); // (td, f, w) - - // (rt_0, \mu_i, \nu_0, \nu_i, auth_0, auth_j, ((f_i(x_j)))) - type Proof = ( - MT::InnerDigest, - Vec, - F, - Vec, - Vec>, - Vec>>, - Vec>, - ); - - fn index( - prover_state: &mut ProverState, - index: Self::Index, - ) -> spongefish::VerificationResult<(Self::ProverKey, Self::VerifierKey)> { - let (m, n, k) = index.config(); - // initialize prover state for fs - // TODO for R1CS - prover_state.public_message(&index.description()); - prover_state.prover_message(&F::from(m as u32)); - prover_state.prover_message(&F::from(n as u32)); - prover_state.prover_message(&F::from(k as u32)); - Ok(((index.clone(), m, n, k), (m, n, k))) - } - - fn prove( - &self, - pk: Self::ProverKey, - prover_state: &mut ProverState, - witnesses: Self::Witnesses, - instances: Self::Instances, - acc_instances: Self::AccumulatorInstances, - acc_witnesses: Self::AccumulatorWitnesses, - ) -> Result< - ( - (Self::AccumulatorInstances, Self::AccumulatorWitnesses), - Self::Proof, - ), - ProverError, - > { - debug_assert!(instances.len() > 1); - debug_assert_eq!(witnesses.len(), instances.len()); - debug_assert_eq!(acc_witnesses.0.len(), acc_instances.0.len()); - - let (l1, l) = (self.config.l1, self.config.l); - let l2 = l - l1; - debug_assert_eq!(l1 + l2, l); - - debug_assert!(l.is_power_of_two()); - - //////////////////////// - // 1. Parsing phase - //////////////////////// - // a. index - #[allow(non_snake_case)] - let (M, N, k) = (pk.1, pk.2, pk.3); - #[allow(non_snake_case)] - let (log_M, log_l) = (log2(M) as usize, log2(l) as usize); - - debug_assert_eq!(instances[0].len(), N - k); - - // b. and c. statements and accumulators - // d. absorb parameters - absorb_instances(prover_state, &instances); - absorb_accumulated_instances::(prover_state, &acc_instances); - - //////////////////////// - // 2. PESAT Reduction - //////////////////////// - let n = self.code.code_len(); - let log_n = log2(n) as usize; - - // a. encode witnesses - let (codewords, leaves) = build_codeword_leaves(&self.code, &witnesses, l1); - - // b. evaluation claims - let mus = codewords.iter().map(|f| f[0]).collect::>(); - - // c. commit to witnesses - let td_0 = MerkleTree::::new( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - leaves.chunks_exact(l1).collect::>(), - )?; - - // d. absorb commitment and code evaluations - let root_bytes: [u8; 32] = td_0 - .root() - .as_ref() - .try_into() - .expect("root must be 32 bytes"); - prover_state.prover_message(&root_bytes); - prover_state.prover_messages(&mus); - - // e. zero check randomness and f. bundled evaluations - let taus = (0..l1) - .map(|_| prover_state.verifier_messages_vec::(log_M)) - .collect::>(); - - //////////////////////// - // 3. Constrained Code Accumulation - //////////////////////// - // a. zero check randomness - let omega: F = prover_state.verifier_message(); - - let tau = prover_state.verifier_messages_vec::(log_l); - - // b. define [...] - // c. sumcheck protocol. `compute_hypercube_eq_evals` builds the full - // `2^log_l` table in O(2^log_l) via the incremental build-up — - // faster than the previous O(log_l · 2^log_l) per-point loop. - let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); - - let alpha_vecs = concat_slices(&acc_instances.1, &vec![vec![F::zero(); log_n]; l1]); - - // build the z (x, w) vectors - let z_vecs: Vec> = acc_instances - .3 - .1 - .iter() - .zip(&acc_witnesses.2) - .chain(instances.iter().zip(&witnesses)) - .map(|(x, w)| concat_slices(x, w)) - .collect(); - - let beta_vecs: Vec> = acc_instances.3 .0.into_iter().chain(taus).collect(); - - // Twin Constraint sumcheck via the new `SumcheckProver` trait. - // Wire format: `d+1` evaluations per round (vs. the old `d+1` - // coefficient-form emission). `CoefficientProverLSB` keeps the - // LSB/pair-split semantics the `TwinConstraintEvaluator` was built - // for. - let tablewise = vec![ - concat_slices(&acc_witnesses.1, &codewords), // u - z_vecs, // z - alpha_vecs, // a - beta_vecs, // b - ]; - let pw = vec![tau_eq_evals]; // tau - - let r1cs = self.p.constraints(); - let degree = 1 + (log_n + 1).max(log_M + 2); - let evaluator = TwinConstraintEvaluator { - r1cs, - omega, - degree, - }; - let mut cc = CoefficientProverLSB::new(&evaluator, tablewise, pw); - let gamma = sumcheck(&mut cc, log_l, prover_state, noop_hook).challenges; - debug_assert_eq!(gamma.len(), log_l); - - // e. new oracle and target — after log_l rounds each group has one - // row left; extract by clone. `CoefficientProverLSB` exposes the - // reduced tables read-only. - let reduced_tw = cc.tablewise(); - debug_assert!(reduced_tw.iter().all(|t| t.len() == 1)); - let f = reduced_tw[0][0].clone(); - let z = reduced_tw[1][0].clone(); - let zeta_0 = reduced_tw[2][0].clone(); - let beta_tau = reduced_tw[3][0].clone(); - - // eval the bundled r1cs - let beta_eq_evals = compute_hypercube_eq_evals(log_M, &beta_tau); - let eta = self - .p - .evaluate_bundled(&beta_eq_evals, &z) - .map_err(|_| ProverError::SpongeFish)?; - - let (x, w) = z.split_at(N - k); - let beta = (vec![beta_tau], vec![x.to_vec()]); - let f_hat = DenseMultilinearExtension::from_evaluations_slice(log_n, &f); - let nu_0 = f_hat.fix_variables(&zeta_0)[0]; - - // f. new commitment - let td = MerkleTree::::new( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - f.chunks(1).collect::>(), - )?; - - // g. absorb new commitment and target - let td_root_bytes: [u8; 32] = td - .root() - .as_ref() - .try_into() - .expect("root must be 32 bytes"); - prover_state.prover_message(&td_root_bytes); - prover_state.prover_message(&eta); - prover_state.prover_message(&nu_0); - - // h. ood samples - let n_ood_samples = self.config.s * log_n; - let ood_samples = prover_state.verifier_messages_vec::(n_ood_samples); - let ood_samples = ood_samples.chunks(log_n).collect::>(); - - // i. ood answers - let ood_answers = ood_samples - .iter() - .map(|ood_p| f_hat.fix_variables(ood_p)[0]) - .collect::>(); - - // j. absorb ood answers - prover_state.prover_messages(&ood_answers); - - let mut zetas = vec![zeta_0.as_slice()]; - let mut nus = vec![nu_0]; - zetas.extend(ood_samples); - nus.extend(ood_answers); - - // k. shift queries and zerocheck randomness - let r = 1 + self.config.s + self.config.t; - let log_r = log2(r) as usize; - let n_shift_queries = (self.config.t * log_n).div_ceil(8); - let bytes_shift_queries = prover_state.verifier_messages_vec(n_shift_queries); - let xis = prover_state.verifier_messages_vec(log_r); - - // get shift queries as binary field elements - let binary_shift_queries = bytes_shift_queries - .iter() - .flat_map(byte_to_binary_field_array) - .take(self.config.t * log_n) - .collect::>(); - - let binary_shift_queries = binary_shift_queries.chunks(log_n).collect::>(); - - // build indexes out of the shift queries stored - let shift_queries_indexes: Vec = binary_shift_queries - .iter() - .map(|vals| binary_field_elements_to_usize(vals)) - .collect(); - - zetas.extend(binary_shift_queries); - - // l. sumcheck polynomials - // compute evaluations for xi - - // `xis` has `log_r` elements but only the first `r` entries of the - // eq-evals table are consumed (`r ≤ 2^log_r`). - let xi_eq_evals: Vec = compute_hypercube_eq_evals(log_r, &xis) - .into_iter() - .take(r) - .collect(); - - let ood_evals_vec = (0..1 + self.config.s) - .map(|i| { - let table = compute_hypercube_eq_evals(log_n, zetas[i]); - let scale = xi_eq_evals[i]; - table.into_iter().map(|v| v * scale).collect::>() - }) - .collect::>(); - - // [CBBZ23] optimization from hyperplonk - let id_non_0_eval_sums = - accumulate_sparse_evaluations(zetas, xi_eq_evals, self.config.s, r); - - // Batching inner-product sumcheck. `InnerProductProver` is MSB - // half-split, so the challenge vector is returned in MSB order; - // reverse once to arkworks' LSB-first convention. - let mut ip = InnerProductProver::new( - f.clone(), - batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), - ); - let mut alpha = sumcheck(&mut ip, log_n, prover_state, noop_hook).challenges; - alpha.reverse(); - - // m. new target - let mu = f_hat.fix_variables(&alpha)[0]; - - // n. compute authentication paths - let auth_0 = compute_auth_paths(&td_0, &shift_queries_indexes)?; - - let auth = acc_witnesses - .0 // for each accumulated witness and for each - .iter() - .map(|td| compute_auth_paths(td, &shift_queries_indexes)) - .collect::>>, Error>>()?; - - let all_codewords = acc_witnesses - .1 - .into_iter() - .chain(codewords) - .collect::>(); - - let mut shift_queries_answers = - vec![vec![F::default(); all_codewords.len()]; shift_queries_indexes.len()]; - for (i, idx) in shift_queries_indexes.iter().enumerate() { - let answers = all_codewords.iter().map(|f| f[*idx]).collect::>(); - shift_queries_answers[i] = answers; - } - - let acc_instance = (vec![td.root()], vec![alpha], vec![mu], beta, vec![eta]); - let acc_witness = (vec![td], vec![f], vec![w.to_vec()]); - - // 4. return - Ok(( - (acc_instance, acc_witness), - ( - td_0.root(), - mus, - nu_0, - nus, - auth_0, - auth, - shift_queries_answers, - ), - )) - } - - fn verify<'a>( - &self, - vk: Self::VerifierKey, - verifier_state: &mut VerifierState<'a>, - acc_instance: Self::AccumulatorInstances, - proof: Self::Proof, - ) -> Result<(), VerifierError> { - let (l1, l) = (self.config.l1, self.config.l); - let l2 = l - l1; - - #[allow(non_snake_case)] - let (M, N, k) = (vk.0, vk.1, vk.2); - #[allow(non_snake_case)] - let (log_M, log_l) = (log2(M) as usize, log2(l) as usize); - let n = self.code.code_len(); - let log_n = log2(n) as usize; - let r = 1 + self.config.s + self.config.t; - let log_r = log2(r) as usize; - - // 1. Parse statement. - let (l1_xs, (l2_roots, l2_alphas, l2_mus, (l2_taus, l2_xs), l2_etas)) = - parse_statement::(verifier_state, l1, l2, N - k, log_n, log_M)?; - - // 2. Pre-twin-constraint transcript reads. - let (rt_0, l1_mus, l1_taus, omega, tau) = - derive_pre_twin_constraint::(verifier_state, l1, log_l, log_M)?; - - // 3. σ₁ = Σ_i τ_eq(i) · (μ_i + ω·η_i). - let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); - let etas_chain = concat_slices(&l2_etas, &vec![F::zero(); l1]); - let sigma_1 = tau_eq_evals - .into_iter() - .zip( - l2_mus - .iter() - .copied() - .chain(l1_mus.iter().copied()) - .zip(etas_chain), - ) - .fold(F::zero(), |acc, (eq_tau, (mu, eta_i))| { - acc + eq_tau * (mu + omega * eta_i) - }); - - // 4. Twin-constraint sumcheck via `effsc::sumcheck_verify`. The - // library enforces round consistency (`q(0)+q(1)==claim`) and - // returns `(challenges, final_claim)`. The oracle check — verifying - // `final_claim == eq(τ,γ)·(ν₀ + ω·η)` — is warp's responsibility - // and runs below once η and ν₀ have been read from the transcript. - let tc_degree = 1 + (log_n + 1).max(log_M + 2); - let (gamma_sumcheck, tc_final_claim) = { - let mut wrap = EffscVerifierTranscript(verifier_state); - let res = sumcheck_verify(sigma_1, tc_degree, log_l, &mut wrap, |_, _| Ok(()))?; - (res.challenges, res.final_claim) - }; - - // 5. Between-sumchecks reads: td, η, ν₀, OOD, shift bytes, ξ. - let (_td, eta, mut nus, ood_samples, bytes_shift_queries, xi) = - derive_between_sumchecks::( - verifier_state, - log_n, - self.config.s, - self.config.t, - log_r, - )?; - - // 6. Deferred twin-constraint oracle check. - (eq_poly_non_binary(&tau, &gamma_sumcheck) * (nus[0] + omega * eta) == tc_final_claim) - .ok_or_err(VerifierError::Target)?; - - // 7. Proximity check. Must run *before* the batching sumcheck because - // σ₂ consumes `proof.6` (shift-query answers); a tampered row - // would otherwise surface as a batching-round consistency failure - // rather than the expected ShiftQuery/NumShiftQueries variant. - let binary_shift_queries_flat = bytes_shift_queries - .iter() - .flat_map(byte_to_binary_field_array) - .take(self.config.t * log_n) - .collect::>(); - let binary_shift_queries = binary_shift_queries_flat - .chunks(log_n) - .collect::>(); - let shift_queries_indexes: Vec = binary_shift_queries - .iter() - .map(|vals| binary_field_elements_to_usize(vals)) - .collect(); - - (proof.6.len() == self.config.t).ok_or_err(VerifierError::NumShiftQueries)?; - for (i, path) in proof.4.iter().enumerate() { - (path.leaf_index == shift_queries_indexes[i]) - .ok_or_err(VerifierError::ShiftQueryIndex)?; - let is_valid = path.verify( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - &rt_0, - &proof.6[i][l2..], - )?; - is_valid.ok_or_err(VerifierError::ShiftQuery)?; - } - (proof.5.len() == l2).ok_or_err(VerifierError::NumL2Instances)?; - for (i, paths) in proof.5.iter().enumerate() { - (paths.len() == self.config.t).ok_or_err(VerifierError::NumShiftQueries)?; - let root = &l2_roots[i]; - for (j, path) in paths.iter().enumerate() { - (path.leaf_index == shift_queries_indexes[j]) - .ok_or_err(VerifierError::ShiftQueryIndex)?; - let is_valid = path.verify( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - root, - [proof.6[j][i]], - )?; - is_valid.ok_or_err(VerifierError::ShiftQuery)?; - } - } - - // 8. Derive σ₂ and the reduced α/μ expectation. - let gamma_eq_evals = compute_hypercube_eq_evals(log_l, &gamma_sumcheck); - let alpha_vecs = concat_slices(&l2_alphas, &vec![vec![F::zero(); log_n]; l1]); - let zeta_0 = scale_and_sum(&alpha_vecs, &gamma_eq_evals); - - let mut nu_s_t = vec![F::default(); self.config.t]; - for (i, v_jk) in proof.6.iter().enumerate() { - nu_s_t[i] = v_jk - .iter() - .zip(&gamma_eq_evals) - .fold(F::zero(), |acc, (v, eq)| acc + *eq * *v); - } - nus.extend(nu_s_t); - - let xi_eq_evals = compute_hypercube_eq_evals(log_r, &xi); - let sigma_2 = xi_eq_evals - .iter() - .zip(&nus) - .fold(F::zero(), |acc, (xi_eq, nu)| acc + *xi_eq * nu); - - // 9. Batching (inner-product) sumcheck. Library handles round - // consistency; warp performs the oracle check: - // `final_claim == μ · Σ eq(ζ_i, α_lsb)·ξ_eq(i)`. MSB half-split - // → reverse the challenge vector once before feeding to - // eq_poly_non_binary (arkworks MLE convention). - let (alpha_sumcheck, batching_final_claim) = { - let mut wrap = EffscVerifierTranscript(verifier_state); - let mut res = sumcheck_verify(sigma_2, 2, log_n, &mut wrap, |_, _| Ok(()))?; - res.challenges.reverse(); - (res.challenges, res.final_claim) - }; - - let mut zeta_eqs = Vec::with_capacity(r); - zeta_eqs.push(eq_poly_non_binary(&zeta_0, &alpha_sumcheck)); - for chunk in ood_samples.chunks(log_n) { - zeta_eqs.push(eq_poly_non_binary(chunk, &alpha_sumcheck)); - } - for zeta in &binary_shift_queries { - zeta_eqs.push(eq_poly_non_binary(zeta, &alpha_sumcheck)); - } - debug_assert_eq!(zeta_eqs.len(), r); - let expected_batching = acc_instance.2[0] - * zeta_eqs - .into_iter() - .zip(&xi_eq_evals) - .fold(F::zero(), |acc, (a, b)| acc + a * *b); - (expected_batching == batching_final_claim).ok_or_err(VerifierError::Target)?; - - // 10. Accumulator consistency: new α and β. - (acc_instance.1[0] == alpha_sumcheck).ok_or_err(VerifierError::CodeEvaluationPoint)?; - - let betas = l2_taus - .into_iter() - .chain(l1_taus) - .zip(l2_xs.into_iter().chain(l1_xs)) - .map(|(tau_i, x)| concat_slices(&tau_i, &x)) - .collect::>>(); - let beta = scale_and_sum(&betas, &gamma_eq_evals); - let expected_beta = concat_slices(&acc_instance.3 .0[0], &acc_instance.3 .1[0]); - (expected_beta == beta).ok_or_err(VerifierError::CircuitEvaluationPoint)?; - - Ok(()) - } - - fn decide( - &self, - acc_witness: Self::AccumulatorWitnesses, - acc_instance: Self::AccumulatorInstances, - ) -> Result<(), WARPError> { - let (mt, f, w) = acc_witness; - let (rt, alpha, mu, beta, eta) = acc_instance; - - let computed_mt = MerkleTree::::new( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - f[0].chunks(1).collect::>(), - )?; - (rt[0] == computed_mt.root()).ok_or_err(DeciderError::MerkleRoot)?; - (mt[0].root() == computed_mt.root()).ok_or_err(DeciderError::MerkleTrapDoor)?; - - let f_hat = DenseMultilinearExtension::from_evaluations_slice( - log2(self.code.code_len()) as usize, - &f[0], - ); - (f_hat.evaluate(&alpha[0]) == mu[0]).ok_or_err(DeciderError::MLExtensionEvaluation)?; - - let tau = &beta.0[0]; - - let tau_zero_evader = compute_hypercube_eq_evals(tau.len(), tau); - - let mut z = beta.1[0].clone(); - z.extend(w[0].clone()); - let computed_eta = self.p.evaluate_bundled(&tau_zero_evader, &z).unwrap(); - (computed_eta == eta[0]).ok_or_err(DeciderError::BundledEvaluation)?; - - let computed_f = self.code.encode(&w[0]); - (f[0] == computed_f).ok_or_err(DeciderError::EncodedWitness)?; - - Ok(()) - } -} - -#[cfg(test)] -pub mod test { - use super::crypto::merkle::blake3::Blake3MerkleTreeParams; - use super::AccumulationScheme; - use crate::serialize::{AccInstanceSerializer, AccWitnessSerializer, ProofSerializer}; - use crate::{ - relations::{ - r1cs::{ - hashchain::{ - compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness, - }, - R1CS, - }, - BundledPESAT, Relation, ToPolySystem, - }, - utils::poseidon, - }; - - use ark_bls12_381::Fr as BLS12_381; - use ark_codes::{ - reed_solomon::{config::ReedSolomonConfig, ReedSolomon}, - traits::LinearCode, - }; - use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; - use ark_ff::UniformRand; - use ark_serialize::{CanonicalSerialize, Compress}; - use ark_std::rand::thread_rng; - - use std::marker::PhantomData; - - use super::{WARPConfig, WARP}; - - #[test] - pub fn warp_test() { - let l1 = 4; - let s = 8; - let t = 7; - let hash_chain_size = 10; - let mut rng = thread_rng(); - let poseidon_config = poseidon::initialize_poseidon_config::(); - let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( - poseidon_config.clone(), - hash_chain_size, - )) - .unwrap(); - let code_config = - ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); - let code = ReedSolomon::new(code_config.clone()); - - let instances_witnesses: (Vec>, Vec>) = (0..l1) - .map(|_| { - let preimage = vec![BLS12_381::rand(&mut rng)]; - let instance = HashChainInstance { - digest: compute_hash_chain::>( - &poseidon_config, - &preimage, - hash_chain_size, - ), - }; - let witness = HashChainWitness { - preimage, - _crhs_scheme: PhantomData::>, - }; - let relation = HashChainRelation::, CRHGadget<_>>::new( - instance, - witness, - (poseidon_config.clone(), hash_chain_size), - ); - (relation.x, relation.w) - }) - .unzip(); - - let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( - poseidon_config.clone(), - hash_chain_size, - )) - .unwrap(); - - let warp_config = WARPConfig::new(l1, l1, s, t, r1cs.config(), code.code_len()); - let hash_chain_warp = WARP::< - BLS12_381, - R1CS, - _, - Blake3MerkleTreeParams, - >::new( - warp_config.clone(), code.clone(), r1cs.clone(), (), () - ); - - let (mut acc_roots, mut acc_alphas, mut acc_mus, mut acc_taus, mut acc_xs, mut acc_eta) = - (vec![], vec![], vec![], vec![], vec![], vec![]); - let (mut acc_tds, mut acc_f, mut acc_ws) = (vec![], vec![], vec![]); - - for _ in 0..l1 { - let domainsep = spongefish::domain_separator!("test::warp"); - let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); - let ((acc_x, acc_w), _pf) = 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(); - acc_roots.push(acc_x.0[0].clone()); - acc_alphas.push(acc_x.1[0].clone()); - acc_mus.push(acc_x.2[0]); - acc_taus.push(acc_x.3 .0[0].clone()); - acc_xs.push(acc_x.3 .1[0].clone()); - acc_eta.push(acc_x.4[0]); - - acc_tds.push(acc_w.0[0].clone()); - acc_f.push(acc_w.1[0].clone()); - acc_ws.push(acc_w.2[0].clone()); - } - - let domainsep = spongefish::domain_separator!("test::warp"); - let warp_config = - WARPConfig::<_, R1CS>::new(8, l1, s, t, r1cs.config(), code.code_len()); - - let hash_chain_warp = WARP::< - BLS12_381, - R1CS, - _, - Blake3MerkleTreeParams, - >::new( - warp_config.clone(), code.clone(), r1cs.clone(), (), () - ); - - let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); - let ((acc_x, acc_w), pf) = hash_chain_warp - .prove( - (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), - &mut prover_state, - instances_witnesses.1, - instances_witnesses.0, - (acc_roots, acc_alphas, acc_mus, (acc_taus, acc_xs), acc_eta), - (acc_tds, acc_f, acc_ws), - ) - .unwrap(); - - let narg_str = prover_state.narg_string().to_vec(); - let domainsep_v = spongefish::domain_separator!("test::warp"); - let mut verifier_state = domainsep_v - .without_session() - .instance(&0u32) - .std_verifier(&narg_str); - hash_chain_warp - .verify( - (r1cs.m, r1cs.n, r1cs.k), - &mut verifier_state, - acc_x.clone(), - pf.clone(), - ) - .unwrap(); - hash_chain_warp - .decide(acc_w.clone(), acc_x.clone()) - .unwrap(); - - let acc_x_to_serde = - AccInstanceSerializer::<_, Blake3MerkleTreeParams>::new(acc_x); - let acc_w_to_serde = - AccWitnessSerializer::<_, Blake3MerkleTreeParams>::new(acc_w); - let proof_to_serde = ProofSerializer::new(pf); - - println!( - "acc_x size: {}", - acc_x_to_serde.serialized_size(Compress::Yes) - ); - println!( - "acc_w size: {}", - acc_w_to_serde.serialized_size(Compress::Yes) - ); - println!( - "proof size: {}", - proof_to_serde.serialized_size(Compress::Yes) - ); - println!("narg_str size: {}", narg_str.len()); - } - - #[test] - pub fn warp_test_goldilocks() { - use crate::utils::fields::Goldilocks; - - let l1 = 4; - let s = 8; - let t = 7; - let hash_chain_size = 10; - let mut rng = thread_rng(); - let poseidon_config = poseidon::initialize_poseidon_config::(); - let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( - poseidon_config.clone(), - hash_chain_size, - )) - .unwrap(); - let code_config = - ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); - let code = ReedSolomon::new(code_config); - - let instances_witnesses: (Vec>, Vec>) = (0..l1) - .map(|_| { - let preimage = vec![Goldilocks::rand(&mut rng)]; - let instance = HashChainInstance { - digest: compute_hash_chain::>( - &poseidon_config, - &preimage, - hash_chain_size, - ), - }; - let witness = HashChainWitness { - preimage, - _crhs_scheme: PhantomData::>, - }; - let relation = HashChainRelation::, CRHGadget<_>>::new( - instance, - witness, - (poseidon_config.clone(), hash_chain_size), - ); - (relation.x, relation.w) - }) - .unzip(); - - let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( - poseidon_config.clone(), - hash_chain_size, - )) - .unwrap(); - - let warp_config = WARPConfig::new(l1, l1, s, t, r1cs.config(), code.code_len()); - let hash_chain_warp = WARP::< - Goldilocks, - R1CS, - _, - Blake3MerkleTreeParams, - >::new( - warp_config.clone(), code.clone(), r1cs.clone(), (), () - ); - - let (mut acc_roots, mut acc_alphas, mut acc_mus, mut acc_taus, mut acc_xs, mut acc_eta) = - (vec![], vec![], vec![], vec![], vec![], vec![]); - let (mut acc_tds, mut acc_f, mut acc_ws) = (vec![], vec![], vec![]); - - for _ in 0..l1 { - let domainsep = spongefish::domain_separator!("test::warp"); - let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); - let ((acc_x, acc_w), _pf) = 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(); - acc_roots.push(acc_x.0[0].clone()); - acc_alphas.push(acc_x.1[0].clone()); - acc_mus.push(acc_x.2[0]); - acc_taus.push(acc_x.3 .0[0].clone()); - acc_xs.push(acc_x.3 .1[0].clone()); - acc_eta.push(acc_x.4[0]); - - acc_tds.push(acc_w.0[0].clone()); - acc_f.push(acc_w.1[0].clone()); - acc_ws.push(acc_w.2[0].clone()); - } - - let domainsep = spongefish::domain_separator!("test::warp"); - // Use 8 (2*l1) for the total accumulation size to test multi-instance accumulation - let warp_config = - WARPConfig::<_, R1CS>::new(8, l1, s, t, r1cs.config(), code.code_len()); - - let hash_chain_warp = WARP::< - Goldilocks, - R1CS, - _, - Blake3MerkleTreeParams, - >::new( - warp_config.clone(), code.clone(), r1cs.clone(), (), () - ); - - let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); - let ((acc_x, acc_w), pf) = hash_chain_warp - .prove( - (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), - &mut prover_state, - instances_witnesses.1, - instances_witnesses.0, - (acc_roots, acc_alphas, acc_mus, (acc_taus, acc_xs), acc_eta), - (acc_tds, acc_f, acc_ws), - ) - .unwrap(); - - let narg_str = prover_state.narg_string().to_vec(); - let domainsep_v = spongefish::domain_separator!("test::warp"); - let mut verifier_state = domainsep_v - .without_session() - .instance(&0u32) - .std_verifier(&narg_str); - hash_chain_warp - .verify( - (r1cs.m, r1cs.n, r1cs.k), - &mut verifier_state, - acc_x.clone(), - pf.clone(), - ) - .unwrap(); - hash_chain_warp - .decide(acc_w.clone(), acc_x.clone()) - .unwrap(); - - let acc_x_to_serde = - AccInstanceSerializer::<_, Blake3MerkleTreeParams>::new(acc_x); - let acc_w_to_serde = - AccWitnessSerializer::<_, Blake3MerkleTreeParams>::new(acc_w); - let proof_to_serde = ProofSerializer::new(pf); - - println!( - "Goldilocks acc_x size: {}", - acc_x_to_serde.serialized_size(Compress::Yes) - ); - println!( - "Goldilocks acc_w size: {}", - acc_w_to_serde.serialized_size(Compress::Yes) - ); - println!( - "Goldilocks proof size: {}", - proof_to_serde.serialized_size(Compress::Yes) - ); - println!("Goldilocks narg_str size: {}", narg_str.len()); - } -} +pub use crate::accumulation_scheme::{ + AccumulatorInstance, AccumulatorWitness, WarpAccumulationScheme, WarpProof, WarpProverKey, + WarpVerifierKey, +}; +pub use crate::config::WarpConfig; +pub use crate::error::{DeciderError, ProverError, VerifierError, WarpError}; diff --git a/src/params/mod.rs b/src/params/mod.rs new file mode 100644 index 0000000..f51f0d2 --- /dev/null +++ b/src/params/mod.rs @@ -0,0 +1,137 @@ +//! Soundness-driven `(s, t)` selection for WarpAccumulationScheme. +//! Paired spec: `docs/paper-mods/mod4_parameter_selection.tex`. + +pub mod presets; +pub mod select; +pub mod types; +pub mod validate; + +pub use presets::{lookup, Preset, PRESETS}; +pub use select::select; +pub use types::{ParamError, Params, Regime, SecurityLevel, SoundnessBound}; +pub use validate::{inspect, validate}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn select_validate_roundtrip_provable() { + let lambda = SecurityLevel::STANDARD_128; + let p = select(lambda, 254, 0.5, Regime::Provable).unwrap(); + assert!( + p.t >= 256, + "provable t at λ=128, ρ=1/2 should need ≥ 256 queries, got {}", + p.t + ); + let bound = validate(&p, 254, 0.5, Regime::Provable, lambda).unwrap(); + assert!( + bound.meets(lambda), + "selected params should self-validate: {bound:?}" + ); + } + + #[test] + fn select_validate_roundtrip_conjectured() { + let lambda = SecurityLevel::STANDARD_128; + let p = select(lambda, 254, 0.5, Regime::Conjectured).unwrap(); + // Conjectured halves the query count at ρ=0.5 (2 vs 1 bits per query). + assert!(p.t >= 128 && p.t < 256); + let bound = validate(&p, 254, 0.5, Regime::Conjectured, lambda).unwrap(); + assert!(bound.meets(lambda)); + } + + #[test] + fn smaller_rate_needs_fewer_queries() { + let lambda = SecurityLevel::STANDARD_128; + let p_half = select(lambda, 254, 0.5, Regime::Provable).unwrap(); + let p_eighth = select(lambda, 254, 0.125, Regime::Provable).unwrap(); + assert!( + p_eighth.t < p_half.t, + "ρ=1/8 needs fewer queries than ρ=1/2: {} vs {}", + p_eighth.t, + p_half.t + ); + } + + #[test] + fn conjectured_needs_fewer_queries_than_provable() { + let lambda = SecurityLevel::STANDARD_128; + let p_prov = select(lambda, 254, 0.5, Regime::Provable).unwrap(); + let p_conj = select(lambda, 254, 0.5, Regime::Conjectured).unwrap(); + assert!(p_conj.t < p_prov.t); + } + + #[test] + fn field_too_small_is_rejected() { + // 64-bit field, 128-bit target: should fail admissibility. + assert!(matches!( + select(SecurityLevel::STANDARD_128, 64, 0.5, Regime::Provable), + Err(ParamError::FieldTooSmall { .. }) + )); + } + + #[test] + fn invalid_rate_is_rejected() { + let lambda = SecurityLevel::STANDARD_80; + assert_eq!( + select(lambda, 254, 0.0, Regime::Provable), + Err(ParamError::InvalidRate) + ); + assert_eq!( + select(lambda, 254, 1.0, Regime::Conjectured), + Err(ParamError::InvalidRate) + ); + } + + #[test] + fn presets_self_validate() { + // Every preset should achieve its claimed lambda under its regime. + for preset in PRESETS { + // Use 254-bit field so field_admissible passes at 128-bit. + let bound = validate( + &preset.params, + 254, + preset.code_rate(), + preset.regime, + preset.lambda, + ) + .unwrap(); + assert!( + bound.meets(preset.lambda), + "preset {:?} rate={:?} regime={:?} fails its own target: {:?}", + preset.lambda, + preset.code_rate(), + preset.regime, + bound + ); + } + } + + #[test] + fn presets_match_select_output() { + for preset in PRESETS { + let recomputed = select(preset.lambda, 254, preset.code_rate(), preset.regime).unwrap(); + assert_eq!( + recomputed, + preset.params, + "preset drift: {:?} rate={:?} regime={:?}", + preset.lambda, + preset.code_rate(), + preset.regime + ); + } + } + + #[test] + fn lookup_round_trips() { + let p = lookup(SecurityLevel::STANDARD_128, 1, 2, Regime::Conjectured).unwrap(); + assert_eq!(p.params.t, 128); + } + + #[test] + fn lookup_misses_on_unknown_rate() { + // We have 1/2 and 1/8 on file; 1/4 is not a preset. + assert!(lookup(SecurityLevel::STANDARD_128, 1, 4, Regime::Provable).is_none()); + } +} diff --git a/src/params/presets.rs b/src/params/presets.rs new file mode 100644 index 0000000..551ff86 --- /dev/null +++ b/src/params/presets.rs @@ -0,0 +1,89 @@ +//! Pre-computed `(λ, rate, regime) → (s, t)` rows. Regenerate via +//! `cargo run --bin warp-params -- table` after changing the bounds. + +use super::types::{Params, Regime, SecurityLevel}; + +pub struct Preset { + pub lambda: SecurityLevel, + pub code_rate_num: u32, + pub code_rate_den: u32, + pub regime: Regime, + pub params: Params, +} + +impl Preset { + pub fn code_rate(&self) -> f64 { + self.code_rate_num as f64 / self.code_rate_den as f64 + } +} + +pub const PRESETS: &[Preset] = &[ + Preset { + lambda: SecurityLevel::STANDARD_80, + code_rate_num: 1, + code_rate_den: 2, + regime: Regime::Provable, + params: Params { s: 8, t: 160 }, + }, + Preset { + lambda: SecurityLevel::STANDARD_80, + code_rate_num: 1, + code_rate_den: 2, + regime: Regime::Conjectured, + params: Params { s: 8, t: 80 }, + }, + Preset { + lambda: SecurityLevel::STANDARD_80, + code_rate_num: 1, + code_rate_den: 8, + regime: Regime::Provable, + params: Params { s: 8, t: 54 }, + }, + Preset { + lambda: SecurityLevel::STANDARD_80, + code_rate_num: 1, + code_rate_den: 8, + regime: Regime::Conjectured, + params: Params { s: 8, t: 27 }, + }, + Preset { + lambda: SecurityLevel::STANDARD_128, + code_rate_num: 1, + code_rate_den: 2, + regime: Regime::Provable, + params: Params { s: 8, t: 256 }, + }, + Preset { + lambda: SecurityLevel::STANDARD_128, + code_rate_num: 1, + code_rate_den: 2, + regime: Regime::Conjectured, + params: Params { s: 8, t: 128 }, + }, + Preset { + lambda: SecurityLevel::STANDARD_128, + code_rate_num: 1, + code_rate_den: 8, + regime: Regime::Provable, + params: Params { s: 8, t: 86 }, + }, + Preset { + lambda: SecurityLevel::STANDARD_128, + code_rate_num: 1, + code_rate_den: 8, + regime: Regime::Conjectured, + params: Params { s: 8, t: 43 }, + }, +]; + +/// Exact-rational match. `None` if the row isn't tabulated. +pub fn lookup( + lambda: SecurityLevel, + num: u32, + den: u32, + regime: Regime, +) -> Option<&'static Preset> { + PRESETS.iter().find(|p| { + p.lambda == lambda && p.code_rate_num == num && p.code_rate_den == den && p.regime == regime + }) +} diff --git a/src/params/select.rs b/src/params/select.rs new file mode 100644 index 0000000..81827bd --- /dev/null +++ b/src/params/select.rs @@ -0,0 +1,33 @@ +use super::types::{ParamError, Params, Regime, SecurityLevel}; + +const S_MIN: usize = 8; + +/// We require `log₂|F| ≥ λ + FIELD_EPSILON` so polylog noise is negligible. +pub const FIELD_EPSILON: u32 = 40; + +pub fn select( + lambda: SecurityLevel, + field_bits: u32, + code_rate: f64, + regime: Regime, +) -> Result { + if !(0.0 < code_rate && code_rate < 1.0) { + return Err(ParamError::InvalidRate); + } + if field_bits < lambda.bits() + FIELD_EPSILON { + return Err(ParamError::FieldTooSmall { + field_bits, + lambda: lambda.bits(), + }); + } + + let bits_per_query = match regime { + Regime::Provable => 0.5 * (-code_rate.log2()), + Regime::Conjectured => -code_rate.log2(), + }; + debug_assert!(bits_per_query > 0.0); + + let t = (lambda.bits() as f64 / bits_per_query).ceil() as usize; + + Ok(Params { s: S_MIN, t }) +} diff --git a/src/params/types.rs b/src/params/types.rs new file mode 100644 index 0000000..fe5c730 --- /dev/null +++ b/src/params/types.rs @@ -0,0 +1,62 @@ +/// Target soundness in bits: `SecurityLevel(128)` means error ≤ 2⁻¹²⁸. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct SecurityLevel(pub u32); + +impl SecurityLevel { + pub const STANDARD_80: Self = Self(80); + pub const STANDARD_100: Self = Self(100); + pub const STANDARD_128: Self = Self(128); + pub const STANDARD_192: Self = Self(192); + pub const STANDARD_256: Self = Self(256); + + pub fn bits(self) -> u32 { + self.0 + } +} + +/// Which list-decoding regime to assume. `Provable`: Johnson bound (radius +/// `1 − √ρ`). `Conjectured`: list-decoding to `1 − ρ`; halves `t`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Regime { + Provable, + Conjectured, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Params { + pub s: usize, + pub t: usize, +} + +#[derive(Clone, Copy, Debug)] +pub struct SoundnessBound { + pub proximity_bits: f64, + pub field_admissible: bool, + pub ood_admissible: bool, +} + +impl SoundnessBound { + pub fn meets(&self, target: SecurityLevel) -> bool { + self.field_admissible && self.ood_admissible && self.proximity_bits >= target.bits() as f64 + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ParamError { + InvalidRate, + FieldTooSmall { + field_bits: u32, + lambda: u32, + }, + /// Returned by `validate()` when `params.s` is below the OOD-samples + /// minimum required for the chosen target. + OodSamplesTooFew { + s: usize, + min: usize, + }, + /// Returned by `validate()` when proximity soundness is below target. + ProximitySoundnessBelowTarget { + proximity_bits: f64, + target_bits: u32, + }, +} diff --git a/src/params/validate.rs b/src/params/validate.rs new file mode 100644 index 0000000..7305746 --- /dev/null +++ b/src/params/validate.rs @@ -0,0 +1,70 @@ +use super::select::FIELD_EPSILON; +use super::types::{ParamError, Params, Regime, SecurityLevel, SoundnessBound}; + +const S_MIN: usize = 8; + +/// Fail-closed soundness check. Returns `Ok(bound)` only when every +/// admissibility flag passes and proximity soundness meets the target. +/// +/// For diagnostic output that returns the bound regardless of pass/fail, +/// use [`inspect`]. +pub fn validate( + params: &Params, + field_bits: u32, + code_rate: f64, + regime: Regime, + target: SecurityLevel, +) -> Result { + let bound = inspect(params, field_bits, code_rate, regime, target)?; + if !bound.field_admissible { + return Err(ParamError::FieldTooSmall { + field_bits, + lambda: target.bits(), + }); + } + if !bound.ood_admissible { + return Err(ParamError::OodSamplesTooFew { + s: params.s, + min: S_MIN, + }); + } + if bound.proximity_bits < target.bits() as f64 { + return Err(ParamError::ProximitySoundnessBelowTarget { + proximity_bits: bound.proximity_bits, + target_bits: target.bits(), + }); + } + Ok(bound) +} + +/// Diagnostic / non-failing variant of [`validate`]. Returns the full +/// [`SoundnessBound`] even when admissibility checks fail (only structural +/// errors like an invalid rate produce `Err`). Intended for CLI / debug +/// output; security-relevant callers must use [`validate`]. +pub fn inspect( + params: &Params, + field_bits: u32, + code_rate: f64, + regime: Regime, + target: SecurityLevel, +) -> Result { + if !(0.0 < code_rate && code_rate < 1.0) { + return Err(ParamError::InvalidRate); + } + + let bits_per_query = match regime { + Regime::Provable => 0.5 * (-code_rate.log2()), + Regime::Conjectured => -code_rate.log2(), + }; + + let proximity_bits = (params.t as f64) * bits_per_query; + let field_admissible = field_bits >= target.bits() + FIELD_EPSILON; + let ood_admissible = params.s >= S_MIN; + + let _ = target; + Ok(SoundnessBound { + proximity_bits, + field_admissible, + ood_admissible, + }) +} diff --git a/src/profile/counters.rs b/src/profile/counters.rs new file mode 100644 index 0000000..b7a3f78 --- /dev/null +++ b/src/profile/counters.rs @@ -0,0 +1,213 @@ +//! Thread-local op counters for crate-boundary events (Merkle ops, MLE +//! materialisations, sumcheck rounds). No-op without the `profile` feature. + +#[cfg(feature = "profile")] +use std::cell::Cell; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Counter { + EncodeCalls, + MerkleTreeBuilds, + MerklePathsGenerated, + MerklePathsVerified, + MleMaterializations, + OracleLeafQueries, + OraclePointQueries, + TwinConstraintRounds, + BatchingRounds, + OodPointQueries, +} + +impl Counter { + pub const ALL: &'static [Counter] = &[ + Counter::EncodeCalls, + Counter::MerkleTreeBuilds, + Counter::MerklePathsGenerated, + Counter::MerklePathsVerified, + Counter::MleMaterializations, + Counter::OracleLeafQueries, + Counter::OraclePointQueries, + Counter::TwinConstraintRounds, + Counter::BatchingRounds, + Counter::OodPointQueries, + ]; + + pub fn name(self) -> &'static str { + match self { + Counter::EncodeCalls => "encode_calls", + Counter::MerkleTreeBuilds => "merkle_tree_builds", + Counter::MerklePathsGenerated => "merkle_paths_generated", + Counter::MerklePathsVerified => "merkle_paths_verified", + Counter::MleMaterializations => "mle_materializations", + Counter::OracleLeafQueries => "oracle_leaf_queries", + Counter::OraclePointQueries => "oracle_point_queries", + Counter::TwinConstraintRounds => "twin_constraint_rounds", + Counter::BatchingRounds => "batching_rounds", + Counter::OodPointQueries => "ood_point_queries", + } + } +} + +#[cfg(feature = "profile")] +thread_local! { + static COUNTERS: Counters = const { Counters::new() }; +} + +#[cfg(feature = "profile")] +struct Counters { + encode_calls: Cell, + merkle_tree_builds: Cell, + merkle_paths_generated: Cell, + merkle_paths_verified: Cell, + mle_materializations: Cell, + oracle_leaf_queries: Cell, + oracle_point_queries: Cell, + twin_constraint_rounds: Cell, + batching_rounds: Cell, + ood_point_queries: Cell, +} + +#[cfg(feature = "profile")] +impl Counters { + const fn new() -> Self { + Self { + encode_calls: Cell::new(0), + merkle_tree_builds: Cell::new(0), + merkle_paths_generated: Cell::new(0), + merkle_paths_verified: Cell::new(0), + mle_materializations: Cell::new(0), + oracle_leaf_queries: Cell::new(0), + oracle_point_queries: Cell::new(0), + twin_constraint_rounds: Cell::new(0), + batching_rounds: Cell::new(0), + ood_point_queries: Cell::new(0), + } + } + + fn cell(&self, c: Counter) -> &Cell { + match c { + Counter::EncodeCalls => &self.encode_calls, + Counter::MerkleTreeBuilds => &self.merkle_tree_builds, + Counter::MerklePathsGenerated => &self.merkle_paths_generated, + Counter::MerklePathsVerified => &self.merkle_paths_verified, + Counter::MleMaterializations => &self.mle_materializations, + Counter::OracleLeafQueries => &self.oracle_leaf_queries, + Counter::OraclePointQueries => &self.oracle_point_queries, + Counter::TwinConstraintRounds => &self.twin_constraint_rounds, + Counter::BatchingRounds => &self.batching_rounds, + Counter::OodPointQueries => &self.ood_point_queries, + } + } +} + +#[cfg(feature = "profile")] +#[inline] +pub fn bump(c: Counter, n: u64) { + COUNTERS.with(|cs| { + let cell = cs.cell(c); + cell.set(cell.get().saturating_add(n)); + }); +} + +#[cfg(not(feature = "profile"))] +#[inline] +pub fn bump(_c: Counter, _n: u64) {} + +#[cfg(feature = "profile")] +pub fn snapshot() -> Snapshot { + let mut values = [0u64; Counter::ALL.len()]; + COUNTERS.with(|cs| { + for (i, &c) in Counter::ALL.iter().enumerate() { + values[i] = cs.cell(c).get(); + } + }); + Snapshot { values } +} + +#[cfg(not(feature = "profile"))] +pub fn snapshot() -> Snapshot { + Snapshot {} +} + +#[cfg(feature = "profile")] +#[derive(Clone, Copy, Debug)] +pub struct Snapshot { + values: [u64; Counter::ALL.len()], +} + +#[cfg(feature = "profile")] +impl Snapshot { + pub fn get(&self, c: Counter) -> u64 { + let idx = Counter::ALL.iter().position(|x| *x == c).unwrap(); + self.values[idx] + } + + /// `later.delta(&earlier)` returns `later - earlier`, per counter. + pub fn delta(&self, earlier: &Snapshot) -> Delta { + let mut values = [0u64; Counter::ALL.len()]; + for (i, slot) in values.iter_mut().enumerate() { + *slot = self.values[i].saturating_sub(earlier.values[i]); + } + Delta { values } + } +} + +#[cfg(not(feature = "profile"))] +#[derive(Clone, Copy, Debug)] +pub struct Snapshot {} + +#[cfg(not(feature = "profile"))] +impl Snapshot { + pub fn get(&self, _c: Counter) -> u64 { + 0 + } + pub fn delta(&self, _earlier: &Snapshot) -> Delta { + Delta {} + } +} + +#[cfg(feature = "profile")] +#[derive(Clone, Copy, Debug)] +pub struct Delta { + values: [u64; Counter::ALL.len()], +} + +#[cfg(feature = "profile")] +impl Delta { + pub fn get(&self, c: Counter) -> u64 { + let idx = Counter::ALL.iter().position(|x| *x == c).unwrap(); + self.values[idx] + } + + /// Iterate over `(Counter, delta)` pairs whose delta is non-zero. + pub fn iter_nonzero(&self) -> impl Iterator + '_ { + Counter::ALL + .iter() + .zip(self.values.iter()) + .filter_map(|(c, v)| if *v > 0 { Some((*c, *v)) } else { None }) + } +} + +#[cfg(not(feature = "profile"))] +#[derive(Clone, Copy, Debug)] +pub struct Delta {} + +#[cfg(not(feature = "profile"))] +impl Delta { + pub fn get(&self, _c: Counter) -> u64 { + 0 + } + pub fn iter_nonzero(&self) -> std::iter::Empty<(Counter, u64)> { + std::iter::empty() + } +} + +#[macro_export] +macro_rules! count_ops { + ($counter:ident) => { + $crate::profile::counters::bump($crate::profile::counters::Counter::$counter, 1); + }; + ($counter:ident, $n:expr) => { + $crate::profile::counters::bump($crate::profile::counters::Counter::$counter, $n); + }; +} diff --git a/src/profile/layer.rs b/src/profile/layer.rs new file mode 100644 index 0000000..1c27fb3 --- /dev/null +++ b/src/profile/layer.rs @@ -0,0 +1,166 @@ +//! Tracing Layer that emits one `warp.profile.v1` JSON record per closed +//! span: `{phase, wall_ns, cpu_ns, rss_delta_bytes, counters, dimensions}`. + +use std::collections::BTreeMap; +use std::io::Write; +use std::sync::Mutex; +use std::time::Instant; + +use tracing::field::{Field, Visit}; +use tracing::span::{Attributes, Id}; +use tracing::Subscriber; +use tracing_subscriber::layer::Context; +use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::Layer; + +use crate::profile::counters::{self, Snapshot}; +use crate::profile::{rss, timing}; + +/// What we stash on each span at enter-time. +struct SpanStart { + wall: Instant, + cpu_ns: Option, + rss_bytes: Option, + counters: Snapshot, +} + +/// Dimensions (numeric span fields) captured at span-creation time. +struct Dimensions(BTreeMap); + +impl Visit for Dimensions { + fn record_i64(&mut self, field: &Field, value: i64) { + self.0.insert(field.name().to_owned(), value as i128); + } + fn record_u64(&mut self, field: &Field, value: u64) { + self.0.insert(field.name().to_owned(), value as i128); + } + fn record_i128(&mut self, field: &Field, value: i128) { + self.0.insert(field.name().to_owned(), value); + } + fn record_u128(&mut self, field: &Field, value: u128) { + self.0.insert(field.name().to_owned(), value as i128); + } + fn record_bool(&mut self, field: &Field, value: bool) { + self.0.insert(field.name().to_owned(), value as i128); + } + fn record_debug(&mut self, _field: &Field, _value: &dyn std::fmt::Debug) {} +} + +/// A tracing `Layer` that emits `warp.profile.v1` JSON records on +/// span close. Thread-safe: wraps the writer in a `Mutex`. +pub struct JsonLayer { + writer: Mutex, +} + +impl JsonLayer { + pub fn new(writer: W) -> Self { + Self { + writer: Mutex::new(writer), + } + } +} + +impl Layer for JsonLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, + W: Write + Send + 'static, +{ + fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { + let mut dims = Dimensions(BTreeMap::new()); + attrs.record(&mut dims); + if let Some(span) = ctx.span(id) { + span.extensions_mut().insert(dims); + } + } + + fn on_enter(&self, id: &Id, ctx: Context<'_, S>) { + let Some(span) = ctx.span(id) else { return }; + let start = SpanStart { + wall: Instant::now(), + cpu_ns: timing::thread_cpu_ns(), + rss_bytes: rss::peak_rss_bytes(), + counters: counters::snapshot(), + }; + span.extensions_mut().insert(start); + } + + fn on_close(&self, id: Id, ctx: Context<'_, S>) { + let Some(span) = ctx.span(&id) else { return }; + let ext = span.extensions(); + let Some(start) = ext.get::() else { + return; + }; + + let wall_ns = start.wall.elapsed().as_nanos() as u64; + let cpu_ns = match (timing::thread_cpu_ns(), start.cpu_ns) { + (Some(end), Some(begin)) => Some(end.saturating_sub(begin)), + _ => None, + }; + let rss_delta = match (rss::peak_rss_bytes(), start.rss_bytes) { + (Some(end), Some(begin)) => Some(end.saturating_sub(begin) as i64), + _ => None, + }; + let delta = counters::snapshot().delta(&start.counters); + + let dims_default = Dimensions(BTreeMap::new()); + let dims = ext.get::().unwrap_or(&dims_default); + + let mut out = Vec::with_capacity(256); + let _ = write!(out, "{{\"schema\":\"warp.profile.v1\",\"phase\":"); + write_json_string(&mut out, span.name()); + let _ = write!(out, ",\"wall_ns\":{wall_ns}"); + if let Some(c) = cpu_ns { + let _ = write!(out, ",\"cpu_ns\":{c}"); + } + if let Some(r) = rss_delta { + let _ = write!(out, ",\"rss_delta_bytes\":{r}"); + } + + // counters + let _ = write!(out, ",\"counters\":{{"); + let mut first = true; + for (c, v) in delta.iter_nonzero() { + if !first { + let _ = write!(out, ","); + } + first = false; + let _ = write!(out, "\"{}\":{}", c.name(), v); + } + let _ = write!(out, "}}"); + + // dimensions + let _ = write!(out, ",\"dimensions\":{{"); + let mut first = true; + for (k, v) in &dims.0 { + if !first { + let _ = write!(out, ","); + } + first = false; + write_json_string(&mut out, k); + let _ = write!(out, ":{v}"); + } + let _ = writeln!(out, "}}}}"); + + if let Ok(mut w) = self.writer.lock() { + let _ = w.write_all(&out); + } + } +} + +fn write_json_string(out: &mut Vec, s: &str) { + out.push(b'"'); + for b in s.bytes() { + match b { + b'"' => out.extend_from_slice(b"\\\""), + b'\\' => out.extend_from_slice(b"\\\\"), + b'\n' => out.extend_from_slice(b"\\n"), + b'\r' => out.extend_from_slice(b"\\r"), + b'\t' => out.extend_from_slice(b"\\t"), + 0x00..=0x1F => { + let _ = write!(out, "\\u{b:04x}"); + } + _ => out.push(b), + } + } + out.push(b'"'); +} diff --git a/src/profile/mod.rs b/src/profile/mod.rs new file mode 100644 index 0000000..f4379e9 --- /dev/null +++ b/src/profile/mod.rs @@ -0,0 +1,65 @@ +//! Opt-in profile instrumentation. The `profile` feature pulls in +//! `tracing-subscriber` and `libc` and lets `init_fmt`/`init_json` render +//! the always-emitted spans. Without the feature, init functions no-op. + +pub mod counters; +pub mod rss; +pub mod timing; + +#[cfg(feature = "profile")] +pub mod layer; + +#[cfg(feature = "profile")] +pub fn init() -> bool { + init_fmt() +} + +#[cfg(not(feature = "profile"))] +pub fn init() -> bool { + false +} + +#[cfg(feature = "profile")] +pub fn init_fmt() -> bool { + use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warp=info")); + tracing_subscriber::registry() + .with(env_filter) + .with( + fmt::layer() + .with_target(false) + .with_span_events(fmt::format::FmtSpan::CLOSE) + .with_writer(std::io::stderr), + ) + .try_init() + .is_ok() +} + +#[cfg(not(feature = "profile"))] +pub fn init_fmt() -> bool { + false +} + +#[cfg(feature = "profile")] +pub fn init_json(writer: W) -> bool +where + W: std::io::Write + Send + 'static, +{ + use tracing_subscriber::{prelude::*, EnvFilter}; + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warp=info")); + tracing_subscriber::registry() + .with(env_filter) + .with(layer::JsonLayer::new(writer)) + .try_init() + .is_ok() +} + +#[cfg(not(feature = "profile"))] +pub fn init_json(_writer: W) -> bool +where + W: std::io::Write + Send + 'static, +{ + false +} diff --git a/src/profile/rss.rs b/src/profile/rss.rs new file mode 100644 index 0000000..a1f95b5 --- /dev/null +++ b/src/profile/rss.rs @@ -0,0 +1,33 @@ +//! Process peak RSS via `getrusage(RUSAGE_SELF)`. `ru_maxrss` is bytes on +//! macOS, kilobytes on Linux; we return bytes. Monotonic — use deltas. + +#[cfg(feature = "profile")] +pub fn peak_rss_bytes() -> Option { + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + // SAFETY: rusage is POD; kernel writes on success. + let mut ru: libc::rusage = unsafe { std::mem::zeroed() }; + let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) }; + if rc != 0 { + return None; + } + let maxrss = ru.ru_maxrss as u64; + #[cfg(target_os = "macos")] + { + Some(maxrss) + } + #[cfg(target_os = "linux")] + { + Some(maxrss.saturating_mul(1024)) + } + } + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + { + None + } +} + +#[cfg(not(feature = "profile"))] +pub fn peak_rss_bytes() -> Option { + None +} diff --git a/src/profile/timing.rs b/src/profile/timing.rs new file mode 100644 index 0000000..d4020c8 --- /dev/null +++ b/src/profile/timing.rs @@ -0,0 +1,29 @@ +//! Thread CPU time via `clock_gettime(CLOCK_THREAD_CPUTIME_ID)`. `None` +//! off Linux/macOS or without the `profile` feature. + +#[cfg(feature = "profile")] +pub fn thread_cpu_ns() -> Option { + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + let mut ts = libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }; + // SAFETY: timespec is POD; the kernel writes on success. + let rc = unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) }; + if rc == 0 { + Some((ts.tv_sec as u64).saturating_mul(1_000_000_000) + (ts.tv_nsec as u64)) + } else { + None + } + } + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + { + None + } +} + +#[cfg(not(feature = "profile"))] +pub fn thread_cpu_ns() -> Option { + None +} diff --git a/src/protocol/domainsep/mod.rs b/src/protocol/domainsep/mod.rs deleted file mode 100644 index 32d1386..0000000 --- a/src/protocol/domainsep/mod.rs +++ /dev/null @@ -1,172 +0,0 @@ -use ark_crypto_primitives::merkle_tree::Config; -use ark_ff::Field; - -use spongefish::{ - Decoding, Encoding, NargDeserialize, ProverState, VerificationResult, VerifierState, -}; - -pub type AccInstances = ( - Vec<::InnerDigest>, // rt - Vec>, // alpha - Vec, // mu - (Vec>, Vec>), // (tau, x) - Vec, // eta -); - -pub fn absorb_instances>( - prover_state: &mut ProverState, - instances: &[Vec], -) { - for instance in instances { - for f in instance { - prover_state.prover_message(f); - } - } -} - -pub fn absorb_accumulated_instances< - F: Field + Encoding<[u8]>, - MT: Config + From<[u8; 32]>>, ->( - prover_state: &mut ProverState, - acc_instances: &AccInstances, -) { - // digests (rt) - for digest in &acc_instances.0 { - let bytes: [u8; 32] = digest.as_ref().try_into().expect("digest must be 32 bytes"); - prover_state.prover_message(&bytes); - } - - // alpha - for alpha in &acc_instances.1 { - for f in alpha { - prover_state.prover_message(f); - } - } - - // mu - for f in &acc_instances.2 { - prover_state.prover_message(f); - } - - // taus - for tau in &acc_instances.3 .0 { - for f in tau { - prover_state.prover_message(f); - } - } - - // xs - for x in &acc_instances.3 .1 { - for f in x { - prover_state.prover_message(f); - } - } - - // etas - for f in &acc_instances.4 { - prover_state.prover_message(f); - } -} - -pub type ParsedStatement = (Vec>, AccInstances); - -pub fn parse_statement< - F: Field + NargDeserialize + Encoding<[u8]> + Decoding<[u8]>, - MT: Config + From<[u8; 32]>>, ->( - verifier_state: &mut VerifierState<'_>, - l1: usize, - l2: usize, - instance_len: usize, - log_n: usize, - #[allow(non_snake_case)] log_M: usize, -) -> VerificationResult> { - // f. absorb l1 instances - let mut l1_xs = Vec::with_capacity(l1); - for _ in 0..l1 { - let inst: Vec = verifier_state.prover_messages_vec(instance_len)?; - l1_xs.push(inst); - } - - // l2 instances - let mut l2_roots = Vec::with_capacity(l2); - for _ in 0..l2 { - let bytes: [u8; 32] = verifier_state.prover_message()?; - l2_roots.push(bytes.into()); - } - - let mut l2_alphas = Vec::with_capacity(l2); - for _ in 0..l2 { - let alpha: Vec = verifier_state.prover_messages_vec(log_n)?; - l2_alphas.push(alpha); - } - - let l2_mus: Vec = verifier_state.prover_messages_vec(l2)?; - - let mut l2_taus = Vec::with_capacity(l2); - for _ in 0..l2 { - let tau: Vec = verifier_state.prover_messages_vec(log_M)?; - l2_taus.push(tau); - } - - let mut l2_xs = Vec::with_capacity(l2); - for _ in 0..l2 { - let x: Vec = verifier_state.prover_messages_vec(instance_len)?; - l2_xs.push(x); - } - - let l2_etas: Vec = verifier_state.prover_messages_vec(l2)?; - - Ok(( - l1_xs, - (l2_roots, l2_alphas, l2_mus, (l2_taus, l2_xs), l2_etas), - )) -} - -/// Read `rt_0 + l1_mus`, squeeze `l1_taus + ω + τ`. Runs before the -/// twin-constraint sumcheck on the verifier side. -#[allow(clippy::type_complexity)] -pub fn derive_pre_twin_constraint< - F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, - MT: Config + From<[u8; 32]>>, ->( - vs: &mut VerifierState<'_>, - l1: usize, - log_l: usize, - #[allow(non_snake_case)] log_M: usize, -) -> VerificationResult<(MT::InnerDigest, Vec, Vec>, F, Vec)> { - let rt_0: MT::InnerDigest = <[u8; 32]>::into(vs.prover_message()?); - let l1_mus = vs.prover_messages_vec(l1)?; - let l1_taus = (0..l1) - .map(|_| (0..log_M).map(|_| vs.verifier_message::()).collect()) - .collect(); - let omega = vs.verifier_message(); - let tau = (0..log_l).map(|_| vs.verifier_message::()).collect(); - Ok((rt_0, l1_mus, l1_taus, omega, tau)) -} - -/// Read `td + η + ν₀`, squeeze OOD points, read OOD answers, squeeze shift -/// query bytes and `ξ`. Runs between the two sumchecks on the verifier side. -#[allow(clippy::type_complexity)] -pub fn derive_between_sumchecks< - F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, - MT: Config + From<[u8; 32]>>, ->( - vs: &mut VerifierState<'_>, - log_n: usize, - s: usize, - t: usize, - log_r: usize, -) -> VerificationResult<(MT::InnerDigest, F, Vec, Vec, Vec, Vec)> { - let td: MT::InnerDigest = <[u8; 32]>::into(vs.prover_message()?); - let eta = vs.prover_message()?; - let mut nus = vec![vs.prover_message::()?]; - let ood_samples = (0..s * log_n).map(|_| vs.verifier_message::()).collect(); - nus.extend(vs.prover_messages_vec::(s)?); - let bytes_shift_queries = (0..(t * log_n).div_ceil(8)) - .map(|_| vs.verifier_message::<[u8; 1]>()[0]) - .collect(); - let xi = (0..log_r).map(|_| vs.verifier_message::()).collect(); - Ok((td, eta, nus, ood_samples, bytes_shift_queries, xi)) -} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs deleted file mode 100644 index 6a13b4e..0000000 --- a/src/protocol/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub mod domainsep; - -use effsc::transcript::VerifierTranscript; -use spongefish::{Decoding, Encoding, NargDeserialize, VerifierState}; - -/// Adapter plugging spongefish's [`VerifierState`] into effsc's -/// [`VerifierTranscript`] so the verifier can call -/// [`effsc::verifier::sumcheck_verify`]. Prover side is covered by a blanket -/// impl inside effsc (on `spongefish::ProverState`). -pub struct EffscVerifierTranscript<'s, 'a>(pub &'s mut VerifierState<'a>); - -impl VerifierTranscript for EffscVerifierTranscript<'_, '_> -where - F: ark_ff::Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, -{ - type Error = spongefish::VerificationError; - fn receive(&mut self) -> Result { - self.0.prover_message::() - } - fn challenge(&mut self) -> F { - self.0.verifier_message::() - } -} diff --git a/src/relations/description.rs b/src/relations/description.rs index ded163e..16a749d 100644 --- a/src/relations/description.rs +++ b/src/relations/description.rs @@ -38,24 +38,23 @@ impl SerializableConstraintMatrices { .unwrap(); constraint_system.finalize(); - let num_instance_variables = constraint_system.num_instance_variables(); - let num_witness_variables = constraint_system.num_witness_variables(); - let num_constraints = constraint_system.num_constraints(); + let cs = constraint_system.into_inner().unwrap(); + let all_matrices = cs.to_matrices().unwrap(); + let r1cs_matrices = all_matrices + .get(R1CS_PREDICATE_LABEL) + .expect("R1CS predicate must exist"); - let mut matrices = constraint_system.to_matrices().unwrap(); - let mut r1cs = matrices.remove(R1CS_PREDICATE_LABEL).unwrap(); - let mut r1cs_iter = r1cs.drain(..); - let a = r1cs_iter.next().unwrap(); - let b = r1cs_iter.next().unwrap(); - let c = r1cs_iter.next().unwrap(); + let num_constraints = cs + .get_predicate_num_constraints(R1CS_PREDICATE_LABEL) + .unwrap_or(0); let serializable = SerializableConstraintMatrices { - num_instance_variables, - num_witness_variables, + num_instance_variables: cs.num_instance_variables(), + num_witness_variables: cs.num_witness_variables(), num_constraints, - a: SerializableConstraintMatrices::serialize_nested_field(a), - b: SerializableConstraintMatrices::serialize_nested_field(b), - c: SerializableConstraintMatrices::serialize_nested_field(c), + a: Self::serialize_nested_field(r1cs_matrices[0].clone()), + b: Self::serialize_nested_field(r1cs_matrices[1].clone()), + c: Self::serialize_nested_field(r1cs_matrices[2].clone()), }; let serialized = serde_json::to_string(&serializable).unwrap(); serialized.into_bytes() diff --git a/src/relations/mod.rs b/src/relations/mod.rs index 998c24a..283b2c1 100644 --- a/src/relations/mod.rs +++ b/src/relations/mod.rs @@ -4,16 +4,15 @@ pub mod r1cs; pub use description::SerializableConstraintMatrices; use ark_ff::Field; -use r1cs::R1CS; -use crate::error::WARPError; +use crate::error::WarpError; pub trait Relation { type Instance; type Witness; type Config; fn constraints(&self) -> usize; - fn description(config: &Self::Config) -> Vec; + fn describe_from_config(config: &Self::Config) -> Vec; fn instance(&self) -> Self::Instance; fn new(instance: Self::Instance, witness: Self::Witness, config: Self::Config) -> Self; fn public_config(&self) -> Vec; @@ -23,16 +22,16 @@ pub trait Relation { fn witness(&self) -> Self::Witness; } -pub trait BundledPESAT { +pub trait PolyPredicate { type Config; - type Constraints; - fn evaluate_bundled(&self, zero_evader_evals: &[F], z: &[F]) -> Result; + fn evaluate_bundled(&self, zero_evader_evals: &[F], z: &[F]) -> Result; fn config(&self) -> Self::Config; fn description(&self) -> Vec; - fn constraints(&self) -> &Self::Constraints; + fn constraints(&self) -> &r1cs::R1CSConstraints; } -pub trait ToPolySystem: Relation { - // generate an r1cs polynomial system ((A, B, C), M, N, k) for a relation - fn into_r1cs(config: &Self::Config) -> Result, WARPError>; +pub trait Arithmetize { + type Config; + type Predicate; + fn arithmetize(config: &Self::Config) -> Result; } diff --git a/src/relations/r1cs/hashchain/mod.rs b/src/relations/r1cs/hashchain/mod.rs index 4f34fe3..c94c535 100644 --- a/src/relations/r1cs/hashchain/mod.rs +++ b/src/relations/r1cs/hashchain/mod.rs @@ -21,16 +21,19 @@ pub use synthesizer::HashChainSynthesizer; pub use witness::HashChainWitness; use super::R1CS; -use crate::error::WARPError; -use crate::relations::ToPolySystem; +use crate::error::WarpError; +use crate::relations::Arithmetize; impl< F: PrimeField + Absorb, H: CRHScheme, HG: CRHSchemeGadget], OutputVar = FpVar>, - > ToPolySystem for HashChainRelation + > Arithmetize for HashChainRelation { - fn into_r1cs(config: &Self::Config) -> Result, WARPError> { + type Config = (H::Parameters, usize); + type Predicate = R1CS; + + fn arithmetize(config: &Self::Config) -> Result, WarpError> { let (params, hash_chain_size) = config; let preimage = vec![F::ZERO]; let digest = compute_hash_chain::(params, &preimage, *hash_chain_size); diff --git a/src/relations/r1cs/hashchain/relation.rs b/src/relations/r1cs/hashchain/relation.rs index 2bd4999..3fd295c 100644 --- a/src/relations/r1cs/hashchain/relation.rs +++ b/src/relations/r1cs/hashchain/relation.rs @@ -56,7 +56,7 @@ where self.constraint_system.num_constraints() } - fn description(config: &Self::Config) -> Vec { + fn describe_from_config(config: &Self::Config) -> Vec { let (hash_config, hash_chain_size) = (config.0.clone(), config.1); let zero_witness = HashChainWitness:: { preimage: vec![F::zero()], @@ -94,11 +94,17 @@ where .unwrap(); constraint_system.finalize(); - let cs = constraint_system.into_inner().unwrap(); - let x = cs.instance_assignment().unwrap().to_vec(); - let w = cs.witness_assignment().unwrap().to_vec(); + // Extract assignments via the ref (borrow the inner CS) + let x = constraint_system + .borrow() + .map(|cs| cs.instance_assignment().unwrap().to_vec()) + .unwrap(); + let w = constraint_system + .borrow() + .map(|cs| cs.witness_assignment().unwrap().to_vec()) + .unwrap(); Self { - constraint_system: ConstraintSystemRef::new(cs), + constraint_system, config: hash_config, instance, witness, @@ -202,7 +208,7 @@ mod tests { } #[test] - fn description() { + fn describe_from_config() { let hash_chain_size = 1; let zero_witness = HashChainWitness:: { preimage: vec![BLS12_381::zero()], @@ -223,7 +229,7 @@ mod tests { ); assert!(relation.verify()); let description: Vec = - HashChainRelation::::description(&( + HashChainRelation::::describe_from_config(&( initialize_poseidon_config(), hash_chain_size, )); diff --git a/src/relations/r1cs/hashchain/witness.rs b/src/relations/r1cs/hashchain/witness.rs index 5144bf1..a716a03 100644 --- a/src/relations/r1cs/hashchain/witness.rs +++ b/src/relations/r1cs/hashchain/witness.rs @@ -10,7 +10,20 @@ where H: CRHScheme, { pub preimage: Vec, - pub _crhs_scheme: PhantomData, + pub(crate) _crhs_scheme: PhantomData, +} + +impl HashChainWitness +where + F: Field + PrimeField, + H: CRHScheme, +{ + pub fn new(preimage: Vec) -> Self { + Self { + preimage, + _crhs_scheme: PhantomData, + } + } } impl Clone for HashChainWitness diff --git a/src/relations/r1cs/mod.rs b/src/relations/r1cs/mod.rs index b6bd5d9..201acee 100644 --- a/src/relations/r1cs/mod.rs +++ b/src/relations/r1cs/mod.rs @@ -1,12 +1,13 @@ pub mod hashchain; use ark_ff::Field; -use ark_relations::gr1cs::{ConstraintSystemRef, R1CS_PREDICATE_LABEL}; -use effsc::hypercube::Ascending; +use ark_relations::gr1cs::ConstraintSystemRef; +use rayon::prelude::*; -use crate::error::WARPError; +use crate::error::WarpError; +use crate::relations::SerializableConstraintMatrices; -use super::BundledPESAT; +use super::PolyPredicate; pub type R1CSConstraints = Vec<(Vec<(F, usize)>, Vec<(F, usize)>, Vec<(F, usize)>)>; @@ -15,42 +16,57 @@ pub struct R1CS { // we access linear combinations using binary hypercube points // point -> (a_i, b_i, c_i) // point is encoded via the n least significant bits of a usize - pub p: R1CSConstraints, - pub m: usize, - pub n: usize, - pub k: usize, + pub constraints_vec: R1CSConstraints, + pub m_num_constraints: usize, + pub n_num_variables: usize, + pub k_num_witness_vars: usize, pub log_m: usize, pub log_n: usize, } impl TryFrom> for R1CS { - type Error = WARPError; + type Error = WarpError; fn try_from(cs: ConstraintSystemRef) -> Result { - let mut matrices = cs.to_matrices().unwrap(); - let mut r1cs = matrices.remove(R1CS_PREDICATE_LABEL).unwrap(); - let mut r1cs_iter = r1cs.drain(..); - let a_mat = r1cs_iter.next().unwrap(); - let b_mat = r1cs_iter.next().unwrap(); - let c_mat = r1cs_iter.next().unwrap(); - - let num_constraints = cs.num_constraints(); - let num_instance_variables = cs.num_instance_variables(); - let num_witness_variables = cs.num_witness_variables(); + use ark_relations::gr1cs::R1CS_PREDICATE_LABEL; + + let inner = cs.into_inner().ok_or(WarpError::R1CSConstruction { + reason: "constraint system has outstanding borrows", + })?; + let all_matrices = inner + .to_matrices() + .map_err(|_| WarpError::R1CSConstruction { + reason: "constraint system not finalized or matrices unavailable", + })?; + let r1cs_matrices = + all_matrices + .get(R1CS_PREDICATE_LABEL) + .ok_or(WarpError::R1CSConstruction { + reason: "R1CS predicate not present in constraint system", + })?; + + let num_constraints = inner + .get_predicate_num_constraints(R1CS_PREDICATE_LABEL) + .unwrap_or(0); // number of constraints should be to be power of 2 let m = num_constraints.next_power_of_two(); - let n = num_instance_variables + num_witness_variables; - let k = num_witness_variables; + let n = inner.num_instance_variables() + inner.num_witness_variables(); + let k = inner.num_witness_variables(); + if n == 0 { + return Err(WarpError::R1CSConstruction { + reason: "n = num_instance_variables + num_witness_variables must be > 0", + }); + } - // both `unwrap()` calls below are safe since warp/lib.rs forbids compiling on platforms - // with 16-bits pointers width + // Safe: `m` is always ≥ 1 (next_power_of_two of any usize is ≥ 1) and `n > 0` + // checked above. usize→u32 cast is safe on ≥32-bit platforms per lib.rs. let log_m = m.ilog2().try_into().unwrap(); let log_n = n.ilog2().try_into().unwrap(); - let mut a = a_mat.into_iter(); - let mut b = b_mat.into_iter(); - let mut c = c_mat.into_iter(); + let mut a = r1cs_matrices[0].clone().into_iter(); + let mut b = r1cs_matrices[1].clone().into_iter(); + let mut c = r1cs_matrices[2].clone().into_iter(); let mut p = vec![]; for _ in 0..m { // when there are no constraints left, we store an empty one @@ -61,10 +77,10 @@ impl TryFrom> for R1CS { } Ok(R1CS { - p, - m, - n, - k, + constraints_vec: p, + m_num_constraints: m, + n_num_variables: n, + k_num_witness_vars: k, log_m, log_n, }) @@ -73,19 +89,22 @@ impl TryFrom> for R1CS { impl R1CS { // evaluate the given sparse linear combination over the provided z vector - fn eval_lc(lc: &[(F, usize)], z: &[F]) -> Result { + fn eval_lc(lc: &[(F, usize)], z: &[F]) -> Result { let mut acc = F::zero(); for (coeff, var) in lc.iter() { acc += *coeff * z.get(*var) - .ok_or(WARPError::R1CSWitnessSize(z.len(), *var))?; + .ok_or(WarpError::R1CSWitnessSize(z.len(), *var))?; } Ok(acc) } // eval the R1CS i-th linear combination, where i is represented as an hypercube point - pub fn eval_p_i(&self, z: &[F], i: usize) -> Result { - let (a_i, b_i, c_i) = self.p.get(i).ok_or(WARPError::R1CSNonExistingLC)?; + pub fn eval_p_i(&self, z: &[F], i: usize) -> Result { + let (a_i, b_i, c_i) = self + .constraints_vec + .get(i) + .ok_or(WarpError::R1CSNonExistingLC)?; let eval_a_i = Self::eval_lc(a_i, z)?; let eval_b_i = Self::eval_lc(b_i, z)?; let eval_c_i = Self::eval_lc(c_i, z)?; @@ -93,31 +112,69 @@ impl R1CS { } } -impl BundledPESAT for R1CS { +impl PolyPredicate for R1CS { type Config = (usize, usize, usize); - type Constraints = R1CSConstraints; - - fn evaluate_bundled(&self, zero_evader_evals: &[F], z: &[F]) -> Result { - // TODO: multithread this - Ascending::new(self.log_m).try_fold(F::ZERO, |acc, p| { - let index = p.index; - let eq_tau_i = *zero_evader_evals - .get(index) - .ok_or(WARPError::ZeroEvaderSize(zero_evader_evals.len(), index))?; - let p_i = self.eval_p_i(z, index)?; - Ok(acc + eq_tau_i * p_i) - }) + + fn evaluate_bundled(&self, zero_evader_evals: &[F], z: &[F]) -> Result { + if zero_evader_evals.len() < self.m_num_constraints { + return Err(WarpError::ZeroEvaderSize( + zero_evader_evals.len(), + self.m_num_constraints - 1, + )); + } + (0..self.m_num_constraints) + .into_par_iter() + .map(|i| -> Result { + let p_i = self.eval_p_i(z, i)?; + Ok(zero_evader_evals[i] * p_i) + }) + .try_reduce(|| F::ZERO, |acc, x| Ok(acc + x)) } fn config(&self) -> Self::Config { - (self.m, self.n, self.k) + ( + self.m_num_constraints, + self.n_num_variables, + self.k_num_witness_vars, + ) } fn description(&self) -> Vec { - todo!() + // Serializes the *concrete matrix triple* so two R1CS systems with + // identical (m, n, k) but different constraints absorb to different + // bytes (and therefore distinct transcripts). Without this, + // `WarpAccumulationScheme::index` would only commit to dimensions — a soundness hole + // in any downstream protocol that trusts `index()` to bind the + // relation. + let a: Vec> = self + .constraints_vec + .iter() + .map(|(a, _, _)| a.clone()) + .collect(); + let b: Vec> = self + .constraints_vec + .iter() + .map(|(_, b, _)| b.clone()) + .collect(); + let c: Vec> = self + .constraints_vec + .iter() + .map(|(_, _, c)| c.clone()) + .collect(); + let serializable = SerializableConstraintMatrices { + num_instance_variables: self.n_num_variables - self.k_num_witness_vars, + num_witness_variables: self.k_num_witness_vars, + num_constraints: self.m_num_constraints, + a: SerializableConstraintMatrices::serialize_nested_field(a), + b: SerializableConstraintMatrices::serialize_nested_field(b), + c: SerializableConstraintMatrices::serialize_nested_field(c), + }; + serde_json::to_string(&serializable) + .expect("matrix serialization is infallible") + .into_bytes() } - fn constraints(&self) -> &Self::Constraints { - &self.p + fn constraints(&self) -> &R1CSConstraints { + &self.constraints_vec } } diff --git a/src/serialize.rs b/src/serialize.rs index d9846ff..47365af 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -1,115 +1,116 @@ -use ark_crypto_primitives::merkle_tree::{Config, MerkleTree, Path}; -use ark_ff::{Field, PrimeField}; -use ark_serialize::CanonicalSerialize; +use ark_ff::Field; +use ark_serialize::{CanonicalSerialize, Compress, SerializationError, Valid, Write}; +use ark_vc::mvc::MultiVectorCommitment; -pub type AccWitnessTuple = (Vec>, Vec>, Vec>); +use crate::accumulation_scheme::{AccumulatorInstance, AccumulatorWitness, WarpProof}; -#[allow(clippy::type_complexity)] -pub type AccInstanceTuple = ( - Vec<::InnerDigest>, - Vec>, - Vec, - (Vec>, Vec>), - Vec, -); +// `AccumulatorInstance` and `WarpProof` carry generic associated types +// (`V::Commitment`) whose serializability isn't implied by the trait +// itself. Putting the bound in a separate `impl` block (rather than on +// the struct) keeps the bulk of the IOR / orchestrator code free of that +// dep — only the size-printing path pulls it in. -#[allow(clippy::type_complexity)] -pub type ProofTuple = ( - ::InnerDigest, - Vec, - F, - Vec, - Vec>, - Vec>>, - Vec>, -); +impl CanonicalSerialize for AccumulatorInstance +where + F: Field + CanonicalSerialize, + V: MultiVectorCommitment, + V::Commitment: CanonicalSerialize, +{ + fn serialize_with_mode( + &self, + mut writer: W, + compress: Compress, + ) -> Result<(), SerializationError> { + self.rt_commitments + .serialize_with_mode(&mut writer, compress)?; + self.alpha_fold_vectors + .serialize_with_mode(&mut writer, compress)?; + self.mu_claimed_evals + .serialize_with_mode(&mut writer, compress)?; + let taus: Vec<&Vec> = self.beta_twin_pairs.iter().map(|p| &p.tau).collect(); + let xs: Vec<&Vec> = self.beta_twin_pairs.iter().map(|p| &p.x).collect(); + taus.serialize_with_mode(&mut writer, compress)?; + xs.serialize_with_mode(&mut writer, compress)?; + self.eta_predicate_evals + .serialize_with_mode(&mut writer, compress)?; + Ok(()) + } -#[derive(CanonicalSerialize)] -pub struct AccWitnessSerializer< - F: Field + PrimeField, - MT: Config + From<[u8; 32]>>, -> { - pub rt: MT::InnerDigest, - pub f: Vec, - pub w: Vec, + fn serialized_size(&self, compress: Compress) -> usize { + let taus: Vec<&Vec> = self.beta_twin_pairs.iter().map(|p| &p.tau).collect(); + let xs: Vec<&Vec> = self.beta_twin_pairs.iter().map(|p| &p.x).collect(); + self.rt_commitments.serialized_size(compress) + + self.alpha_fold_vectors.serialized_size(compress) + + self.mu_claimed_evals.serialized_size(compress) + + taus.serialized_size(compress) + + xs.serialized_size(compress) + + self.eta_predicate_evals.serialized_size(compress) + } } -impl + From<[u8; 32]>>> - AccWitnessSerializer +impl Valid for AccumulatorInstance +where + F: Field + CanonicalSerialize, + V: MultiVectorCommitment, + V::Commitment: CanonicalSerialize, { - pub fn new(acc_witness: AccWitnessTuple) -> Self { - assert_eq!(acc_witness.0.len(), 1); - assert_eq!(acc_witness.1.len(), 1); - assert_eq!(acc_witness.2.len(), 1); - let f = acc_witness.1[0].clone(); - let w = acc_witness.2[0].clone(); - Self { - rt: acc_witness.0[0].root(), - f, - w, - } + fn check(&self) -> Result<(), SerializationError> { + Ok(()) } } -#[derive(CanonicalSerialize)] -pub struct AccInstanceSerializer< - F: Field + PrimeField, - MT: Config + From<[u8; 32]>>, -> { - pub rt: MT::InnerDigest, - pub alpha: Vec, - pub mu: F, - pub beta: (Vec, Vec), - pub eta: F, -} - -impl + From<[u8; 32]>>> - AccInstanceSerializer +impl CanonicalSerialize for WarpProof +where + F: Field + CanonicalSerialize, + V: MultiVectorCommitment, + V::Commitment: CanonicalSerialize, { - pub fn new(acc_instance: AccInstanceTuple) -> Self { - assert_eq!(acc_instance.0.len(), 1); - assert_eq!(acc_instance.1.len(), 1); - assert_eq!(acc_instance.2.len(), 1); - assert_eq!(acc_instance.3 .0.len(), 1); - assert_eq!(acc_instance.3 .1.len(), 1); - assert_eq!(acc_instance.4.len(), 1); - let beta = (acc_instance.3 .0[0].clone(), acc_instance.3 .1[0].clone()); - Self { - rt: acc_instance.0[0].clone(), - alpha: acc_instance.1[0].clone(), - mu: acc_instance.2[0], - beta, - eta: acc_instance.4[0], - } + fn serialize_with_mode( + &self, + mut writer: W, + compress: Compress, + ) -> Result<(), SerializationError> { + self.rt_0_fresh_commitment + .serialize_with_mode(&mut writer, compress)?; + self.mu_i_first_codeword_coords + .serialize_with_mode(&mut writer, compress)?; + self.nu_0_oracle_eval + .serialize_with_mode(&mut writer, compress)?; + self.nu_i_oracle_evals + .serialize_with_mode(&mut writer, compress)?; + self.shift_query_answers + .serialize_with_mode(&mut writer, compress)?; + Ok(()) } -} -#[derive(CanonicalSerialize)] -pub struct ProofSerializer< - F: Field + PrimeField, - MT: Config + From<[u8; 32]>>, -> { - pub rt_0: MT::InnerDigest, - pub mu_i: Vec, - pub nu_0: F, - pub nu_i: Vec, - pub auth_0: Vec>, - pub auth_j: Vec>>, - pub f_i_x_j: Vec>, + fn serialized_size(&self, compress: Compress) -> usize { + self.rt_0_fresh_commitment.serialized_size(compress) + + self.mu_i_first_codeword_coords.serialized_size(compress) + + self.nu_0_oracle_eval.serialized_size(compress) + + self.nu_i_oracle_evals.serialized_size(compress) + + self.shift_query_answers.serialized_size(compress) + } } -impl + From<[u8; 32]>>> - ProofSerializer +impl Valid for WarpProof +where + F: Field + CanonicalSerialize, + V: MultiVectorCommitment, + V::Commitment: CanonicalSerialize, { - pub fn new(proof: ProofTuple) -> Self { - Self { - rt_0: proof.0, - mu_i: proof.1, - nu_0: proof.2, - nu_i: proof.3, - auth_0: proof.4, - auth_j: proof.5, - f_i_x_j: proof.6, - } + fn check(&self) -> Result<(), SerializationError> { + Ok(()) } } + +/// `AccumulatorWitness` deliberately drops `td` (the full Merkle tree) +/// from the serialized form: only `w` ships across the wire — the tree +/// is reconstructable by re-encoding `w`. Used for proof-size reporting, +/// not on-the-wire serialization. +pub fn acc_witness_size(acc_witness: &AccumulatorWitness, compress: Compress) -> usize +where + F: Field + CanonicalSerialize, + V: MultiVectorCommitment, +{ + acc_witness.w_witnesses.serialized_size(compress) +} diff --git a/src/traits.rs b/src/traits.rs deleted file mode 100644 index 0bc6925..0000000 --- a/src/traits.rs +++ /dev/null @@ -1,55 +0,0 @@ -use ark_crypto_primitives::merkle_tree::Config; -use ark_ff::Field; -use spongefish::{ProverState, VerificationResult, VerifierState}; - -use crate::error::{ProverError, VerifierError, WARPError}; - -pub type WARPAccumResult = ( - ( - >::AccumulatorInstances, - >::AccumulatorWitnesses, - ), - >::Proof, -); - -pub trait AccumulationScheme { - type Index; - type ProverKey; - type VerifierKey; - type AccumulatorInstances; - type AccumulatorWitnesses; - type Instances; - type Witnesses; - type Proof; - - // on given index, returns prover and verifier keys - fn index( - prover_state: &mut ProverState, - index: Self::Index, - ) -> VerificationResult<(Self::ProverKey, Self::VerifierKey)>; - - // prove accumulation of instances and witnesses with previous accumulators `accs` - fn prove( - &self, - pk: Self::ProverKey, - prover_state: &mut ProverState, - witnesses: Self::Witnesses, - instances: Self::Instances, - acc_instances: Self::AccumulatorInstances, - acc_witnesses: Self::AccumulatorWitnesses, - ) -> Result, ProverError>; - - fn verify<'a>( - &self, - vk: Self::VerifierKey, - verifier_state: &mut VerifierState<'a>, - acc_instance: Self::AccumulatorInstances, - proof: Self::Proof, - ) -> Result<(), VerifierError>; - - fn decide( - &self, - acc_witness: Self::AccumulatorWitnesses, - acc_instance: Self::AccumulatorInstances, - ) -> Result<(), WARPError>; -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d134388..644cb1b 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,7 @@ use ark_ff::{Field, PrimeField}; pub mod fields; +pub mod poly; pub mod poseidon; pub const fn chunk_size_bytes(modulus_bit_size: u32) -> usize { @@ -11,30 +12,6 @@ pub fn chunk_size() -> usize { chunk_size_bytes(F::MODULUS_BIT_SIZE) } -pub fn bytes_to_vec_f(bytes: &[u8]) -> Vec { - bytes - .chunks(chunk_size::()) - .map(|chunk| F::from_le_bytes_mod_order(chunk)) - .collect() -} - -pub fn byte_to_binary_field_array(byte: &u8) -> Vec { - (0..8) - .map(|i| { - let val = (byte >> i) & 1 == 1; - // return in field element and in binary - F::from(val) - }) - .collect::>() -} - -pub fn binary_field_elements_to_usize(elements: &[F]) -> usize { - elements - .iter() - .rev() - .fold(0, |acc, &b| (acc << 1) | b.is_one() as usize) -} - pub fn concat_slices(a: &[F], b: &[F]) -> Vec { let mut v = Vec::::with_capacity(a.len() + b.len()); v.extend_from_slice(a); diff --git a/src/utils/poly.rs b/src/utils/poly.rs new file mode 100644 index 0000000..efefdde --- /dev/null +++ b/src/utils/poly.rs @@ -0,0 +1,25 @@ +use ark_ff::Field; + +/// `eq(τ, y)` where `y ∈ {0,1}^{τ.len()}` is the Boolean point whose bit `j` +/// is `(point >> j) & 1`. Matches the LSB-indexed formula used by +/// [`effsc::hypercube::compute_hypercube_eq_evals`], i.e. +/// `eq_poly(τ, i) == compute_hypercube_eq_evals(τ.len(), τ)[i]`. +pub fn eq_poly(tau: &[F], point: usize) -> F { + let num_variables = tau.len(); + (0..num_variables).fold(F::one(), |acc, j| { + let bit = (point >> j) & 1; + if bit == 1 { + acc * tau[j] + } else { + acc * (F::one() - tau[j]) + } + }) +} + +pub fn eq_poly_non_binary(x: &[F], y: &[F]) -> F { + assert_eq!(x.len(), y.len()); + let res = x.iter().zip(y).fold(F::one(), |acc, (x_i, y_i)| { + acc * (*x_i * *y_i + (F::one() - x_i) * (F::one() - y_i)) + }); + res +} diff --git a/tests/integration_warp.rs b/tests/integration_warp.rs new file mode 100644 index 0000000..9045e61 --- /dev/null +++ b/tests/integration_warp.rs @@ -0,0 +1,347 @@ +//! End-to-end integration tests for the full WarpAccumulationScheme prove / verify / decide +//! cycle. Previously lived in `src/lib.rs` as an inline `#[cfg(test)]` +//! module; moved here so `src/lib.rs` stays focused on the orchestrator. +//! +//! Two suites: +//! +//! - `warp_test` on BLS12-381 (≈254-bit multi-limb) +//! - `warp_test_goldilocks` on Goldilocks (64-bit SmallFp) +//! +//! The suites are deliberately duplicated rather than generic-over-`F`: +//! their associated-type bounds (Merkle config, poseidon config, etc.) +//! differ enough that a single generic function would need a long +//! `where` clause for marginal DRY gain. + +use ark_bls12_381::Fr as BLS12_381; +use ark_codes::{ + reed_solomon::{config::ReedSolomonConfig, ReedSolomon}, + traits::LinearCode, +}; +use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; +use ark_ff::UniformRand; +use ark_mt::{ + blake3::Blake3FieldHasher, hash_region::HashRegion, scheme::MerkleCommitment, + shape::PerfectBinary, +}; +use ark_serialize::{CanonicalSerialize, Compress}; +use ark_std::rand::thread_rng; +use ark_vc::{mvc::MultiVectorCommitment, vc::VectorCommitment}; + +use warp::accumulation_scheme::AccumulationScheme; +use warp::accumulation_scheme::{ + AccumulatorInstance, AccumulatorWitness, WarpProverKey, WarpVerifierKey, +}; +use warp::config::WarpConfig; +use warp::relations::{ + r1cs::{ + hashchain::{compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness}, + R1CS, + }, + Arithmetize, PolyPredicate, Relation, +}; +use warp::serialize::acc_witness_size; +use warp::utils::poseidon; +use warp::WarpAccumulationScheme; + +/// Concrete Merkle scheme used by the tests below. Plain perfect-binary +/// tree with a Blake3 field-symbol hasher — no caps, no multi-region. +type MerkleVc = MerkleCommitment>, PerfectBinary>; + +fn build_keys( + code_len: usize, + num_queries: usize, +) -> ( + as VectorCommitment>::CommitterKey, + as VectorCommitment>::VerifierKey, +) +where + F: ark_ff::PrimeField, + Blake3FieldHasher: Default + Clone + 'static, +{ + let mut rng = thread_rng(); + let pp = + as MultiVectorCommitment>::setup_multiple(0, code_len, num_queries, &mut rng) + .expect("setup_multiple"); + as MultiVectorCommitment>::trim_multiple(&pp, 0, code_len, num_queries) + .expect("trim_multiple") +} + +#[test] +fn warp_test() { + let l1 = 4; + let s = 8; + let t = 7; + let hash_chain_size = 10; + let mut rng = thread_rng(); + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::arithmetize(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + let code_config = ReedSolomonConfig::::default( + r1cs.k_num_witness_vars, + r1cs.k_num_witness_vars.next_power_of_two(), + ); + let code = ReedSolomon::new(code_config.clone()); + + let instances_witnesses: (Vec>, Vec>) = (0..l1) + .map(|_| { + let preimage = vec![BLS12_381::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness::>::new(preimage); + let relation = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (relation.x, relation.w) + }) + .unzip(); + + let r1cs = HashChainRelation::, CRHGadget<_>>::arithmetize(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + + let warp_config = WarpConfig::new(l1, 0, s, t, r1cs.config(), code.code_len()); + let (ck, vk) = build_keys::(code.code_len(), t); + let hash_chain_warp = WarpAccumulationScheme::< + BLS12_381, + R1CS, + _, + MerkleVc, + >::new(warp_config.clone(), code.clone(), r1cs.clone(), ck, vk); + + let mut acc_x = AccumulatorInstance::empty(); + let mut acc_w = AccumulatorWitness::empty(); + + for _ in 0..l1 { + let domainsep = spongefish::domain_separator!("test::warp"); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + let ((new_x, new_w), _pf) = hash_chain_warp + .prove( + WarpProverKey { + index: r1cs.clone(), + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + &mut prover_state, + instances_witnesses.1.clone(), + instances_witnesses.0.clone(), + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ) + .unwrap(); + acc_x = acc_x.extend(new_x); + acc_w = acc_w.extend(new_w); + } + + let domainsep = spongefish::domain_separator!("test::warp"); + let warp_config = + WarpConfig::<_, R1CS>::new(l1, 4, s, t, r1cs.config(), code.code_len()); + + let (ck, vk) = build_keys::(code.code_len(), t); + let hash_chain_warp = WarpAccumulationScheme::< + BLS12_381, + R1CS, + _, + MerkleVc, + >::new(warp_config.clone(), code.clone(), r1cs.clone(), ck, vk); + + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + let ((acc_x, acc_w), pf) = hash_chain_warp + .prove( + WarpProverKey { + index: r1cs.clone(), + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + &mut prover_state, + instances_witnesses.1, + instances_witnesses.0, + acc_x, + acc_w, + ) + .unwrap(); + + let narg_str = prover_state.narg_string().to_vec(); + let domainsep_v = spongefish::domain_separator!("test::warp"); + let mut verifier_state = domainsep_v + .without_session() + .instance(&0u32) + .std_verifier(&narg_str); + hash_chain_warp + .verify( + WarpVerifierKey { + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + &mut verifier_state, + acc_x.clone(), + pf.clone(), + ) + .unwrap(); + hash_chain_warp.decide(&acc_x, &acc_w).unwrap(); + + println!("acc_x size: {}", acc_x.serialized_size(Compress::Yes)); + println!("acc_w size: {}", acc_witness_size(&acc_w, Compress::Yes)); + println!("proof size: {}", pf.serialized_size(Compress::Yes)); + println!("narg_str size: {}", narg_str.len()); +} + +#[test] +fn warp_test_goldilocks() { + use warp::utils::fields::Goldilocks; + + let l1 = 4; + let s = 8; + let t = 7; + let hash_chain_size = 10; + let mut rng = thread_rng(); + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::arithmetize(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + let code_config = ReedSolomonConfig::::default( + r1cs.k_num_witness_vars, + r1cs.k_num_witness_vars.next_power_of_two(), + ); + let code = ReedSolomon::new(code_config); + + let instances_witnesses: (Vec>, Vec>) = (0..l1) + .map(|_| { + let preimage = vec![Goldilocks::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness::>::new(preimage); + let relation = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (relation.x, relation.w) + }) + .unzip(); + + let r1cs = HashChainRelation::, CRHGadget<_>>::arithmetize(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + + let warp_config = WarpConfig::new(l1, 0, s, t, r1cs.config(), code.code_len()); + let (ck, vk) = build_keys::(code.code_len(), t); + let hash_chain_warp = WarpAccumulationScheme::< + Goldilocks, + R1CS, + _, + MerkleVc, + >::new(warp_config.clone(), code.clone(), r1cs.clone(), ck, vk); + + let mut acc_x = AccumulatorInstance::empty(); + let mut acc_w = AccumulatorWitness::empty(); + + for _ in 0..l1 { + let domainsep = spongefish::domain_separator!("test::warp"); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + let ((new_x, new_w), _pf) = hash_chain_warp + .prove( + WarpProverKey { + index: r1cs.clone(), + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + &mut prover_state, + instances_witnesses.1.clone(), + instances_witnesses.0.clone(), + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ) + .unwrap(); + acc_x = acc_x.extend(new_x); + acc_w = acc_w.extend(new_w); + } + + let domainsep = spongefish::domain_separator!("test::warp"); + // Use 8 (2*l1) for the total accumulation size to test multi-instance accumulation + let warp_config = + WarpConfig::<_, R1CS>::new(l1, 4, s, t, r1cs.config(), code.code_len()); + + let (ck, vk) = build_keys::(code.code_len(), t); + let hash_chain_warp = WarpAccumulationScheme::< + Goldilocks, + R1CS, + _, + MerkleVc, + >::new(warp_config.clone(), code.clone(), r1cs.clone(), ck, vk); + + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + let ((acc_x, acc_w), pf) = hash_chain_warp + .prove( + WarpProverKey { + index: r1cs.clone(), + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + &mut prover_state, + instances_witnesses.1, + instances_witnesses.0, + acc_x, + acc_w, + ) + .unwrap(); + + let narg_str = prover_state.narg_string().to_vec(); + let domainsep_v = spongefish::domain_separator!("test::warp"); + let mut verifier_state = domainsep_v + .without_session() + .instance(&0u32) + .std_verifier(&narg_str); + hash_chain_warp + .verify( + WarpVerifierKey { + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + &mut verifier_state, + acc_x.clone(), + pf.clone(), + ) + .unwrap(); + hash_chain_warp.decide(&acc_x, &acc_w).unwrap(); + + println!( + "Goldilocks acc_x size: {}", + acc_x.serialized_size(Compress::Yes) + ); + println!( + "Goldilocks acc_w size: {}", + acc_witness_size(&acc_w, Compress::Yes) + ); + println!( + "Goldilocks proof size: {}", + pf.serialized_size(Compress::Yes) + ); + println!("Goldilocks narg_str size: {}", narg_str.len()); +} diff --git a/tests/profile_json.rs b/tests/profile_json.rs new file mode 100644 index 0000000..f24acdb --- /dev/null +++ b/tests/profile_json.rs @@ -0,0 +1,182 @@ +//! End-to-end check that Plan O's JSON layer emits well-formed +//! `warp.profile.v1` records with phase names, dimensions, and non-zero +//! op counters. +//! +//! Runs only under `--features profile` (see the `cfg` below). Without +//! the feature there's no JSON layer to test. + +#![cfg(feature = "profile")] + +use std::io::{self, Write}; +use std::sync::{Arc, Mutex}; + +use ark_bls12_381::Fr as BLS12_381; +use ark_codes::{ + reed_solomon::{config::ReedSolomonConfig, ReedSolomon}, + traits::LinearCode, +}; +use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; +use ark_mt::blake3::Blake3FieldHasher; +use ark_std::rand::thread_rng; +use ark_std::UniformRand; +use warp::accumulation_scheme::{AccumulatorInstance, AccumulatorWitness, WarpProverKey}; +use warp::config::WarpConfig; +use warp::relations::{ + r1cs::{ + hashchain::{compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness}, + R1CS, + }, + Arithmetize, PolyPredicate, Relation, +}; +use warp::utils::poseidon; +use warp::WarpAccumulationScheme; + +/// `Arc>>` wrapped so it implements `io::Write`. +#[derive(Clone)] +struct SharedBuf(Arc>>); + +impl Write for SharedBuf { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.lock().unwrap().extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +#[test] +fn json_layer_emits_phase_records() { + let sink = SharedBuf(Arc::new(Mutex::new(Vec::new()))); + let installed = warp::profile::init_json(sink.clone()); + assert!( + installed, + "json subscriber install should succeed on first call" + ); + + // Minimum viable prove run — same shape as the top-level warp_test. + let l1 = 4; + let s = 8; + let t = 7; + let hash_chain_size = 10; + let mut rng = thread_rng(); + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::arithmetize(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + let code_config = ReedSolomonConfig::::default( + r1cs.k_num_witness_vars, + r1cs.k_num_witness_vars.next_power_of_two(), + ); + let code = ReedSolomon::new(code_config.clone()); + + let (instances, witnesses): (Vec<_>, Vec<_>) = (0..l1) + .map(|_| { + let preimage = vec![BLS12_381::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness::>::new(preimage); + let relation = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (relation.x, relation.w) + }) + .unzip(); + + let warp_config = WarpConfig::new(l1, 0, s, t, r1cs.config(), code.code_len()); + let hash_chain_warp = + WarpAccumulationScheme::, _, Blake3FieldHasher>::new( + warp_config, + code, + r1cs.clone(), + Blake3FieldHasher::::new(), + ); + + let domainsep = spongefish::domain_separator!("test::profile_json"); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + + hash_chain_warp + .prove( + WarpProverKey { + index: r1cs.clone(), + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + &mut prover_state, + witnesses, + instances, + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ) + .unwrap(); + + // Inspect collected records. + let bytes = sink.0.lock().unwrap().clone(); + let text = String::from_utf8(bytes).expect("JSON output is UTF-8"); + assert!( + !text.is_empty(), + "JSON sink should contain at least one record" + ); + + let lines: Vec<&str> = text.lines().collect(); + assert!( + !lines.is_empty(), + "expected newline-delimited JSON, got: {text:?}" + ); + + // Every line is a record carrying the schema tag. + for (i, line) in lines.iter().enumerate() { + assert!( + line.contains(r#""schema":"warp.profile.v1""#), + "line {i} missing schema: {line}" + ); + assert!( + line.contains(r#""wall_ns""#), + "line {i} missing wall_ns: {line}" + ); + assert!( + line.contains(r#""counters""#), + "line {i} missing counters: {line}" + ); + assert!( + line.contains(r#""dimensions""#), + "line {i} missing dimensions: {line}" + ); + } + + // Every top-level phase must appear at least once. + for phase in [ + "warp.prove", + "pesat", + "twin_constraint", + "ood", + "batching", + "proximity", + ] { + let needle = format!(r#""phase":"{phase}""#); + assert!( + text.contains(&needle), + "expected a record for phase `{phase}`, got lines: {lines:#?}" + ); + } + + // At least one record must have non-empty counters (pesat bumps several). + assert!( + text.contains(r#""merkle_tree_builds":"#), + "expected merkle_tree_builds counter in output: {text}" + ); + assert!( + text.contains(r#""encode_calls":"#), + "expected encode_calls counter in output: {text}" + ); +} diff --git a/tests/snapshot_fs.rs b/tests/snapshot_fs.rs new file mode 100644 index 0000000..0bc3358 --- /dev/null +++ b/tests/snapshot_fs.rs @@ -0,0 +1,150 @@ +//! Fiat-Shamir snapshot test — regression oracle for the ark-iop extraction. +//! +//! Captures byte-identical hashes of `narg_string` (transcript) and the +//! serialized proof for a fixed-seed warp run. Any change to the transcript +//! semantics (encoding order, byte layout, missing absorb, etc.) breaks this +//! test before it corrupts a real prover. + +use ark_codes::{ + reed_solomon::{config::ReedSolomonConfig, ReedSolomon}, + traits::LinearCode, +}; +use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; +use ark_ff::UniformRand; +use ark_mt::{ + blake3::Blake3FieldHasher, hash_region::HashRegion, scheme::MerkleCommitment, + shape::PerfectBinary, +}; +use ark_serialize::{CanonicalSerialize, Compress}; +use ark_std::rand::{rngs::StdRng, SeedableRng}; +use ark_vc::mvc::MultiVectorCommitment; + +use warp::accumulation_scheme::{AccumulatorInstance, AccumulatorWitness, WarpProverKey}; +use warp::config::WarpConfig; +use warp::relations::{ + r1cs::{ + hashchain::{compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness}, + R1CS, + }, + Arithmetize, PolyPredicate, Relation, +}; +use warp::utils::{fields::Goldilocks, poseidon}; +use warp::WarpAccumulationScheme; + +type F = Goldilocks; +type VC = MerkleCommitment>, PerfectBinary>; + +fn hex_hash(bytes: &[u8]) -> String { + blake3::hash(bytes).to_hex().to_string() +} + +/// After capturing initial values, set these to `Some(...)`. Until then the +/// test prints the observed values and skips assertion — useful for the +/// first run after upgrading dependencies. +// Bumped on AccumulationScheme prologue replacing IOP prologue at the +// orchestrator level. Adds `AS:WarpAccumulationScheme-AccScheme|` prefix + the inner IOP +// NAME (`WarpAccumulationScheme|`) before the IOR list — 32 extra bytes vs the prior +// IOP-only prologue. +const EXPECTED_NARG_HASH: Option<&str> = + Some("83e5f65136d0ab5571a4f4c49e254a6bc178a264def7fba256cbdbd444b0f80d"); +const EXPECTED_NARG_LEN: Option = Some(1616); +const EXPECTED_PROOF_HASH: Option<&str> = + Some("68aec437a44825f513f3b5bfa55049e2c466b533bb0aede65dc63c6f76b7ec9c"); +const EXPECTED_PROOF_LEN: Option = Some(224); + +#[test] +fn fs_transcript_snapshot_goldilocks() { + let mut rng = StdRng::seed_from_u64(0xFA1A_F542); + // Compact but representative: exercises all 5 IORs at least once. + let l1 = 2; + let s = 2; + let t = 4; + let hash_chain_size = 4; + + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::arithmetize(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + + let code = ReedSolomon::new(ReedSolomonConfig::::default( + r1cs.k_num_witness_vars, + r1cs.k_num_witness_vars.next_power_of_two(), + )); + + let instances_witnesses: (Vec>, Vec>) = (0..l1) + .map(|_| { + let preimage = vec![F::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness::>::new(preimage); + let rel = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (rel.x, rel.w) + }) + .unzip(); + + let warp_config = WarpConfig::new(l1, 0, s, t, r1cs.config(), code.code_len()); + let pp = ::setup_multiple(0, code.code_len(), t, &mut rng) + .expect("setup_multiple"); + let (ck, vk) = ::trim_multiple(&pp, 0, code.code_len(), t) + .expect("trim_multiple"); + let warp_inst = + WarpAccumulationScheme::, _, VC>::new(warp_config, code, r1cs.clone(), ck, vk); + + let domainsep = spongefish::domain_separator!("test::snapshot"); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + let (_acc, pf) = warp_inst + .prove( + WarpProverKey { + index: r1cs.clone(), + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + &mut prover_state, + instances_witnesses.1, + instances_witnesses.0, + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ) + .unwrap(); + + let narg = prover_state.narg_string().to_vec(); + let mut pf_bytes = Vec::new(); + pf.serialize_with_mode(&mut pf_bytes, Compress::Yes) + .unwrap(); + + let narg_hash = hex_hash(&narg); + let pf_hash = hex_hash(&pf_bytes); + + println!( + "fs_snapshot:\n narg_len = {}\n narg_hash = {}\n proof_len = {}\n proof_hash = {}", + narg.len(), + narg_hash, + pf_bytes.len(), + pf_hash, + ); + + if let Some(expected) = EXPECTED_NARG_LEN { + assert_eq!(narg.len(), expected, "narg_string length changed"); + } + if let Some(expected) = EXPECTED_NARG_HASH { + assert_eq!(narg_hash, expected, "Fiat-Shamir transcript drift detected"); + } + if let Some(expected) = EXPECTED_PROOF_LEN { + assert_eq!(pf_bytes.len(), expected, "proof serialized length changed"); + } + if let Some(expected) = EXPECTED_PROOF_HASH { + assert_eq!(pf_hash, expected, "proof bytes changed"); + } +} diff --git a/tests/verifier_negative.rs b/tests/verifier_negative.rs new file mode 100644 index 0000000..6864497 --- /dev/null +++ b/tests/verifier_negative.rs @@ -0,0 +1,371 @@ +//! Negative-path verifier tests. +//! +//! The existing `warp_test` exercises only the happy path. This file +//! covers the other direction: for each cleanly-triggerable +//! [`warp::error::VerifierError`] variant, produce a valid proof, tamper +//! with one field, and assert the verifier rejects the proof with the +//! *specific* expected error. +//! +//! Catches a class of bug that's otherwise invisible: "verifier looks +//! correct on valid proofs but accepts broken ones." Several real-world +//! SNARKs have shipped that way. +//! +//! Not every error variant is reachable through a one-field tamper. +//! Variants we don't cover here: +//! +//! - `SpongeFish` / `ArkError` wrap underlying errors; hitting them +//! requires transcript-byte-level corruption rather than proof-object +//! tampering, which would exercise spongefish/arkworks, not our code. +//! - `NumSumcheckRounds` is derived from the transcript; a sibling of +//! `SpongeFish`. +//! - `SumcheckRound` is not raised from any code path right now. + +use ark_bls12_381::Fr as BLS12_381; +use ark_codes::{ + reed_solomon::{config::ReedSolomonConfig, ReedSolomon}, + traits::LinearCode, +}; +use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; +use ark_mt::{ + blake3::Blake3FieldHasher, hash_region::HashRegion, scheme::MerkleCommitment, + shape::PerfectBinary, +}; +use ark_std::rand::thread_rng; +use ark_std::UniformRand; +use ark_vc::{mvc::MultiVectorCommitment, vc::VectorCommitment}; + +use warp::accumulation_scheme::{ + AccumulatorInstance, AccumulatorWitness, WarpProof, WarpProverKey, WarpVerifierKey, +}; +use warp::config::WarpConfig; +use warp::error::VerifierError; +use warp::relations::{ + r1cs::{ + hashchain::{compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness}, + R1CS, + }, + Arithmetize, PolyPredicate, Relation, +}; +use warp::utils::poseidon; +use warp::WarpAccumulationScheme; + +type F = BLS12_381; +type V = MerkleCommitment>, PerfectBinary>; +type WarpT = WarpAccumulationScheme, ReedSolomon, V>; + +fn build_keys( + code_len: usize, + num_queries: usize, +) -> ( + ::CommitterKey, + ::VerifierKey, +) { + let mut rng = thread_rng(); + let pp = ::setup_multiple(0, code_len, num_queries, &mut rng) + .expect("setup_multiple"); + ::trim_multiple(&pp, 0, code_len, num_queries) + .expect("trim_multiple") +} + +/// Everything the verifier needs to re-check, plus enough dimensions +/// to re-derive the verifier state. +struct Fixture { + warp: WarpT, + vk: WarpVerifierKey, + acc_x: AccumulatorInstance, + proof: WarpProof, + narg_str: Vec, +} + +impl Fixture { + fn verify( + &self, + acc_x: AccumulatorInstance, + proof: WarpProof, + ) -> Result<(), VerifierError> { + let domainsep_v = spongefish::domain_separator!("test::warp::negative"); + let mut verifier_state = domainsep_v + .without_session() + .instance(&0u32) + .std_verifier(&self.narg_str); + self.warp.verify(self.vk, &mut verifier_state, acc_x, proof) + } +} + +/// Build a real proof against the hash-chain relation, with both fresh +/// and accumulated components so negative tests can target any field. +fn make_fixture() -> Fixture { + let l1 = 4; + let s = 8; + let t = 7; + let hash_chain_size = 10; + let mut rng = thread_rng(); + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::arithmetize(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + let code_config = ReedSolomonConfig::::default( + r1cs.k_num_witness_vars, + r1cs.k_num_witness_vars.next_power_of_two(), + ); + let code = ReedSolomon::new(code_config); + + let (instances, witnesses): (Vec<_>, Vec<_>) = (0..l1) + .map(|_| { + let preimage = vec![F::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness::>::new(preimage); + let relation = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (relation.x, relation.w) + }) + .unzip(); + + // Phase 1: produce `l1` single-round acc states so we have a non-trivial + // accumulator to feed phase 2 (l2 > 0 so NumL2Instances is reachable). + let warp_cfg1 = WarpConfig::new(l1, 0, s, t, r1cs.config(), code.code_len()); + let (ck1, vk1) = build_keys(code.code_len(), t); + let w1 = WarpAccumulationScheme::, _, V>::new( + warp_cfg1, + code.clone(), + r1cs.clone(), + ck1, + vk1, + ); + + let mut acc_x = AccumulatorInstance::empty(); + let mut acc_w = AccumulatorWitness::empty(); + + for _ in 0..l1 { + let ds = spongefish::domain_separator!("test::warp::negative"); + let mut ps = ds.without_session().instance(&0u32).std_prover(); + let ((new_x, new_w), _) = w1 + .prove( + WarpProverKey { + index: r1cs.clone(), + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + &mut ps, + witnesses.clone(), + instances.clone(), + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ) + .unwrap(); + acc_x = acc_x.extend(new_x); + acc_w = acc_w.extend(new_w); + } + + // Phase 2: the "real" prove with l2 > 0 accumulated instances. + let warp_cfg2 = WarpConfig::<_, R1CS>::new(l1, 4, s, t, r1cs.config(), code.code_len()); + let (ck2, vk2) = build_keys(code.code_len(), t); + let warp = + WarpAccumulationScheme::, _, V>::new(warp_cfg2, code, r1cs.clone(), ck2, vk2); + + let ds = spongefish::domain_separator!("test::warp::negative"); + let mut ps = ds.without_session().instance(&0u32).std_prover(); + let ((acc_x, _acc_w), proof) = warp + .prove( + WarpProverKey { + index: r1cs.clone(), + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + &mut ps, + witnesses, + instances, + acc_x, + acc_w, + ) + .unwrap(); + + Fixture { + warp, + vk: WarpVerifierKey { + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + acc_x, + proof, + narg_str: ps.narg_string().to_vec(), + } +} + +fn assert_err(result: Result<(), VerifierError>, expected: &str) { + match result { + Ok(()) => panic!("expected `{expected}`, got Ok(())"), + Err(err) => { + let dbg = format!("{err:?}"); + assert!(dbg.contains(expected), "expected `{expected}`, got `{dbg}`"); + } + } +} + +// Sanity check: a fresh fixture always verifies. +#[test] +fn happy_path_verifies() { + let fix = make_fixture(); + fix.verify(fix.acc_x.clone(), fix.proof.clone()) + .expect("untampered proof must verify"); +} + +#[test] +fn tampered_alpha_raises_code_evaluation_point() { + let fix = make_fixture(); + let mut acc_x = fix.acc_x.clone(); + acc_x.alpha_fold_vectors[0][0] += F::from(1u64); + assert_err(fix.verify(acc_x, fix.proof.clone()), "CodeEvaluationPoint"); +} + +#[test] +fn tampered_beta_tau_raises_circuit_evaluation_point() { + let fix = make_fixture(); + let mut acc_x = fix.acc_x.clone(); + acc_x.beta_twin_pairs[0].tau[0] += F::from(1u64); + assert_err( + fix.verify(acc_x, fix.proof.clone()), + "CircuitEvaluationPoint", + ); +} + +#[test] +fn tampered_beta_x_raises_circuit_evaluation_point() { + let fix = make_fixture(); + let mut acc_x = fix.acc_x.clone(); + acc_x.beta_twin_pairs[0].x[0] += F::from(1u64); + assert_err( + fix.verify(acc_x, fix.proof.clone()), + "CircuitEvaluationPoint", + ); +} + +#[test] +fn truncated_shift_query_answers_raises_num_shift_queries() { + let fix = make_fixture(); + let mut proof = fix.proof.clone(); + proof.shift_query_answers.pop(); + assert_err(fix.verify(fix.acc_x.clone(), proof), "NumShiftQueries"); +} + +#[test] +fn tampered_shift_query_answer_raises_target() { + let fix = make_fixture(); + let mut proof = fix.proof.clone(); + // Tampering a `shift_query_answer` is caught at two distinct points: + // (a) Batching's sumcheck consumes the answer to compute `nu_i` and + // its final-claim check fails (`Target`), and (b) the trait's + // `check_multiple` would also fail because the leaf no longer + // hashes to the committed root (`ShiftQuery`). With the verify + // ordering Batching → check_multiple, (a) fires first. + proof.shift_query_answers[0][0] += F::from(1u64); + assert_err(fix.verify(fix.acc_x.clone(), proof), "Target"); +} + +// `truncated_auth_j_raises_num_l2_instances` removed: opening proofs now +// live in the spongefish transcript (written by `V::open_multiple`), so +// there are no longer separate `auth_j` fields on `WarpProof` to truncate. +// Equivalent coverage would require transcript-byte tampering, which is a +// spongefish-layer concern, not warp's. + +#[test] +fn tampered_mu_raises_target() { + let fix = make_fixture(); + let mut acc_x = fix.acc_x.clone(); + acc_x.mu_claimed_evals[0] += F::from(1u64); + assert_err(fix.verify(acc_x, fix.proof.clone()), "Target"); +} + +#[test] +fn prove_rejects_mismatched_instance_witness_lengths() { + use warp::error::ProverError; + + let l1 = 4; + let s = 8; + let t = 7; + let hash_chain_size = 4; + let mut rng = thread_rng(); + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::arithmetize(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + let code_config = ReedSolomonConfig::::default( + r1cs.k_num_witness_vars, + r1cs.k_num_witness_vars.next_power_of_two(), + ); + let code = ReedSolomon::new(code_config); + + let (instances, witnesses): (Vec<_>, Vec<_>) = (0..l1) + .map(|_| { + let preimage = vec![F::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness::>::new(preimage); + let relation = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (relation.x, relation.w) + }) + .unzip(); + + let warp_cfg = WarpConfig::new(l1, 0, s, t, r1cs.config(), code.code_len()); + let (ck, vk) = build_keys(code.code_len(), t); + let warp = + WarpAccumulationScheme::, _, V>::new(warp_cfg, code, r1cs.clone(), ck, vk); + + let mut witnesses_short = witnesses; + witnesses_short.pop(); + + let ds = spongefish::domain_separator!("test::warp::prove_negative"); + let mut ps = ds.without_session().instance(&0u32).std_prover(); + let result = warp.prove( + WarpProverKey { + index: r1cs.clone(), + m_num_constraints: r1cs.m_num_constraints, + n_num_variables: r1cs.n_num_variables, + k_num_witness_vars: r1cs.k_num_witness_vars, + }, + &mut ps, + witnesses_short, + instances, + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ); + + let err = result + .err() + .expect("prove must reject mismatched instance/witness lengths"); + match err { + ProverError::InstanceWitnessLengthMismatch { + instances, + witnesses, + } => { + assert_eq!(instances, l1); + assert_eq!(witnesses, l1 - 1); + } + other => panic!("expected InstanceWitnessLengthMismatch, got {other:?}"), + } +}