Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 77 additions & 51 deletions crypto/stark/src/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,19 +236,31 @@ pub(crate) struct LdeTwiddles<F: IsFFTField> {
impl<F: IsFFTField> LdeTwiddles<F> {
/// Construct twiddles and coset weights for a domain of the given size and blowup factor.
fn new(domain: &Domain<F>) -> 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<F>,
) -> Self {
let lde_size = domain_size * blowup_factor;

let domain_size_inv = FieldElement::<F>::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
};
Expand Down Expand Up @@ -402,6 +414,58 @@ where
result
}

/// 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<F>(
columns: &[Vec<FieldElement<F>>],
blowup_factor: usize,
coset_offset: &FieldElement<F>,
) -> Option<Commitment>
where
F: IsFFTField + Send + Sync,
FieldElement<F>: 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::<F>::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<Vec<FieldElement<F>>> = columns_iter
.map(|col| {
Polynomial::coset_lde_full::<F>(
col,
blowup_factor,
&twiddles.coset_weights,
&twiddles.inv,
&twiddles.fwd,
)
})
.collect::<Result<_, FFTError>>()
.ok()?;

let hashed_leaves = keccak_leaves_bit_reversed(&lde);
let tree = BatchedMerkleTree::<F>::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
Expand Down Expand Up @@ -507,10 +571,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. Only available under
/// 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. Only available under
/// `cfg(test)` (in-crate) or with the `test-utils` Cargo feature
/// (cross-crate tests).
#[cfg(any(test, feature = "test-utils"))]
Expand All @@ -521,53 +586,14 @@ pub trait IsStarkProver<
) -> Option<Commitment>
where
FieldElement<Field>: AsBytes + Sync + Send,
FieldElement<FieldExtension>: 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::<Field>(&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<E>(
columns: &[Vec<FieldElement<E>>],
domain: &Domain<Field>,
twiddles: &LdeTwiddles<Field>,
) -> Vec<Vec<FieldElement<E>>>
where
E: IsSubFieldOf<FieldExtension> + Send + Sync,
Field: IsSubFieldOf<E>,
FieldElement<E>: 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::<Field>(
col,
domain.blowup_factor,
&twiddles.coset_weights,
&twiddles.inv,
&twiddles.fwd,
)
})
.collect::<Result<Vec<Vec<FieldElement<E>>>, _>>()
.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.
Expand Down
55 changes: 55 additions & 0 deletions crypto/stark/src/tests/prover_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,3 +520,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<Vec<Felt>> = (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::<GoldilocksField>(&columns, blowup_factor, &coset_offset)
.expect("fused commit");

// Old two-step path: interpolate -> evaluate on LDE -> bit-reverse -> Merkle.
let mut lde: Vec<Vec<Felt>> = columns
.iter()
.map(|col| {
let poly = Polynomial::interpolate_fft::<GoldilocksField>(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::<GoldilocksField>::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}"
);
}
}
2 changes: 1 addition & 1 deletion prover/src/constraints/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion prover/src/constraints/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
4 changes: 4 additions & 0 deletions prover/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,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 {
Expand All @@ -175,6 +178,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}"),
}
}
}
Expand Down
94 changes: 13 additions & 81 deletions prover/src/tables/bitwise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,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};

// =========================================================================
Expand Down Expand Up @@ -189,95 +187,29 @@ static BITWISE_COMMITMENT: OnceLock<Commitment> = 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<Vec<FE>> = (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<Vec<FE>> = {
let mut cols: Vec<Vec<FE>> = (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<Polynomial<FE>> = columns
.par_iter()
.map(|col| {
Polynomial::interpolate_fft::<GoldilocksField>(col)
.expect("FFT interpolation failed for bitwise column")
})
.collect();

#[cfg(not(feature = "parallel"))]
let polys: Vec<Polynomial<FE>> = columns
.iter()
.map(|col| {
Polynomial::interpolate_fft::<GoldilocksField>(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<Vec<FE>> = 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<Vec<FE>> = 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<Vec<FE>> = (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::<GoldilocksField>::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.
Expand Down
Loading
Loading