Skip to content

test(lfm): gate the per-table epoch-verifier wrap on every suite run - #953

Merged
MauroToscano merged 4 commits into
per-table-gpufrom
pt/per-table-wrap-gate
Sep 7, 2026
Merged

test(lfm): gate the per-table epoch-verifier wrap on every suite run#953
MauroToscano merged 4 commits into
per-table-gpufrom
pt/per-table-wrap-gate

Conversation

@MauroToscano

@MauroToscano MauroToscano commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

The gap

The only NON-ignored epoch-verifier wrap prove test was the_fixture_epoch_wraps_batched, and its inner epoch is batched. All four per-table equivalents are #[ignore]d for cost: the_wrap_proves_and_verifies, the_real_block_epoch_wraps, the_from_proof_final_epoch_wraps, the_real_block_proves_and_wraps_end_to_end.

So the per-table proof FORMAT was covered — the leg suites prove it, and the batched wrap's own proof goes through lfm_prove — but the per-table epoch verifier PROGRAM was never proved on a suite run, only executed. By the method's rule 2 an execute-only test says nothing about the chips: where the executor mirrors a computation the chip also does, only a prove+verify run sees the chip.

With the batched format being deleted, that arm would have become the untested one. This is the per-table twin that keeps it from being.

What it does

the_fixture_epoch_wraps, not ignored: the min-preset fibonacci fixture epoch, epoch_program with legs, build_artifacts_with_hasher(.., BLOCK_HASHER), lfm_prove, verify_against, the epoch's own published oracles checked by value, and two falsification arms.

Two arms, not one check twice. A flipped main root makes the wrap UNBUILDABLE and never reaches the verifier, so it says nothing about verify_against's reject path. The second arm hands the verifier the real proof under a moved claimed word, which absorb_lfm_statement binds against, and asserts rejection. One shows the machine will not lie; the other shows the verifier catches a lie. About 0.16s of the 9.2s.

wrap_run_from's third arm, the moved PROGRAM DIGEST, is the registry premise rather than this program's, and machine_tests::verify_against_artifacts_agrees_with_the_registry_path (machine_tests.rs:5403) already holds it on a trivial program.

It takes EpochInputs::fixture() rather than from_env(), exactly as the batched twin does, so a measurement run's LFM_CENSUS_* variables cannot turn a suite gate into a real-block run.

The pin, named rather than implied

epoch_program builds at WrapHash::production(). Under an algebraic pin that lowers the emitter's Merkle work to Instr::Hash and consults the LFM_HASH socket, so this site is in the class that must be proved under BLOCK_HASHER. It names the pin twice over: build_artifacts_with_hasher for the artifacts and lfm_cell_counts_with_hasher for the numbers, so neither the proof nor the census can be taken at HasherKind::default().

The rule is quoted inline in the test doc rather than cited: the document carrying it lands with the RPX pin and does not exist on this branch, so a citation would dangle. A program built at WrapHash::production() emits Instr::Hash and must be proved under BLOCK_HASHER. A program that pins a byte hash on its own builder emits none, never consults the socket, and is correct at the registry's blessed default under every pin.

On a byte pin that naming is inert, and that is the trap: ByteWrapHash emits no Instr::Hash at all, so a defaulted socket hasher is free and correct here and becomes wrong only when the pin moves. Written pinned now so the flip is not a bug-hunt later, which is what it was at f6ca405c and again for fri_tests / join_tests at v10.

The gate therefore runs unchanged when the RPX pin PR lands: the test reads the pin.

Why the allowlist took no new file

prover/src/tests/hash_pin_enumeration.rs is a blessed-set scan over files that MENTION an implied-hash symbol. This site mentions none, so blessing lfm/wrap_tests.rs would fire the gate's own stale assertion and break it.

The registration is instead an amended reason on the existing lfm/airs.rs entry, naming the new pinned consumer and recording why it must not be simplified back to the defaulting census pair: the LFM_HASH chip's width is tenant-dependent, so a defaulted census under an algebraic pin reports the WRONG chip rather than a smaller number. That is the pin branch's own lesson — "test-only" is not "safe", it is only "production-safe" — applied to a site that now proves against pinned artifacts on every suite run.

Evidence

Box A, commit b7995d53, verbatim:

inner epoch: fibonacci fixture, 2^3 cycles, 8 cycles executed, 25 sub-proofs, proved in 3.1s
★ THE PER-TABLE WRAPPED PROGRAM
   210782 instructions / 1 keccak permutations / 16461 arena words / 1 chunks
   census at Test: 42096912 main + 13320972 aux ext = 82059828 base-field equivalents; PROJECTED peak RSS 2.6 GiB (a projection from slice 0's coefficient, not a measurement of this run)
★ PER-TABLE WRAP PROVED AND VERIFIED (inner epoch [2 x18, 3, 4 x2, 5 x2, 7, 20], blowup 2, 1 query)
   prove 3.8s / verify 0.16s / proof 45953352 bytes / 168 published words / 15 sub-proofs
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1163 filtered out; finished in 9.20s
test tests::hash_pin_enumeration::no_call_site_outside_the_pin_reaches_a_default_alias ... ok
test result: ok. 1 passed

make lint, box A: exit 0 over all six passes at b7995d53, and again at b5ad16ff. Re-running at the pushed head fee4e770.

The shape, and the wall

shape min preset, blowup 2 at one query, fibonacci fixture epoch, 25 inner sub-proofs, every leg
program 210,782 instructions, 16,461 arena words
census at the pin 42,096,912 main + 13,320,972 aux ext = 82,059,828 base-field equivalents
wall 9.20s on box A. CI expectation 30-60s on a 2-4 vCPU runner under --test-threads=1, ? INFERRED
wrap proof 45,953,352 bytes over 15 wrap sub-proofs

Memory and time are separate verdicts and are not averaged. Peak RSS was not measured on this run; the 2.6 GiB the test prints is a projection from an existing coefficient and is labelled as one in the output.

The min preset is the floor on the preset axis: MIN_PROOF_OPTIONS is already blowup 2 at one query, and the leg set is the epoch's own sub-proof count rather than a choice. A knob to trade the WRAP proof's own query count for wall time was written and then removed once the shape was measured — at 9.2s there is nothing to buy, and a dormant setting that quietly weakens a suite gate is worse than no setting.

The 46 MB proof is the framework query count over 15 wrap sub-proofs, not anything about the inner epoch. It is fine for a gate that builds, verifies and drops it in-process.

Two things deliberately NOT done

Slice 0's numbers are annotated, not corrected (35c00aa6, after review). An earlier revision of this PR claimed the ~2.25M figure "was recorded against a different shape". That was wrong and is retracted: wrap_run passes EpochInputs::from_env, which with no LFM_CENSUS_* set is byte-for-byte EpochInputs::fixture, so in a clean environment slice 0 and the new gate are the same program built from the same epoch. One program carrying two figures an order of magnitude apart is the finding.

The ⚠ therefore sits on slice 0's signature and on MEASURED_BYTES_PER_CELL's provenance line, where the doubtful numbers live, rather than on the new test. An LFM_CENSUS_*-overridden run is named as the likely explanation and marked as a hypothesis; "the shape shrank since" is not excluded. Neither value is changed. The coefficient especially: a ratio of two numbers taken together on one run stays valid whichever shape that run was, and re-deriving it from a shape whose RSS nobody measured would swap a misfiled observation for a fabricated one. What it needs is one --ignored run of slice 0 in a clean environment, after which all three move together or none do. The measured figure for THIS test is recorded in its own doc so the stale one stops circulating for this shape.

epoch_verify_tests.rs:505 is left alone. the_assembled_epoch_verifier_runs is non-ignored and executes a program built at WrapHash::production() under a literal TestPermutation rather than BLOCK_HASHER — the same class this PR closes, and tracked as a separate follow-up.

Not run here

The RPX pin. per-table-gpu is cut from the unpinned base, so the branch default is the BLAKE3 control and there is no RPX pin to run against yet. The pin lands as its own PR; this gate needs no change when it does.

The only non-ignored epoch-verifier wrap prove test was
`the_fixture_epoch_wraps_batched`, and its inner epoch is batched. All four
per-table equivalents are `#[ignore]`d, so a suite run never PROVED the
per-table assembled epoch verifier — only executed it, which by method rule 2
says nothing about the chips. With the batched format on its way out that arm
would have been the untested one.

`the_fixture_epoch_wraps` is its per-table twin, not ignored: the min-preset
fibonacci fixture epoch, `epoch_program` with legs, `lfm_prove`,
`verify_against`, the epoch's own published oracles, and one tamper arm (a
flipped main root must make the wrap unbuildable, in execution rather than in
the prover).

Both hash axes are NAMED, per HASH-PINNING.md's classification rule: the
program is built at `WrapHash::production()`, so under an algebraic pin it
emits `Instr::Hash` and consults the socket. The site therefore takes its
artifacts from `build_artifacts_with_hasher(.., BLOCK_HASHER)` and its cell
counts from `lfm_cell_counts_with_hasher(.., BLOCK_HASHER)` rather than either
defaulting form — inert under a byte pin, load-bearing the moment the pin
moves. The `lfm/airs.rs` allowlist reason records that pairing so the call
site is not simplified back to the defaulting census.

A lean body rather than `wrap_run_from`: the measurement harness emits the
spine a second time for the closed-form check, walks the census and the
row-cliff panel, and runs three falsification arms. None of that is what this
gate claims, so the gate does not pay for it.
…its query knob

The gate was written with a const that could trade the WRAP proof's own query
count for wall time, against a worry that the shape would not fit a suite run.
The measurement says otherwise: 210,782 instructions, 82,059,828
base-field-equivalent cells, and 9.20s for the whole test on a 48-core box, of
which 3.8s is the prove and 0.16s the verify. There is nothing to buy, so the
knob is removed rather than left dormant — a setting that quietly weakens a
suite gate is worse than no setting, and the gate now proves under the same
unreduced options every leg suite proved under.

The measured shape goes in the test's doc as a table, because the neighbouring
slice-0 doc quotes ~2.25M instructions for nominally this same epoch and preset
and that is an order of magnitude high for it. Slice 0's own figure is left
alone: which shape it belongs to is for whoever re-runs it to establish, not
something to infer from a measurement of a different test.

Also recorded: the 46 MB wrap proof is the framework query count over 15
sub-proofs rather than anything about the inner epoch, and it is fine for a
gate that builds, verifies and drops it in-process.
The previous commit claimed the ~2.25M-instruction figure "was recorded
against a different shape". That is not supported by the code and should not
have been asserted. `wrap_run` passes `EpochInputs::from_env`, which with no
`LFM_CENSUS_*` set is byte-for-byte `EpochInputs::fixture`, so in a clean
environment slice 0 and `the_fixture_epoch_wraps` build the same epoch through
the same `real_epoch_from` and emit the same `epoch_program(&e, true)`. One
program, two figures an order of magnitude apart.

So the warning moves to where the doubtful numbers live rather than sitting on
the new test. Slice 0's signature now says both its figures, and the
481,327,124-cell point `MEASURED_BYTES_PER_CELL` attributes to it, are
unverified for the shape that signature describes; that an `LFM_CENSUS_*`-
overridden run is the likely explanation, marked as the hypothesis it is; and
that the fix is one `--ignored` run with a clean environment, not a rewrite.

`MEASURED_BYTES_PER_CELL`'s value is deliberately unchanged. A ratio of two
numbers taken together on one run is a valid coefficient whichever shape that
run was, and re-deriving it from a shape whose RSS nobody measured would swap
a misfiled observation for a fabricated one. Only its provenance line is
annotated.

Also records the CI-class expectation the reviewer asked for: 30-60s on a
2-4 vCPU runner under `--test-threads=1`, marked inferred, scaled from the box
wall rather than measured on a runner.
…f citing it

Three review nits on the per-table wrap gate.

The classification rule was cited as `HASH-PINNING.md`, which does not exist on
this branch — the document lands with the RPX pin. A doc pointer that dangles
is worse than no pointer, so the rule is quoted inline instead and the doc is
self-contained under either pin.

The gate had one falsification arm and the batched twin has two. The missing
one matters more than its cost suggests: a flipped root makes the wrap
UNBUILDABLE and never reaches the verifier at all, so `verify_against`'s reject
path was untouched by the gate. It proved the machine will not lie without ever
proving the verifier catches a lie. The moved-claimed-word arm hands the
verifier the real proof under a claim it does not answer and asserts rejection,
for about 0.16s against a 9s test.

The harness doc no longer claims `wrap_run_from`'s third arm is unnecessary
work in general: the moved PROGRAM DIGEST is the registry premise rather than
this program's, and
`machine_tests::verify_against_artifacts_agrees_with_the_registry_path`
(machine_tests.rs:5403) already holds it on a trivial program.

Kept as a separate commit rather than folded into 35c00aa, which is already
pushed; amending it would mean force-pushing a SHA the reviewer has seen.
@MauroToscano
MauroToscano merged commit a42554c into per-table-gpu Sep 7, 2026
10 of 13 checks passed
@MauroToscano
MauroToscano deleted the pt/per-table-wrap-gate branch September 7, 2026 19:15
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