From 384f476870c96ab989cd5bb6f6705d2bb7ea01be Mon Sep 17 00:00:00 2001 From: diegokingston Date: Tue, 19 May 2026 15:06:28 -0300 Subject: [PATCH 01/10] refactor(prover): panic!('Invalid carry index') -> unreachable!() `carry_idx` is set during constraint construction and only ever takes values 0 or 1. The default match arm cannot be reached at runtime, so unreachable!() with a documenting message is the idiomatic spelling. --- prover/src/constraints/cpu.rs | 2 +- prover/src/constraints/templates.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prover/src/constraints/cpu.rs b/prover/src/constraints/cpu.rs index 546f2f2a4..a740011f0 100644 --- a/prover/src/constraints/cpu.rs +++ b/prover/src/constraints/cpu.rs @@ -595,7 +595,7 @@ impl NextPcAddConstraint { let carry = match self.carry_idx { 0 => self.compute_carry_0(step), 1 => self.compute_carry_1(step), - _ => panic!("Invalid carry index"), + _ => unreachable!("carry_idx is always 0 or 1; constructed internally"), }; // (1 - branch_cond) * carry * (1 - carry) diff --git a/prover/src/constraints/templates.rs b/prover/src/constraints/templates.rs index 09237145f..333591d8d 100644 --- a/prover/src/constraints/templates.rs +++ b/prover/src/constraints/templates.rs @@ -449,7 +449,7 @@ impl AddConstraint { let carry = match self.carry_idx { 0 => self.compute_carry_0(step), 1 => self.compute_carry_1(step), - _ => panic!("Invalid carry index"), + _ => unreachable!("carry_idx is always 0 or 1; constructed internally"), }; if self.cond_cols.is_empty() { From b5ea52259af15f43f1b5cf330b10999e9380b456 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Tue, 19 May 2026 15:14:52 -0300 Subject: [PATCH 02/10] refactor(prover/trace_builder): extract push_u64_as_halfwords helper Nine sites in `trace_builder.rs` duplicated the same shift-by-[0,16,32,48] loop that decomposes a u64 into four IS_HALFWORD bitwise lookups. Replace with a single `push_u64_as_halfwords(ops, op_type, value)` helper at the top of the module; each call site collapses to one self-documenting line. --- prover/src/tables/trace_builder.rs | 103 ++++++++--------------------- 1 file changed, 28 insertions(+), 75 deletions(-) diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index f83763280..2f642596e 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -288,6 +288,24 @@ impl RegisterState { // Helper Functions // ============================================================================= +/// Pushes the four halfwords of `value` (bits 0-15, 16-31, 32-47, 48-63) as +/// `(low_byte, high_byte)` bitwise lookups of the given `op_type`. Used by +/// the various `collect_bitwise_from_*` helpers to range-check u64 limbs. +fn push_u64_as_halfwords( + ops: &mut Vec, + op_type: BitwiseOperationType, + value: u64, +) { + for shift in [0, 16, 32, 48] { + let half = ((value >> shift) & 0xFFFF) as u16; + ops.push(BitwiseOperation::halfword( + op_type, + (half & 0xFF) as u8, + (half >> 8) as u8, + )); + } +} + /// Get byte count and signed flag from CpuOperation memory flags. fn cpu_op_to_bytes_and_signed(op: &CpuOperation) -> (usize, bool) { let byte_count = if op.decode.memory_8bytes { @@ -1111,14 +1129,7 @@ fn collect_bitwise_from_lt(lt_ops: &[LtOperation]) -> Vec { // IS_HALFWORD lookups for lhs_sub_rhs[0..4] let lhs_sub_rhs = op.lhs.wrapping_sub(op.rhs); - for shift in [0, 16, 32, 48] { - let half = ((lhs_sub_rhs >> shift) & 0xFFFF) as u16; - bitwise_ops.push(BitwiseOperation::halfword( - BitwiseOperationType::IsHalf, - (half & 0xFF) as u8, - (half >> 8) as u8, - )); - } + push_u64_as_halfwords(&mut bitwise_ops, BitwiseOperationType::IsHalf, lhs_sub_rhs); // IS_HALFWORD lookups for lhs[1] and rhs[1] let lhs_1 = ((op.lhs >> 32) & 0xFFFF) as u16; @@ -1151,25 +1162,9 @@ fn collect_bitwise_from_mul(mul_ops: &[(MulOperation, bool)]) -> Vec> shift) & 0xFFFF) as u16; - bitwise_ops.push(BitwiseOperation::halfword( - BitwiseOperationType::IsHalf, - (half & 0xFF) as u8, - (half >> 8) as u8, - )); - } - - // IS_HALF for hi halfwords - for shift in [0, 16, 32, 48] { - let half = ((hi >> shift) & 0xFFFF) as u16; - bitwise_ops.push(BitwiseOperation::halfword( - BitwiseOperationType::IsHalf, - (half & 0xFF) as u8, - (half >> 8) as u8, - )); - } + // IS_HALF for lo and hi halfwords + push_u64_as_halfwords(&mut bitwise_ops, BitwiseOperationType::IsHalf, lo); + push_u64_as_halfwords(&mut bitwise_ops, BitwiseOperationType::IsHalf, hi); // IS_B20 for carry[0..4] range checks let raw_products = op.compute_raw_products(); @@ -1225,36 +1220,15 @@ fn collect_bitwise_from_dvrm(dvrm_ops: &[(DvrmOperation, bool)]) -> Vec> shift) & 0xFFFF) as u16; - bitwise_ops.push(BitwiseOperation::halfword( - BitwiseOperationType::IsHalf, - (half & 0xFF) as u8, - (half >> 8) as u8, - )); - } + push_u64_as_halfwords(&mut bitwise_ops, BitwiseOperationType::IsHalf, r); // IS_HALF for n_sub_r[0..4] (DVRM-C14) let n_sub_r = op.n.wrapping_sub(r); - for shift in [0, 16, 32, 48] { - let half = ((n_sub_r >> shift) & 0xFFFF) as u16; - bitwise_ops.push(BitwiseOperation::halfword( - BitwiseOperationType::IsHalf, - (half & 0xFF) as u8, - (half >> 8) as u8, - )); - } + push_u64_as_halfwords(&mut bitwise_ops, BitwiseOperationType::IsHalf, n_sub_r); // IS_HALF for q[0..4] (DVRM-C11) let q = op.compute_quotient(); - for shift in [0, 16, 32, 48] { - let half = ((q >> shift) & 0xFFFF) as u16; - bitwise_ops.push(BitwiseOperation::halfword( - BitwiseOperationType::IsHalf, - (half & 0xFF) as u8, - (half >> 8) as u8, - )); - } + push_u64_as_halfwords(&mut bitwise_ops, BitwiseOperationType::IsHalf, q); // ZERO lookups per raw op (multiplicity = μ_sum = μ_q + μ_r) @@ -1623,26 +1597,12 @@ fn collect_bitwise_from_commit(commit_ops: &[CommitOperation]) -> Vec> shift) & 0xFFFF) as u16; - lookups.push(BitwiseOperation::halfword( - BitwiseOperationType::IsHalf, - (half & 0xFF) as u8, - ((half >> 8) & 0xFF) as u8, - )); - } + push_u64_as_halfwords(&mut lookups, BitwiseOperationType::IsHalf, count_decr); // IsHalfword for address_incr halfwords (4 halfwords, mult = mu) // All real rows send these, matching the spec's unconditional mult = mu. let address_incr = op.address.wrapping_add(1); - for shift in [0, 16, 32, 48] { - let half = ((address_incr >> shift) & 0xFFFF) as u16; - lookups.push(BitwiseOperation::halfword( - BitwiseOperationType::IsHalf, - (half & 0xFF) as u8, - ((half >> 8) & 0xFF) as u8, - )); - } + push_u64_as_halfwords(&mut lookups, BitwiseOperationType::IsHalf, address_incr); // Zero bus for end detection (mult = mu) // Input: (65535 - cd_0) + (65535 - cd_1) + (65535 - cd_2) + (65535 - cd_3) @@ -1701,14 +1661,7 @@ fn collect_bitwise_from_keccak(keccak_ops: &[KeccakOperation]) -> Vec> shift) & 0xFFFF) as u16; - ops.push(BitwiseOperation::halfword( - BitwiseOperationType::IsHalf, - (half & 0xFF) as u8, - ((half >> 8) & 0xFF) as u8, - )); - } + push_u64_as_halfwords(&mut ops, BitwiseOperationType::IsHalf, ptr); } // Replay keccak round computation to extract bitwise lookups From 0509a12298b810fc694a8c3c4adda2a5b9087ed6 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Tue, 19 May 2026 15:25:52 -0300 Subject: [PATCH 03/10] refactor(prover): extract shared preprocessed-table commit pipeline DECODE, BITWISE, KECCAK_RC, PAGE, and REGISTER all committed their precomputed columns through the same six-step pipeline (interpolate -> LDE -> bit-reverse -> columns2rows -> Merkle build), with five copies of the same three .expect("... ...") strings differing only by the table label. `bitwise.rs` additionally dual-forked every step under `#[cfg(feature = "parallel")]`. Extract `commit_preprocessed_columns(columns, options, label)` into a new `tables/preprocessed.rs`. The cfg-fork lives there only. Each table's `compute_preprocessed_commitment` collapses to building its own column data plus a single helper call. Side effect: the four tables that were previously sequential (decode, keccak_rc, page, register) now use the parallel path uniformly. Net -116 LOC across 7 files; one site to maintain when the pipeline evolves (eg. Merkle backend swap). --- prover/src/tables/bitwise.rs | 94 +++++-------------------------- prover/src/tables/decode.rs | 46 ++------------- prover/src/tables/keccak_rc.rs | 43 ++------------ prover/src/tables/mod.rs | 1 + prover/src/tables/page.rs | 39 ++----------- prover/src/tables/preprocessed.rs | 75 ++++++++++++++++++++++++ prover/src/tables/register.rs | 38 ++----------- 7 files changed, 110 insertions(+), 226 deletions(-) create mode 100644 prover/src/tables/preprocessed.rs diff --git a/prover/src/tables/bitwise.rs b/prover/src/tables/bitwise.rs index 455f696f2..43e0df73e 100644 --- a/prover/src/tables/bitwise.rs +++ b/prover/src/tables/bitwise.rs @@ -28,17 +28,15 @@ use std::sync::OnceLock; -use math::fft::cpu::bit_reversing::in_place_bit_reverse_permute; -use math::polynomial::Polynomial; -use stark::config::{BatchedMerkleTree, Commitment}; +use stark::config::Commitment; use stark::lookup::{BusInteraction, BusValue, Multiplicity, Packing}; use stark::proof::options::ProofOptions; -use stark::prover::evaluate_polynomial_on_lde_domain; -use stark::trace::{TraceTable, columns2rows}; +use stark::trace::TraceTable; #[cfg(feature = "parallel")] use rayon::prelude::*; +use super::preprocessed::commit_preprocessed_columns; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField}; // ========================================================================= @@ -188,95 +186,29 @@ static BITWISE_COMMITMENT: OnceLock = OnceLock::new(); /// because FRI queries can target any index in [0, N*blowup). A raw-value commitment /// would only have N leaves, unable to verify queries at indices >= N. fn compute_preprocessed_commitment(options: &ProofOptions) -> Commitment { - // Step 1: Generate precomputed columns in parallel - // Each column is generated independently by iterating over all row indices + // Generate the 2^20 rows of the bitwise lookup table by column. Each + // column is independent, so build them in parallel under `--features + // parallel`. #[cfg(feature = "parallel")] let columns: Vec> = (0..NUM_PRECOMPUTED_COLS) .into_par_iter() .map(|col_idx| { (0..NUM_ROWS) - .map(|idx| { - let row = generate_bitwise_row(idx); - FE::from(row[col_idx]) - }) + .map(|idx| FE::from(generate_bitwise_row(idx)[col_idx])) .collect() }) .collect(); #[cfg(not(feature = "parallel"))] - let columns: Vec> = { - let mut cols: Vec> = (0..NUM_PRECOMPUTED_COLS) - .map(|_| Vec::with_capacity(NUM_ROWS)) - .collect(); - for idx in 0..NUM_ROWS { - let row = generate_bitwise_row(idx); - for (col_idx, &value) in row.iter().enumerate() { - cols[col_idx].push(FE::from(value)); - } - } - cols - }; - - // Step 2: Interpolate each column to a polynomial (parallel) - #[cfg(feature = "parallel")] - let polys: Vec> = columns - .par_iter() - .map(|col| { - Polynomial::interpolate_fft::(col) - .expect("FFT interpolation failed for bitwise column") - }) - .collect(); - - #[cfg(not(feature = "parallel"))] - let polys: Vec> = columns - .iter() - .map(|col| { - Polynomial::interpolate_fft::(col) - .expect("FFT interpolation failed for bitwise column") - }) - .collect(); - - // Step 3: Evaluate polynomials on LDE domain (parallel) - let blowup_factor = options.blowup_factor as usize; - let coset_offset = FE::from(options.coset_offset); - - #[cfg(feature = "parallel")] - let mut lde_columns: Vec> = polys - .par_iter() - .map(|poly| { - evaluate_polynomial_on_lde_domain(poly, blowup_factor, NUM_ROWS, &coset_offset) - .expect("LDE evaluation failed for bitwise polynomial") - }) - .collect(); - - #[cfg(not(feature = "parallel"))] - let mut lde_columns: Vec> = polys - .iter() - .map(|poly| { - evaluate_polynomial_on_lde_domain(poly, blowup_factor, NUM_ROWS, &coset_offset) - .expect("LDE evaluation failed for bitwise polynomial") + let columns: Vec> = (0..NUM_PRECOMPUTED_COLS) + .map(|col_idx| { + (0..NUM_ROWS) + .map(|idx| FE::from(generate_bitwise_row(idx)[col_idx])) + .collect() }) .collect(); - // Step 4: Bit-reverse permute (parallel) - #[cfg(feature = "parallel")] - lde_columns.par_iter_mut().for_each(|col| { - in_place_bit_reverse_permute(col); - }); - - #[cfg(not(feature = "parallel"))] - for col in lde_columns.iter_mut() { - in_place_bit_reverse_permute(col); - } - - // Step 5: Convert columns to rows for Merkle tree - let lde_rows = columns2rows(lde_columns); - - // Step 6: Build Merkle tree over LDE (N * blowup leaves) - let tree = BatchedMerkleTree::::build(&lde_rows) - .expect("Failed to build Merkle tree for bitwise LDE"); - - tree.root + commit_preprocessed_columns(columns, options, "bitwise") } /// Returns the preprocessed commitment for the bitwise table, with caching. diff --git a/prover/src/tables/decode.rs b/prover/src/tables/decode.rs index 913867b59..e68286445 100644 --- a/prover/src/tables/decode.rs +++ b/prover/src/tables/decode.rs @@ -38,14 +38,12 @@ use executor::elf::Elf; use executor::vm::instruction::decoding::{Instruction, InstructionError}; use executor::vm::memory::U64HashMap; -use math::fft::cpu::bit_reversing::in_place_bit_reverse_permute; -use math::polynomial::Polynomial; -use stark::config::{BatchedMerkleTree, Commitment}; +use stark::config::Commitment; use stark::lookup::{BusInteraction, BusValue, Multiplicity, Packing}; use stark::proof::options::ProofOptions; -use stark::prover::evaluate_polynomial_on_lde_domain; -use stark::trace::{TraceTable, columns2rows}; +use stark::trace::TraceTable; +use super::preprocessed::commit_preprocessed_columns; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField}; // Re-export DecodeEntry from types for backwards compatibility @@ -251,11 +249,10 @@ pub fn compute_precomputed_commitment( instructions: &U64HashMap, options: &ProofOptions, ) -> Commitment { - // Step 1: Generate trace (MU=0, we only need precomputed columns) + // Generate the trace (MU=0; only the precomputed columns are committed). let (trace, _pc_to_row) = generate_decode_trace(instructions); let num_rows = trace.num_rows(); - // Step 2: Extract precomputed columns (0..NUM_PRECOMPUTED_COLS) let columns: Vec> = (0..NUM_PRECOMPUTED_COLS) .map(|col_idx| { (0..num_rows) @@ -263,40 +260,7 @@ pub fn compute_precomputed_commitment( .collect() }) .collect(); - - // Step 3: Interpolate each column to a polynomial - let polys: Vec> = columns - .iter() - .map(|col| { - Polynomial::interpolate_fft::(col) - .expect("FFT interpolation failed for decode column") - }) - .collect(); - - // Step 4: Evaluate polynomials on LDE domain (N * blowup_factor points) - let blowup_factor = options.blowup_factor as usize; - let coset_offset = FE::from(options.coset_offset); - let mut lde_columns: Vec> = polys - .iter() - .map(|poly| { - evaluate_polynomial_on_lde_domain(poly, blowup_factor, num_rows, &coset_offset) - .expect("LDE evaluation failed for decode polynomial") - }) - .collect(); - - // Step 5: Bit-reverse permute (same as prover) - for col in lde_columns.iter_mut() { - in_place_bit_reverse_permute(col); - } - - // Step 6: Convert columns to rows for Merkle tree - let lde_rows = columns2rows(lde_columns); - - // Step 7: Build Merkle tree over LDE (N * blowup leaves) - let tree = BatchedMerkleTree::::build(&lde_rows) - .expect("Failed to build Merkle tree for decode LDE"); - - tree.root + commit_preprocessed_columns(columns, options, "decode") } // ========================================================================= diff --git a/prover/src/tables/keccak_rc.rs b/prover/src/tables/keccak_rc.rs index c2e14d643..5d9639190 100644 --- a/prover/src/tables/keccak_rc.rs +++ b/prover/src/tables/keccak_rc.rs @@ -9,14 +9,13 @@ use std::sync::OnceLock; -use math::fft::cpu::bit_reversing::in_place_bit_reverse_permute; use math::field::element::FieldElement; -use math::polynomial::Polynomial; -use stark::config::{BatchedMerkleTree, Commitment}; +use stark::config::Commitment; use stark::lookup::{BusInteraction, BusValue, Multiplicity, Packing}; use stark::proof::options::ProofOptions; -use stark::prover::evaluate_polynomial_on_lde_domain; -use stark::trace::{TraceTable, columns2rows}; +use stark::trace::TraceTable; + +use super::preprocessed::commit_preprocessed_columns; use executor::vm::instruction::execution::KECCAK_RC; @@ -75,7 +74,6 @@ pub const fn generate_row(round: usize) -> [u64; NUM_PRECOMPUTED_COLS] { static KECCAK_RC_COMMITMENT: OnceLock = OnceLock::new(); fn compute_preprocessed_commitment(options: &ProofOptions) -> Commitment { - // Generate precomputed columns let mut columns: Vec> = (0..NUM_PRECOMPUTED_COLS) .map(|_| Vec::with_capacity(NUM_ROWS)) .collect(); @@ -85,38 +83,7 @@ fn compute_preprocessed_commitment(options: &ProofOptions) -> Commitment { columns[col_idx].push(FE::from(value)); } } - - // Interpolate each column to a polynomial - let polys: Vec> = columns - .iter() - .map(|col| { - Polynomial::interpolate_fft::(col) - .expect("FFT interpolation failed for keccak_rc column") - }) - .collect(); - - // Evaluate on LDE domain - let blowup_factor = options.blowup_factor as usize; - let coset_offset = FE::from(options.coset_offset); - let mut lde_columns: Vec> = polys - .iter() - .map(|poly| { - evaluate_polynomial_on_lde_domain(poly, blowup_factor, NUM_ROWS, &coset_offset) - .expect("LDE evaluation failed for keccak_rc polynomial") - }) - .collect(); - - // Bit-reverse permute - for col in lde_columns.iter_mut() { - in_place_bit_reverse_permute(col); - } - - // Build Merkle tree - let lde_rows = columns2rows(lde_columns); - let tree = BatchedMerkleTree::::build(&lde_rows) - .expect("Failed to build Merkle tree for keccak_rc LDE"); - - tree.root + commit_preprocessed_columns(columns, options, "keccak_rc") } #[inline] diff --git a/prover/src/tables/mod.rs b/prover/src/tables/mod.rs index 4a6032ef2..d8a92459a 100644 --- a/prover/src/tables/mod.rs +++ b/prover/src/tables/mod.rs @@ -38,6 +38,7 @@ pub mod memw_aligned; pub mod memw_register; pub mod mul; pub mod page; +pub mod preprocessed; pub mod register; pub mod shift; pub mod trace_builder; diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index a4597e1b8..ad756ed51 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -33,13 +33,12 @@ use std::collections::HashMap; use std::sync::OnceLock; -use math::fft::cpu::bit_reversing::in_place_bit_reverse_permute; -use math::polynomial::Polynomial; -use stark::config::{BatchedMerkleTree, Commitment}; +use stark::config::Commitment; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::proof::options::ProofOptions; -use stark::prover::evaluate_polynomial_on_lde_domain; -use stark::trace::{TraceTable, columns2rows}; +use stark::trace::TraceTable; + +use super::preprocessed::commit_preprocessed_columns; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField}; @@ -264,34 +263,8 @@ pub fn compute_precomputed_commitment(config: &PageConfig, options: &ProofOption }; } - let columns = [offset_col, init_col]; - - let polys: Vec> = columns - .iter() - .map(|col| { - Polynomial::interpolate_fft::(col) - .expect("FFT interpolation failed for page column") - }) - .collect(); - - let blowup_factor = options.blowup_factor as usize; - let coset_offset = FE::from(options.coset_offset); - let mut lde_columns: Vec> = polys - .iter() - .map(|poly| { - evaluate_polynomial_on_lde_domain(poly, blowup_factor, num_rows, &coset_offset) - .expect("LDE evaluation failed for page polynomial") - }) - .collect(); - - for col in lde_columns.iter_mut() { - in_place_bit_reverse_permute(col); - } - - let lde_rows = columns2rows(lde_columns); - let tree = BatchedMerkleTree::::build(&lde_rows) - .expect("Failed to build Merkle tree for page LDE"); - tree.root + let columns = vec![offset_col, init_col]; + commit_preprocessed_columns(columns, options, "page") } /// Returns the preprocessed commitment for a PAGE table, with caching for zero-init pages. diff --git a/prover/src/tables/preprocessed.rs b/prover/src/tables/preprocessed.rs new file mode 100644 index 000000000..cebcefb49 --- /dev/null +++ b/prover/src/tables/preprocessed.rs @@ -0,0 +1,75 @@ +//! Shared commitment pipeline for preprocessed tables. +//! +//! The DECODE, BITWISE, KECCAK_RC, PAGE, and REGISTER tables all commit their +//! precomputed columns through the same six steps: +//! +//! 1. Interpolate each column on the trace domain (FFT). +//! 2. Evaluate every polynomial on the LDE coset. +//! 3. Bit-reverse permute each LDE column. +//! 4. Transpose columns -> rows. +//! 5. Build a batched Merkle tree over the rows. +//! 6. Return the tree root. +//! +//! This module factors the pipeline out so each table only has to build its +//! own columns. + +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +use math::fft::cpu::bit_reversing::in_place_bit_reverse_permute; +use math::polynomial::Polynomial; +use stark::config::{BatchedMerkleTree, Commitment}; +use stark::proof::options::ProofOptions; +use stark::prover::evaluate_polynomial_on_lde_domain; +use stark::trace::columns2rows; + +use super::types::{FE, GoldilocksField}; + +/// Run the full preprocessed-commitment pipeline on `columns`. +/// +/// All columns must have the same length (the trace domain size, typically a +/// power of two). `table_label` is included in panic messages on failure of +/// the FFT / LDE / Merkle steps; these are construction-time failures on the +/// table's own data and indicate a bug in the code, never adversarial input. +pub fn commit_preprocessed_columns( + columns: Vec>, + options: &ProofOptions, + table_label: &'static str, +) -> Commitment { + let num_rows = columns[0].len(); + let blowup_factor = options.blowup_factor as usize; + let coset_offset = FE::from(options.coset_offset); + + let interpolate = |col: &Vec| { + Polynomial::interpolate_fft::(col) + .unwrap_or_else(|_| panic!("FFT interpolation failed for {table_label} column")) + }; + let to_lde = |poly: &Polynomial| { + evaluate_polynomial_on_lde_domain(poly, blowup_factor, num_rows, &coset_offset) + .unwrap_or_else(|_| panic!("LDE evaluation failed for {table_label} polynomial")) + }; + + #[cfg(feature = "parallel")] + let polys: Vec> = columns.par_iter().map(interpolate).collect(); + #[cfg(not(feature = "parallel"))] + let polys: Vec> = columns.iter().map(interpolate).collect(); + + #[cfg(feature = "parallel")] + let mut lde_columns: Vec> = polys.par_iter().map(to_lde).collect(); + #[cfg(not(feature = "parallel"))] + let mut lde_columns: Vec> = polys.iter().map(to_lde).collect(); + + #[cfg(feature = "parallel")] + lde_columns + .par_iter_mut() + .for_each(|col| in_place_bit_reverse_permute(col)); + #[cfg(not(feature = "parallel"))] + for col in lde_columns.iter_mut() { + in_place_bit_reverse_permute(col); + } + + let lde_rows = columns2rows(lde_columns); + let tree = BatchedMerkleTree::::build(&lde_rows) + .unwrap_or_else(|| panic!("Failed to build Merkle tree for {table_label} LDE")); + tree.root +} diff --git a/prover/src/tables/register.rs b/prover/src/tables/register.rs index 976b40444..21576fef9 100644 --- a/prover/src/tables/register.rs +++ b/prover/src/tables/register.rs @@ -20,15 +20,13 @@ use std::collections::HashMap; -use math::fft::cpu::bit_reversing::in_place_bit_reverse_permute; -use math::polynomial::Polynomial; -use stark::config::{BatchedMerkleTree, Commitment}; +use stark::config::Commitment; use stark::lookup::{BusInteraction, BusValue, Multiplicity, Packing}; use stark::proof::options::ProofOptions; -use stark::prover::evaluate_polynomial_on_lde_domain; -use stark::trace::{TraceTable, columns2rows}; +use stark::trace::TraceTable; use super::page::STACK_TOP; +use super::preprocessed::commit_preprocessed_columns; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField}; // ========================================================================= @@ -207,34 +205,8 @@ pub fn compute_precomputed_commitment(options: &ProofOptions, entry_point: u64) init_col[i] = FE::from(init_value_for_address(word_addr, entry_point) as u64); } - let columns = [offset_col, init_col]; - - let polys: Vec> = columns - .iter() - .map(|col| { - Polynomial::interpolate_fft::(col) - .expect("FFT interpolation failed for register column") - }) - .collect(); - - let blowup_factor = options.blowup_factor as usize; - let coset_offset = FE::from(options.coset_offset); - let mut lde_columns: Vec> = polys - .iter() - .map(|poly| { - evaluate_polynomial_on_lde_domain(poly, blowup_factor, num_rows, &coset_offset) - .expect("LDE evaluation failed for register polynomial") - }) - .collect(); - - for col in lde_columns.iter_mut() { - in_place_bit_reverse_permute(col); - } - - let lde_rows = columns2rows(lde_columns); - let tree = BatchedMerkleTree::::build(&lde_rows) - .expect("Failed to build Merkle tree for register LDE"); - tree.root + let columns = vec![offset_col, init_col]; + commit_preprocessed_columns(columns, options, "register") } /// Returns the preprocessed commitment for the REGISTER table. From 84efce87c85b4511eab96136f56a8076e02dd470 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Tue, 19 May 2026 15:40:59 -0300 Subject: [PATCH 04/10] refactor(prover): replace adversarial .expect() with Error::InvalidExecutorLog A malformed executor log could previously panic the prover via several .expect() sites: - `commit_count exceeds u32 range` (collect_commit_memw_ops, collect_ops_from_cpu) - `commit index exceeds u32 range` - `keccak state lane address overflows u64` (collect_keccak_memw_ops, collect_ops_from_cpu, collect_bitwise_from_keccak) - `keccak state byte address overflows u64` Each is now returned as `Error::InvalidExecutorLog(&'static str)`, threaded through the four `collect_*` helpers' new `Result` return types. Callers were already in `Result`-returning paths, so propagation is a single `?` per site. Test caller wraps with `.expect("...")`. Defense in depth: the executor is the first validation layer; the prover no longer crashes if a regression there allows an out-of-range value through. Mirrors the verifier panic-removal work on `cleanup/stark-verifier-refactor`. --- prover/src/lib.rs | 4 ++ prover/src/tables/trace_builder.rs | 105 +++++++++++++++++------------ 2 files changed, 67 insertions(+), 42 deletions(-) diff --git a/prover/src/lib.rs b/prover/src/lib.rs index dbe13d20b..16439fe4e 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -166,6 +166,9 @@ pub enum Error { Prover(String), /// Proof contains invalid table_counts (e.g. zero for a required table) InvalidTableCounts(String), + /// Executor log violates an invariant the prover relies on + /// (e.g. commit-count overflow, keccak state address arithmetic overflow). + InvalidExecutorLog(&'static str), } impl fmt::Display for Error { @@ -179,6 +182,7 @@ impl fmt::Display for Error { Error::Execution(msg) => write!(f, "execution error: {msg}"), Error::Prover(msg) => write!(f, "proving error: {msg}"), Error::InvalidTableCounts(msg) => write!(f, "invalid table_counts: {msg}"), + Error::InvalidExecutorLog(reason) => write!(f, "invalid executor log: {reason}"), } } } diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 2f642596e..ec0c6fcdc 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -378,15 +378,18 @@ fn collect_ops_from_cpu( cpu_ops: &[CpuOperation], memory_state: &mut MemoryState, register_state: &mut RegisterState, -) -> ( - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, -) { +) -> Result< + ( + Vec, + Vec, + Vec, + Vec, + Vec, + Vec, + Vec, + ), + Error, +> { let mut memw_ops = Vec::with_capacity(cpu_ops.len() * 3); let mut load_ops = Vec::with_capacity(cpu_ops.len() / 8 + 1); let mut lt_ops = Vec::with_capacity(cpu_ops.len() / 10 + 1); @@ -422,12 +425,13 @@ fn collect_ops_from_cpu( memory_state, current_commit_index as u64, )); - let reg_commit_ops = collect_commit_memw_ops(op, register_state, memory_state); + let reg_commit_ops = collect_commit_memw_ops(op, register_state, memory_state)?; memw_ops.extend(reg_commit_ops); - let count = u32::try_from(op.commit_count).expect("commit_count exceeds u32 range"); + let count = u32::try_from(op.commit_count) + .map_err(|_| Error::InvalidExecutorLog("commit_count exceeds u32 range"))?; current_commit_index = current_commit_index .checked_add(count) - .expect("commit index exceeds u32 range"); + .ok_or(Error::InvalidExecutorLog("commit index exceeds u32 range"))?; debug_assert_eq!( current_commit_index, register_state.read_index().0, @@ -441,14 +445,17 @@ fn collect_ops_from_cpu( let state_addr = op.keccak_state_addr; let mut input = [0u64; 25]; for (i, lane) in input.iter_mut().enumerate() { - let addr = state_addr - .checked_add(i as u64 * 8) - .expect("keccak state address range must be validated by the executor"); + let addr = + state_addr + .checked_add(i as u64 * 8) + .ok_or(Error::InvalidExecutorLog( + "keccak state lane address overflows u64", + ))?; let mut val = 0u64; for b in 0..8 { - let byte_addr = addr - .checked_add(b as u64) - .expect("keccak state address range must be validated by the executor"); + let byte_addr = addr.checked_add(b as u64).ok_or(Error::InvalidExecutorLog( + "keccak state byte address overflows u64", + ))?; let (byte_val, _ts) = memory_state.read_byte(byte_addr); val |= (byte_val as u64) << (b * 8); } @@ -458,7 +465,7 @@ fn collect_ops_from_cpu( executor::vm::instruction::execution::keccak_f1600(&mut output); // collect_keccak_memw_ops handles memory_state + register_state updates let keccak_memw_ops = - collect_keccak_memw_ops(op, &input, &output, memory_state, register_state); + collect_keccak_memw_ops(op, &input, &output, memory_state, register_state)?; memw_ops.extend(keccak_memw_ops); keccak_ops.push(KeccakOperation { timestamp: op.timestamp, @@ -504,7 +511,7 @@ fn collect_ops_from_cpu( "commit_ops count should match accumulated commit index plus end rows" ); - ( + Ok(( memw_ops, load_ops, lt_ops, @@ -512,7 +519,7 @@ fn collect_ops_from_cpu( bitwise_ops, commit_ops, keccak_ops, - ) + )) } /// Collects a LOAD operation and corresponding MEMW read from CpuOperation. @@ -706,7 +713,7 @@ fn collect_commit_memw_ops( op: &CpuOperation, register_state: &mut RegisterState, memory_state: &mut MemoryState, -) -> Vec { +) -> Result, Error> { let ts = op.timestamp; let buf_addr = op.commit_buf_addr; let count = op.commit_count; @@ -760,9 +767,11 @@ fn collect_commit_memw_ops( // Read+write x254 (global commit index) at ts { let (old_index, old_ts) = register_state.read_index(); + let count_u32 = u32::try_from(count) + .map_err(|_| Error::InvalidExecutorLog("commit_count exceeds u32 range"))?; let new_index = old_index - .checked_add(u32::try_from(count).expect("commit_count exceeds u32 range")) - .expect("commit index exceeds u32 range"); + .checked_add(count_u32) + .ok_or(Error::InvalidExecutorLog("commit index exceeds u32 range"))?; let old_value = [old_index as u64, 0, 0, 0, 0, 0, 0, 0]; let new_value = [new_index as u64, 0, 0, 0, 0, 0, 0, 0]; let old_timestamps = [old_ts, 0, 0, 0, 0, 0, 0, 0]; @@ -784,7 +793,7 @@ fn collect_commit_memw_ops( memory_state.write_byte(addr, byte_val, ts); } - memw_ops + Ok(memw_ops) } /// Collects HALT finalization MEMW operations for all 33 registers. @@ -863,7 +872,7 @@ fn collect_keccak_memw_ops( output: &[u64; 25], memory_state: &mut MemoryState, register_state: &mut RegisterState, -) -> Vec { +) -> Result, Error> { let ts = op.timestamp; let state_addr = op.keccak_state_addr; let mut memw_ops = Vec::with_capacity(26); // 1 register read + 25 lane ops @@ -884,9 +893,12 @@ fn collect_keccak_memw_ops( // input = [0, state_ptr, output_state, timestamp, 0, 0, 1], output = input_state // The MEMW table sees: old=input_state, value=output_state, is_read=true. for (lane_idx, (&in_lane, &out_lane)) in input.iter().zip(output.iter()).enumerate() { - let lane_addr = state_addr - .checked_add(lane_idx as u64 * 8) - .expect("keccak state address range must be validated by the executor"); + let lane_addr = + state_addr + .checked_add(lane_idx as u64 * 8) + .ok_or(Error::InvalidExecutorLog( + "keccak state lane address overflows u64", + ))?; let mut old_bytes = [0u64; 8]; let mut old_timestamps = [0u64; 8]; @@ -894,7 +906,9 @@ fn collect_keccak_memw_ops( old_bytes[b] = (in_lane >> (b * 8)) & 0xFF; let byte_addr = lane_addr .checked_add(b as u64) - .expect("keccak state address range must be validated by the executor"); + .ok_or(Error::InvalidExecutorLog( + "keccak state byte address overflows u64", + ))?; let (_old_val, old_ts) = memory_state.read_byte(byte_addr); old_timestamps[b] = old_ts; } @@ -912,12 +926,14 @@ fn collect_keccak_memw_ops( for (b, &val) in value_bytes.iter().enumerate() { let byte_addr = lane_addr .checked_add(b as u64) - .expect("keccak state address range must be validated by the executor"); + .ok_or(Error::InvalidExecutorLog( + "keccak state byte address overflows u64", + ))?; memory_state.write_byte(byte_addr, val as u8, ts); } } - memw_ops + Ok(memw_ops) } /// @@ -1631,7 +1647,9 @@ fn collect_bitwise_from_commit(commit_ops: &[CommitOperation]) -> Vec Vec { +fn collect_bitwise_from_keccak( + keccak_ops: &[KeccakOperation], +) -> Result, Error> { use executor::vm::instruction::execution::{KECCAK_RC, KECCAK_RHO}; let mut ops = Vec::new(); @@ -1658,9 +1676,12 @@ fn collect_bitwise_from_keccak(keccak_ops: &[KeccakOperation]) -> Vec Vec Date: Tue, 19 May 2026 16:21:18 -0300 Subject: [PATCH 05/10] refactor(prover): introduce BusInteractionsBuilder + migrate lt.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most table `bus_interactions()` functions are dense walls of `BusInteraction::sender(... vec![BusValue::Packed { ... Packing::Direct }])` blocks — 5-7 lines of structural noise per logical interaction. Add `tables/bus_builder.rs` with a small builder whose intent-named methods describe the constraint rather than the data structure: - `send_halfword(col, &mu)` — IS_HALFWORD range check on a direct-packed col - `send_msb16(in_col, out_col, &mu)` — MSB16 lookup pair - `send_b20(col, &mu)` / `send_b20_linear(&mu, terms)` — B20 checks - `send(...)` / `recv(...)` — generic forms - `raw(interaction)` — escape hatch for one-off shapes No macros: plain Rust methods, navigable with go-to-definition. `Multiplicity` is taken by reference and cloned inside helpers so a single `let mu = ...;` can drive many interactions without per-site `.clone()`. Migrate `tables/lt.rs` as the first table — its 9 interactions go from ~120 to ~40 LOC and each reads as a spec line. Behavior unchanged; 24 lt_tests pass. The mul/dvrm/shift/keccak_rnd migrations are bigger and ship as follow-up commits, after this API gets review. --- prover/src/tables/bus_builder.rs | 143 ++++++++++++++++++++++++++++ prover/src/tables/lt.rs | 158 ++++++++----------------------- prover/src/tables/mod.rs | 1 + 3 files changed, 184 insertions(+), 118 deletions(-) create mode 100644 prover/src/tables/bus_builder.rs diff --git a/prover/src/tables/bus_builder.rs b/prover/src/tables/bus_builder.rs new file mode 100644 index 000000000..8099e77d6 --- /dev/null +++ b/prover/src/tables/bus_builder.rs @@ -0,0 +1,143 @@ +//! Builder for table `bus_interactions()` declarations. +//! +//! Each table's `bus_interactions()` function returns a `Vec` +//! describing every lookup the table sends or receives. The same idioms repeat +//! across tables: send a direct-packed column to a halfword range check, send +//! a (column, derived) pair to MSB16, range-check a virtual linear combination +//! via IS_B20, etc. +//! +//! Without a builder these are 5-7 line `BusInteraction::sender(...)` blocks +//! with a `vec![BusValue::Packed{...}]` argument, repeated dozens of times. +//! The builder reduces each interaction to a single intent-named call that +//! reads as a spec line ("send `col` to IS_HALFWORD with multiplicity mu"). +//! +//! No macros: plain Rust methods, discoverable via `rust-analyzer`. Use the +//! `raw(...)` escape hatch for one-off interactions that do not fit a helper. + +use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; + +use super::types::BusId; + +/// Accumulator for a table's `bus_interactions()` declaration. +pub struct BusInteractionsBuilder { + inner: Vec, +} + +impl BusInteractionsBuilder { + pub fn new() -> Self { + Self { inner: Vec::new() } + } + + pub fn with_capacity(n: usize) -> Self { + Self { + inner: Vec::with_capacity(n), + } + } + + pub fn into_vec(self) -> Vec { + self.inner + } + + // ------------------------------------------------------------------------- + // Sender helpers + // ------------------------------------------------------------------------- + + /// Send a single direct-packed column to a halfword range check. + /// IS_HALFWORD[col] + /// + /// `mult` is taken by reference and cloned so callers can reuse the same + /// `Multiplicity` value across many calls without an explicit `.clone()` + /// at each site. + pub fn send_halfword(&mut self, col: usize, mult: &Multiplicity) -> &mut Self { + self.inner.push(BusInteraction::sender( + BusId::IsHalfword, + mult.clone(), + vec![packed_direct(col)], + )); + self + } + + /// Send a (input, output) pair to MSB16. + /// MSB16[input_col] -> output_col + pub fn send_msb16( + &mut self, + input_col: usize, + output_col: usize, + mult: &Multiplicity, + ) -> &mut Self { + self.inner.push(BusInteraction::sender( + BusId::Msb16, + mult.clone(), + vec![packed_direct(input_col), packed_direct(output_col)], + )); + self + } + + /// Send a B20 range check on a virtual linear-combination value (e.g. a carry). + /// IS_B20[linear_terms] + pub fn send_b20_linear(&mut self, mult: &Multiplicity, terms: Vec) -> &mut Self { + self.inner.push(BusInteraction::sender( + BusId::IsB20, + mult.clone(), + vec![BusValue::linear(terms)], + )); + self + } + + /// Send a direct-packed column to a B20 range check. + /// IS_B20[col] + pub fn send_b20(&mut self, col: usize, mult: &Multiplicity) -> &mut Self { + self.inner.push(BusInteraction::sender( + BusId::IsB20, + mult.clone(), + vec![packed_direct(col)], + )); + self + } + + /// Generic sender with caller-provided values. Takes ownership of `mult`; + /// use when sending exactly one interaction with this multiplicity. + pub fn send(&mut self, bus_id: BusId, mult: Multiplicity, values: Vec) -> &mut Self { + self.inner + .push(BusInteraction::sender(bus_id, mult, values)); + self + } + + // ------------------------------------------------------------------------- + // Receiver helpers + // ------------------------------------------------------------------------- + + /// Generic receiver with caller-provided values. + pub fn recv(&mut self, bus_id: BusId, mult: Multiplicity, values: Vec) -> &mut Self { + self.inner + .push(BusInteraction::receiver(bus_id, mult, values)); + self + } + + // ------------------------------------------------------------------------- + // Escape hatch + // ------------------------------------------------------------------------- + + /// Push a fully-constructed `BusInteraction` (sender or receiver) without + /// going through a helper. Use when the interaction does not match any + /// named idiom on this builder. + pub fn raw(&mut self, interaction: BusInteraction) -> &mut Self { + self.inner.push(interaction); + self + } +} + +impl Default for BusInteractionsBuilder { + fn default() -> Self { + Self::new() + } +} + +/// Build a `BusValue::Packed` with `Packing::Direct` from a column index. +/// Exported because some callers compose a `vec![packed_direct(c), ...]` by hand. +pub fn packed_direct(col: usize) -> BusValue { + BusValue::Packed { + start_column: col, + packing: Packing::Direct, + } +} diff --git a/prover/src/tables/lt.rs b/prover/src/tables/lt.rs index da1bc948e..3c0cb2df0 100644 --- a/prover/src/tables/lt.rs +++ b/prover/src/tables/lt.rs @@ -32,6 +32,7 @@ use stark::lookup::{BusInteraction, BusValue, Multiplicity, Packing}; use stark::table::TableView; use stark::trace::TraceTable; +use super::bus_builder::{BusInteractionsBuilder, packed_direct}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, SHIFT_16}; // ========================================================================= @@ -204,124 +205,45 @@ pub fn generate_lt_trace( /// - **Sends** IS_HALFWORD lookups for lhs_sub_rhs range checks /// - **Receives** LT lookups from other tables (CPU) pub fn bus_interactions() -> Vec { - vec![ - // MSB16[lhs[2]] -> lhs_msb - // Input: lhs[2] (half containing MSB) - // Output: lhs_msb - BusInteraction::sender( - BusId::Msb16, - Multiplicity::Column(cols::MU), - vec![ - BusValue::Packed { - start_column: cols::LHS_2, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::LHS_MSB, - packing: Packing::Direct, - }, - ], - ), - // MSB16[rhs[2]] -> rhs_msb - BusInteraction::sender( - BusId::Msb16, - Multiplicity::Column(cols::MU), - vec![ - BusValue::Packed { - start_column: cols::RHS_2, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::RHS_MSB, - packing: Packing::Direct, - }, - ], - ), - // IS_HALFWORD[lhs_sub_rhs[0]] - BusInteraction::sender( - BusId::IsHalfword, - Multiplicity::Column(cols::MU), - vec![BusValue::Packed { - start_column: cols::LHS_SUB_RHS_0, - packing: Packing::Direct, - }], - ), - // IS_HALFWORD[lhs_sub_rhs[1]] - BusInteraction::sender( - BusId::IsHalfword, - Multiplicity::Column(cols::MU), - vec![BusValue::Packed { - start_column: cols::LHS_SUB_RHS_1, - packing: Packing::Direct, - }], - ), - // IS_HALFWORD[lhs_sub_rhs[2]] - BusInteraction::sender( - BusId::IsHalfword, - Multiplicity::Column(cols::MU), - vec![BusValue::Packed { - start_column: cols::LHS_SUB_RHS_2, - packing: Packing::Direct, - }], - ), - // IS_HALFWORD[lhs_sub_rhs[3]] - BusInteraction::sender( - BusId::IsHalfword, - Multiplicity::Column(cols::MU), - vec![BusValue::Packed { - start_column: cols::LHS_SUB_RHS_3, - packing: Packing::Direct, - }], - ), - // IS_HALFWORD[lhs[1]] - BusInteraction::sender( - BusId::IsHalfword, - Multiplicity::Column(cols::MU), - vec![BusValue::Packed { - start_column: cols::LHS_1, - packing: Packing::Direct, - }], - ), - // IS_HALFWORD[rhs[1]] - BusInteraction::sender( - BusId::IsHalfword, - Multiplicity::Column(cols::MU), - vec![BusValue::Packed { - start_column: cols::RHS_1, - packing: Packing::Direct, - }], - ), - // LT[lhs, rhs, signed] -> lt (receiver) - // lhs is DWordHHW, rhs is DWordHHW, signed is Bit, lt is Bit - // Uses DWordHHW packing: reads 3 columns (Word, Half, Half), produces 2 bus elements [lo32, hi32] - // This allows DWordWL senders (like MEMW timestamps) to match via Packing::DWordWL - BusInteraction::receiver( - BusId::Lt, - Multiplicity::Column(cols::MU), - vec![ - // lhs as DWordHHW (reads 3 columns: Word, Half, Half; produces 2 elements: [lo32, hi32]) - BusValue::Packed { - start_column: cols::LHS_0, - packing: Packing::DWordHHW, - }, - // rhs as DWordHHW (reads 3 columns, produces 2 elements) - BusValue::Packed { - start_column: cols::RHS_0, - packing: Packing::DWordHHW, - }, - // signed - BusValue::Packed { - start_column: cols::SIGNED, - packing: Packing::Direct, - }, - // lt (output) - BusValue::Packed { - start_column: cols::LT, - packing: Packing::Direct, - }, - ], - ), - ] + let mut b = BusInteractionsBuilder::with_capacity(9); + let mu = Multiplicity::Column(cols::MU); + + // MSB16[lhs[2]] -> lhs_msb, MSB16[rhs[2]] -> rhs_msb. + b.send_msb16(cols::LHS_2, cols::LHS_MSB, &mu); + b.send_msb16(cols::RHS_2, cols::RHS_MSB, &mu); + + // IS_HALFWORD range checks on lhs_sub_rhs[0..4], lhs[1], rhs[1]. + for col in [ + cols::LHS_SUB_RHS_0, + cols::LHS_SUB_RHS_1, + cols::LHS_SUB_RHS_2, + cols::LHS_SUB_RHS_3, + cols::LHS_1, + cols::RHS_1, + ] { + b.send_halfword(col, &mu); + } + + // LT[lhs, rhs, signed] -> lt (receiver). DWordHHW reads 3 columns and + // produces 2 bus elements [lo32, hi32]; matches DWordWL senders like MEMW. + b.recv( + BusId::Lt, + mu, + vec![ + BusValue::Packed { + start_column: cols::LHS_0, + packing: Packing::DWordHHW, + }, + BusValue::Packed { + start_column: cols::RHS_0, + packing: Packing::DWordHHW, + }, + packed_direct(cols::SIGNED), + packed_direct(cols::LT), + ], + ); + + b.into_vec() } /// Compute virtual carry[0] and carry[1] for the addition rhs + lhs_sub_rhs = lhs diff --git a/prover/src/tables/mod.rs b/prover/src/tables/mod.rs index d8a92459a..e2d0dc1b6 100644 --- a/prover/src/tables/mod.rs +++ b/prover/src/tables/mod.rs @@ -23,6 +23,7 @@ pub mod types; pub mod bitwise; pub mod branch; +pub mod bus_builder; pub mod commit; pub mod cpu; pub mod decode; From a04e3f8fe01f7a58a4e68bbdf8ab2a65d29111ae Mon Sep 17 00:00:00 2001 From: diegokingston Date: Tue, 19 May 2026 16:44:33 -0300 Subject: [PATCH 06/10] refactor(prover): migrate shift/mul/dvrm bus_interactions to builder Apply BusInteractionsBuilder to the three remaining medium tables: - `shift.rs::bus_interactions` ~225 -> ~110 LOC - `mul.rs::bus_interactions` ~310 -> ~115 LOC - `dvrm.rs::bus_interactions` ~580 -> ~200 LOC Net -380 LOC. Each interaction reads as a spec line; the IS_HALFWORD, MSB16, and IS_B20 senders use the dedicated `send_halfword`, `send_msb16`, and `send_b20_linear` helpers, while heterogeneous interactions (Mul/Lt/Zero/Hwsl/Shift/Dvrm receivers) use the generic `send`/`recv` methods. Behavior unchanged; per-table tests pass (16 dvrm, 24 lt). Pre-existing UnknownSyscall(5) ELF failures unrelated. --- prover/src/tables/dvrm.rs | 381 +++++++++---------------------------- prover/src/tables/mul.rs | 250 +++++++----------------- prover/src/tables/shift.rs | 193 ++++++------------- 3 files changed, 222 insertions(+), 602 deletions(-) diff --git a/prover/src/tables/dvrm.rs b/prover/src/tables/dvrm.rs index 30352e125..7549ab830 100644 --- a/prover/src/tables/dvrm.rs +++ b/prover/src/tables/dvrm.rs @@ -38,6 +38,7 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing} use stark::table::TableView; use stark::trace::TraceTable; +use super::bus_builder::{BusInteractionsBuilder, packed_direct}; use super::types::{ BusId, FE, GoldilocksExtension, GoldilocksField, NEG_INV_2_16, NEG_INV_2_32, NEG_INV_2_48, NEG_INV_2_64, SHIFT_16, @@ -382,137 +383,52 @@ pub fn generate_dvrm_trace( /// - **Sends** ZERO lookups for div_by_zero, overflow, NEG carries (×5) /// - **Receives** DVRM lookups from CPU table (×2: quotient and remainder) pub fn bus_interactions() -> Vec { - let mut interactions = Vec::new(); + let mut b = BusInteractionsBuilder::with_capacity(20); + let mu_sum = Multiplicity::Sum(cols::MU_Q, cols::MU_R); + let signed = Multiplicity::Column(cols::SIGNED); - // DVRM-A1.i (IS_HALF[n[i]]) and DVRM-A2.i (IS_HALF[d[i]]) are assumptions: - // the CPU (sender) is responsible for range-checking n and d before sending - // to DVRM. The DVRM table does NOT send these IS_HALF lookups. + // DVRM-A1.i/A2.i (IS_HALF[n[i]] / IS_HALF[d[i]]) are sent by the CPU + // (sender side); the DVRM table does NOT replay them. - // ------------------------------------------------------------------------- - // DVRM-C13.i: IS_HALF[r[i]] (×4), multiplicity: μ_q + μ_r - // ------------------------------------------------------------------------- + // DVRM-C13.i: IS_HALF[r[i]] x4. for col in [cols::R_0, cols::R_1, cols::R_2, cols::R_3] { - interactions.push(BusInteraction::sender( - BusId::IsHalfword, - Multiplicity::Sum(cols::MU_Q, cols::MU_R), - vec![BusValue::Packed { - start_column: col, - packing: Packing::Direct, - }], - )); + b.send_halfword(col, &mu_sum); } - - // ------------------------------------------------------------------------- - // DVRM-C14.i: IS_HALF[n_sub_r[i]] (×4), multiplicity: μ_q + μ_r - // ------------------------------------------------------------------------- + // DVRM-C14.i: IS_HALF[n_sub_r[i]] x4. for col in [ cols::N_SUB_R_0, cols::N_SUB_R_1, cols::N_SUB_R_2, cols::N_SUB_R_3, ] { - interactions.push(BusInteraction::sender( - BusId::IsHalfword, - Multiplicity::Sum(cols::MU_Q, cols::MU_R), - vec![BusValue::Packed { - start_column: col, - packing: Packing::Direct, - }], - )); + b.send_halfword(col, &mu_sum); } - - // ------------------------------------------------------------------------- - // DVRM-C11.i: IS_HALF[q[i]] (×4), multiplicity: μ_q + μ_r - // ------------------------------------------------------------------------- + // DVRM-C11.i: IS_HALF[q[i]] x4. for col in [cols::Q_0, cols::Q_1, cols::Q_2, cols::Q_3] { - interactions.push(BusInteraction::sender( - BusId::IsHalfword, - Multiplicity::Sum(cols::MU_Q, cols::MU_R), - vec![BusValue::Packed { - start_column: col, - packing: Packing::Direct, - }], - )); + b.send_halfword(col, &mu_sum); } - // ------------------------------------------------------------------------- - // DVRM-C18 (SIGN): MSB16[sign_n; n[3]] when signed=1 - // Multiplicity: Column(SIGNED) = 0 or 1 per unique row. - // The trace builder deduplicates MSB16 lookups per unique op. - // ------------------------------------------------------------------------- - interactions.push(BusInteraction::sender( - BusId::Msb16, - Multiplicity::Column(cols::SIGNED), - vec![ - BusValue::Packed { - start_column: cols::N_3, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::SIGN_N, - packing: Packing::Direct, - }, - ], - )); + // DVRM-C18/C19/C20 (SIGN): MSB16[n[3]] -> sign_n, MSB16[r[3]] -> sign_r, + // MSB16[d[3]] -> sign_d when signed=1. Trace builder deduplicates per + // unique op, so multiplicity is Column(SIGNED) (0 or 1). + b.send_msb16(cols::N_3, cols::SIGN_N, &signed); + b.send_msb16(cols::R_3, cols::SIGN_R, &signed); + b.send_msb16(cols::D_3, cols::SIGN_D, &signed); - // ------------------------------------------------------------------------- - // DVRM-C19 (SIGN): MSB16[sign_r; r[3]] when signed=1 - // ------------------------------------------------------------------------- - interactions.push(BusInteraction::sender( - BusId::Msb16, - Multiplicity::Column(cols::SIGNED), - vec![ - BusValue::Packed { - start_column: cols::R_3, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::SIGN_R, - packing: Packing::Direct, - }, - ], - )); - - // ------------------------------------------------------------------------- - // DVRM-C20 (SIGN): MSB16[sign_d; d[3]] when signed=1 - // ------------------------------------------------------------------------- - interactions.push(BusInteraction::sender( - BusId::Msb16, - Multiplicity::Column(cols::SIGNED), - vec![ - BusValue::Packed { - start_column: cols::D_3, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::SIGN_D, - packing: Packing::Direct, - }, - ], - )); - - // ------------------------------------------------------------------------- - // DVRM-C2: LT[1-div_by_zero; abs_r, abs_d, 0] - // Verify |r| < |d| when d != 0 - // multiplicity: μ_q + μ_r - // ------------------------------------------------------------------------- - interactions.push(BusInteraction::sender( + // DVRM-C2: LT[abs_r < abs_d when d != 0]; multiplicity mu_q + mu_r. + b.send( BusId::Lt, - Multiplicity::Sum(cols::MU_Q, cols::MU_R), + mu_sum.clone(), vec![ - // abs_r as DWordWL (2 words → 2 elements) BusValue::Packed { start_column: cols::ABS_R_0, packing: Packing::DWordWL, }, - // abs_d as DWordWL (2 words → 2 elements) BusValue::Packed { start_column: cols::ABS_D_0, packing: Packing::DWordWL, }, - // signed = 0 (unsigned comparison of absolute values) BusValue::constant(0), - // lt_result = 1 - div_by_zero BusValue::linear(vec![ LinearTerm::Constant(1), LinearTerm::Column { @@ -521,109 +437,65 @@ pub fn bus_interactions() -> Vec { }, ]), ], - )); + ); - // ------------------------------------------------------------------------- - // DVRM-C9: MUL[n_sub_r::DWordWL; d, signed, q, sign_q, 0] - // Verify n - r = d * q (lower 64 bits) - // multiplicity: μ_q + μ_r - // ------------------------------------------------------------------------- - interactions.push(BusInteraction::sender( + // DVRM-C9: MUL[d, signed, q, sign_q] -> n_sub_r (lo); selector = 0. + b.send( BusId::Mul, - Multiplicity::Sum(cols::MU_Q, cols::MU_R), + mu_sum.clone(), vec![ - // d as DWordHL (lhs) BusValue::Packed { start_column: cols::D_0, packing: Packing::DWordHL, }, - // lhs_signed = signed - BusValue::Packed { - start_column: cols::SIGNED, - packing: Packing::Direct, - }, - // q as DWordHL (rhs) + packed_direct(cols::SIGNED), BusValue::Packed { start_column: cols::Q_0, packing: Packing::DWordHL, }, - // rhs_signed = sign_q - BusValue::Packed { - start_column: cols::SIGN_Q, - packing: Packing::Direct, - }, - // result: n_sub_r as DWordHL (lower 64 bits of d*q) + packed_direct(cols::SIGN_Q), BusValue::Packed { start_column: cols::N_SUB_R_0, packing: Packing::DWordHL, }, - // muldiv_selector = 0 (lo) BusValue::constant(0), ], - )); + ); - // ------------------------------------------------------------------------- - // DVRM-C10: MUL[extension_n_sub_r::DWordWL; d, signed, q, sign_q, 1] - // Verify upper 64 bits of d * q = sign extension of n_sub_r - // multiplicity: μ_q + μ_r - // ------------------------------------------------------------------------- - interactions.push(BusInteraction::sender( + // DVRM-C10: MUL[d, signed, q, sign_q] -> extension of n_sub_r (hi); selector = 1. + // Each result halfword equals sign_n_sub_r * 65535, packed as DWordWL [lo32, hi32]. + let ext_coeff = (SIGN_FILL + SIGN_FILL * SHIFT_16) as i64; + b.send( BusId::Mul, - Multiplicity::Sum(cols::MU_Q, cols::MU_R), + mu_sum.clone(), vec![ - // d as DWordHL (lhs) BusValue::Packed { start_column: cols::D_0, packing: Packing::DWordHL, }, - // lhs_signed = signed - BusValue::Packed { - start_column: cols::SIGNED, - packing: Packing::Direct, - }, - // q as DWordHL (rhs) + packed_direct(cols::SIGNED), BusValue::Packed { start_column: cols::Q_0, packing: Packing::DWordHL, }, - // rhs_signed = sign_q - BusValue::Packed { - start_column: cols::SIGN_Q, - packing: Packing::Direct, - }, - // result: sign extension of n_sub_r as DWordHL - // Each halfword = sign_n_sub_r * 65535 - // lo32 = sign_n_sub_r * (65535 + 65535 * 2^16) = sign_n_sub_r * 0xFFFFFFFF - // hi32 = same + packed_direct(cols::SIGN_Q), BusValue::linear(vec![LinearTerm::Column { - coefficient: (SIGN_FILL + SIGN_FILL * SHIFT_16) as i64, + coefficient: ext_coeff, column: cols::SIGN_N_SUB_R, }]), BusValue::linear(vec![LinearTerm::Column { - coefficient: (SIGN_FILL + SIGN_FILL * SHIFT_16) as i64, + coefficient: ext_coeff, column: cols::SIGN_N_SUB_R, }]), - // muldiv_selector = 1 (hi) BusValue::constant(1), ], - )); - - // ========================================================================= - // ZERO interactions (C3, C5, C8, C20) - // ========================================================================= + ); - // ------------------------------------------------------------------------- - // DVRM-C3: sign_r ⇒ NEG - // carry[0] = 2^-32 * ((r::DWordWL)[0] + abs_r[0]) - // carry[1] = 2^-32 * ((r::DWordWL)[1] + abs_r[1] + carry[0]) - // ZERO[1-carry[0]; r[0]+r[1]] with multiplicity sign_r - // ZERO[1-carry[1]; r[0]+r[1]+r[2]+r[3]] with multiplicity sign_r - // ------------------------------------------------------------------------- - - // C3a: 1 - carry[0] = 1 - 2^-32*r[0] - 2^-16*r[1] - 2^-32*abs_r[0] - interactions.push(BusInteraction::sender( + // DVRM-C3 (NEG): two ZERO checks gated by sign_r. + let sign_r = Multiplicity::Column(cols::SIGN_R); + b.send( BusId::Zero, - Multiplicity::Column(cols::SIGN_R), + sign_r.clone(), vec![ BusValue::linear(vec![ LinearTerm::Column { @@ -651,13 +523,10 @@ pub fn bus_interactions() -> Vec { }, ]), ], - )); - - // C3b: 1 - carry[1] = 1 - 2^-32*r[2] - 2^-16*r[3] - 2^-32*abs_r[1] - // - 2^-64*r[0] - 2^-48*r[1] - 2^-64*abs_r[0] - interactions.push(BusInteraction::sender( + ); + b.send( BusId::Zero, - Multiplicity::Column(cols::SIGN_R), + sign_r, vec![ BusValue::linear(vec![ LinearTerm::Column { @@ -679,7 +548,6 @@ pub fn bus_interactions() -> Vec { ]), BusValue::linear(vec![ LinearTerm::Constant(1), - // Current-level terms (carry[1] direct) LinearTerm::ColumnUnsigned { coefficient: NEG_INV_2_32, column: cols::ABS_R_1, @@ -692,7 +560,6 @@ pub fn bus_interactions() -> Vec { coefficient: NEG_INV_2_16, column: cols::R_3, }, - // carry[0]-dependent terms (shifted by additional 2^-32) LinearTerm::ColumnUnsigned { coefficient: NEG_INV_2_64, column: cols::ABS_R_0, @@ -707,18 +574,13 @@ pub fn bus_interactions() -> Vec { }, ]), ], - )); - - // ------------------------------------------------------------------------- - // DVRM-C5: sign_d ⇒ NEG - // carry[0] = 2^-32 * ((d::DWordWL)[0] + abs_d[0]) - // carry[1] = 2^-32 * ((d::DWordWL)[1] + abs_d[1] + carry[0]) - // ------------------------------------------------------------------------- + ); - // C5a: 1 - carry[0] = 1 - 2^-32*d[0] - 2^-16*d[1] - 2^-32*abs_d[0] - interactions.push(BusInteraction::sender( + // DVRM-C5 (NEG): two ZERO checks gated by sign_d. + let sign_d = Multiplicity::Column(cols::SIGN_D); + b.send( BusId::Zero, - Multiplicity::Column(cols::SIGN_D), + sign_d.clone(), vec![ BusValue::linear(vec![ LinearTerm::Column { @@ -746,13 +608,10 @@ pub fn bus_interactions() -> Vec { }, ]), ], - )); - - // C5b: 1 - carry[1] = 1 - 2^-32*d[2] - 2^-16*d[3] - 2^-32*abs_d[1] - // - 2^-64*d[0] - 2^-48*d[1] - 2^-64*abs_d[0] - interactions.push(BusInteraction::sender( + ); + b.send( BusId::Zero, - Multiplicity::Column(cols::SIGN_D), + sign_d, vec![ BusValue::linear(vec![ LinearTerm::Column { @@ -774,7 +633,6 @@ pub fn bus_interactions() -> Vec { ]), BusValue::linear(vec![ LinearTerm::Constant(1), - // Current-level terms (carry[1] direct) LinearTerm::ColumnUnsigned { coefficient: NEG_INV_2_32, column: cols::ABS_D_1, @@ -787,7 +645,6 @@ pub fn bus_interactions() -> Vec { coefficient: NEG_INV_2_16, column: cols::D_3, }, - // carry[0]-dependent terms (shifted by additional 2^-32) LinearTerm::ColumnUnsigned { coefficient: NEG_INV_2_64, column: cols::ABS_D_0, @@ -802,16 +659,14 @@ pub fn bus_interactions() -> Vec { }, ]), ], - )); + ); - // ------------------------------------------------------------------------- - // DVRM-C8: ZERO[overflow; overflow_sum] multiplicity: μ_q + μ_r - // overflow_sum = n[0]+n[1]+n[2]+(n[3]-2^15*sign_n)+(1-sign_n)+(65535-d[0])+...+(65535-d[3]) - // Each term ≥ 0, total ≤ 2^19. Sum is 0 iff overflow condition holds. - // ------------------------------------------------------------------------- - interactions.push(BusInteraction::sender( + // DVRM-C8: ZERO[overflow; overflow_sum]. overflow_sum is the long linear + // combination of n[*], sign_n, and d[*] derived in the spec; vanishes iff + // overflow condition holds. + b.send( BusId::Zero, - Multiplicity::Sum(cols::MU_Q, cols::MU_R), + mu_sum.clone(), vec![ BusValue::linear(vec![ LinearTerm::Column { @@ -831,10 +686,10 @@ pub fn bus_interactions() -> Vec { column: cols::N_3, }, LinearTerm::Column { - coefficient: -32769, // -(2^15 + 1) * sign_n + coefficient: -32769, column: cols::SIGN_N, }, - LinearTerm::Constant(1 + 4 * 65535), // 262141 + LinearTerm::Constant(1 + 4 * 65535), LinearTerm::Column { coefficient: -1, column: cols::D_0, @@ -852,20 +707,14 @@ pub fn bus_interactions() -> Vec { column: cols::D_3, }, ]), - BusValue::Packed { - start_column: cols::OVERFLOW, - packing: Packing::Direct, - }, + packed_direct(cols::OVERFLOW), ], - )); + ); - // ------------------------------------------------------------------------- - // DVRM-C17: ZERO[div_by_zero; d[0]+d[1]+d[2]+d[3]] - // multiplicity: μ_q + μ_r - // ------------------------------------------------------------------------- - interactions.push(BusInteraction::sender( + // DVRM-C17: ZERO[div_by_zero; sum(d[*])]. + b.send( BusId::Zero, - Multiplicity::Sum(cols::MU_Q, cols::MU_R), + mu_sum, vec![ BusValue::linear(vec![ LinearTerm::Column { @@ -885,80 +734,36 @@ pub fn bus_interactions() -> Vec { column: cols::D_3, }, ]), - BusValue::Packed { - start_column: cols::DIV_BY_ZERO, - packing: Packing::Direct, - }, + packed_direct(cols::DIV_BY_ZERO), ], - )); + ); - // ------------------------------------------------------------------------- - // DVRM-C21: Receiver for quotient result - // DVRM[q::DWordWL; n, d, signed, 0] with multiplicity -μ_q - // ------------------------------------------------------------------------- - interactions.push(BusInteraction::receiver( - BusId::Dvrm, - Multiplicity::Column(cols::MU_Q), - vec![ - // n as DWordHL (4 halfwords → 2 words) - BusValue::Packed { - start_column: cols::N_0, - packing: Packing::DWordHL, - }, - // d as DWordHL - BusValue::Packed { - start_column: cols::D_0, - packing: Packing::DWordHL, - }, - // signed - BusValue::Packed { - start_column: cols::SIGNED, - packing: Packing::Direct, - }, - // q as DWordHL (result) - BusValue::Packed { - start_column: cols::Q_0, - packing: Packing::DWordHL, - }, - // muldiv_selector = 0 (quotient) - BusValue::constant(0), - ], - )); - - // ------------------------------------------------------------------------- - // DVRM-C22: Receiver for remainder result - // DVRM[r::DWordWL; n, d, signed, 1] with multiplicity -μ_r - // ------------------------------------------------------------------------- - interactions.push(BusInteraction::receiver( - BusId::Dvrm, - Multiplicity::Column(cols::MU_R), - vec![ - // n as DWordHL - BusValue::Packed { - start_column: cols::N_0, - packing: Packing::DWordHL, - }, - // d as DWordHL - BusValue::Packed { - start_column: cols::D_0, - packing: Packing::DWordHL, - }, - // signed - BusValue::Packed { - start_column: cols::SIGNED, - packing: Packing::Direct, - }, - // r as DWordHL (result) - BusValue::Packed { - start_column: cols::R_0, - packing: Packing::DWordHL, - }, - // muldiv_selector = 1 (remainder) - BusValue::constant(1), - ], - )); + // DVRM-C21/C22: receivers for quotient (selector = 0) and remainder (selector = 1). + for (result_col, mult_col, selector) in [(cols::Q_0, cols::MU_Q, 0), (cols::R_0, cols::MU_R, 1)] + { + b.recv( + BusId::Dvrm, + Multiplicity::Column(mult_col), + vec![ + BusValue::Packed { + start_column: cols::N_0, + packing: Packing::DWordHL, + }, + BusValue::Packed { + start_column: cols::D_0, + packing: Packing::DWordHL, + }, + packed_direct(cols::SIGNED), + BusValue::Packed { + start_column: result_col, + packing: Packing::DWordHL, + }, + BusValue::constant(selector), + ], + ); + } - interactions + b.into_vec() } // ========================================================================= diff --git a/prover/src/tables/mul.rs b/prover/src/tables/mul.rs index ecb72a4d1..87ea6faf3 100644 --- a/prover/src/tables/mul.rs +++ b/prover/src/tables/mul.rs @@ -38,6 +38,7 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing} use stark::table::TableView; use stark::trace::TraceTable; +use super::bus_builder::{BusInteractionsBuilder, packed_direct}; use super::types::{ BusId, FE, GoldilocksExtension, GoldilocksField, INV_2_32, INV_2_64, INV_2_96, INV_2_128, NEG_INV_2_16, NEG_INV_2_32, NEG_INV_2_48, NEG_INV_2_64, NEG_INV_2_80, NEG_INV_2_96, @@ -362,84 +363,36 @@ pub fn generate_mul_trace( /// - **Sends** IS_B20 lookups for carry range checks (×4) /// - **Receives** MUL lookups from CPU table (×2: lo and hi) pub fn bus_interactions() -> Vec { - let mut interactions = Vec::new(); - - // ------------------------------------------------------------------------- - // MSB16 lookups for sign bit extraction - // ------------------------------------------------------------------------- - // MSB16[lhs[3]] -> lhs_is_negative (when lhs_signed=1) - interactions.push(BusInteraction::sender( - BusId::Msb16, - Multiplicity::Column(cols::LHS_SIGNED), - vec![ - BusValue::Packed { - start_column: cols::LHS_3, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::LHS_IS_NEGATIVE, - packing: Packing::Direct, - }, - ], - )); - - // MSB16[rhs[3]] -> rhs_is_negative (when rhs_signed=1) - interactions.push(BusInteraction::sender( - BusId::Msb16, - Multiplicity::Column(cols::RHS_SIGNED), - vec![ - BusValue::Packed { - start_column: cols::RHS_3, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::RHS_IS_NEGATIVE, - packing: Packing::Direct, - }, - ], - )); - - // ------------------------------------------------------------------------- - // IS_HALF lookups for lo range checks (multiplicity: mu_lo + mu_hi) - // ------------------------------------------------------------------------- + let mut b = BusInteractionsBuilder::with_capacity(11); + let mu_sum = Multiplicity::Sum(cols::MU_LO, cols::MU_HI); + + // MSB16 sign extraction for lhs[3] and rhs[3]. + b.send_msb16( + cols::LHS_3, + cols::LHS_IS_NEGATIVE, + &Multiplicity::Column(cols::LHS_SIGNED), + ); + b.send_msb16( + cols::RHS_3, + cols::RHS_IS_NEGATIVE, + &Multiplicity::Column(cols::RHS_SIGNED), + ); + + // IS_HALF range checks on lo[0..4] and hi[0..4] (mult = mu_lo + mu_hi). for col in [cols::LO_0, cols::LO_1, cols::LO_2, cols::LO_3] { - interactions.push(BusInteraction::sender( - BusId::IsHalfword, - Multiplicity::Sum(cols::MU_LO, cols::MU_HI), - vec![BusValue::Packed { - start_column: col, - packing: Packing::Direct, - }], - )); + b.send_halfword(col, &mu_sum); } - - // ------------------------------------------------------------------------- - // IS_HALF lookups for hi range checks (multiplicity: mu_lo + mu_hi) - // ------------------------------------------------------------------------- for col in [cols::HI_0, cols::HI_1, cols::HI_2, cols::HI_3] { - interactions.push(BusInteraction::sender( - BusId::IsHalfword, - Multiplicity::Sum(cols::MU_LO, cols::MU_HI), - vec![BusValue::Packed { - start_column: col, - packing: Packing::Direct, - }], - )); + b.send_halfword(col, &mu_sum); } - // ------------------------------------------------------------------------- - // IS_B20 lookups for carry range checks (multiplicity: mu_lo + mu_hi) - // Carries are virtual columns computed as linear combinations: - // carry[0] = 2^-32 * (raw_product[0] - res[0]) - // carry[i] = 2^-32 * (raw_product[i] + carry[i-1] - res[i]) - // where res = [lo_word0, lo_word1, hi_word0, hi_word1] - // ------------------------------------------------------------------------- - - // carry[0] = 2^-32 * raw_product[0] - 2^-32 * lo[0] - 2^-16 * lo[1] - interactions.push(BusInteraction::sender( - BusId::IsB20, - Multiplicity::Sum(cols::MU_LO, cols::MU_HI), - vec![BusValue::linear(vec![ + // IS_B20 range checks on the four virtual carries. Each carry is a linear + // combination of raw_product[*] and lo[*]/hi[*] limbs (see compute_carries): + // carry[0] = 2^-32 * raw_product[0] - 2^-32 * lo[0] - 2^-16 * lo[1] + // carry[i] = 2^-32 * raw_product[i] + (next-shifted predecessors) - (lo/hi limbs) + b.send_b20_linear( + &mu_sum, + vec![ LinearTerm::ColumnUnsigned { coefficient: INV_2_32, column: cols::RAW_PRODUCT_0, @@ -452,15 +405,11 @@ pub fn bus_interactions() -> Vec { coefficient: NEG_INV_2_16, column: cols::LO_1, }, - ])], - )); - - // carry[1] = 2^-32 * raw_product[1] + 2^-64 * raw_product[0] - // - 2^-64 * lo[0] - 2^-48 * lo[1] - 2^-32 * lo[2] - 2^-16 * lo[3] - interactions.push(BusInteraction::sender( - BusId::IsB20, - Multiplicity::Sum(cols::MU_LO, cols::MU_HI), - vec![BusValue::linear(vec![ + ], + ); + b.send_b20_linear( + &mu_sum, + vec![ LinearTerm::ColumnUnsigned { coefficient: INV_2_32, column: cols::RAW_PRODUCT_1, @@ -485,16 +434,11 @@ pub fn bus_interactions() -> Vec { coefficient: NEG_INV_2_16, column: cols::LO_3, }, - ])], - )); - - // carry[2] = 2^-32 * raw_product[2] + 2^-64 * raw_product[1] + 2^-96 * raw_product[0] - // - 2^-96 * lo[0] - 2^-80 * lo[1] - 2^-64 * lo[2] - 2^-48 * lo[3] - // - 2^-32 * hi[0] - 2^-16 * hi[1] - interactions.push(BusInteraction::sender( - BusId::IsB20, - Multiplicity::Sum(cols::MU_LO, cols::MU_HI), - vec![BusValue::linear(vec![ + ], + ); + b.send_b20_linear( + &mu_sum, + vec![ LinearTerm::ColumnUnsigned { coefficient: INV_2_32, column: cols::RAW_PRODUCT_2, @@ -531,16 +475,11 @@ pub fn bus_interactions() -> Vec { coefficient: NEG_INV_2_16, column: cols::HI_1, }, - ])], - )); - - // carry[3] = 2^-32 * raw_product[3] + 2^-64 * raw_product[2] + 2^-96 * raw_product[1] + 2^-128 * raw_product[0] - // - 2^-128 * lo[0] - 2^-112 * lo[1] - 2^-96 * lo[2] - 2^-80 * lo[3] - // - 2^-64 * hi[0] - 2^-48 * hi[1] - 2^-32 * hi[2] - 2^-16 * hi[3] - interactions.push(BusInteraction::sender( - BusId::IsB20, - Multiplicity::Sum(cols::MU_LO, cols::MU_HI), - vec![BusValue::linear(vec![ + ], + ); + b.send_b20_linear( + &mu_sum, + vec![ LinearTerm::ColumnUnsigned { coefficient: INV_2_32, column: cols::RAW_PRODUCT_3, @@ -589,86 +528,37 @@ pub fn bus_interactions() -> Vec { coefficient: NEG_INV_2_16, column: cols::HI_3, }, - ])], - )); - - // ------------------------------------------------------------------------- - // MUL receiver for lo result - // ------------------------------------------------------------------------- - // MUL[lhs, lhs_signed, rhs, rhs_signed, lo, 0] per spec MUL-C7 - interactions.push(BusInteraction::receiver( - BusId::Mul, - Multiplicity::Column(cols::MU_LO), - vec![ - // lhs as DWordHL (4 halfwords -> 2 words) - BusValue::Packed { - start_column: cols::LHS_0, - packing: Packing::DWordHL, - }, - // lhs_signed - BusValue::Packed { - start_column: cols::LHS_SIGNED, - packing: Packing::Direct, - }, - // rhs as DWordHL - BusValue::Packed { - start_column: cols::RHS_0, - packing: Packing::DWordHL, - }, - // rhs_signed - BusValue::Packed { - start_column: cols::RHS_SIGNED, - packing: Packing::Direct, - }, - // lo as DWordHL (result) - BusValue::Packed { - start_column: cols::LO_0, - packing: Packing::DWordHL, - }, - // muldiv_selector = 0 (lo) - BusValue::constant(0), ], - )); - - // ------------------------------------------------------------------------- - // MUL receiver for hi result - // ------------------------------------------------------------------------- - // MUL[lhs, lhs_signed, rhs, rhs_signed, hi, 1] per spec MUL-C8 - interactions.push(BusInteraction::receiver( - BusId::Mul, - Multiplicity::Column(cols::MU_HI), - vec![ - // lhs as DWordHL - BusValue::Packed { - start_column: cols::LHS_0, - packing: Packing::DWordHL, - }, - // lhs_signed - BusValue::Packed { - start_column: cols::LHS_SIGNED, - packing: Packing::Direct, - }, - // rhs as DWordHL - BusValue::Packed { - start_column: cols::RHS_0, - packing: Packing::DWordHL, - }, - // rhs_signed - BusValue::Packed { - start_column: cols::RHS_SIGNED, - packing: Packing::Direct, - }, - // hi as DWordHL (result) - BusValue::Packed { - start_column: cols::HI_0, - packing: Packing::DWordHL, - }, - // muldiv_selector = 1 (hi) - BusValue::constant(1), - ], - )); + ); + + // MUL receivers: lo result (selector = 0, MUL-C7) and hi result (selector = 1, MUL-C8). + for (result_col, mult_col, selector) in + [(cols::LO_0, cols::MU_LO, 0), (cols::HI_0, cols::MU_HI, 1)] + { + b.recv( + BusId::Mul, + Multiplicity::Column(mult_col), + vec![ + BusValue::Packed { + start_column: cols::LHS_0, + packing: Packing::DWordHL, + }, + packed_direct(cols::LHS_SIGNED), + BusValue::Packed { + start_column: cols::RHS_0, + packing: Packing::DWordHL, + }, + packed_direct(cols::RHS_SIGNED), + BusValue::Packed { + start_column: result_col, + packing: Packing::DWordHL, + }, + BusValue::constant(selector), + ], + ); + } - interactions + b.into_vec() } // ========================================================================= diff --git a/prover/src/tables/shift.rs b/prover/src/tables/shift.rs index 9014799e5..9812cf905 100644 --- a/prover/src/tables/shift.rs +++ b/prover/src/tables/shift.rs @@ -24,6 +24,7 @@ use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing} use stark::table::TableView; use stark::trace::TraceTable; +use super::bus_builder::{BusInteractionsBuilder, packed_direct}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, SHIFT_16}; // ========================================================================= @@ -377,48 +378,33 @@ pub fn generate_shift_trace( /// Creates all bus interactions for the SHIFT table. pub fn bus_interactions() -> Vec { - let mut interactions = Vec::with_capacity(11); + let mut b = BusInteractionsBuilder::with_capacity(11); + let mu = Multiplicity::Column(cols::MU); + let one_minus_zbs = Multiplicity::Negated(cols::ZBS); - // SHIFT-C14: MSB16[in[3]] → is_negative | signed - interactions.push(BusInteraction::sender( - BusId::Msb16, - Multiplicity::Column(cols::SIGNED), - vec![ - // in[3] as halfword: x + 256*y (in[3] is stored as single Half column) - BusValue::Packed { - start_column: cols::IN_3, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::IS_NEGATIVE, - packing: Packing::Direct, - }, - ], - )); + // SHIFT-C14: MSB16[in[3]] -> is_negative | signed. + b.send_msb16( + cols::IN_3, + cols::IS_NEGATIVE, + &Multiplicity::Column(cols::SIGNED), + ); - // SHIFT-C1: AND_BYTE[shift, 15] → bit_shift | left (= μ - direction) - interactions.push(BusInteraction::sender( + // SHIFT-C1: AND_BYTE[shift, 15] -> bit_shift | mu - direction. + b.send( BusId::AndByte, Multiplicity::Diff(cols::MU, cols::DIRECTION), vec![ - BusValue::Packed { - start_column: cols::SHIFT_AMOUNT, - packing: Packing::Direct, - }, + packed_direct(cols::SHIFT_AMOUNT), BusValue::constant(15), - BusValue::Packed { - start_column: cols::BIT_SHIFT, - packing: Packing::Direct, - }, + packed_direct(cols::BIT_SHIFT), ], - )); - - // SHIFT-C2: AND_BYTE[256 - zbs * 16 - shift, 15] → bit_shift | right (= direction) - // 256 - shift would overflow a byte when shift = 0. Subtracting zbs * 16 keeps it in - // [0,255]. - // When zbs = 1, shift is a multiple of 16 (i.e. shift ∈ [0, 240]), so - // 256 - 16 - shift ∈ [0,255]. - interactions.push(BusInteraction::sender( + ); + + // SHIFT-C2: AND_BYTE[256 - zbs*16 - shift, 15] -> bit_shift | direction. + // 256 - shift would overflow a byte when shift = 0. Subtracting zbs*16 + // keeps it in [0, 255]: when zbs = 1, shift is a multiple of 16 (in + // [0, 240]), so 256 - 16 - shift in [0, 255]. + b.send( BusId::AndByte, Multiplicity::Column(cols::DIRECTION), vec![ @@ -434,78 +420,45 @@ pub fn bus_interactions() -> Vec { }, ]), BusValue::constant(15), - BusValue::Packed { - start_column: cols::BIT_SHIFT, - packing: Packing::Direct, - }, + packed_direct(cols::BIT_SHIFT), ], - )); + ); - // SHIFT-C3: ZERO[bit_shift] → zbs | μ - // ZERO receiver expects [x + 256*y + 65536*z, zero_flag] - // bit_shift is a byte (0-15), so y=0, z=0: just send bit_shift directly - interactions.push(BusInteraction::sender( + // SHIFT-C3: ZERO[bit_shift] -> zbs | mu. + // ZERO receiver expects [x + 256*y + 65536*z, zero_flag]; bit_shift is a + // byte (0-15), so y=0, z=0 means just sending bit_shift directly. + b.send( BusId::Zero, - Multiplicity::Column(cols::MU), - vec![ - BusValue::Packed { - start_column: cols::BIT_SHIFT, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::ZBS, - packing: Packing::Direct, - }, - ], - )); + mu.clone(), + vec![packed_direct(cols::BIT_SHIFT), packed_direct(cols::ZBS)], + ); - // SHIFT-C4.i: HWSL[in[i], bit_shift] → [X[i], Y[i]] for i∈[0,3] | 1 - zbs - // HWSL receiver: [x + 256*y (halfword), z (shift amount), SLL, SLLC] - let one_minus_zbs = Multiplicity::Negated(cols::ZBS); + // SHIFT-C4.i: HWSL[in[i], bit_shift] -> [X[i], Y[i]] for i in 0..4 | 1 - zbs. for i in 0..4 { - interactions.push(BusInteraction::sender( + b.send( BusId::Hwsl, one_minus_zbs.clone(), vec![ - BusValue::Packed { - start_column: cols::IN[i], - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::BIT_SHIFT, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::X[i], - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::Y[i], - packing: Packing::Direct, - }, + packed_direct(cols::IN[i]), + packed_direct(cols::BIT_SHIFT), + packed_direct(cols::X[i]), + packed_direct(cols::Y[i]), ], - )); + ); } - // SHIFT-C7: HWSL[extension, bit_shift] → [X[4], extension - X[4]] | 1 - zbs - // extension = 65535 * is_negative (virtual) - // second output = extension - X[4] (the carry, expressed as a linear combination) - interactions.push(BusInteraction::sender( + // SHIFT-C7: HWSL[extension, bit_shift] -> [X[4], extension - X[4]] | 1 - zbs. + // extension = 65535 * is_negative (virtual). + b.send( BusId::Hwsl, - one_minus_zbs.clone(), + one_minus_zbs, vec![ BusValue::linear(vec![LinearTerm::Column { coefficient: 65535, column: cols::IS_NEGATIVE, }]), - BusValue::Packed { - start_column: cols::BIT_SHIFT, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::X_4, - packing: Packing::Direct, - }, + packed_direct(cols::BIT_SHIFT), + packed_direct(cols::X_4), BusValue::linear(vec![ LinearTerm::Column { coefficient: 65535, @@ -517,21 +470,15 @@ pub fn bus_interactions() -> Vec { }, ]), ], - )); + ); - // SHIFT-C11: AND_BYTE[encoded_limb; shift, mask] | μ - // encoded = (1 - ls[0]) + 15*ls[1] + 31*ls[2] + 47*ls[3] - // mask = 48 - 32 * word_instr - interactions.push(BusInteraction::sender( + // SHIFT-C11: AND_BYTE[shift, mask] -> encoded_limb | mu. + // mask = 48 - 32 * word_instr; encoded = 48 - 48*ls_raw[0] - 32*ls_raw[1] - 16*ls_raw[2]. + b.send( BusId::AndByte, - Multiplicity::Column(cols::MU), + mu.clone(), vec![ - // first input: shift - BusValue::Packed { - start_column: cols::SHIFT_AMOUNT, - packing: Packing::Direct, - }, - // second input: mask = 48 - 32 * word_instr + packed_direct(cols::SHIFT_AMOUNT), BusValue::linear(vec![ LinearTerm::Constant(48), LinearTerm::Column { @@ -539,10 +486,6 @@ pub fn bus_interactions() -> Vec { column: cols::WORD_INSTR, }, ]), - // result: encoded limb_shift - // = (1 - ls[0]) + 15*ls[1] + 31*ls[2] + 47*ls[3] - // substituting ls[3] = 1 - ls_raw[0] - ls_raw[1] - ls_raw[2]: - // = 48 - 48*ls_raw[0] - 32*ls_raw[1] - 16*ls_raw[2] BusValue::linear(vec![ LinearTerm::Constant(48), LinearTerm::Column { @@ -559,47 +502,29 @@ pub fn bus_interactions() -> Vec { }, ]), ], - )); + ); - // SHIFT-C15: SHIFT[out; in, shift, direction, signed, word_instr] | -μ (receiver) - interactions.push(BusInteraction::receiver( + // SHIFT-C15: SHIFT[out; in, shift, direction, signed, word_instr] | mu (receiver). + b.recv( BusId::Shift, - Multiplicity::Column(cols::MU), + mu, vec![ - // out as DWordWL (2 elements) BusValue::Packed { start_column: cols::OUT_0, packing: Packing::DWordWL, }, - // in as DWordHL (4 halfwords → 2 elements) BusValue::Packed { start_column: cols::IN_0, packing: Packing::DWordHL, }, - // shift - BusValue::Packed { - start_column: cols::SHIFT_AMOUNT, - packing: Packing::Direct, - }, - // direction - BusValue::Packed { - start_column: cols::DIRECTION, - packing: Packing::Direct, - }, - // signed - BusValue::Packed { - start_column: cols::SIGNED, - packing: Packing::Direct, - }, - // word_instr - BusValue::Packed { - start_column: cols::WORD_INSTR, - packing: Packing::Direct, - }, + packed_direct(cols::SHIFT_AMOUNT), + packed_direct(cols::DIRECTION), + packed_direct(cols::SIGNED), + packed_direct(cols::WORD_INSTR), ], - )); + ); - interactions + b.into_vec() } // ========================================================================= From 6d838d3ff9d21e1664d844afce52b328bde8fc86 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Tue, 19 May 2026 16:51:46 -0300 Subject: [PATCH 07/10] refactor(prover/keccak_rnd): migrate bus_interactions to builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `keccak_rnd::bus_interactions` was the largest single bus_interactions function in the crate (~460 LOC of repeated XOR_BYTE / AND_BYTE / IS_BYTE / HWSL senders). Migrate to BusInteractionsBuilder. Add two more helpers to bus_builder: - `send_is_byte(col, &mu)` — IS_BYTE on a direct-packed column. - `send_xor_byte(x, y, result, &mu)` — XOR_BYTE on three direct cols. These collapse the four XOR_BYTE/IS_BYTE walls (theta Cxz chain, theta final, chi, iota; plus rho IS_BYTE range checks) into one-line calls. Net -215 LOC on keccak_rnd.rs (~460 -> ~245 in the function); the rest of the file (column layout, constraints) is unchanged. --- prover/src/tables/bus_builder.rs | 31 +++ prover/src/tables/keccak_rnd.rs | 341 +++++++++---------------------- 2 files changed, 126 insertions(+), 246 deletions(-) diff --git a/prover/src/tables/bus_builder.rs b/prover/src/tables/bus_builder.rs index 8099e77d6..fdcb2b4a0 100644 --- a/prover/src/tables/bus_builder.rs +++ b/prover/src/tables/bus_builder.rs @@ -103,6 +103,37 @@ impl BusInteractionsBuilder { self } + /// Send an IS_BYTE range check on a single direct-packed column. + /// IS_BYTE[col] + pub fn send_is_byte(&mut self, col: usize, mult: &Multiplicity) -> &mut Self { + self.inner.push(BusInteraction::sender( + BusId::IsByte, + mult.clone(), + vec![packed_direct(col)], + )); + self + } + + /// Send an XOR_BYTE lookup over three direct-packed columns (x, y, x ^ y). + pub fn send_xor_byte( + &mut self, + x_col: usize, + y_col: usize, + result_col: usize, + mult: &Multiplicity, + ) -> &mut Self { + self.inner.push(BusInteraction::sender( + BusId::XorByte, + mult.clone(), + vec![ + packed_direct(x_col), + packed_direct(y_col), + packed_direct(result_col), + ], + )); + self + } + // ------------------------------------------------------------------------- // Receiver helpers // ------------------------------------------------------------------------- diff --git a/prover/src/tables/keccak_rnd.rs b/prover/src/tables/keccak_rnd.rs index 277281583..4937d5202 100644 --- a/prover/src/tables/keccak_rnd.rs +++ b/prover/src/tables/keccak_rnd.rs @@ -30,9 +30,10 @@ use executor::vm::instruction::execution::{KECCAK_RC, KECCAK_RHO}; use stark::constraints::transition::{TransitionConstraint, TransitionConstraintEvaluator}; -use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; +use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity}; use stark::trace::TraceTable; +use super::bus_builder::{BusInteractionsBuilder, packed_direct}; use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField}; // ========================================================================= @@ -443,57 +444,31 @@ pub fn generate_keccak_rnd_trace( #[allow(clippy::needless_range_loop)] pub fn bus_interactions() -> Vec { - let mut interactions = Vec::with_capacity(1371); + let mut b = BusInteractionsBuilder::with_capacity(1371); + let mu = Multiplicity::Column(cols::MU); - // --- IO group (3) --- - - // 1. KECCAK bus: receive (timestamp, round, start[200]) - // Per spec keccak_round.toml: input = ["timestamp", "round", "start"] where - // start is [[[Byte, 8], 5], 5] — 200 Byte elements, each its own bus element. + // KECCAK bus receiver: (timestamp, round, start[200]). { let mut values = vec![ - BusValue::Packed { - start_column: cols::TIMESTAMP_0, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::TIMESTAMP_1, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::ROUND, - packing: Packing::Direct, - }, + packed_direct(cols::TIMESTAMP_0), + packed_direct(cols::TIMESTAMP_1), + packed_direct(cols::ROUND), ]; for x in 0..5 { for y in 0..5 { - for b in 0..8 { - values.push(BusValue::Packed { - start_column: cols::start(x, y, b), - packing: Packing::Direct, - }); + for byte in 0..8 { + values.push(packed_direct(cols::start(x, y, byte))); } } } - interactions.push(BusInteraction::receiver( - BusId::Keccak, - Multiplicity::Column(cols::MU), - values, - )); + b.recv(BusId::Keccak, mu.clone(), values); } - // 2. KECCAK bus: send (timestamp, round+1, out[200]) - // out[0][0] = iota, out[x][y] = chi for (x,y) != (0,0) + // KECCAK bus sender: (timestamp, round+1, out[200]). { let mut values = vec![ - BusValue::Packed { - start_column: cols::TIMESTAMP_0, - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::TIMESTAMP_1, - packing: Packing::Direct, - }, + packed_direct(cols::TIMESTAMP_0), + packed_direct(cols::TIMESTAMP_1), BusValue::linear(vec![ LinearTerm::Column { coefficient: 1, @@ -504,106 +479,62 @@ pub fn bus_interactions() -> Vec { ]; for x in 0..5 { for y in 0..5 { - for b in 0..8 { + for byte in 0..8 { let col = if x == 0 && y == 0 { - cols::IOTA + b + cols::IOTA + byte } else { - cols::chi(x, y, b) + cols::chi(x, y, byte) }; - values.push(BusValue::Packed { - start_column: col, - packing: Packing::Direct, - }); + values.push(packed_direct(col)); } } } - interactions.push(BusInteraction::sender( - BusId::Keccak, - Multiplicity::Column(cols::MU), - values, - )); + b.send(BusId::Keccak, mu.clone(), values); } - // 3. KECCAK_RC: lookup (round) → rc[8] + // KECCAK_RC lookup: round -> rc[8]. { - let mut values = vec![BusValue::Packed { - start_column: cols::ROUND, - packing: Packing::Direct, - }]; - for b in 0..8 { - values.push(BusValue::Packed { - start_column: cols::rc(b), - packing: Packing::Direct, - }); + let mut values = vec![packed_direct(cols::ROUND)]; + for byte in 0..8 { + values.push(packed_direct(cols::rc(byte))); } - interactions.push(BusInteraction::sender( - BusId::KeccakRc, - Multiplicity::Column(cols::MU), - values, - )); + b.send(BusId::KeccakRc, mu.clone(), values); } - // --- Theta: Cxz chain XOR_BYTE (160) --- - // Stage 0: XOR(start[x,0,z], start[x,1,z]) → Cxz[x,0,z] + // Theta: Cxz chain XOR_BYTE (5 x 8 = 40 stage-0 + 5 x 3 x 8 = 120 stage 1..3 = 160 total). + // Stage 0: XOR(start[x,0,b], start[x,1,b]) -> Cxz[x,0,b]. for x in 0..5 { - for b in 0..8 { - interactions.push(BusInteraction::sender( - BusId::XorByte, - Multiplicity::Column(cols::MU), - vec![ - BusValue::Packed { - start_column: cols::start(x, 0, b), - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::start(x, 1, b), - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::cxz(x, 0, b), - packing: Packing::Direct, - }, - ], - )); + for byte in 0..8 { + b.send_xor_byte( + cols::start(x, 0, byte), + cols::start(x, 1, byte), + cols::cxz(x, 0, byte), + &mu, + ); } } - // Stages 1..3: XOR(Cxz[x,stage-1,z], start[x,stage+1,z]) → Cxz[x,stage,z] + // Stages 1..3: XOR(Cxz[x, stage-1, b], start[x, stage+1, b]) -> Cxz[x, stage, b]. for x in 0..5 { for stage in 1..4usize { let y = stage + 1; - for b in 0..8 { - interactions.push(BusInteraction::sender( - BusId::XorByte, - Multiplicity::Column(cols::MU), - vec![ - BusValue::Packed { - start_column: cols::cxz(x, stage - 1, b), - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::start(x, y, b), - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::cxz(x, stage, b), - packing: Packing::Direct, - }, - ], - )); + for byte in 0..8 { + b.send_xor_byte( + cols::cxz(x, stage - 1, byte), + cols::start(x, y, byte), + cols::cxz(x, stage, byte), + &mu, + ); } } } - // --- Theta: HWSL for rotated C (20) --- - // HWSL(C[x] halfword[hw], 1) → (Cxz_left, Cxz_right) - // Cxz_right is a single carry bit zero-extended to a halfword (spec d75944ee). + // Theta: HWSL for rotated C (20). HWSL(C[x] halfword[hw], 1) -> (rot_left, rot_right_bit). for x in 0..5 { for hw in 0..4 { - interactions.push(BusInteraction::sender( + b.send( BusId::Hwsl, - Multiplicity::Column(cols::MU), + mu.clone(), vec![ - // Input halfword: Cxz[x][3][hw*2] + 256 * Cxz[x][3][hw*2+1] BusValue::linear(vec![ LinearTerm::Column { coefficient: 1, @@ -614,9 +545,7 @@ pub fn bus_interactions() -> Vec { column: cols::cxz(x, 3, hw * 2 + 1), }, ]), - // Shift amount = 1 BusValue::constant(1), - // Output: shifted BusValue::linear(vec![ LinearTerm::Column { coefficient: 1, @@ -627,102 +556,67 @@ pub fn bus_interactions() -> Vec { column: cols::cxz_left(x, hw * 2 + 1), }, ]), - // Output: carry (single bit cast to Half — high byte = 0). - BusValue::Packed { - start_column: cols::cxz_right_bit(x, hw), - packing: Packing::Direct, - }, + packed_direct(cols::cxz_right_bit(x, hw)), ], - )); + ); } } - // --- Theta: IS_BYTE range checks on Cxz_left (40) --- - // Cxz_right uses IS_BIT polynomial constraints (see create_constraints). + // Theta: IS_BYTE range checks on Cxz_left (40). for x in 0..5 { - for b in 0..8 { - interactions.push(BusInteraction::sender( - BusId::IsByte, - Multiplicity::Column(cols::MU), - vec![BusValue::Packed { - start_column: cols::cxz_left(x, b), - packing: Packing::Direct, - }], - )); + for byte in 0..8 { + b.send_is_byte(cols::cxz_left(x, byte), &mu); } } - // --- Theta: Dxz XOR_BYTE (40) --- - // D[x][b] = C[(x-1)%5][b] XOR rotated_C[(x+1)%5][b] - // rotated_C[x'][b] = Cxz_left[x'][b] + (1 - b%2) * Cxz_right[x'][(b/2 - 1)%4] - // (spec d75944ee/9143370f). For odd b only Cxz_left contributes. + // Theta: Dxz XOR_BYTE (40). D[x][b] = C[(x-1)%5][b] XOR rotated_C[(x+1)%5][b]. for x in 0..5 { - for b in 0..8 { + for byte in 0..8 { let mut rotated_c_terms = vec![LinearTerm::Column { coefficient: 1, - column: cols::cxz_left((x + 1) % 5, b), + column: cols::cxz_left((x + 1) % 5, byte), }]; - if let Some(hw) = cols::cxz_right_bit_for_byte(b) { + if let Some(hw) = cols::cxz_right_bit_for_byte(byte) { rotated_c_terms.push(LinearTerm::Column { coefficient: 1, column: cols::cxz_right_bit((x + 1) % 5, hw), }); } - interactions.push(BusInteraction::sender( + b.send( BusId::XorByte, - Multiplicity::Column(cols::MU), + mu.clone(), vec![ - BusValue::Packed { - start_column: cols::cxz((x + 4) % 5, 3, b), - packing: Packing::Direct, - }, + packed_direct(cols::cxz((x + 4) % 5, 3, byte)), BusValue::linear(rotated_c_terms), - BusValue::Packed { - start_column: cols::dxz(x, b), - packing: Packing::Direct, - }, + packed_direct(cols::dxz(x, byte)), ], - )); + ); } } - // --- Theta final: XOR_BYTE (200) --- - // theta[x][y][b] = start[x][y][b] XOR D[x][b] + // Theta final: XOR_BYTE (200). theta[x][y][b] = start[x][y][b] XOR D[x][b]. for x in 0..5 { for y in 0..5 { - for b in 0..8 { - interactions.push(BusInteraction::sender( - BusId::XorByte, - Multiplicity::Column(cols::MU), - vec![ - BusValue::Packed { - start_column: cols::start(x, y, b), - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::dxz(x, b), - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::theta(x, y, b), - packing: Packing::Direct, - }, - ], - )); + for byte in 0..8 { + b.send_xor_byte( + cols::start(x, y, byte), + cols::dxz(x, byte), + cols::theta(x, y, byte), + &mu, + ); } } } - // --- Rho: HWSL (100) --- - // HWSL(theta[x][y] halfword[hw], rnc[x][y]) → (rot_left, rot_right) - // rnc is inlined as a constant: KECCAK_RHO[x][y] % 16. + // Rho: HWSL (100). HWSL(theta[x][y] halfword[hw], rnc[x][y]) -> (rot_left, rot_right). + // rnc inlined as KECCAK_RHO[x][y] % 16. for x in 0..5 { for y in 0..5 { let rnc_val = (KECCAK_RHO[x][y] % 16) as u64; for hw in 0..4 { - interactions.push(BusInteraction::sender( + b.send( BusId::Hwsl, - Multiplicity::Column(cols::MU), + mu.clone(), vec![ BusValue::linear(vec![ LinearTerm::Column { @@ -756,47 +650,30 @@ pub fn bus_interactions() -> Vec { }, ]), ], - )); + ); } } } - // --- Rho: IS_BYTE range checks on rot_left + rot_right (400) --- + // Rho: IS_BYTE range checks on rot_left + rot_right (400). for x in 0..5 { for y in 0..5 { - for b in 0..8 { - interactions.push(BusInteraction::sender( - BusId::IsByte, - Multiplicity::Column(cols::MU), - vec![BusValue::Packed { - start_column: cols::rot_left(x, y, b), - packing: Packing::Direct, - }], - )); - interactions.push(BusInteraction::sender( - BusId::IsByte, - Multiplicity::Column(cols::MU), - vec![BusValue::Packed { - start_column: cols::rot_right(x, y, b), - packing: Packing::Direct, - }], - )); + for byte in 0..8 { + b.send_is_byte(cols::rot_left(x, y, byte), &mu); + b.send_is_byte(cols::rot_right(x, y, byte), &mu); } } } - // --- Chi: AND_BYTE (200) --- - // chi_ands[x][y][b] = (255 - pi[(x+1)%5][y][b]) AND pi[(x+2)%5][y][b] - // pi is virtual: pi[x][y][z] = rot_left[sx,sy,l_byte] + rot_right[sx,sy,r_byte] - // with src lane (sx,sy) = ((x+3y)%5, x) and byte offsets from KECCAK_RHO. + // Chi: AND_BYTE (200). chi_ands[x][y][b] = (255 - pi[(x+1)%5][y][b]) AND pi[(x+2)%5][y][b]. for x in 0..5 { for y in 0..5 { - for b in 0..8 { - let (p1_l, p1_r) = cols::pi_src_cols((x + 1) % 5, y, b); - let (p2_l, p2_r) = cols::pi_src_cols((x + 2) % 5, y, b); - interactions.push(BusInteraction::sender( + for byte in 0..8 { + let (p1_l, p1_r) = cols::pi_src_cols((x + 1) % 5, y, byte); + let (p2_l, p2_r) = cols::pi_src_cols((x + 2) % 5, y, byte); + b.send( BusId::AndByte, - Multiplicity::Column(cols::MU), + mu.clone(), vec![ BusValue::linear(vec![ LinearTerm::Constant(255), @@ -819,25 +696,21 @@ pub fn bus_interactions() -> Vec { column: p2_r, }, ]), - BusValue::Packed { - start_column: cols::chi_ands(x, y, b), - packing: Packing::Direct, - }, + packed_direct(cols::chi_ands(x, y, byte)), ], - )); + ); } } } - // --- Chi: XOR_BYTE (200) --- - // chi[x][y][b] = pi[x][y][b] XOR chi_ands[x][y][b] (pi virtual). + // Chi: XOR_BYTE (200). chi[x][y][b] = pi[x][y][b] XOR chi_ands[x][y][b] (pi virtual). for x in 0..5 { for y in 0..5 { - for b in 0..8 { - let (p_l, p_r) = cols::pi_src_cols(x, y, b); - interactions.push(BusInteraction::sender( + for byte in 0..8 { + let (p_l, p_r) = cols::pi_src_cols(x, y, byte); + b.send( BusId::XorByte, - Multiplicity::Column(cols::MU), + mu.clone(), vec![ BusValue::linear(vec![ LinearTerm::Column { @@ -849,44 +722,20 @@ pub fn bus_interactions() -> Vec { column: p_r, }, ]), - BusValue::Packed { - start_column: cols::chi_ands(x, y, b), - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::chi(x, y, b), - packing: Packing::Direct, - }, + packed_direct(cols::chi_ands(x, y, byte)), + packed_direct(cols::chi(x, y, byte)), ], - )); + ); } } } - // --- Iota: XOR_BYTE (8) --- - // iota[b] = chi[0][0][b] XOR rc[b] - for b in 0..8 { - interactions.push(BusInteraction::sender( - BusId::XorByte, - Multiplicity::Column(cols::MU), - vec![ - BusValue::Packed { - start_column: cols::chi(0, 0, b), - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::rc(b), - packing: Packing::Direct, - }, - BusValue::Packed { - start_column: cols::iota(b), - packing: Packing::Direct, - }, - ], - )); + // Iota: XOR_BYTE (8). iota[b] = chi[0][0][b] XOR rc[b]. + for byte in 0..8 { + b.send_xor_byte(cols::chi(0, 0, byte), cols::rc(byte), cols::iota(byte), &mu); } - interactions + b.into_vec() } // ========================================================================= From 47ed2a1297900c2d6da7b89e067277b973cbaa98 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Wed, 20 May 2026 13:05:05 -0300 Subject: [PATCH 08/10] perf(prover): commit preprocessed columns via one fused coset-LDE pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `commit_preprocessed_columns` interpolated each column to coefficient form and then evaluated it back onto the LDE coset — two full FFT pipelines per column (iFFT, then scale+FFT), plus a rebuilt twiddle table on every call. Replace it with `stark::prover::commit_lde_columns`: the same path the prover uses for ordinary trace columns. Each column goes from evaluation form straight to its LDE in a single fused `Polynomial::coset_lde_full` pass; the inverse/forward twiddle tables and coset weights are built once (`LdeTwiddles::from_params`) and shared across every column. The Merkle commit reuses `keccak_leaves_bit_reversed` — the prover's own leaf hashing. `commit_preprocessed_columns` is now a thin adapter; there is no separate interpolate-then-extend round trip and no per-column twiddle rebuild. - `LdeTwiddles::from_params(domain_size, blowup, coset_offset)` exposed so twiddles can be built without a full `Domain` (which also allocates the LDE coset vector). `LdeTwiddles::new(&Domain)` now delegates to it. - `commit_lde_columns` added as a public `stark::prover` entry point. - New test `commit_lde_columns_matches_interpolate_then_evaluate` pins the invariant that the fused path yields the byte-identical Merkle root the old interpolate-then-evaluate pipeline did, for n = 2^2..2^8. 125 stark tests pass; 273 prover lib tests pass (77 pre-existing UnknownSyscall(5) ELF failures unrelated). `make lint` clean. --- crypto/stark/src/prover.rs | 72 ++++++++++++++++++++++++-- crypto/stark/src/tests/prover_tests.rs | 55 ++++++++++++++++++++ prover/src/tables/preprocessed.rs | 72 +++++--------------------- 3 files changed, 137 insertions(+), 62 deletions(-) diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 68f50ea53..6f1466fd5 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -273,19 +273,31 @@ pub struct LdeTwiddles { impl LdeTwiddles { /// Construct twiddles and coset weights for a domain of the given size and blowup factor. fn new(domain: &Domain) -> Self { - let domain_size = domain.interpolation_domain_size; - let lde_size = domain_size * domain.blowup_factor; + Self::from_params( + domain.interpolation_domain_size, + domain.blowup_factor, + &domain.coset_offset, + ) + } + + /// Construct twiddles and coset weights directly from the domain parameters, + /// without building a full [`Domain`] (which also allocates the LDE coset). + pub fn from_params( + domain_size: usize, + blowup_factor: usize, + coset_offset: &FieldElement, + ) -> Self { + let lde_size = domain_size * blowup_factor; let domain_size_inv = FieldElement::::from(domain_size as u64) .inv() .expect("domain_size is power of two"); - let offset = &domain.coset_offset; let coset_weights = { let mut w = Vec::with_capacity(domain_size); let mut offset_power = domain_size_inv; for _ in 0..domain_size { w.push(offset_power.clone()); - offset_power = offset * &offset_power; + offset_power = coset_offset * &offset_power; } w }; @@ -425,6 +437,58 @@ where .collect() } +/// Commit a set of equal-length columns through the prover's own column +/// pipeline and return the Merkle root. +/// +/// Each column is taken from evaluation form straight to its LDE in a single +/// **fused** coset-LDE pass (`Polynomial::coset_lde_full`) — there is no +/// separate "interpolate to coefficients, then evaluate again" round trip. +/// The inverse/forward twiddle tables and coset weights are built once and +/// shared across every column. +/// +/// This is exactly how `commit_main_trace` / `commit_preprocessed_trace` +/// commit trace columns, so a preprocessed table that derives its hardcoded +/// commitment through this function is guaranteed to match the root the +/// prover produces (and the verifier checks). +pub fn commit_lde_columns( + columns: &[Vec>], + blowup_factor: usize, + coset_offset: &FieldElement, +) -> Option +where + F: IsFFTField + Send + Sync, + FieldElement: AsBytes + Sync + Send + ByteConversion, +{ + if columns.is_empty() || columns[0].is_empty() { + return None; + } + let n = columns[0].len(); + // Twiddles + coset weights: built once, reused for every column. + let twiddles = LdeTwiddles::::from_params(n, blowup_factor, coset_offset); + + #[cfg(feature = "parallel")] + let columns_iter = columns.par_iter(); + #[cfg(not(feature = "parallel"))] + let columns_iter = columns.iter(); + + let lde: Vec>> = columns_iter + .map(|col| { + Polynomial::coset_lde_full::( + col, + blowup_factor, + &twiddles.coset_weights, + &twiddles.inv, + &twiddles.fwd, + ) + }) + .collect::>() + .ok()?; + + let hashed_leaves = keccak_leaves_bit_reversed(&lde); + let tree = BatchedMerkleTree::::build_from_hashed_leaves(hashed_leaves)?; + Some(tree.root) +} + /// Compute Keccak-256 leaf hashes for `commit_composition_polynomial`: one /// leaf per row-pair, where leaf `i` hashes the BE concatenation of /// `parts[..][br_0] ++ parts[..][br_1]` with diff --git a/crypto/stark/src/tests/prover_tests.rs b/crypto/stark/src/tests/prover_tests.rs index 1355b363d..625c32994 100644 --- a/crypto/stark/src/tests/prover_tests.rs +++ b/crypto/stark/src/tests/prover_tests.rs @@ -519,3 +519,58 @@ fn test_deep_poly_direct_2n_matches_interpolate_fft_extend() { ); } } + +/// Pins the invariant that `commit_lde_columns` (single fused coset-LDE pass +/// per column) produces the exact same Merkle root as the older two-step +/// "interpolate to coefficients, then evaluate on the LDE domain" pipeline. +/// +/// Preprocessed tables derive their hardcoded commitments through +/// `commit_lde_columns`; this guards against the fused path ever drifting +/// from what the verifier recomputes. +#[test] +fn commit_lde_columns_matches_interpolate_then_evaluate() { + use crate::config::BatchedMerkleTree; + use crate::prover::commit_lde_columns; + use crate::trace::columns2rows; + use math::fft::cpu::bit_reversing::in_place_bit_reverse_permute; + + let blowup_factor = 4usize; + let coset_offset = Felt::from(3u64); + + for order in 2..=8u32 { + let n = 1usize << order; + // A couple of deterministic columns of length n. + let columns: Vec> = (0..3) + .map(|c| { + (0..n) + .map(|i| Felt::from((i * 31 + c * 7 + 1) as u64)) + .collect() + }) + .collect(); + + // Fused path under test. + let fused = commit_lde_columns::(&columns, blowup_factor, &coset_offset) + .expect("fused commit"); + + // Old two-step path: interpolate -> evaluate on LDE -> bit-reverse -> Merkle. + let mut lde: Vec> = columns + .iter() + .map(|col| { + let poly = Polynomial::interpolate_fft::(col).unwrap(); + evaluate_polynomial_on_lde_domain(&poly, blowup_factor, n, &coset_offset).unwrap() + }) + .collect(); + for col in lde.iter_mut() { + in_place_bit_reverse_permute(col); + } + let rows = columns2rows(lde); + let two_step = BatchedMerkleTree::::build(&rows) + .expect("two-step commit") + .root; + + assert_eq!( + fused, two_step, + "fused coset-LDE commit diverged from interpolate-then-evaluate at n=2^{order}" + ); + } +} diff --git a/prover/src/tables/preprocessed.rs b/prover/src/tables/preprocessed.rs index cebcefb49..e6593dcac 100644 --- a/prover/src/tables/preprocessed.rs +++ b/prover/src/tables/preprocessed.rs @@ -1,75 +1,31 @@ //! Shared commitment pipeline for preprocessed tables. //! -//! The DECODE, BITWISE, KECCAK_RC, PAGE, and REGISTER tables all commit their -//! precomputed columns through the same six steps: -//! -//! 1. Interpolate each column on the trace domain (FFT). -//! 2. Evaluate every polynomial on the LDE coset. -//! 3. Bit-reverse permute each LDE column. -//! 4. Transpose columns -> rows. -//! 5. Build a batched Merkle tree over the rows. -//! 6. Return the tree root. -//! -//! This module factors the pipeline out so each table only has to build its -//! own columns. +//! DECODE, BITWISE, KECCAK_RC, PAGE, and REGISTER all commit their precomputed +//! columns the same way the prover commits any trace column: a single fused +//! coset-LDE pass per column (shared twiddles), then a bit-reversed batched +//! Merkle tree. This module is a thin adapter over `stark::prover::commit_lde_columns` +//! so the preprocessed-commitment path stays byte-identical to the prover's. -#[cfg(feature = "parallel")] -use rayon::prelude::*; - -use math::fft::cpu::bit_reversing::in_place_bit_reverse_permute; -use math::polynomial::Polynomial; -use stark::config::{BatchedMerkleTree, Commitment}; +use stark::config::Commitment; use stark::proof::options::ProofOptions; -use stark::prover::evaluate_polynomial_on_lde_domain; -use stark::trace::columns2rows; +use stark::prover::commit_lde_columns; use super::types::{FE, GoldilocksField}; -/// Run the full preprocessed-commitment pipeline on `columns`. +/// Commit the precomputed `columns` of a preprocessed table and return the +/// Merkle root. /// -/// All columns must have the same length (the trace domain size, typically a -/// power of two). `table_label` is included in panic messages on failure of -/// the FFT / LDE / Merkle steps; these are construction-time failures on the -/// table's own data and indicate a bug in the code, never adversarial input. +/// All columns must share the same power-of-two length. `table_label` is only +/// used for the panic message if the commit fails — a commit failure here is a +/// code bug on the table's own data, never adversarial input. pub fn commit_preprocessed_columns( columns: Vec>, options: &ProofOptions, table_label: &'static str, ) -> Commitment { - let num_rows = columns[0].len(); let blowup_factor = options.blowup_factor as usize; let coset_offset = FE::from(options.coset_offset); - let interpolate = |col: &Vec| { - Polynomial::interpolate_fft::(col) - .unwrap_or_else(|_| panic!("FFT interpolation failed for {table_label} column")) - }; - let to_lde = |poly: &Polynomial| { - evaluate_polynomial_on_lde_domain(poly, blowup_factor, num_rows, &coset_offset) - .unwrap_or_else(|_| panic!("LDE evaluation failed for {table_label} polynomial")) - }; - - #[cfg(feature = "parallel")] - let polys: Vec> = columns.par_iter().map(interpolate).collect(); - #[cfg(not(feature = "parallel"))] - let polys: Vec> = columns.iter().map(interpolate).collect(); - - #[cfg(feature = "parallel")] - let mut lde_columns: Vec> = polys.par_iter().map(to_lde).collect(); - #[cfg(not(feature = "parallel"))] - let mut lde_columns: Vec> = polys.iter().map(to_lde).collect(); - - #[cfg(feature = "parallel")] - lde_columns - .par_iter_mut() - .for_each(|col| in_place_bit_reverse_permute(col)); - #[cfg(not(feature = "parallel"))] - for col in lde_columns.iter_mut() { - in_place_bit_reverse_permute(col); - } - - let lde_rows = columns2rows(lde_columns); - let tree = BatchedMerkleTree::::build(&lde_rows) - .unwrap_or_else(|| panic!("Failed to build Merkle tree for {table_label} LDE")); - tree.root + commit_lde_columns::(&columns, blowup_factor, &coset_offset) + .unwrap_or_else(|| panic!("failed to commit preprocessed columns for {table_label}")) } From a37c8d5e5bf26dc0672fd17e25f75cb30d9a48ef Mon Sep 17 00:00:00 2001 From: diegokingston Date: Wed, 20 May 2026 13:18:08 -0300 Subject: [PATCH 09/10] refactor(prover): drop dead code from bus_builder + preprocessed adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review of the PR surfaced unused API: - `BusInteractionsBuilder::send_b20` — no caller; every B20 send in the migrated tables is a linear combination (`send_b20_linear`). Removed. - `BusInteractionsBuilder::raw` — the "escape hatch" had no caller; the generic `send` / `recv` cover every heterogeneous interaction. Removed. - `BusInteractionsBuilder::new` + its `Default` impl — every `bus_interactions()` knows its interaction count and uses `with_capacity`; the zero-arg constructor was never called. Removed (which also removes the `Default` that only existed to satisfy `clippy::new_without_default`). - `commit_preprocessed_columns`: dropped the redundant `::` turbofish on `commit_lde_columns` — `F` is inferred from the arguments — and the now-unused `GoldilocksField` import. Added a note on why the prover crate is monomorphic over Goldilocks while `commit_lde_columns` itself stays generic. No behavior change. 125 stark + 273 prover lib tests pass; lint clean. --- prover/src/tables/bus_builder.rs | 39 ++++--------------------------- prover/src/tables/preprocessed.rs | 6 +++-- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/prover/src/tables/bus_builder.rs b/prover/src/tables/bus_builder.rs index fdcb2b4a0..02ee86a4f 100644 --- a/prover/src/tables/bus_builder.rs +++ b/prover/src/tables/bus_builder.rs @@ -11,8 +11,8 @@ //! The builder reduces each interaction to a single intent-named call that //! reads as a spec line ("send `col` to IS_HALFWORD with multiplicity mu"). //! -//! No macros: plain Rust methods, discoverable via `rust-analyzer`. Use the -//! `raw(...)` escape hatch for one-off interactions that do not fit a helper. +//! No macros: plain Rust methods, discoverable via `rust-analyzer`. Heterogeneous +//! interactions that do not match a named helper use the generic `send` / `recv`. use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; @@ -24,10 +24,8 @@ pub struct BusInteractionsBuilder { } impl BusInteractionsBuilder { - pub fn new() -> Self { - Self { inner: Vec::new() } - } - + /// Create a builder, pre-sizing for `n` interactions. Every `bus_interactions()` + /// knows its interaction count up front, so there is no zero-arg constructor. pub fn with_capacity(n: usize) -> Self { Self { inner: Vec::with_capacity(n), @@ -84,17 +82,6 @@ impl BusInteractionsBuilder { self } - /// Send a direct-packed column to a B20 range check. - /// IS_B20[col] - pub fn send_b20(&mut self, col: usize, mult: &Multiplicity) -> &mut Self { - self.inner.push(BusInteraction::sender( - BusId::IsB20, - mult.clone(), - vec![packed_direct(col)], - )); - self - } - /// Generic sender with caller-provided values. Takes ownership of `mult`; /// use when sending exactly one interaction with this multiplicity. pub fn send(&mut self, bus_id: BusId, mult: Multiplicity, values: Vec) -> &mut Self { @@ -144,24 +131,6 @@ impl BusInteractionsBuilder { .push(BusInteraction::receiver(bus_id, mult, values)); self } - - // ------------------------------------------------------------------------- - // Escape hatch - // ------------------------------------------------------------------------- - - /// Push a fully-constructed `BusInteraction` (sender or receiver) without - /// going through a helper. Use when the interaction does not match any - /// named idiom on this builder. - pub fn raw(&mut self, interaction: BusInteraction) -> &mut Self { - self.inner.push(interaction); - self - } -} - -impl Default for BusInteractionsBuilder { - fn default() -> Self { - Self::new() - } } /// Build a `BusValue::Packed` with `Packing::Direct` from a column index. diff --git a/prover/src/tables/preprocessed.rs b/prover/src/tables/preprocessed.rs index e6593dcac..00be423c9 100644 --- a/prover/src/tables/preprocessed.rs +++ b/prover/src/tables/preprocessed.rs @@ -10,7 +10,7 @@ use stark::config::Commitment; use stark::proof::options::ProofOptions; use stark::prover::commit_lde_columns; -use super::types::{FE, GoldilocksField}; +use super::types::FE; /// Commit the precomputed `columns` of a preprocessed table and return the /// Merkle root. @@ -26,6 +26,8 @@ pub fn commit_preprocessed_columns( let blowup_factor = options.blowup_factor as usize; let coset_offset = FE::from(options.coset_offset); - commit_lde_columns::(&columns, blowup_factor, &coset_offset) + // `F` is inferred as GoldilocksField from `columns` / `coset_offset` — the + // prover crate is monomorphic over Goldilocks; the genericity lives in `stark`. + commit_lde_columns(&columns, blowup_factor, &coset_offset) .unwrap_or_else(|| panic!("failed to commit preprocessed columns for {table_label}")) } From 7ea6c8b116d067f7d68e74080d6f5ad77a8221d0 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Wed, 20 May 2026 14:16:35 -0300 Subject: [PATCH 10/10] refactor(stark): converge test commit helper onto commit_lde_columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review of PR #596's commit pipeline: Duplicate LDE-commit path - `compute_precomputed_commitment_for_testing` re-implemented what the new `commit_lde_columns` already does: it routed through `Domain::new` (which eagerly builds the LDE-coset roots-of-unity vector, never used for a commitment), then `LdeTwiddles::new`, `compute_lde_from_columns_cached` and `commit_columns_bit_reversed`. It now delegates to `commit_lde_columns` directly — one fused coset-LDE pass with shared twiddles — so the test helper and the production preprocessed-commit path are the same code. - That made `compute_lde_from_columns_cached` dead (its only caller was the helper; its doc still claimed phase-A/C/Round-2-4 use that no longer exists). Removed, along with the now-unneeded `FieldExtension: AsBytes` bound on the helper. No more wasted twiddle/Domain work - The old helper built a full `Domain` (trace + LDE roots-of-unity cosets) purely to hand `blowup_factor`/`coset_offset` to the LDE. `commit_lde_columns` builds only the `LdeTwiddles` it needs, once, shared across all columns. Panic -> documented invariant - `commit_preprocessed_columns` previously did `commit_lde_columns(...).unwrap_or_else(|| panic!("failed to commit..."))`, which reads like a runtime error path. `commit_lde_columns` returns `None` *only* for empty input, and a preprocessed table always has a fixed non-empty column set. Replaced with an explicit `assert!` naming the table (the real invariant) plus an `.expect` documenting why the `Option` is then provably `Some`. Behavior-preserving: 125 stark lib tests pass (incl. the commit_lde_columns parity test) and the bitwise preprocessed-soundness tests that drive the test helper. Lint clean. --- crypto/stark/src/prover.rs | 56 +++++-------------------------- prover/src/tables/preprocessed.rs | 17 +++++++--- 2 files changed, 22 insertions(+), 51 deletions(-) diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 6f1466fd5..3e108b5cc 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -576,10 +576,11 @@ pub trait IsStarkProver< Some((tree, root)) } - /// Compute the LDE commitment for a subset of columns from a trace (for testing). + /// Compute the LDE commitment for a subset of trace columns (test helper). /// - /// This helper computes the same commitment the prover generates internally, - /// useful for setting up soundness test scenarios. + /// Delegates to [`commit_lde_columns`] — the same fused coset-LDE + Merkle + /// pipeline the prover uses internally — so soundness tests pin against the + /// real commitment instead of a parallel re-implementation. fn compute_precomputed_commitment_for_testing( trace: &TraceTable, air: &impl AIR, @@ -587,53 +588,14 @@ pub trait IsStarkProver< ) -> Option where FieldElement: AsBytes + Sync + Send, - FieldElement: AsBytes + Sync + Send, { - let domain = Domain::new(air, trace.num_rows()); let columns = trace.columns_main(); let precomputed: Vec<_> = columns.into_iter().take(num_precomputed_cols).collect(); - let twiddles = LdeTwiddles::new(&domain); - let evals = - Self::compute_lde_from_columns_cached::(&precomputed, &domain, &twiddles); - let (_, commitment) = Self::commit_columns_bit_reversed(&evals)?; - Some(commitment) - } - - /// Compute LDE evaluations with pre-computed twiddle factors and coset weights. - /// - /// Accepts shared [`LdeTwiddles`] to avoid redundant twiddle generation and weight - /// computation across phases (A, C, Rounds 2-4). - fn compute_lde_from_columns_cached( - columns: &[Vec>], - domain: &Domain, - twiddles: &LdeTwiddles, - ) -> Vec>> - where - E: IsSubFieldOf + Send + Sync, - Field: IsSubFieldOf, - FieldElement: Send + Sync, - { - if columns.is_empty() { - return Vec::new(); - } - - #[cfg(not(feature = "parallel"))] - let columns_iter = columns.iter(); - #[cfg(feature = "parallel")] - let columns_iter = columns.par_iter(); - - columns_iter - .map(|col| { - Polynomial::coset_lde_full::( - col, - domain.blowup_factor, - &twiddles.coset_weights, - &twiddles.inv, - &twiddles.fwd, - ) - }) - .collect::>>, _>>() - .expect("coset LDE computation") + commit_lde_columns( + &precomputed, + air.options().blowup_factor as usize, + &FieldElement::from(air.options().coset_offset), + ) } /// Expand each column in-place from N evaluations to N×blowup LDE evaluations. diff --git a/prover/src/tables/preprocessed.rs b/prover/src/tables/preprocessed.rs index 00be423c9..3d5f2c574 100644 --- a/prover/src/tables/preprocessed.rs +++ b/prover/src/tables/preprocessed.rs @@ -15,9 +15,9 @@ use super::types::FE; /// Commit the precomputed `columns` of a preprocessed table and return the /// Merkle root. /// -/// All columns must share the same power-of-two length. `table_label` is only -/// used for the panic message if the commit fails — a commit failure here is a -/// code bug on the table's own data, never adversarial input. +/// All columns must share the same power-of-two length and be non-empty. +/// `table_label` names the table in the invariant-failure message: an empty +/// column set is a table-definition bug, never adversarial input. pub fn commit_preprocessed_columns( columns: Vec>, options: &ProofOptions, @@ -26,8 +26,17 @@ pub fn commit_preprocessed_columns( let blowup_factor = options.blowup_factor as usize; let coset_offset = FE::from(options.coset_offset); + // `commit_lde_columns` only returns `None` for empty input. A preprocessed + // table always has a fixed, non-empty column set, so empty `columns` here is + // a table-definition bug — assert it explicitly (naming the table) rather + // than letting it surface as an opaque commit failure downstream. + assert!( + !columns.is_empty() && !columns[0].is_empty(), + "{table_label}: preprocessed table has no columns to commit (table-definition bug)", + ); + // `F` is inferred as GoldilocksField from `columns` / `coset_offset` — the // prover crate is monomorphic over Goldilocks; the genericity lives in `stark`. commit_lde_columns(&columns, blowup_factor, &coset_offset) - .unwrap_or_else(|| panic!("failed to commit preprocessed columns for {table_label}")) + .expect("commit_lde_columns is infallible for the non-empty columns asserted above") }