From bbde34aa83a80e13f0e7aa086402a1370f2ebe38 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Thu, 21 May 2026 18:08:09 -0300 Subject: [PATCH 1/2] perf(prover): batch-invert COMMIT bus fingerprints; doc/dedup fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compute_commit_bus_offset — batch inversion - The COMMIT-bus offset loop inverted one fingerprint per public-output byte: N separate cubic-extension inversions (each a Fermat-chain exponentiation). Collect the N fingerprints and `inplace_batch_inverse` instead — 1 inversion + O(3N) muls. This runs on the verify path (`verify_with_options` → `compute_expected_commit_bus_balance`), which matters for the RISC-V recursion guest. Behaviour-identical: a zero fingerprint still yields `None` (batch inverse `Err`s on any zero). TableCounts::total — fix copy-pasted doc - `total()`'s doc comment was `validate()`'s text verbatim plus the real one-liner tacked on the end. Replaced with just the accurate summary. replay_transcript_phase_a — drop duplicated arm - Both branches of the `if air.is_preprocessed()` ended with the same `append_bytes(main_merkle_root)`. Hoisted it out; transcript order is unchanged. 273 prover lib tests pass; lint clean. --- prover/src/lib.rs | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index dbe13d20..7c6ca838 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -84,11 +84,7 @@ pub struct TableCounts { } impl TableCounts { - /// Validate that all required tables have at least one chunk. - /// - /// A zero count for any table would remove its constraints from verification, - /// allowing a malicious prover to bypass soundness checks. - /// Sum of all chunk counts across split tables. + /// Sum of all chunk counts across the split tables. pub fn total(&self) -> usize { self.cpu + self.lt @@ -454,10 +450,8 @@ pub(crate) fn replay_transcript_phase_a( for (air, proof) in airs.iter().zip(&multi_proof.proofs) { if air.is_preprocessed() { transcript.append_bytes(&air.precomputed_commitment()); - transcript.append_bytes(&proof.lde_trace_main_merkle_root); - } else { - transcript.append_bytes(&proof.lde_trace_main_merkle_root); } + transcript.append_bytes(&proof.lde_trace_main_merkle_root); } let z: FieldElement = transcript.sample_field_element(); let alpha: FieldElement = transcript.sample_field_element(); @@ -486,15 +480,27 @@ pub(crate) fn compute_commit_bus_offset( let bus_id = FieldElement::::from(BusId::Commit as u64); let alpha_sq = alpha * alpha; - let mut total = FieldElement::::zero(); - for (i, &value) in public_output.iter().enumerate() { - let linear_combination = bus_id - + (FieldElement::::from(i as u64) * alpha) - + (FieldElement::::from(value as u64) * alpha_sq); - let fingerprint = z - linear_combination; - total += fingerprint.inv().ok()?; - } - Some(total) + // fingerprint_i = z - (BusId::Commit + i·α + value_i·α²) + let mut fingerprints: Vec> = public_output + .iter() + .enumerate() + .map(|(i, &value)| { + let linear_combination = bus_id + + (FieldElement::::from(i as u64) * alpha) + + (FieldElement::::from(value as u64) * alpha_sq); + z - linear_combination + }) + .collect(); + + // Batch inversion: 1 inversion + O(3N) muls instead of N field inversions. + // `Err` iff some fingerprint is zero (a collision) — treat as failure. + FieldElement::inplace_batch_inverse(&mut fingerprints).ok()?; + + Some( + fingerprints + .iter() + .fold(FieldElement::::zero(), |acc, term| acc + term), + ) } /// Compute the expected COMMIT bus balance for a `MultiProof`. From e67667fccd7bf571f83f458e17e030bd48aa3630 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Fri, 22 May 2026 18:14:58 -0300 Subject: [PATCH 2/2] Add tests for compute_commit_bus_offset empty, success, and zero fingerprint paths --- .../tests/compute_commit_bus_offset_tests.rs | 100 ++++++++++++++++++ prover/src/tests/mod.rs | 2 + 2 files changed, 102 insertions(+) create mode 100644 prover/src/tests/compute_commit_bus_offset_tests.rs diff --git a/prover/src/tests/compute_commit_bus_offset_tests.rs b/prover/src/tests/compute_commit_bus_offset_tests.rs new file mode 100644 index 00000000..79c092ae --- /dev/null +++ b/prover/src/tests/compute_commit_bus_offset_tests.rs @@ -0,0 +1,100 @@ +//! Unit tests for `compute_commit_bus_offset`. +//! +//! Pins the three behaviours the verify-path helper must preserve: +//! empty input short-circuit, success-path equivalence with a naive +//! per-element-inverse reference, and the zero-fingerprint failure path. + +use math::field::element::FieldElement; + +use crate::compute_commit_bus_offset; +use crate::tables::types::{BusId, GoldilocksExtension}; + +type E = GoldilocksExtension; + +/// Reference implementation: one `inv()` per fingerprint, then sum. +/// Mirrors the original loop bit-for-bit modulo addition order, so any +/// future refactor of the batched routine must remain equivalent to this. +fn naive_offset( + public_output: &[u8], + z: &FieldElement, + alpha: &FieldElement, +) -> Option> { + let bus_id = FieldElement::::from(BusId::Commit as u64); + let alpha_sq = alpha * alpha; + let mut total = FieldElement::::zero(); + for (i, &value) in public_output.iter().enumerate() { + let lc = bus_id + + (FieldElement::::from(i as u64) * alpha) + + (FieldElement::::from(value as u64) * alpha_sq); + let fingerprint = z - lc; + total += fingerprint.inv().ok()?; + } + Some(total) +} + +#[test] +fn test_empty_public_output_returns_zero() { + let z = FieldElement::::from(7u64); + let alpha = FieldElement::::from(11u64); + assert_eq!( + compute_commit_bus_offset(&[], &z, &alpha), + Some(FieldElement::::zero()), + ); +} + +#[test] +fn test_non_empty_matches_naive_per_element_inverse() { + let z = FieldElement::::from(987_654_321u64); + let alpha = FieldElement::::from(31_415_926u64); + let public_output: [u8; 5] = [0x01, 0x02, 0xff, 0x10, 0x80]; + + let batched = compute_commit_bus_offset(&public_output, &z, &alpha); + let naive = naive_offset(&public_output, &z, &alpha); + + assert_eq!(batched, naive); + assert!(batched.is_some(), "no fingerprint should collide here"); +} + +#[test] +fn test_longer_input_matches_naive() { + let z = FieldElement::::from(0xdead_beefu64); + let alpha = FieldElement::::from(0xcafe_babeu64); + let public_output: Vec = (0..=255u16).map(|x| x as u8).collect(); + + let batched = compute_commit_bus_offset(&public_output, &z, &alpha); + let naive = naive_offset(&public_output, &z, &alpha); + + assert_eq!(batched, naive); + assert!(batched.is_some()); +} + +#[test] +fn test_zero_fingerprint_returns_none() { + // Craft fingerprint_0 = 0: i = 0, value = 0, then + // fingerprint_0 = z - (BusId::Commit + 0·α + 0·α²) = z - BusId::Commit. + // Setting z = BusId::Commit forces the collision regardless of alpha. + let z = FieldElement::::from(BusId::Commit as u64); + let alpha = FieldElement::::from(42u64); + let public_output: [u8; 1] = [0]; + + assert_eq!( + compute_commit_bus_offset(&public_output, &z, &alpha), + None, + "zero fingerprint must propagate as None", + ); +} + +#[test] +fn test_zero_fingerprint_in_middle_returns_none() { + // Same idea at i = 2, so some valid fingerprints precede the zero one. + let alpha = FieldElement::::from(5u64); + let alpha_sq = alpha * alpha; + let bus_id = FieldElement::::from(BusId::Commit as u64); + // value = 3 at index 2 → z = BusId + 2α + 3α² forces fingerprint_2 = 0. + let z = bus_id + + (FieldElement::::from(2u64) * alpha) + + (FieldElement::::from(3u64) * alpha_sq); + let public_output: [u8; 4] = [1, 2, 3, 4]; + + assert_eq!(compute_commit_bus_offset(&public_output, &z, &alpha), None,); +} diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index dc5f3fe2..89bab730 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -9,6 +9,8 @@ pub mod branch_constraints_tests; #[cfg(test)] pub mod commit_tests; #[cfg(test)] +pub mod compute_commit_bus_offset_tests; +#[cfg(test)] pub mod constraints_tests; #[cfg(all(test, feature = "disk-spill"))] pub mod count_table_lengths_drift_tests;