test(lfm): gate the per-table epoch-verifier wrap on every suite run - #953
Merged
Conversation
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.
This was referenced Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_programwith 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, whichabsorb_lfm_statementbinds 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, andmachine_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 thanfrom_env(), exactly as the batched twin does, so a measurement run'sLFM_CENSUS_*variables cannot turn a suite gate into a real-block run.The pin, named rather than implied
epoch_programbuilds atWrapHash::production(). Under an algebraic pin that lowers the emitter's Merkle work toInstr::Hashand consults theLFM_HASHsocket, so this site is in the class that must be proved underBLOCK_HASHER. It names the pin twice over:build_artifacts_with_hasherfor the artifacts andlfm_cell_counts_with_hasherfor the numbers, so neither the proof nor the census can be taken atHasherKind::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()emitsInstr::Hashand must be proved underBLOCK_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:
ByteWrapHashemits noInstr::Hashat 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 atf6ca405cand again forfri_tests/join_testsat 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.rsis a blessed-set scan over files that MENTION an implied-hash symbol. This site mentions none, so blessinglfm/wrap_tests.rswould fire the gate's ownstaleassertion and break it.The registration is instead an amended reason on the existing
lfm/airs.rsentry, naming the new pinned consumer and recording why it must not be simplified back to the defaulting census pair: theLFM_HASHchip'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:make lint, box A: exit 0 over all six passes atb7995d53, and again atb5ad16ff. Re-running at the pushed headfee4e770.The shape, and the wall
--test-threads=1, ? INFERREDMemory 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_OPTIONSis 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.25Mfigure "was recorded against a different shape". That was wrong and is retracted:wrap_runpassesEpochInputs::from_env, which with noLFM_CENSUS_*set is byte-for-byteEpochInputs::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. AnLFM_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--ignoredrun 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:505is left alone.the_assembled_epoch_verifier_runsis non-ignored and executes a program built atWrapHash::production()under a literalTestPermutationrather thanBLOCK_HASHER— the same class this PR closes, and tracked as a separate follow-up.Not run here
The RPX pin.
per-table-gpuis 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.