Skip to content

Cleanup/prover src refactor#596

Open
diegokingston wants to merge 15 commits into
mainfrom
cleanup/prover-src-refactor
Open

Cleanup/prover src refactor#596
diegokingston wants to merge 15 commits into
mainfrom
cleanup/prover-src-refactor

Conversation

@diegokingston
Copy link
Copy Markdown
Collaborator

No description provided.

`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.
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.
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("... <table> ...") 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).
…ecutorLog

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`.
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.
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.
@github-actions
Copy link
Copy Markdown

Codex Code Review

Findings

Performance: bitwise.rs now generates the full 2^20 BITWISE table once per precomputed column in non-parallel builds. The previous non-parallel path generated each row once and pushed all 11 columns, so this turns ~1M generate_bitwise_row calls into ~11M. Keep the old row-wise loop for #[cfg(not(feature = "parallel"))] and use the column-wise version only for Rayon.

No security vulnerabilities or correctness bugs found in the reviewed diff.

I could not run cargo check -p prover because rustup tried to write under /home/runner/.rustup, which is read-only in this environment. git diff --check passed.

Comment thread prover/src/tables/preprocessed.rs Outdated
options: &ProofOptions,
table_label: &'static str,
) -> Commitment {
let num_rows = columns[0].len();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low – panic on empty columns

columns[0] panics if called with an empty slice. All current callers are safe, but this is an undocumented precondition that future callers could violate silently.

Suggested change
let num_rows = columns[0].len();
debug_assert!(!columns.is_empty(), "commit_preprocessed_columns: columns must be non-empty");
let num_rows = columns[0].len();


/// 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<LinearTerm>) -> &mut Self {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low – parameter order inconsistency

Every other named helper is (col, mult) or (input_col, output_col, mult)mult is always last. Here mult comes before terms, which makes the API harder to learn and grep for patterns.

Suggested change
pub fn send_b20_linear(&mut self, mult: &Multiplicity, terms: Vec<LinearTerm>) -> &mut Self {
pub fn send_b20_linear(&mut self, terms: Vec<LinearTerm>, mult: &Multiplicity) -> &mut Self {

(and update the one call-site in the codebase accordingly)

// Read+write x254 (global commit index) at ts
{
let (old_index, old_ts) = register_state.read_index();
let count_u32 = u32::try_from(count)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low – duplicate validation

collect_ops_from_cpu already validates and converts op.commit_count to u32 (line ~430) before calling this function. Here the same field is read from the struct and converted again independently.

Consider accepting count_u32: u32 as a parameter instead of re-reading and re-validating op.commit_count. This would also eliminate the near-duplicate error message.

@claude
Copy link
Copy Markdown

claude Bot commented May 19, 2026

Code Review

Good refactoring PR. The three main wins — factoring out the preprocessed commitment pipeline, converting expect/panic to Result for executor log validation, and the push_u64_as_halfwords deduplication — are all solid improvements with no correctness issues found.

Four minor findings (all Low severity):

1. Panic on empty columnspreprocessed.rs:39
columns[0] panics if the slice is empty. All current callers are safe, but it is an undocumented precondition. See inline comment for a one-line fix.

2. Duplicate commit_count -> u32 validation
collect_ops_from_cpu validates and converts op.commit_count to u32 (line 430), then calls collect_commit_memw_ops which independently reads the same struct field and repeats the identical conversion (line 770). The inner function could accept the already-validated u32 as a parameter. See inline comment.

3. Inconsistent error type for the same invariant
count_table_lengths (line 2625–2626, unchanged in this PR) uses Error::Execution("commit_count exceeds u32 range".into()) for the same overflow that collect_ops_from_cpu and collect_commit_memw_ops now classify as Error::InvalidExecutorLog. Should be unified so callers can distinguish a corrupt executor log from a generic execution failure.

4. Parameter order inconsistency in BusInteractionsBuilder
send_b20_linear puts mult before terms, while every other named helper has mult last. See inline comment.

`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.
@diegokingston diegokingston marked this pull request as ready for review May 19, 2026 20:08
@github-actions
Copy link
Copy Markdown

Codex Code Review

Found one issue:

  • Medium - malformed commit logs can still OOM/hang before validation: In trace_builder.rs, expand_commit_operations_for_ecall is called before the new u32::try_from(op.commit_count) validation at line 430. That expansion loops 0..=count, so a huge commit_count can allocate/run for a very long time before returning InvalidExecutorLog. The same applies inside collect_commit_memw_ops, which allocates Vec::with_capacity(5 + count as usize) before checking count at trace_builder.rs. Validate and bound commit_count before any expansion/allocation/looping.

Verification note: I attempted cargo check -p prover, but rustup failed because /home/runner/.rustup/tmp is read-only in this environment.

Comment thread prover/src/tables/preprocessed.rs Outdated
options: &ProofOptions,
table_label: &'static str,
) -> Commitment {
let num_rows = columns[0].len();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — implicit panic on empty column slice

columns[0] panics if columns is empty. All current callers are safe, but the invariant is silent. Consider asserting it:

Suggested change
let num_rows = columns[0].len();
assert!(!columns.is_empty(), "commit_preprocessed_columns: no columns for {table_label}");
let num_rows = columns[0].len();

Comment on lines +430 to +431
let count = u32::try_from(op.commit_count)
.map_err(|_| Error::InvalidExecutorLog("commit_count exceeds u32 range"))?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — redundant conversion

collect_commit_memw_ops (line 428, called with ?) already validates commit_count fits in u32 and returns Err if it doesn't, so this second try_from can never return Err. The checked_add on current_commit_index is still meaningful (it guards a separate counter), but the conversion itself is dead error-path code.

@claude
Copy link
Copy Markdown

claude Bot commented May 19, 2026

Review

Clean refactoring PR. No security issues or correctness bugs found. Two low-severity observations left as inline comments:

  1. preprocessed.rs:39 — implicit panic on empty column slicecolumns[0] panics if columns is empty. All callers are safe today, but the invariant is invisible to future callers. Adding an assert! makes it explicit.

  2. trace_builder.rs:430-431 — dead error path — The u32::try_from(op.commit_count) conversion in collect_ops_from_cpu can never return Err because collect_commit_memw_ops (called on line 428 with ?) already validates the same value and propagates the error first. The checked_add on current_commit_index below it is still needed (different counter), so only the try_from call is redundant.

diegokingston and others added 6 commits May 20, 2026 13:05
`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.
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
  `::<GoldilocksField>` 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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants