Skip to content

refactor(stark): derive constraint metadata from the single eval body#772

Merged
MauroToscano merged 3 commits into
feat/single-source-constraints-switchfrom
feat/derive-constraint-meta
Jul 3, 2026
Merged

refactor(stark): derive constraint metadata from the single eval body#772
MauroToscano merged 3 commits into
feat/single-source-constraints-switchfrom
feat/derive-constraint-meta

Conversation

@MauroToscano

@MauroToscano MauroToscano commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What

Stacked on #764 (single-source constraints). Makes each table's eval body the single source of truth and cleans up how a constraint declares its metadata. Two commits:

  1. Derive meta() from the body. The hand-written per-table meta() (a parallel list of ConstraintMeta) is gone — it's now derived by running the same eval body through a no-op MetaBuilder. No second list to keep in sync.
  2. Split the three concepts that were welded into positional argswhich constraint (idx), how big (degree), which rows (exemptions):
    • Degree → per-table. Only the per-table max is consumed (composition_poly_degree_bound), so it's declared once via ConstraintSet::max_degree() (default 2; override to 3 on degree-3 tables) instead of on every emit. ConstraintMeta drops its degree field.
    • Row-domain → a named type. RowDomain::except_last(n) replaces the bare exemption int, kept at the emit site but no longer conflated with degree.

The everyday constraint

// before (#764):            after:
b.emit_base(3, 2, expr);     b.emit_base(3, expr);          // degree gone
// exemptions (example AIRs only): before was three unlabeled ints
b.emit_base_exempt(0, 1, 3 + 32 - 16 - 1, a2 - a1 - a0);
// after — named, degree off the call:
b.emit_base_rows(0, RowDomain::except_last(3 + 32 - 16 - 1), a2 - a1 - a0);

Every one of the ~18 production tables now writes emit_base(idx, expr); only the ~10 crypto/stark example AIRs use except_last(n) (they're the only ones with end-exemptions). Degree-3 tables carry one fn max_degree(&self) -> usize { 3 }.

Why it's safe

  • Composition bound byte-identical. composition_poly_degree_bound reads constraint_set.max_degree().max(logup_max_degree(layout)); each degree-3 table declares max_degree() equal to its former per-constraint max, and the framework folds in the LogUp max (batched terms 3, accumulator 1 + absorbed). Wire-identical with feat(stark): single-source constraints — one definition per constraint, verified cross-version [stacked on #763] #764 — the gate is cross-version verification.
  • Degree stays hand-declared, never auto-measured (auto-measuring would retighten the deliberately over-declared ecsm/ecdas convolution tails and move the bound).
  • The capture path asserts every constraint's measured degree <= max_degree() — this caught keccak_rnd's μ-gated (degree-3) IS_BIT during migration, which would otherwise have shipped a too-low bound.
  • Metadata still 100% derived from the one body; exact-once / dense-index / base-prefix invariants unchanged; prover hot path never touches metadata.

Verification

  • cargo check --workspace --tests — clean.
  • 169 stark tests (incl. logup_single_source_tests and the fibonacci examples' prove/verify, which exercise RowDomain::except_last end-to-end).
  • 96 prover constraint tests, incl. constraint_program_tests::all_table_programs_match_folders (every table's derived program vs the folders), the per-table constraint counts, and the measured <= max_degree() gates.
  • make lint clean (fmt + clippy -D warnings, all feature sets).

Not run locally: the full ELF proving suite and the cross-version-verification gate (server/CI) — the definitive checks for a transcription like this.

Metadata (kind, degree, end-exemptions) is now DERIVED from each table's
`eval` body instead of hand-maintained in a parallel `meta()` method, so the
two can no longer drift out of sync.

- `ConstraintBuilder::emit_base/emit_ext` now take the constraint's `degree`;
  new `emit_*_exempt` variants also take `end_exemptions`. The prover and
  verifier folders ignore both (dead args -> no hot-path cost); only metadata
  derivation reads them.
- `ConstraintSet::meta()` becomes a provided default that runs the body through
  a new no-op `MetaBuilder` (records {idx, kind, degree, end_exemptions}).
  Kind is implied by which sink is called; degree stays hand-declared, now at
  the emit site next to the expression it describes.
- Every per-table `meta()` override, every `*_meta` helper twin, and
  `logup_meta` are deleted; LogUp metadata is derived from
  `emit_logup_constraints` the same way.

Net -217 LoC across all tables + LogUp + the crypto/stark examples.

Verified: workspace + all test targets compile; 169 stark tests and 96 prover
constraint tests pass (including constraint_program_tests::
all_table_programs_match_folders and the per-table constraint counts); the
capture->interpreter differential and declared-vs-measured-degree gates hold;
`make lint` clean on all feature sets.
Removes per-constraint `degree` and the positional-int emit args, replacing
them with two orthogonal, self-describing concepts:

- Degree: only the per-table MAX is consumed (by composition_poly_degree_bound),
  so it is declared once via `ConstraintSet::max_degree()` (default 2, overridden
  to 3 on the degree-3 tables) instead of on every emit call. `ConstraintMeta`
  drops its `degree` field; the bound now reads
  `max_degree().max(logup_max_degree(layout))`.
- Row-domain: `emit_base_exempt(idx, degree, n, e)` becomes
  `emit_base_rows(idx, RowDomain::except_last(n), e)` — a named type, so the rare
  end-exemption reads in plain language and is no longer welded onto degree
  (three unlabeled ints -> one named argument). Only the crypto/stark example
  AIRs use it; every production table's emit is now `emit_base(idx, expr)`.

The composition-poly bound stays byte-identical: each degree-3 table declares
`max_degree()` == its former per-constraint max, and the framework folds in the
LogUp max via `logup_max_degree`. The capture path asserts each constraint's
measured degree is `<= max_degree()` — which caught keccak_rnd's mu-gated
(degree-3) IS_BIT during migration.

Verified: workspace + all test targets compile; 169 stark tests and 96 prover
constraint tests pass (including constraint_program_tests::
all_table_programs_match_folders and the per-table measured<=max_degree gates);
`make lint` clean on all feature sets.
@MauroToscano

Copy link
Copy Markdown
Contributor Author

/bench-abba 32

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

ABBA tiebreaker started on the bench server (~30–40 min). The bench server is occupied until it finishes.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

ABBA tiebreaker — c3663247cb vs main (32 pairs)

❌ Run failed. Last log lines:

   Compiling ethrex-crypto v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-trie v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-common v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling axum-extra v0.10.3
   Compiling ethrex-levm v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-l2-common v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-storage v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-metrics v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-storage-rollup v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-vm v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-blockchain v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-p2p v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-rpc v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-l2-rpc v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-fixtures v0.1.0 (/home/app/actions-runner/_work/lambda_vm/lambda_vm/tooling/ethrex-fixtures)
    Finished `release` profile [optimized] target(s) in 34.67s
wrote executor/tests/ethrex_5_transfers.bin (18381 bytes): block #1 with 5/5 transfer(s) [N senders -> N recipients]
==> Cached binaries are for different refs/features; rebuilding.
==> Building both prover binaries in isolated worktree /tmp/abba_wt
Preparing worktree (detached HEAD b44c615d)
==> Building cli @ b44c615d12 -> cli_B  (features: jemalloc-stats)
==> Building cli @ c3663247cb -> cli_A  (features: jemalloc-stats)
==> Running 32 interleaved pairs  (improvement: + = PR faster)
   pair  1/32   A=16.766s  B=16.496s   PR -1.64% (+=faster)
   pair  2/32   A=16.868s  B=16.816s   PR -0.31% (+=faster)
   pair  3/32   A=17.009s  B=16.656s   PR -2.12% (+=faster)
   pair  4/32   A=16.848s  B=16.669s   PR -1.07% (+=faster)
   pair  5/32   A=17.029s  B=16.826s   PR -1.21% (+=faster)
   pair  6/32   A=16.824s  B=16.456s   PR -2.24% (+=faster)
   pair  7/32   A=16.740s  B=16.833s   PR +0.55% (+=faster)

The RowDomain refactor turned emit_base/emit_ext into provided defaults that
forward to emit_base_rows/emit_ext_rows, adding a call hop on the
per-constraint-per-row prover path (vs #764's direct folder emit). A paired
ABBA vs #764 (12 pairs) showed a real ~1.1% regression: paired-t 95% CI
[-1.99%, -0.27%], Wilcoxon W=10 (significant), 10/12 pairs slower.

Marking the forwarding defaults and ProverEvalFolder's emit_*_rows #[inline]
collapses the hop back to a direct slice write. Pure codegen hint — no value
or wire change, so cross-verification stays green.
@MauroToscano MauroToscano marked this pull request as ready for review July 3, 2026 19:57
@MauroToscano MauroToscano merged commit aa2f649 into feat/single-source-constraints-switch Jul 3, 2026
11 checks passed
@MauroToscano MauroToscano deleted the feat/derive-constraint-meta branch July 3, 2026 19:57
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