From 0727ee01e2af3953937b5c9c66d218b74180f809 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Tue, 22 Sep 2026 14:48:25 +0100 Subject: [PATCH 1/4] Reworked honest proof test to exclude large shape. --- crates/tests/src/lib.rs | 59 +++++++++++++++++++++++++-------- crates/tests/tests/large.rs | 66 +++++++++++++++++++++++++++++++++++++ crates/tests/tests/prove.rs | 8 ++--- 3 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 crates/tests/tests/large.rs diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 81f51aeb..3de5aa6c 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -33,19 +33,16 @@ pub fn packed_witness(shape: Shape, rng: &mut impl Rng) -> Vec { .collect() } -/// An instance whose claim actually holds, committed under a real scheme. -pub struct Instance { +/// A claim that actually holds, with the witness it is about, before any +/// commitment. What the fold round needs and nothing the opening does. +#[derive(Debug, Clone)] +pub struct HonestClaim { pub params: BitZParams, - pub prover: prover::BitZProver, - pub verifier: verifier::BitZVerifier, pub claim: LinearClaim>, - pub pcs: Pcs, - pub com: Root, - pub data: ProverData, pub packed: Vec, } -impl Instance { +impl HonestClaim { /// Builds a random witness and the target its own fold produces, so the /// claim is true by construction rather than by asserting the code agrees /// with itself. @@ -53,16 +50,15 @@ impl Instance { /// `mu` comes straight from the definition — `sum_j v^(2)_j pi_q(eta_j)` /// with `eta_j` read bit by bit — not from the reconstruction the verifier /// runs. - pub fn honest(shape: Shape, seed: u64) -> Self { - let mut rng = ChaCha8Rng::seed_from_u64(seed); + pub fn new(shape: Shape, rng: &mut impl Rng) -> Self { let params = BitZParams::::new(shape, smallest_generator()).unwrap(); - let packed = packed_witness(shape, &mut rng); + let packed = packed_witness(shape, rng); let row_weights: Vec> = (0..shape.rows()) - .map(|_| Fq::from(sample_below_q(&mut rng))) + .map(|_| Fq::from(sample_below_q(rng))) .collect(); let column_weights: Vec> = (0..shape.columns()) - .map(|_| Fq::from(sample_below_q(&mut rng))) + .map(|_| Fq::from(sample_below_q(rng))) .collect(); let table = params.table(&packed).unwrap(); @@ -79,6 +75,40 @@ impl Instance { let claim = LinearClaim::new(¶ms, row_weights, column_weights, target).unwrap(); + Self { + params, + claim, + packed, + } + } + + pub fn table(&self) -> BitTable<'_> { + self.params.table(&self.packed).unwrap() + } +} + +/// An instance whose claim actually holds, committed under a real scheme. +pub struct Instance { + pub params: BitZParams, + pub prover: prover::BitZProver, + pub verifier: verifier::BitZVerifier, + pub claim: LinearClaim>, + pub pcs: Pcs, + pub com: Root, + pub data: ProverData, + pub packed: Vec, +} + +impl Instance { + /// [`HonestClaim::new`] under the `Fast` profile, with a setup per role. + pub fn honest(shape: Shape, seed: u64) -> Self { + let mut rng = ChaCha8Rng::seed_from_u64(seed); + let HonestClaim { + params, + claim, + packed, + } = HonestClaim::new(shape, &mut rng); + let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let (com, data) = pcs.commit(&packed).unwrap(); @@ -112,7 +142,7 @@ impl Instance { /// A uniform integer in `[0, q)`, rejection sampled so the weights are not /// biased toward the low end of the range. -fn sample_below_q(rng: &mut ChaCha8Rng) -> u128 { +fn sample_below_q(rng: &mut impl Rng) -> u128 { loop { let candidate = (u128::from(rng.next_u64()) << 64 | u128::from(rng.next_u64())) & ((1u128 << 100) - 1); @@ -135,6 +165,7 @@ pub fn wide_shape() -> Shape { /// `m = 28` at the reference split, `(t, s) = (17, 11)`: 131072 rows over /// 2048 columns, a 32 MiB witness. Every other fixture sits at the floor, so /// this is the only one whose cost scales the way a real instance does. +/// It's too big to go through the opening in CI-enabled test - see `tests/large.rs`. pub fn large_shape() -> Shape { reference_shape(28) } diff --git a/crates/tests/tests/large.rs b/crates/tests/tests/large.rs new file mode 100644 index 00000000..0f94b66c --- /dev/null +++ b/crates/tests/tests/large.rs @@ -0,0 +1,66 @@ +//! Tests for the large fixture, that does NOT do the opening. +//! +//! At `m = 28` the round trip is out of reach for a test. The reduction +//! materializes one field element per committed bit and every product-tree +//! layer above them, some 10 GiB, and an unoptimised build needs over an hour +//! for it. +//! +//! This test is designed to test what's possible in under a minute. + +use common::LinearClaim; +use field::Fq; +use num_traits::ConstOne; +use prover::BitZProver; +use rand_chacha::ChaCha8Rng; +use rand_core::SeedableRng; +use tests::{HonestClaim, WINDOW, large_shape, prover_transcript, verifier_transcript}; +use verifier::{BitZVerifier, ReceiveError}; + +#[test] +fn the_fold_round_trips_on_the_large_shape() { + let shape = large_shape(); + let honest = HonestClaim::new(shape, &mut ChaCha8Rng::seed_from_u64(31)); + let prover = BitZProver::new(honest.params, WINDOW); + let verifier = BitZVerifier::new(honest.params, WINDOW); + + let mut transcript = prover_transcript(); + let sent = prover + .send_fold(&honest.claim, &honest.table(), &mut transcript) + .unwrap(); + let proof = transcript.finish(); + assert_eq!(proof.narg_string.len(), 16 * shape.columns()); + assert!(proof.hints.is_empty()); + + let mut transcript = verifier_transcript(&proof); + let received = verifier + .receive_fold(&honest.claim, &mut transcript) + .expect("honest proof"); + assert_eq!(sent, received); + assert_eq!(received.row_images.len(), shape.rows()); + assert_eq!(received.zeta.len(), shape.log_columns()); + transcript.check_eof().expect("both streams exhausted"); + + // `k_1 (q - 1)` is 117 bits wide here; one past it is still refused. + let mut transcript = prover_transcript(); + for _ in 0..shape.columns() { + transcript.prover_message(&(verifier.fold_bound() + 1).to_le_bytes()); + } + let over = transcript.finish(); + assert_eq!( + verifier.receive_fold(&honest.claim, &mut verifier_transcript(&over)), + Err(ReceiveError::FoldOutOfRange) + ); + + // The honest folds against a claim off by one. + let retargeted = LinearClaim::new( + &honest.params, + honest.claim.row_weights().to_vec(), + honest.claim.column_weights().to_vec(), + honest.claim.target() + Fq::ONE, + ) + .unwrap(); + assert_eq!( + verifier.receive_fold(&retargeted, &mut verifier_transcript(&proof)), + Err(ReceiveError::TargetMismatch) + ); +} diff --git a/crates/tests/tests/prove.rs b/crates/tests/tests/prove.rs index d464853c..3c87650f 100644 --- a/crates/tests/tests/prove.rs +++ b/crates/tests/tests/prove.rs @@ -5,9 +5,7 @@ use field::{F128, Fq}; use num_traits::{ConstOne, ConstZero}; use pcs::{HashKind, LigeritoProfile, Pcs, VerifyError as PcsVerifyError}; use prover::ProveError; -use tests::{ - Instance, large_shape, narrow_shape, prover_transcript, verifier_transcript, wide_shape, -}; +use tests::{Instance, narrow_shape, prover_transcript, verifier_transcript, wide_shape}; use transcript::Proof; use verifier::{ReceiveError, VerifyError}; @@ -27,8 +25,8 @@ fn prove(instance: &Instance) -> Proof { } #[test] -fn an_honest_proof_verifies_on_every_shape_the_profile_admits() { - for shape in [narrow_shape(), wide_shape(), large_shape()] { +fn an_honest_proof_verifies_on_both_floor_shapes() { + for shape in [narrow_shape(), wide_shape()] { let instance = Instance::honest(shape, 31); let proof = prove(&instance); From de27c0e5c14c5e25507b28f29f45b66056750452 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Tue, 22 Sep 2026 16:15:52 +0100 Subject: [PATCH 2/4] Force-enable `aes` feature of `aarch64` targets --- .cargo/config.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.cargo/config.toml b/.cargo/config.toml index 3049a7b1..6516aaf6 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,6 @@ # Frame pointers needed for samply/Instruments to get accurate/deep call stacks on macOS. [build] rustflags = ["-C", "force-frame-pointers=yes"] + +[target.aarch64-unknown-linux-gnu] +rustflags = ["-C", "force-frame-pointers=yes", "-C", "target-feature=+aes"] From 940ea9985a0761e53cce4ff9ae471271e4461c9c Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Tue, 22 Sep 2026 20:19:29 +0100 Subject: [PATCH 3/4] Add `CARGO_PROFILE_TEST_OPT_LEVEL: 3` to CI builds --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0aa915a9..2b2f60d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: env: CARGO_TERM_COLOR: always RUSTFLAGS: -D warnings + CARGO_PROFILE_TEST_OPT_LEVEL: 3 jobs: fmt: From db05e1ab045dfc05dbcddcd5866b6d15cdf23c06 Mon Sep 17 00:00:00 2001 From: Alexander Abdugafarov Date: Tue, 22 Sep 2026 20:39:37 +0100 Subject: [PATCH 4/4] Add `opt-level = 3` to Keccak related crates --- Cargo.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 3fd55899..7ff204d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,3 +62,10 @@ debug = true split-debuginfo = "packed" lto = false codegen-units = 16 + +# Tests run the dev profile. +# The Fiat–Shamir sponge is pure-Rust Keccak, which is ~20x slower unoptimized. +[profile.dev] +package.keccak.opt-level = 3 +package.sha3.opt-level = 3 +package.spongefish.opt-level = 3