Skip to content

feat(stark): prune redundant g·z (next-row) trace-OOD openings#827

Merged
diegokingston merged 3 commits into
feat/logup-acc-current-rowfrom
feat/logup-ood-gz-pruning
Jul 15, 2026
Merged

feat(stark): prune redundant g·z (next-row) trace-OOD openings#827
diegokingston merged 3 commits into
feat/logup-acc-current-rowfrom
feat/logup-ood-gz-pruning

Conversation

@diegokingston

Copy link
Copy Markdown
Collaborator

Step 2 of OOD g·z pruning. Stacked on #823 (forward accumulation) — review/merge that first; this PR's base is feat/logup-acc-current-row, so the diff shows only the pruning.

What

A transition constraint only reads a handful of columns at the next row (z·g). After step 1 made the LogUp accumulator the sole next-row reader, the entire next-row half of the trace-OOD block — every column except the accumulator — is redundant. This PR stops sending and stops opening it.

Concretely the next-row OOD block shrinks from the full trace width W to |transition window| columns (one per bus table, the accumulator). Proof shrinks; more importantly the verifier / recursion guest does ~half the DEEP trace-term work (2·W·S → (W + |window|)·S), which is the cycle win the benchmark measures.

How (CUDA-safe by construction)

  • New crate::ood module — pure, prover=verifier-identical helpers that derive the surviving-opening layout purely from public AIR metadata (step_size, column count, trace_ood_next_row_columns()), never from proof dimensions.
  • Transition windowAIR::trace_ood_next_row_columns() (Plonky3's "transition window", per-column). Default is conservative (all columns → no pruning) so any AIR reading the next row stays correct without opting in; AirWithBuses overrides to [accumulator].
  • Fiat-Shamir unchanged — both sides still sample one γ; only the trace/composition split point moves. The prover keeps its rectangular W×2S DEEP (CPU and GPU) by scattering the sampled γ-powers into the full grid with zeros at pruned positions — zero terms vanish, so the DEEP polynomial is identical and the GPU kernel needs no change. The proof carries only the two surviving blocks and the transcript absorbs only those.
  • Verifier reconstructs the full grid from the two blocks and skips pruned next-row terms in the DEEP loop, after validating both block shapes against AIR metadata (extends the shape guard from the previous commit).

New proof field trace_ood_next_evaluations (next-row block); trace_ood_evaluations is now just the current-row block. Wire format is bincode/serde so the change is transparent; the recursion guest recompiles the same verifier source.

Tests

  • Full stark lib suite green (198), incl. multi_prove_ram roundtrips (pruned proofs verify) and soundness negatives (tampered / mis-shaped OOD rejected).
  • test_gz_pruning_reduces_next_row_openings — next-row block is 1 column vs full width, and still verifies.
  • ood module unit tests (split/reconstruct/scatter-zeros/surviving-count).
  • Inline VM-table prove+verify green. GPU DEEP path unchanged by construction (not rebuilt here).

…roof

The verifier read the trace-OOD table's shape from the (prover-controlled)
proof dimensions (`num_main = trace_ood_evaluations.width - num_aux`), only
indirectly cross-checked. Reject any proof whose OOD table is not exactly the
AIR-derived size — height = transition_offsets.len() * step_size, width =
main + aux — before any use of the table, and take the main width from the AIR
rather than the proof. A malicious prover can no longer reshape the table
(e.g. drop a column) to dodge a constraint check or trigger an out-of-bounds
read in the frame reconstruction.

This is the shape-from-metadata invariant (I3) that the upcoming g·z OOD
pruning relies on: once the next-row block is pruned to just the accumulator
column, the verifier must reconstruct the reduced shape purely from public
AIR metadata, identically to the prover.

Test: `test_malformed_ood_table_shape_rejected` (drop a column from an
otherwise-valid ADD proof's OOD table → rejected). Full stark suite green (192).
Adds an AIR-metadata method (default empty) naming the full-width `[main|aux]`
column indices that transition constraints read at the NEXT row (offset 1) —
lambda_vm's fine-grained analogue of Plonky3's transition window (a whole-row
"uses next row?" flag). `AirWithBuses` overrides it to return exactly the
accumulator column, the sole next-row read after forward accumulation.

This is the public, prover=verifier-identical source of truth for which OOD
openings survive at g·z. The upcoming pruning consumes it on both sides so the
reduced OOD shape is derived from AIR metadata, never from the proof.

Test: `test_trace_ood_next_row_columns_is_accumulator_only`. stark suite green (193).
Only the columns a transition constraint reads at the next row (the AIR
transition window, `trace_ood_next_row_columns`) need an OOD opening at g·z.
After forward accumulation that is just the LogUp accumulator, so the next-row
half of the trace-OOD collapses from the full width W to one column per bus
table — a smaller proof and, more importantly, ~halved DEEP trace-term work in
the verifier / recursion guest (the cycle win).

Design (CUDA-safe — the GPU DEEP kernel is untouched):
- New `crate::ood` module of pure, prover=verifier-identical helpers deriving
  the surviving-opening layout from public AIR metadata (invariant I3).
- Fiat-Shamir: both sides sample `num_surviving_trace_openings` DEEP gamma
  powers (was `2·step_size·W`). The gamma is one field element either way, so
  transcript consumption is unchanged; only the trace/composition split moves.
- Prover keeps its rectangular W×(2·step_size) DEEP (CPU and GPU) by scattering
  the sampled powers into the full grid with ZEROS at pruned positions — zero
  terms vanish, so the DEEP polynomial is identical with no kernel change. The
  proof carries only the two surviving blocks (`trace_ood_evaluations` =
  current-row S×W, new `trace_ood_next_evaluations` = next-row S×|window|), and
  the transcript absorbs only those.
- Verifier reconstructs the full grid from the two blocks and SKIPS pruned
  next-row terms in the DEEP loop (the cycle saving), after validating both
  block shapes against AIR metadata (extends the I3 guard).

Default `trace_ood_next_row_columns` is conservative (every column — no
pruning), so any AIR that reads the next row stays correct without overriding;
`AirWithBuses` overrides to the accumulator column. Proof is bincode/serde, so
the wire change is transparent; the recursion guest recompiles the same source.

Tests: full stark suite green (198) incl. multi_prove_ram roundtrips (pruned
proofs verify), soundness negatives (tampered/mis-shaped OOD rejected), a
`test_gz_pruning_reduces_next_row_openings` win check (next-row block = 1 col
vs full width, still verifies), and `ood` unit tests. Inline VM-table prove+
verify (bitwise/lt/branch/bus) green. GPU DEEP path unchanged (no cuda build
here; kernel needs no change by construction).
@diegokingston diegokingston merged commit 6939c35 into feat/logup-acc-current-row Jul 15, 2026
7 of 8 checks passed
@diegokingston diegokingston deleted the feat/logup-ood-gz-pruning branch July 15, 2026 23:36
MauroToscano added a commit that referenced this pull request Jul 16, 2026
Bring the batched (unified-shard) prove/verify path to full optimization
parity with main's non-batched g·z OOD trace-opening pruning (#823/#827),
so recursive-verifier benchmarks compare like-for-like. Fiat-Shamir changes
for batched proofs — both sides change together; the format is unreleased
draft-PR-only.

Format (`BatchedTableData`):
- `trace_ood_evaluations` now carries only the current-row block (step_size
  rows, all columns); new `trace_ood_next_evaluations` carries the pruned
  next-row block (transition-window columns only, empty when the AIR reads
  none). Mirrors `StarkProof`'s post-#823 split.

Prover:
- `prove_rounds_1_to_3`: split the full OOD via `OodLayout::split_full` and
  absorb the two blocks (current then pruned-next) — same order/semantics as
  the non-batched round-3 absorption.
- `batched_table_deep_codeword`: draw only `layout.num_surviving()` trace-term
  powers and scatter them with `build_trace_term_coeffs` (rectangular grid,
  zeros at pruned positions) — identical DEEP codeword to non-batched round 4.
- `batched_round_4`: store the split blocks in `BatchedTableData`.

Verifier:
- `batched_verify_rounds_1_to_3`: absorb the two split blocks; add the I3
  OOD-shape guard (mirrors `ood_blocks_well_formed`) in Phase A, before Round 3
  absorbs the next-row block. The current-block width is checked against
  `context().trace_columns` (the physical OOD width) rather than
  `trace_layout().0 + num_aux`, since the batched lane includes step-packed
  AIRs (BitFlags) whose `trace_layout().0` is a logical, not physical, count.
- `batched_verify_round_4`: build one `OodLayout` per table (shared across
  the coeff replay, reconstruction, step 2, the #826 query-invariant hoist,
  and the per-query DEEP); draw pruned trace-term coeffs; reconstruct the full
  OOD grid once via `reconstruct_full`; drive the DEEP pair with the AIR-derived
  `next_row_cols` (real g·z pruning, replacing the previous all-columns hoist).
- `batched_synthetic_table_proof`: carry both split blocks.

Both epoch lanes are consistent: the VM lane prunes (this change); the L2G
lane already prunes via `prove_rounds_2_to_4` / `verify_rounds_2_to_4`.
`batched_verify_epoch` and `EpochProof` make no full-grid assumptions.

Tests: three batched soundness negatives for the split format — a current-row
trace-OOD tamper (transcript/step-2 desync) and the two I3 shape guards
(malformed current-row and next-row blocks). Full `stark` suite 225 passing.
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.

1 participant