Skip to content

feat(lfm): pin per-table-gpu's block path to RPX256 - #967

Merged
MauroToscano merged 23 commits into
per-table-gpufrom
pt/pin-rpx
Sep 8, 2026
Merged

feat(lfm): pin per-table-gpu's block path to RPX256#967
MauroToscano merged 23 commits into
per-table-gpufrom
pt/pin-rpx

Conversation

@MauroToscano

Copy link
Copy Markdown
Contributor

What

Pins per-table-gpu's block path to RPX256, ported from the hash-rpo pin stack and translated to RPX, in a commit order that never leaves the tree where flipping the pin moves registry identities.

  1. The block path names its hasher at the call site; build_artifacts defaults to REGISTRY_HASHER (ports of hash-rpo 5e923567, a6e7f0b7, 28b7a360). per-table-gpu's build_artifacts routed through hash_pin::BLOCK_HASHER, so a pin flip alone would have moved every LFM_REGISTRY program_id; the blessed value and the builder default are now one definition, and the 17 block-path sites (wrap_tests 8, aggregator_tests 7, fri_tests 1, join_tests 1) pass BLOCK_HASHER explicitly.
  2. Every test that names a hash takes it from one source (d66b5682, 75efe380, 048d744c, 73ccc6d7): F1, the Prover::multi_prove / Verifier::multi_verify alias sweep across thirteen files, the Merkle backend aliases through the pin.
  3. The enumeration gate stops enumerating spellings of the default (7226c63c): ALIAS_CALLS plus a CONFIG_ALLOWED allowlist over stark::config, extended here with DeviceTreeBackend and with Prover::compute_precomputed_commitment_for_testing as a third alias call form (it let a BLAKE3 commitment into the bitwise soundness control).
  4. F4: the arena stride is the builder's digest width. SubProofShape, FriShape and TableVerifyShape take the width as an argument; the machine side passes edsl::digest_words(b), the host side proof_arena::words_per_root(), and commitments_to_arena_for(roots, WrapHash) serialises for programs that pin a byte hash on their own builder. Every remaining literal two-words-per-root in the LFM tests and in l2g_binding_program_source follows the width; the executor's arena-length check is strict, so each was an ArenaLenMismatch under the pin.
  5. The pin: BlockStarkHash = RpxStarkHash, BlockTranscript = AlgebraicTranscript built with_seed(BLOCK_HASHER, ..), BLOCK_HASHER = HasherKind::Rpx. Committed first WITHOUT tables as the trial flip HASH-PINNING prescribes.
  6. Both regenerations, statics first, registry second. The four static families (bitwise, keccak_rc, page zero-page, page private-page at blowup 2/4/8), then LFM_REGISTRY regenerated in a tree that already carries them, plus the fifth blessed constant (SUB_DECODE_COMMITMENT_BLOWUP_2). The first registry regeneration ran before the statics and all six registry_drift_* fired at exactly slots 13 and 14, which registry.rs fills from the blessed static constants; the order is now documented in HASH-PINNING.md, hash_pin.rs and both generators.
  7. HASH-PINNING.md for the RPX pin at the repo root: the three axes, the classification rule, the gate, the stride rule, the regeneration order, the cuda posture after feat(math-cuda): RPX256 leaf and tree kernels, launch module and device dispatch (lane K, phase 2) #962 (the RPX pin proves on the device; RPO256/Poseidon still abort), and the tests known red under the pin.

Evidence (box B, CPU; relayed by the coordinator)

  • Control regeneration at the pre-pin head 7bc4c221: both generators exit 0; compared locally against the committed sources, LFM_REGISTRY reproduces byte for byte on all six entries and all twelve static constants are identical.
  • Pre-pin gates at 7bc4c221: 13 passed; 0 failed.
  • Trial flip at af8ae82b (pin, no tables): exit 101, 0 passed; 4 failed, every failure PrecomputedCommitmentMismatch — the loud prove-time failure HASH-PINNING prescribes.
  • First full --lib at 542f9526: 1082 passed; 38 failed; the control full --lib at 7bc4c221: 1119 passed; 1 failed (the pre-existing fixture-output test below). All 37 pin-only failures were classified by file:line; 27 are fixed in the commits above, the remaining ten are the table below. The continuation liveness guard's timeout got headroom (300 s → 1800 s) after the test passed alone three times at each head — 7.29 / 7.32 / 7.55 s at the tip, 15.73 / 15.85 / 15.67 s at the pinned head — showing the pin doubles that path and the old bound was the constraint under suite load; it passed inside the final full run.
  • Final gates at 789c04c8 (23 signed commits): (5a) the §3b list + registry_drift_ + hash_pin + tests::hash_pin_enumerationok. 13 passed; 0 failed; 0 ignored; 1163 filtered out; finished in 167.49s, all six registry_drift_* included; (5b) the two last-fixed tests by name → ok. 2 passed; 0 failed; (5c) both regenerators re-run under the pin are idempotent against the committed sources (LFM_REGISTRY 3,264 hex bytes identical in sequence, all twelve static constants verbatim); (5e) full --libFAILED. 1119 passed; 10 failed; 47 ignored — exactly the ten in the table below, nothing else. ⚠ That run's wall time (4,634 s) is not comparable to the earlier 2,944 s: an orphaned test binary from a killed run shared box B for its first hour, so the suite ran contended; it is a pass/fail gate and the result stands, the duration does not.
  • make lint (both arms): exit 0.

Laptop: cargo check -p lambda-vm-prover --lib --tests and cargo clippy -D warnings -A clippy::op_ref on both feature arms green at every commit; fmt clean; a local replica of the enumeration gate's scan matches the blessed set exactly.

Known red under the pin — out of scope by decision, documented in HASH-PINNING.md

test mechanism disposition
epoch_tests::the_batched_query_census_matches_the_closed_form (3300), epoch_tests::the_assembled_carved_batched_epoch_verifier_runs (3382) census closures count Instr::KeccakF / Instr::Blake3 only; the algebraic wrap hash is Instr::Hash; batched_query_permutations_for has no algebraic arm byte-hash counter model; algebraic arm owed
fri_tests::the_emitted_permutation_count_meets_the_pinned_prediction (867), fri_tests::the_fri_join_adds_no_second_point_derivation (983) byteswap / leaf-decomposition predictions are byte-encoding costs an algebraic leaf does not pay byte-hash counter model; algebraic arm owed
machine_tests::transcript_replay_cell_counts (1543), machine_tests::register_derivation_cost (4437) wrap_hash_rows (3460) dispatches on WrapHash::production() rather than the program's own builder hash, so BLAKE3-built registry programs count zero rows helper must read the program's hash
machine_tests::the_register_derivation_matches_production (4290), machine_tests::the_register_derivation_proves_and_verifies (4545) register_derivation_program is built at WrapHash::Blake3 with no algebraic arm while production's REGISTER commitment follows the pin FEATURE GAP: the machine REGISTER derivation under an algebraic hash (planned separately)
per_table_census_tests::the_blake3_tenant_socket_matches_the_record lane C's guard, firing correctly: the recorded census and its lever-0 figure were produced under the Test/BLAKE3 socket; this build's socket is RPX re-record under the pin (follow-up); the guard is not weakened
epoch_tests::the_closure_rejects_a_moved_index_or_output (4691) the fixture epoch reports an empty public output PRE-EXISTING: the only failure at the pre-pin head
continuation::tests::test_prove_error_mid_pipeline_returns_err (continuation.rs:2492) a liveness guard with a fixed 300 s receive timeout; the pin doubles the test's proving work (alone, 3 runs each: 7.3-7.6 s at the BLAKE3 default, 15.7-15.9 s under RPX) and under the full suite's parallel load the bound fired while the test was still progressing FIXED: the bound is 1800 s; a wedge still hangs forever and is still caught

Behaviour to know

  • Under cuda the RPX pin commits on the device (feat(math-cuda): RPX256 leaf and tree kernels, launch module and device dispatch (lane K, phase 2) #962); RPO256 and Poseidon pins still abort at the first device commit with unimplemented! naming the hash. The first end-to-end GPU prove under RPX is the milestone gate after the merge.
  • registry_drift_* must pass unchanged. A moved identity means the pin leaked into registry identity; the fix is never a re-bless.
  • P3_ARTIFACT_DIR must be fresh for any block run: the driver loads cached artifacts, and a BLAKE3 run's bundle would be fed to an RPX verifier.
  • No proof-format change: the digest stays 32 bytes on the wire.

…ild_artifacts

Reverts 13453ef's scope while keeping what it was right about.

That commit fixed a real defect: lfm_prove_batched takes artifacts.hasher, so the
aggregator inherited a toy permutation from a defaulted build and proved under it
while the host committed under RPO. But it fixed the defect at the SHARED entry
point rather than at the block path's call sites, and the hasher is part of
program IDENTITY — HasherKind::as_tag is folded into lfm_program_id, and
LFM_REGISTRY is blessed under compute_lfm_registry's REGISTRY_HASHER = Test,
whose own doc calls changing it a re-blessing of the whole table rather than a
re-run. So every registry program's identity moved away from the blessed table.

The tree states the invariant in an assertion message.
rpo_chip_tests::the_rpo_choice_moves_the_program_digest_and_no_root asserts that
build_artifacts must default to Test, and poseidon_chip_tests and
blake3_socket_tests carry the same contract under the same name shape. Their
subject is exactly that two hashers are two program identities.

So build_artifacts defaults again, and the block path names BLOCK_HASHER where it
builds: the aggregator's seven sites and the wrap leg's eight. The rule is
checkable rather than a judgement — a program built at WrapHash::production()
emits Instr::Hash and must be proved under the pin; a program that pins a byte
hash on its own builder emits none, never consults the socket, and is correct at
the default under every pin. wrap_tests' keccak-chain census site is the one
exception in that file and keeps the default for that reason.

Not verified: no local runs. The ~21 registry-identity failures this addresses
are machine_tests' registry drift and prove-and-verify set plus the three chip
identity tests; the box is the oracle for whether the count is right.

Note for whoever reads the regeneration control in HASH-PINNING.md: matching all
13,056 registry hex bytes proved the regeneration deterministic and was SILENT on
whether an identity had moved, because compute_lfm_registry pins REGISTRY_HASHER
explicitly and never reads the entry point 13453ef changed. The control
validated the generator against itself.

(cherry picked from commit 5e92356)
…ot to a silent global

Same behaviour, different failure mode. HasherKind::default() on this path was a
silent global: it happened to be Test, and nothing at the call site said why that
was correct. REGISTRY_HASHER is the blessed tenant of LFM_REGISTRY, a named
constant whose doc records that it is folded into every program_id and that a
second hasher becomes additional ROWS rather than a replacement — so the default
now states a fact about registry identity that a reader can check against the
table.

The constant moves from compute_lfm_registry into registry.rs, which is its
proper home: it is a property of the TABLE, not of the generator, and
build_artifacts has to name it. The generator imports it, so the value the table
is blessed under and the value the unparameterised builder defaults to cannot
drift apart — they are one definition.

It also shrinks the enumeration gate's name-based residue. That gate's
stark::config coverage is an allowlist and cannot lag, but HasherKind::default()
and the Prover/Verifier call forms remain name-based and are where a fourth miss
would live. One fewer occurrence on the path that matters most.

(cherry picked from commit a6e7f0b)
…review miss

Option C reverted build_artifacts to the registry's blessed hasher and pinned the
block path at its call sites — the aggregator's seven and the wrap leg's eight.
It missed two: fri_tests::the_fri_leg_proves_and_verifies and
join_tests::the_join_proves_and_verifies each build artifacts at a single site
this lane had not classified, because the earlier pass through those two files
fixed only their execute hasher and never looked at how they built artifacts.

Both programs are built at WrapHash::production(), so they emit Instr::Hash and
the AIR set must carry the pin's tenant. They were passing before C only because
build_artifacts was globally returning BLOCK_HASHER — the same accidental cover
that had been holding up seven wrap_tests sites, which were caught by
classification before the revert could regress them. These two were not, because
the classification sweep enumerated call sites in the files C touched rather than
in every file that builds artifacts for a production-hash program.

The rule that names the fix is the one in HASH-PINNING.md and it applied cleanly
once asked: which program does the site build.

Predicted as a C-review finding before the leg ran, and it is one — a pin missing
where the blessed default was wrong, the opposite-direction error to the one that
started this.

(cherry picked from commit 28b7a36)
Three arms of one class, each a pair of things that must agree on the hash and
did not. All three are no-ops on a byte pin, where BLOCK_HASHER is
HasherKind::Test and a digest is two words, so the base does not move.

(builder, executor) — 24 sites in fri_tests, join_tests and epoch_verify_tests
built their programs at WrapHash::production() and then handed execute the
one-round toy. Under an algebraic pin the program emits Instr::Hash and the walk
reconstructs nothing, surfacing as DivByZero on HONEST paths several thousand
instructions from the cause. Every LfmBuilder::new() in those three files uses
WrapHash::production(), so the replacement is exact rather than a blanket; the
five other files using TestPermutation pin a byte hash on their own builders and
are legitimately toy-hash tests.

(AIR, trace) — 12 sites in machine_tests, blake3_chip_tests and wrap_tests built
traces through the defaulting build_traces while proving against artifacts whose
AIR set follows the pin. The socket chip's WIDTH is tenant-dependent, so
HashConstraints::eval indexes a trace built for another tenant and panics out of
bounds inside a rayon worker. They now take the tenant from artifacts.hasher —
the artifacts they are about to prove against, one source rather than two.
wrap_tests' census test needed its artifacts moved above the execute so that
execution, traces, census and AIRs share it.

This mismatch did not pre-exist: before build_artifacts was pinned, artifacts.hasher
was ALSO Test and the pair agreed by both being wrong. Correcting one half and
leaving the other is what created it.

(writer, reader) — machine_tests' digest_bytes read eight halves from two public
words. An algebraic digest is ONE word of four canonical felts. It now
discriminates on the slice's own length rather than on WrapHash::production(),
because its callers mix registry-pinned two-word programs with
configuration-following one-word ones and a global branch would break the first
group. Same reason the R1f instrument's fix moved this failure here: an emitter
migrated without its reader.

wrap_tests' module doc claimed every permutation in it is TestPermutation, which
these changes make false; corrected rather than left, per SOUNDNESS.md 6.8.

(cherry picked from commit d66b568)
…— name the pin instead

stark::prover::Prover and stark::verifier::Verifier are GenericProver and
GenericVerifier AT DefaultStarkHash. They are the silent spelling of the symbol
the alias-implied-hash class is named after, and commit 98b0798 closed that
class for PRODUCTION paths only while claiming it closed. Nineteen files kept
calling the BLAKE3 alias on branches whose whole purpose is that the pin differs.

Two distinct pairings, both mechanical:

(prover, verifier) — 28 call sites across seven files proved through
multi_prove_ram, which is BlockProver and follows the pin, then verified through
the BLAKE3 alias. bitwise_bus, bitwise, branch_bus, local_to_global_bus, lt_bus,
logup_tests and prove_elfs_tests. prove_elfs_tests was half-migrated across four
axes inside one function: pinned prover, byte prove transcript, pinned bus-balance
replay transcript, aliased verifier. That is the axis-2 half-flip hash_pin's own
module doc calls silent, and it is loud here only because the replay transcript
disagrees with the other two. The inconsistency is what saved it.

(blessed roots, prover) — keccak_probe, blake3_probe, framework_probe and the two
PoCs are internally consistent on the default, so they looked fine. Their AIRs
carry the preprocessed commitments regenerated under the pin, so an alias prover
recomputes a BLAKE3 root against an RPO constant and refuses at
PrecomputedCommitmentMismatch. That is exactly keccak_probe.rs:170's message,
"should reach the verifier, but the prover refused first". Self-consistency
between prover and verifier is not enough when a third thing already moved.

Everything here is a no-op on a byte pin by construction: BlockProver and
BlockVerifier ARE the aliases when BlockStarkHash is DefaultStarkHash, and
block_transcript is DefaultStarkTranscript::new.

The gate gains the two symbols and a different question. It searched for
DefaultStarkHash, DefaultStarkTranscript and HasherKind::default() and not for
the two aliases that silently mean the first — so it looked for the class by the
one name the class never uses, and reported green. It now matches the call forms
Prover::multi_prove and Verifier::multi_verify, excluding lines that name
BlockProver or BlockVerifier.

And its blessed list documents the corrected question: not "is this reachable
from production?" but "is this default PAIRED with a non-default?". Test-only is
not safe, it is only production-safe. Recorded with it: a correct fix to one half
of a wrong-but-consistent pair CREATES the failure, so a red test after such a
fix is evidence the fix worked.

(cherry picked from commit 75efe38)
Eleven files imported DefaultTranscript only to construct one; block_transcript
replaced every construction, so the imports are dead and -D warnings would
refuse the build. Two test_utils::E imports go with them for the same reason.

(cherry picked from commit 048d744)
An eighth variant, and the third time the gate's symbol list has been short.
stark::config::BatchedMerkleTreeBackend<F> is BatchBlake3Backend<F> by
definition, and FriLayerMerkleTreeBackend<F> is PairBlake3Backend<F>. They are
type aliases rather than calls, so neither the DefaultStarkHash search nor the
Prover/Verifier call-form search sees them — and a test comparing a machine leaf
against one of them compares against BLAKE3 whatever the branch pins.

Six sites: fri_tests' byte-identical leaf gate, which is precisely the test whose
whole claim is that the machine's leaf IS the verifier's leaf, and four in
join_tests' coherent-forgery construction.

Both now go through proof_arena::BlockBatched and BlockPair, defined beside
MainBackend so the pin's associated types have one spelling in the crate rather
than one per caller.

Two more defects surfaced while doing it, both in fri_tests' leaf gate:

The leaf was published as two cells. An algebraic digest is one cell whose second
slot repeats the first, so the comparison read one lane twice and would have
passed on a duplicated value. It now publishes leaf.cells() and reads back
whatever width that is.

And join_tests reached hash_data_from_slices as an INHERENT method, which only
the byte backend has. That is the same defect the original trial flip found in
proof_arena::leaf_hash — the algebraic backend implements it through
IsStreamingLeafBackend, so it has to be named through the trait. Four sites.

Not verified: no local runs. The box is the oracle.

(cherry picked from commit 73ccc6d)
Three misses share one root, and it is not carelessness about any particular
name: the gate is LEXICAL and the class is TYPE-LEVEL, so a list of forbidden
names always lags one spelling behind the newest way to denote the default.
DefaultStarkHash as a substring, then the Prover and Verifier call forms, then
the Merkle backend type aliases — each was added after it bit.

So stark::config is now handled by an ALLOWLIST over the namespace rather than a
list of names. Prover legitimately needs exactly three items from that module,
all hash-agnostic: Commitment, CommitmentHash and StarkHash. Everything else it
exports is a spelling of the workspace default, so a new alias added there is
flagged the first time prover names it, without anyone remembering to extend this
file. An allowlist cannot lag.

Deletion would be stronger — let the compiler refuse the spelling rather than a
test — and it was considered and is not available. BatchedMerkleTreeBackend and
FriLayerMerkleTreeBackend have twelve legitimate consumers inside crypto/stark
itself: commitment.rs, gpu_lde.rs and the cuda tests, where the workspace default
IS the correct hash. Marking them deprecated would fire on those under
-D warnings, in the very crate that must keep using them. A clippy
disallowed-types entry was considered too and rejected as unverifiable here: a
workspace-root clippy.toml applies to every crate including crypto/stark, and
whether a prover-local one scopes correctly is version-dependent behaviour this
lane cannot test without running lint.

The blessed list also loses three entries. prove_elfs_tests,
page_offset_forgery_poc and recursion_soundness_gap_poc no longer mention an
implied hash at all after the alias sweep, and the gate's own staleness check
caught them — a blessing kept past its cause is a wish rather than an inventory.

(cherry picked from commit 7226c63)
The allowlist names the hash-agnostic items `prover` may take from
`stark::config`. `DeviceTreeBackend` is one: it is the marker a Merkle backend
implements to name its OWN hash as the device dispatch key, and
`algebraic_commit` implements it for the algebraic backends — the opposite of
reaching a default. Without it the gate flags that file the moment the
allowlist lands on a branch carrying the algebraic dispatch.
…figuration's

SubProofShape::{query_words, opening_words}, FriShape::query_words and
TableVerifyShape::{opening_words, fri_words} sized the sibling stride from
proof_arena::words_per_root(), which reads WrapHash::production(), while every
emitter advances its cursor by edsl::digest_words(b), the BUILDER's width. The
two agree only for a builder at production(); an explicit WrapHash::Algebraic
build on a byte-pinned branch — or a byte-pinned builder under an algebraic pin
— trips the emitters' own stride assertion. The width is now an argument: the
machine side passes edsl::digest_words(b), the host side passes
proof_arena::words_per_root(), the width it serialises roots at, and neither
shape reads the configuration.

The tests' literal-two strides go the same way. The host-side arena offsets and
Select counts in fri_tests / join_tests read words_per_root(); the control
program in join_tests declared its roots arena and indexed its root hints at a
literal two words per digest on the MACHINE side, which the executor's
arena-length check refuses outright under an algebraic pin — it now reads its
own builder's width. Ports hash-rpo's 0998fd6 and closes the two sites it left.
split_whitespace already ignores leading and trailing whitespace, so the trim() the ported allowlist parser did first is redundant; clippy 1.94 refuses it under -D warnings.
The three axes of prover/src/hash_pin.rs move together and nothing else does:
BlockStarkHash = algebraic_commit::RpxStarkHash (what the host commits under),
BlockTranscript = algebraic_transcript::AlgebraicTranscript built by
block_transcript() with BLOCK_HASHER (the Fiat-Shamir object, pinned
separately because StarkHash::Transcript cannot force it), and BLOCK_HASHER =
HasherKind::Rpx (the LFM_HASH socket permutation the block path's programs are
executed and proved under). The workspace default stays BLAKE3 and its
assertions stay intact; the registry keeps its blessed identities because
build_artifacts defaults to REGISTRY_HASHER and the block path names the pin at
its call sites.

This commit carries NO regenerated tables. It is the trial flip HASH-PINNING
prescribes: the prover recomputes an RPX root for a static preprocessed table,
compares it against the blessed BLAKE3 constant, and must fail loudly at prove
time with ProvingError::PrecomputedCommitmentMismatch. A green run here means
the static-commitment path was not exercised. Both regenerations follow.

Under `cuda` this compiles — the algebraic backends are DeviceTreeBackends —
and aborts at the first device commit with `unimplemented!` naming the hash;
every proving gate under this pin is a CPU run until the RPX kernels land.
… named

HASH-PINNING.md records the per-table-gpu pin: the three axes and their RPX
values, REGISTRY_HASHER and the call-site classification rule, the enumeration
gate's allowlist, the builder-width arena stride, both regenerations with the
control-first discipline and what the control does not prove, the width defect,
and the running rules (CPU-only proving under this pin, a fresh artifact
directory, no byte comparison of proofs). Provenance is stated as it is: miden
publishes no known-answer table for RPX, so the anchor is the repo's own host
KAT harness, a self-built oracle. The cuda section says the build COMPILES and
aborts loudly at the first device commit — no compile_error!, no byte-hash
fallback, both lint arms gate.

The regenerator's header and hash_pin.rs's regeneration note both said three
static families; compute_static_commitments rewrites four (page carries a
zero-page and a private-page constant), and now both say so.
…irst

LFM_REGISTRY and the four static preprocessed families (bitwise, keccak_rc,
page zero-page, page private-page, each at blowup 2/4/8), regenerated with
compute_lfm_registry and compute_static_commitments at the pinned head on a
CPU box and pasted from their output.

Control first, at the pre-pin head: both generators reproduced the committed
tables exactly — the registry byte for byte on all six entries after
normalising rustfmt's line wrapping and trailing commas, and all twelve static
constants. Only then were they trusted under the pin.

What the pin moved, per registry entry: 12 of the 15 roots (the three
zero-width slots are unchanged), program_id and prep_root. What it left alone:
kind, blowup_factor, log_heights, keccak_rnd_chunks, chip_set, prep_widths and
hasher, which stays HasherKind::Test because the registry is blessed under
REGISTRY_HASHER, not under the block path's BLOCK_HASHER. All twelve static
constants moved. These tables are generated output and are never hand-edited;
a registry_drift_* failure against them is investigated, not re-blessed.
registry.rs:661-664 fills roots[13] and roots[14] from keccak_rc::preprocessed_commitment and bitwise::preprocessed_commitment, and at coset offset 3 both RETURN the blessed static_commitment constant from the tree rather than recomputing; lfm_program_id folds every root, so program_id inherits them. The previous registry regeneration ran in a tree whose statics were still BLAKE3, so it embedded the old constants in slots 13/14 of every entry, and all six registry_drift_* fired at exactly those two slots once the RPX statics were in the tree. The pre-pin control could not see this: statics and registry were BLAKE3-consistent there.

This is the registry regenerated again at the pinned head, AFTER the RPX statics — the correct order is statics first, registry second. Prediction: all six registry_drift_* pass; on every entry slots 13 and 14 and program_id move (to the RPX keccak_rc@2 / bitwise@2 constants), and slots 0-12, prep_root, log_heights, prep_widths, chip_set, keccak_rnd_chunks and hasher (Test) are unchanged. Statics untouched.
…over

bitwise_tests::compute_trace_commitment built the honest and malicious preprocessed commitments through stark::prover::Prover — the workspace alias, GenericProver at DefaultStarkHash, BLAKE3 whatever the pin says — while multi_prove_ram recomputes the AIR's commitment under the pinned hash. Under an algebraic pin the honest arm therefore fails at prove time with PrecomputedCommitmentMismatch before the test reaches the tamper it is about. It now goes through hash_pin::BlockProver.

The enumeration gate lists Prover::compute_precomputed_commitment_for_testing as a third alias call form, so this spelling of the default is detected from now on; the BlockProver:: exclusion already covers the pinned spelling.
Under the RPX pin a root is one arena word, and these sites still spelled two:

- join_tests' control program walked its sibling digests as `lo/hi` pairs with
  `cursor += 2` while declaring the arena at the builder's width, so the walk
  ran off the arena (ArenaOutOfBounds); it hints each digest at
  `edsl::digest_words` now, like the production emitter.
- fri_tests' tamper table addressed the layer-2 root at `2 * (c - 1)` and named
  a "second word" of the layer-0 root that an algebraic root does not have; the
  offset follows `words_per_root()` and the second-word vector exists only at
  width two.
- epoch_tests' split-DECODE control declared its extra root arena as a literal
  two words; it declares `RootCells::words_per_root` of the builder, the width
  `RootCells::hint` reads back at.
- epoch_tests' two arena-schema closed forms (`expected_arena_words`,
  `expected_batched_arena_words`) counted every root as two words; each root
  term now counts `words_per_root()`. The register vectors, `pc_start` and the
  page bases are not roots and keep their widths.
- wrap_tests read the carved L2G root back as the last eight byte halves; it
  reads the last `lanes_per_root()` lanes and compares them to
  `commitment_lanes` of the bundle's root.
- programs.rs' L2G binding built on the default (keccak) builder and hard-coded
  two words per root. The L2G roots are the block path's own commitments, so
  the program follows `WrapHash::production()` and reads each root through
  `hint_digest`, publishing the digest's own cells; machine_tests'
  `published_root` reads them back at `words_per_root()` through `digest_bytes`.

Prediction: the_controls_show_what_the_join_denies, no_tampered_fri_value_can_pass,
a_split_decode_cell_forges_the_attestation, both *_declares_*_exactly_the_shape_words,
the_fixture_continuation_epoch_wraps_batched_from_proofs and the two l2g_binding
tests go from ArenaLenMismatch / ArenaOutOfBounds / schema mismatch to green
under the pin, and stay green at the BLAKE3 default where every width is two.
proof_arena's `commitment_words` / `commitments_to_arena` serialise a root at
the CONFIGURATION's width, which is right for every program that follows
`WrapHash::production()` and wrong for the ones that pin a byte hash on their
own builder: those read two words per root whatever the pin says, and under an
algebraic pin the host handed them one, so the executor's arena-length check
refused the program (blake3_chip_tests' host/machine tree differential:
"arena 0 expected 8 found 4").

`commitments_to_arena_for(roots, WrapHash)` and `commitment_words_for` are the
host half of the rule the emitters already follow — the stride is the BUILDER's
digest width. The differential feeds its keccak and BLAKE3 programs through it.

Prediction: the_merkle_constructions_agree_with_the_host_under_both_hashes is
green under the pin; the configuration-following helpers are unchanged.
…the RPX pin

SUB_DECODE_COMMITMENT_BLOWUP_2 is a blessed constant outside the four families
compute_static_commitments regenerates: the DECODE preprocessed commitment of
the `sub` asm ELF at blowup 2, computed offline once and embedded the way the
recursion guest embeds its inner program's. It is committed under the block
pin, so it moved with it; regenerated with the `print_decode_commitment_for_sub`
helper at the pinned head. Its doc now names it as the fifth constant and its
regeneration route; HASH-PINNING.md lists it with the other four.

Prediction: decode_commitment_compile_time_const_accepts is green under the pin.
… pin's tenant

merkle_opening_program and l2g_binding_program build at WrapHash::production(), so under an algebraic pin they emit Instr::Hash and must be proved and executed under BLOCK_HASHER — the classification rule in HASH-PINNING.md. Their four machine_tests built artifacts through build_artifacts (the registry's Test default) and their two tamper controls executed at a literal TestPermutation, so the walks ran a toy permutation over an algebraic tree. The keccak instruments and the BLAKE3-built register derivation keep the default: they pin a byte hash on their own builders and never consult the socket.

Prediction: the_merkle_walk_authenticates_a_real_opening, tampered_merkle_opening_rejects, l2g_binding_proves_and_verifies and tampered_l2g_binding_rejects are green under the pin; the two DivByZero failures were the toy permutation, and if one survives this change it is a real emitter defect to investigate, not to paper over.
The registry embeds the static constants: registry.rs fills slots 13 and 14 of
every entry from keccak_rc and bitwise's preprocessed_commitment, which at coset
offset 3 return the BLESSED static constant in the tree rather than recomputing,
and lfm_program_id folds every root. A registry generated before the statics are
pasted therefore carries the outgoing hash's constants, the drift gate fires at
exactly those two slots, and the control-first re-run under the outgoing pin
cannot see it because both tables are self-consistent there. HASH-PINNING.md,
the hash_pin.rs header and both generators' doc comments now state the order
and the mechanism, and list the fifth blessed constant
(SUB_DECODE_COMMITMENT_BLOWUP_2) with its regeneration route.

HASH-PINNING.md's cuda section says what is true after the RPX device kernels
landed: the RPX pin commits on the device, RPO256 and Poseidon still abort at
the first device launch; the GPU wrap under cuda is the gate after the merge.
It also records the arena-stride rule for both sides and the tests known red
under this pin with their mechanism and disposition, so a red run is not
rediscovered from scratch.
…s follow the digest's width

Two sites the stride commit described but did not change. join_tests' control program still hinted each sibling as a lo/hi pair with cursor += 2 while declaring its query arena at the builder's width, so the validator refused the program (ArenaOutOfBounds, arena 4); it hints each digest through edsl::hint_digest and advances by digest_words, as the production emitter does. epoch_tests' positive control asserted that the split-cell program declares exactly two surplus words; the surplus is one root, so it is words_per_root() — one under the pin, two at the BLAKE3 default.

HASH-PINNING's known-red table gains per_table_census_tests::the_blake3_tenant_socket_matches_the_record: lane C's guard firing correctly, because the recorded census was produced under the Test/BLAKE3 socket and this build's is RPX; the guard is not weakened, the census is re-recorded as a follow-up.

Prediction: the_controls_show_what_the_join_denies and the_assembled_verifier_declares_exactly_the_shape_words green under the pin and unchanged at the default.
test_prove_error_mid_pipeline_returns_err bounds a liveness property — the pipeline shuts down on error rather than wedging forever — with a fixed 300 s receive timeout. The regression it guards hangs FOREVER, so any finite bound catches it; the bound only has to clear an honest run under load. An algebraic hash pin doubles this test's own proving work (alone, three runs each: 7.3-7.6 s at the BLAKE3 default, 15.7-15.9 s under RPX), and inside the full --lib suite's parallel load the old bound fired while the test was still making progress. It is 1800 s now; the property asserted is unchanged.

Prediction: the test passes inside the full suite under the pin; a wedge still fails, thirty minutes in instead of five.
@MauroToscano
MauroToscano merged commit 9eeca3d into per-table-gpu Sep 8, 2026
11 of 14 checks passed
@MauroToscano
MauroToscano deleted the pt/pin-rpx branch September 8, 2026 09:02
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