Skip to content

Commit 3218346

Browse files
committed
fix(tests): three more the main-sync port left, and a precompile test that proved the wrong guest
The full lib suite at 413b3a3 (plain libtest, every test regardless of failures) found four red. One was mine to expect; three were pre-existing at the lineage base and invisible to nextest's fail-fast. Three are fixed here. The fourth is a MEASUREMENT and is left red on purpose — see the end. ⛔ THE PRECOMPILE TEST HAS BEEN PROVING THE WRONG GUEST. `a_program_using_a_precompile_is_proved_and_verified` loaded `keccak.elf`, which is `executor/programs/rust/keccak`: a guest whose Cargo.toml depends on `tiny-keccak` and whose `main` hashes in SOFTWARE, issuing no syscall but `commit`. It has never touched a precompile. The accelerator guest is `keccak_precompile`, whose `main` calls `lambda_vm_syscalls::keccak::keccak256` — the `keccak_permute` ecall — over five padding edge cases. ★ And the doc's claim was false BEFORE #977 too; it was merely unfalsifiable. KECCAK and KECCAK_RND were always-on then, so they sat in the table set of every workload whether reached or not, and "which brings KECCAK, KECCAK_RND and KECCAK_RC in" described MACHINE shape while reading as program behaviour. `892c7d1bc` (main's `c2ac5d546`, #977) made them counted and turned a latent falsehood into a visible one — the same shape as SOUNDNESS.md's `T = 24`: a claim no assertion defended. The census that finally said so carries COMMIT[0] and no keccak table at all. Worse, `keccak_precompile.elf` is built by the Makefile's `RUST_PROGRAM_DIRS` wildcard and the prover proved it NOWHERE — its only reference in the tree is `executor/tests/rust.rs`, which runs it in the executor. So the suite had no precompile coverage on the multilinear path at all. Both halves land rather than one: - the precompile case now loads `keccak_precompile.elf` and asserts KECCAK, KECCAK_RND and COMMIT are argued. By PRESENCE, not an ordered census: this guest has never been proved on this path, so there is no measured set, and inventing one would repeat the mistake above. Its prove cost is likewise unmeasured. KECCAK_RC is deliberately not in that list — it is always-on and covered by the prefix, so matching it would let a run reaching no precompile satisfy a check named for one, which is exactly how the software guest passed for as long as it did. - `a_software_hash_guest_argues_the_widest_table_set` keeps the software guest and pins its MEASURED 21-table census, because it is the widest set in the suite: it reaches MEMW, LOAD, STORE, BRANCH and EQ, the five families `the_whole_instruction_set_is_proved_and_verified` does not despite its name, plus public output and two PAGE tables including the stack page. A Rust guest doing ordinary work exercises more of the VM than the asm fixture named for the instruction set. `state_depends_on_every_table_count` read 21 against a literal 20. ✓ VERIFIED cause: #977 moved COMMIT into `TableCounts` — `pub commit: usize` is absent at `892c7d1bc^1` and present now — so `each_count_mut` grew a field and the literal did not. The fix is not 21. It is `statement::NUM_TABLE_KINDS`, the same length `table_count_values` returns as `[u64; NUM_TABLE_KINDS]` and the guest absorbs as `statement_replay::NUM_TABLE_COUNTS`; a bare literal there says nothing about WHICH count is missing and is a second copy of a number the crate already holds. `no_call_site_outside_the_pin_reaches_a_default_alias` flagged `lfm/algebraic_commit.rs`, and it is a FALSE POSITIVE — but not blessed away as one. Both matches are in `#[test] the_host_search_finds_a_valid_nonce_under_rpx` (added by `80d746321`): `GrindingDigest<RpxStarkHash>` and `GrindingDigest<Blake3StarkHash>`. Neither reaches a DEFAULT; both name their hash, which is what §6.7 asks for. The second is a cross-hash CONTROL — BLAKE3 work must not satisfy the RPX predicate — and deleting it makes the test a tautology. The two halves are treated differently on purpose, because the allowlist's own doc says it holds hash-AGNOSTIC items: - `GrindingDigest` joins CONFIG_ALLOWED. Generic over the tag, selects nothing. - `Blake3StarkHash` does NOT. The file joins BLESSED instead, so the concrete tag keeps flagging everywhere else — a site naming it on the block path under an RPX pin is precisely what this gate is for. The BLESSED entry answers the reachability question the list demands rather than stopping at "test-only", which that doc calls one scope too wide: the consumers are `stark::grinding::generate_nonce::<T>` and `is_valid_nonce::<T>`, generic over the tag passed at the call site, so no global is read; and the value never leaves the test body, so the paired-default failure the list exists to catch has no subject here. ⚠ LEFT RED, DELIBERATELY: `the_blake3_tenant_socket_matches_the_record`. `BLAKE3_TENANT_SOCKET` is `Test` and `BLOCK_HASHER` has been `Rpx` since `603c1e155` (2026-09-08), while the census it is a ratio against is `bench_cache/optladder_2026-08-21/TIP/tip-wrappt-24.stdout` — eighteen days older. The instrument is working: it says the module measures a shape nothing proves. Moving the constant to `Rpx` is not a fix, because `RECORDED = (28, 3)` is the *Test* socket's width pair, and that number's own doc says it moves the headline ratio by a third in the FLATTERING direction if wrong. It needs a re-recorded CHIP CENSUS under the current pin, which is a run, not an edit.
1 parent 413b3a3 commit 3218346

3 files changed

Lines changed: 167 additions & 40 deletions

File tree

‎prover/src/tests/hash_pin_enumeration.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ const CONFIG_ALLOWED: &[&str] = &[
7979
"CommitmentHash",
8080
"StarkHash",
8181
"DeviceTreeBackend",
82+
// ★ `GrindingDigest<H>` is generic over the hash tag and selects nothing on
83+
// its own — the same class as `StarkHash` above, and it joined this list
84+
// rather than `BLESSED` for that reason: blessing the FILE would have
85+
// excused every future mention in it, where allowing the hash-agnostic
86+
// GENERIC keeps the gate on the concrete tags a caller pairs it with.
87+
// Added when `80d746321` gave `lfm/algebraic_commit.rs` a host grinding
88+
// test spelling `GrindingDigest<RpxStarkHash>`.
89+
"GrindingDigest",
8290
];
8391

8492
/// Every item named from `stark::config` on this line, `use` lists included.
@@ -197,6 +205,27 @@ const BLESSED: &[(&str, &str)] = &[
197205
"Host-side BYTE-transcript differentials: the oracle for the machine's \
198206
byte `TranscriptReplay` arm is deliberately the byte transcript.",
199207
),
208+
(
209+
"lfm/algebraic_commit.rs",
210+
"One mention, and it is a CROSS-HASH CONTROL that names its hash rather \
211+
than defaulting to it — the opposite of what this gate is for. \
212+
`the_host_search_finds_a_valid_nonce_under_rpx` (added by `80d746321`) \
213+
grinds under `GrindingDigest<RpxStarkHash>`, then declares \
214+
`GrindingDigest<Blake3StarkHash>` to prove a BLAKE3-ground nonce does \
215+
NOT satisfy the RPX predicate. Delete that second type and the test \
216+
becomes a tautology: `generate_nonce::<RpxGrind>` could hash anything \
217+
and every other assertion would still hold. \
218+
✓ REACHABILITY, named as this list demands rather than left at \
219+
'test-only': the CONSUMERS are `stark::grinding::generate_nonce::<T>` \
220+
and `is_valid_nonce::<T>`, both generic over the tag passed at the \
221+
call site, so no global is read and nothing is handed a defaulted \
222+
hash. The value never leaves the `#[test]` body — no artifact, no \
223+
commitment, no trace is built from it — so the paired-default failure \
224+
this list exists to catch (a default handed to a consumer that follows \
225+
the pin) has no subject here. `GrindingDigest` itself is hash-agnostic \
226+
and lives in CONFIG_ALLOWED; this entry covers only the concrete \
227+
`Blake3StarkHash`, which must keep flagging everywhere else.",
228+
),
200229
];
201230

202231
/// Every `.rs` under `dir`, relative to `root`.

‎prover/src/tests/multilinear_table_tests.rs‎

Lines changed: 118 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -525,68 +525,147 @@ fn the_whole_instruction_set_is_proved_and_verified() {
525525
}
526526
}
527527

528-
/// And over a Rust program that calls the keccak precompile — which brings
529-
/// KECCAK, KECCAK_RND and KECCAK_RC in, **and** writes public output, so the
530-
/// statement's share of the bus is load-bearing here and nowhere else.
531-
///
532-
/// ⚠ THE ONLY CASE WITHOUT AN ORDERED CENSUS, and deliberately so. The two
533-
/// above are pinned to sets MEASURED at `788f36a19`; this one PASSED that run,
534-
/// so its assertion never fired and never printed its set, and pinning a list
535-
/// nobody has read would be a literal invented to match a bound. It keeps the
536-
/// bound and gains the checks its own doc has always claimed instead.
537-
///
538-
/// The presence checks are the point: `>= 20` never once asserted that the
539-
/// keccak family or COMMIT is argued here, which is the entire reason this case
540-
/// exists next to the two asm ones. Since #977 those tables are counted, so a
541-
/// workload that stopped reaching the precompile would silently drop them and
542-
/// still clear any bound this case could carry. Both assertions print the whole
543-
/// set, so the run that fails one is also the run that supplies the census.
544-
#[test]
545-
fn a_program_using_a_precompile_is_proved_and_verified() {
528+
/// Proves and verifies the guest at `executor/program_artifacts/rust/<name>.elf`,
529+
/// returning the argued tables by name.
530+
fn prove_and_verify_rust_guest(name: &str) -> Vec<String> {
546531
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
547532
.parent()
548533
.expect("workspace root")
549-
.join("executor/program_artifacts/rust/keccak.elf");
534+
.join("executor/program_artifacts/rust")
535+
.join(format!("{name}.elf"));
550536
let bytes = std::fs::read(&root).unwrap_or_else(|_| panic!("read {}", root.display()));
551-
let elf = Elf::load(&bytes).expect("load keccak.elf");
537+
let elf = Elf::load(&bytes).unwrap_or_else(|e| panic!("load {name}.elf: {e:?}"));
552538
let logs = Executor::new(&elf, vec![])
553539
.expect("executor")
554540
.run()
555541
.expect("run")
556542
.logs;
543+
prove_and_verify_all_tables(elf, &logs)
544+
}
557545

558-
let argued = prove_and_verify_all_tables(elf, &logs);
546+
/// Is `family`, or `family` plus a chunk index, among the argued tables?
547+
///
548+
/// "Equal, or followed by `[`" — never a bare prefix, which would let
549+
/// `KECCAK_RC` answer for `KECCAK`.
550+
fn argues(argued: &[String], family: &str) -> bool {
551+
argued.iter().any(|n| {
552+
let s = n.as_str();
553+
s == family || (s.starts_with(family) && s[family.len()..].starts_with('['))
554+
})
555+
}
556+
557+
/// And over a Rust program that calls the keccak PRECOMPILE — the
558+
/// `keccak_permute` ecall — so KECCAK and KECCAK_RND are argued, **and** which
559+
/// writes public output, so the statement's share of the bus is load-bearing.
560+
///
561+
/// ⛔ THIS CASE PROVED THE WRONG GUEST UNTIL NOW, and the bound is what hid it.
562+
/// It loaded `keccak.elf`, which is `executor/programs/rust/keccak` — a guest
563+
/// whose `Cargo.toml` depends on `tiny-keccak` and whose `main` hashes in
564+
/// SOFTWARE, issuing no syscall but `commit`. It has never touched a precompile.
565+
/// The measured census at `413b3a3b7` carries COMMIT[0] and no keccak table at
566+
/// all, which is what finally said so.
567+
///
568+
/// ★ And the claim was false BEFORE #977 too — it was merely unfalsifiable.
569+
/// KECCAK and KECCAK_RND were always-on then, so they appeared in the table set
570+
/// of every workload, reached or not, and a doc sentence about which tables the
571+
/// program "brings in" described MACHINE shape while reading like program
572+
/// behaviour. `892c7d1bc` (main's `c2ac5d546`, #977) made them counted, which
573+
/// turned a latent falsehood into a visible one. Another instance of the
574+
/// pattern in `SOUNDNESS.md`: a claim no assertion defended.
575+
///
576+
/// The accelerator guest it should have used is `keccak_precompile`, whose
577+
/// `main` calls `lambda_vm_syscalls::keccak::keccak256` over five padding edge
578+
/// cases. It is built by the Makefile's `RUST_PROGRAM_DIRS` wildcard like every
579+
/// other guest, and before this commit **nothing in the prover proved it** — its
580+
/// only reference in the tree is `executor/tests/rust.rs`, which runs it in the
581+
/// executor and never proves it. So the suite had no precompile coverage on the
582+
/// multilinear path at all.
583+
///
584+
/// ⚠ The census here is by PRESENCE, not an ordered list: this guest has never
585+
/// been proved on this path, so there is no measured set to pin, and inventing
586+
/// one would repeat the mistake above. Its cost is likewise unmeasured — five
587+
/// `keccak256` calls including a multi-block input.
588+
///
589+
/// ✓ The ELF resolves on a CI prover shard exactly as `keccak.elf` does, and
590+
/// this is written down so the next reader does not re-ask: the lineage's
591+
/// prover-tests job runs `make compile-programs-asm`, `make compile-programs-rust`
592+
/// and `make compile-recursion-elfs` before `cargo nextest run`, and
593+
/// `compile-programs-rust` builds every directory under
594+
/// `executor/programs/rust/` through the `RUST_PROGRAM_DIRS` wildcard — this
595+
/// guest included, with no per-program list to extend.
596+
#[test]
597+
fn a_program_using_a_precompile_is_proved_and_verified() {
598+
let argued = prove_and_verify_rust_guest("keccak_precompile");
559599
assert_eq!(
560600
always_on_prefix(&argued),
561601
ALWAYS_ON_TABLES,
562602
"every argued set opens with the always-on tables; argued: {}",
563603
argued.join(" ")
564604
);
565-
// The tables this case is ABOUT, by presence rather than by a full census.
566-
// KECCAK_RC is excluded from the prefix match on purpose — it is always-on
567-
// and asserted above, so matching it here would let a run that reaches no
568-
// precompile at all satisfy a check named for one.
569-
for family in ["KECCAK_RND", "COMMIT"] {
570-
// The name is either the bare family or the family plus a chunk index,
571-
// so the match is "equal, or followed by `[`" — never a bare prefix,
572-
// which would let `KECCAK_RC` answer for `KECCAK`.
605+
// KECCAK_RC is deliberately NOT in this list: it is always-on and asserted
606+
// in the prefix above, so matching it here would let a run that reaches no
607+
// precompile satisfy a check named for one — which is exactly how the
608+
// software guest passed as a precompile test for as long as it did.
609+
for family in ["KECCAK", "KECCAK_RND", "COMMIT"] {
573610
assert!(
574-
argued.iter().any(|n| {
575-
let s = n.as_str();
576-
s == family || (s.starts_with(family) && s[family.len()..].starts_with('['))
577-
}),
611+
argues(&argued, family),
578612
"{family} must be argued — it is what this case exists for, and \
579613
since #977 it is a counted table that an unreached workload drops \
580614
silently; argued: {}",
581615
argued.join(" ")
582616
);
583617
}
584-
assert!(
585-
argued.len() >= 20,
586-
"argued {}, fewer than the 20 this workload reached when the bound was \
587-
written: {}",
588-
argued.len(),
589-
argued.join(" ")
618+
}
619+
620+
/// The SOFTWARE-hash guest, kept because it is the widest live table set in the
621+
/// suite — and it is the one the precompile case above used to prove.
622+
///
623+
/// `executor/programs/rust/keccak` hashes with `tiny-keccak` in guest code, so
624+
/// it reaches no accelerator and argues the RV64 core broadly instead: 21
625+
/// tables, including MEMW, LOAD, STORE, BRANCH and EQ — the five families
626+
/// `the_whole_instruction_set_is_proved_and_verified` does NOT reach despite its
627+
/// name. A Rust guest doing ordinary work exercises more of the VM than the asm
628+
/// fixture named for the instruction set, which is worth keeping a case for.
629+
///
630+
/// It also carries public output (COMMIT[0]) and two PAGE tables, one of them
631+
/// the stack page — the only case here that does either.
632+
///
633+
/// MEASURED at `413b3a3b7`, box, CPU-only, no campaign env.
634+
#[test]
635+
fn a_software_hash_guest_argues_the_widest_table_set() {
636+
let argued = prove_and_verify_rust_guest("keccak");
637+
assert_eq!(
638+
argued,
639+
[
640+
"BITWISE",
641+
"DECODE",
642+
"KECCAK_RC",
643+
"REGISTER",
644+
"HALT",
645+
"COMMIT[0]",
646+
"CPU[0]",
647+
"LT[0]",
648+
"SHIFT[0]",
649+
"MEMW[0]",
650+
"MEMW_A[0]",
651+
"LOAD[0]",
652+
"MUL[0]",
653+
"BRANCH[0]",
654+
"PAGE:0x0",
655+
"PAGE:0xfffffffffffc0000",
656+
"MEMW_R[0]",
657+
"EQ[0]",
658+
"BYTEWISE[0]",
659+
"STORE[0]",
660+
"CPU32[0]",
661+
],
662+
"the argued table set moved — a table appeared, vanished, or the \
663+
sub-proof order changed. NOTE what is absent and must stay absent: \
664+
KECCAK, KECCAK_RND, ECSM, ECDAS and HINT. This guest hashes in \
665+
software, and an accelerator appearing here means it is proving \
666+
something else — which is precisely the confusion the precompile case \
667+
above lived in. A separate `!argues(..)` loop would restate the list \
668+
and could only fire after this assertion already had."
590669
);
591670
}
592671

‎prover/src/tests/statement_tests.rs‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,21 @@ fn each_count_mut(counts: &mut TableCounts) -> Vec<(&'static str, &mut usize)> {
131131
/// destructured in `absorb_statement` and then left out of the array it
132132
/// absorbs compiles clean and changes nothing about the state, which is a
133133
/// prover-chosen number the verifier would no longer be bound to.
134+
///
135+
/// ★ THE PROBE COUNT IS THE CONSTANT'S, NOT A LITERAL. It was `20` and read
136+
/// 21, because the main-sync merge `892c7d1bc` (main's `c2ac5d546`, #977) took
137+
/// `FIXED_TABLE_COUNT` 11 -> 5 and moved COMMIT into `TableCounts` alongside
138+
/// the five accelerators already there — ✓ VERIFIED, `pub commit: usize` is
139+
/// absent at `892c7d1bc^1` and present now. `each_count_mut` grew the field and
140+
/// the literal did not.
141+
///
142+
/// A bare literal here is the wrong shape twice over: it says nothing about
143+
/// WHICH count is missing, and it is a second copy of a length
144+
/// `statement::NUM_TABLE_KINDS` already holds — the same number
145+
/// `table_count_values` returns as `[u64; NUM_TABLE_KINDS]` and the guest
146+
/// absorbs as `statement_replay::NUM_TABLE_COUNTS`. Pinned against the
147+
/// constant, a field added to `TableCounts` without a probe fails here, and one
148+
/// added to the encoding without a field fails to compile.
134149
#[test]
135150
fn state_depends_on_every_table_count() {
136151
let baseline = state_after_absorb(b"elf", b"out", &sample_counts(), 1, &sample_ranges(), 7);
@@ -139,7 +154,11 @@ fn state_depends_on_every_table_count() {
139154
.into_iter()
140155
.map(|(name, _)| name)
141156
.collect();
142-
assert_eq!(names.len(), 20, "every count must be probed");
157+
assert_eq!(
158+
names.len(),
159+
crate::statement::NUM_TABLE_KINDS,
160+
"every count the statement encodes must be probed; probing {names:?}"
161+
);
143162

144163
for name in names {
145164
let mut counts = sample_counts();

0 commit comments

Comments
 (0)