Repository navigation
WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress - #1014
Draft
MauroToscano wants to merge 1495 commits into
Draft
MauroToscano wants to merge 1495 commits into
MauroToscano wants to merge 1495 commits into
Conversation
|
Benchmark Results for modified programs 🚀
|
|
Benchmark Results for unmodified programs 🚀
|
MauroToscano
force-pushed
the
noepoch/whir
branch
from
October 1, 2026 14:57
e9107c6 to
97ba486
Compare
MauroToscano
added a commit
that referenced
this pull request
Oct 2, 2026
… leaves The builder kept every walked window until finish, so at the run's end all of its op lists were resident, about 300 B per cycle (0.28 GiB per M cycles: 8.6 GiB on block 25368371 and about 100 GiB on a median mainnet block). WindowedTraceBuilder::drop_streamed_ops keeps only the ops not yet in a handed-out chunk for each of the 8 streamed tables (Tail: the windows' lists moved in whole and freed once used up). As a chunk leaves, the builder takes what the table phase would have read from its ops. It counts BITWISE's lookups from LT, SHIFT, STORE, MEMW_A and MEMW_R, plus DECODE's lookups and the last ECALL from CPU. It derives and keeps the phase-3 LT ops of MEMW and MEMW_A, unless stream_memw_lt already put them in the windows' LT lists. The other walk lists are appended window by window instead of being concatenated at finish. The in-walk BITWISE lookups are counted and dropped. finish runs the table phase over the tails (StreamSkip::tails), padding the dropped CPU chunks and placing the kept LT ops where phase 3 would have derived them. The tables are the same. The new tests compare every table with the whole-run build, and with stream_memw_lt every table with the builder that keeps its windows, plus LT's multiplicities. Both go through push and the split, at window lengths 1 to the whole run, and with chunks small enough that all 8 tables drop. The same comparison runs with KECCAK_RND streamed. Another test checks that after each window each table holds less than a chunk of ops. Removing any one accounting step (LT, MEMW_R or DECODE counts, the CPU padding, either LT prefix) fails the first test. Off by default. Cherry-picked from mem/m1 74e1df1 onto #1014, with the streamed finish's build marks kept.
BlockOptions::layout_ahead = Some(k) lets at most k + 1 streamed chunks be laid out (or in the making) and not yet packed, where the inline layout holds one: a worker takes a permit before it takes a job, so every job taken holds one and the next chunk to pack is never starved, and the packer gives it back once the chunk is packed. None keeps the channel-only bound that BIG 390 saw grow with the block (+2.2 / +4.2 / +8.7 GiB at 1.0 / 1.3 / 1.8x). Production Some(2); layout_workers stays 0 until E3's gates. BLOCK LAYOUT reports the most chunks unpacked at once. Test: with three workers, k = 0 and k = 2 hold at most k + 1 and pack the inline groups (an unbounded permit pool fails it: 8 unpacked at k = 0). Harness knob BLOCK_WHIR_LAYOUT_AHEAD=k|none.
BlockFormat gains `argue` (production PerTable). Under ArgueFormat::Batched each group's tables argue in one batched argue on the group's fork (a lockstep GKR per bin, one front-loaded constraint sumcheck), whose reduced claims feed the group's opening exactly as the per-table points do; the upload, revive, openings and prepared openings are unchanged. The verifier reads the format from its own config, needs that format's messages and none of the other's, verifies each group's argue on its fork, sums the outputs into the block-wide balance and checks each table's preprocessed columns past the prepared prefix. BlockWhirProof gains `argues` (empty under PerTable; MultiProof, the prepared openings and the partition keep their bytes). BlockFormat:: chain_config builds every side's config. Test hooks: ArgueDeviation (a group's faults, the host reference) and VerifierChecks (the mutations). The W3 plan refuses a batched format until its leaves walk it. The real block test takes BLOCK_WHIR_ARGUE=batched and prints the GKR tree refusals and fused counts; the block report prints each group's argue high-water.
Round trips at one group and many (keccak chunks, the dense prepared fixture), the format read from the verifier only (both ways, a foreign bin cap), a group argued with each prover fault, tampered argues, another fork, the block-wide balance and the reduction shape with their mutations. Box only: the per-table default's bytes against noepoch/whir @ 11e2de5 over canonical rows (a block's traces are not reproducible across processes), and the card proving the host reference's bytes.
…M1 streamed-ops drop) into batch/noepoch-argue # Conflicts: # prover/src/tests/block_whir_tests.rs
Under the batched block format a leaf hints each group's batched argue with every count from the tables' shapes and the host's argue_plan at the format's cap, runs the B-4a leg on the group's fork, then each table's preprocessed leg at the point its columns are claimed at (the host's check_preprocessed after verify_argue), leaving the per-table walk's outputs, points and values for the group opening and the prepared openings. The plan costs a batched group by batched_argue_cost, so the leaf partition sees the new costs; the arena writes each group's argue in the hint order and refuses one whose word count is not its shapes'. The per-table walk is unchanged.
Batched leaves execute and close the bus on four small blocks, each group costing no more than its per-table walk; a batched leaf refuses a forged bus output, ladder half, ladder round, constraint round and factor value, and a tampered prepared opening (admitted with the openings left out); the batched tree proves to its derived top, accepted under the batched format and refused under the per-table one. The real-block tree test takes BLOCK_WHIR_ARGUE=batched.
…'s run The digests depend on the ELF bytes, which differ between toolchains (the box's guests are not the laptop's), so the expected digests come in BLOCK_DEFAULT_DIGESTS from the same body run at the base sha on the same machine; the laptop's record stays in the doc comment.
…d layout-ahead) into batch/noepoch-argue One conflict, the real-block banner (both formats' fields kept); the batched tests' BlockOptions take layout_ahead.
The walk is the producer's serial chain: on block 25368371 it never waits for the executor and costs 143 ns a cycle (FAST 590). Four of its costs go, each behind its own part of LAMBDA_VM_WALK_LEAN (unset or 1: all, 0: none, or a comma list of decode, memory, lookups, route), so each can be measured alone in one binary: - decode: every pc's DecodeEntry is built once into dense runs of consecutive pcs (DecodeTable), instead of a hash lookup and a decode of the instruction every cycle. Built from the instruction map, so it answers exactly the pcs the map holds. - memory: a load or store inside one 256 KiB page finds its page once (PagedMem::page_index) instead of once per byte, and the last page found is remembered. Every page allocation then goes through the cache, so a remembered index is never one an allocation moved; an access across a page or past u64::MAX takes the per-byte path. - lookups: the walk no longer lists each cycle's seven CPU lookups and each narrow load's MSB8 lookup (a Vec per cycle); the accumulator counts them from the CPU and LOAD ops it already holds, with the same per-op generator the list was built from. - route: the accumulator routes a window in one pass over its CPU ops instead of one filter pass per segment; each op still goes to every segment whose filter it passes, in CPU order. Only the windowed builder walks lean; the whole-run build keeps the old walk and is the reference the identity tests compare against. The tables are the same with any set of parts.
The windowed build with the lean walk, with the old walk and with each lean part alone, against the whole-run build: six programs (keccak calls, narrow and odd-offset loads and stores), windows of 1 to the whole run, with and without dropping the streamed ops. And each part against what it replaces: the decode table answers exactly the map's pcs (gaps, misaligned pcs, the top of the address space); lean memory accesses read and write what the per-byte ones do (an access across a page, a first touch between two allocated pages, wrapping past u64::MAX, 40k random accesses); the counted lookups equal the listed ones, and differ without the LOAD ops' part on programs with narrow loads; the one-pass routing builds the per-segment routing's segments; the knob's values parse.
The executor kept guest memory in a map of 4-byte words keyed by their address through the identity hasher. hashbrown takes its 7-bit control tag from the hash's top bits, so every address below 2^57 gets the same tag and a probe compares keys one by one; the cost grows with the memory a run touches: 31.6 ns a cycle on block 25368371, 108.5 on the median block 25475471 (FAST 591), where the executor becomes the producer's serial floor once the walk is lean. Memory now keeps 64 KiB pages: page n of the low 8 GiB (code, data, heap, private input) and of the top 8 GiB (the stack) is an index into a directory, any other page an entry of a BTreeMap. A page records which of its 4-byte words were written, so iter_bytes yields exactly the bytes it yielded before (as a set; the order differs). Every load and store, its overflow refusals and load_bytes answer as before. LAMBDA_VM_EXEC_MEMORY=words keeps the word map, as the control the pages are measured against; the differential test drives both through 200k random accesses of every width across pages, directories and the top of the address space.
…cutor memory (E4) (i-exec, FAST 602) The windowed builder walks lean and the executor's guest memory lives in 64 KiB pages found by directory. FAST 602 (record posture, A B B A): base 17.62 -> 17.17 s, phase-A end 10.01 -> 9.56 s; BLOCK SHAPE DIGEST ffaccd25 in all four runs; verify 4/4; real-block determinism 2/2.
…LL kind derived (D-EXEC E1 v2) The walk materializes one CpuOperation per cycle, and the windowed producer is bound by the bytes it moves. arg2 (CPU-A1's multiplex), branch_cond and the seven ECALL-kind flags are functions of the decode entry and rv1/rv2 (an ECALL reads a7 into rv1), so they become methods; COMMIT's buffer and count and KECCAK's and BLAKE3's state address share two operand slots (ecall_args). 128 -> 96 bytes, pinned by a const assertion. Every reader calls the method of the same name; the values are the same.
An op routed to MEMW_A has one old timestamp for all its bytes, and its eight value (old) elements fit one u64: bytes for a memory access, the two 32-bit halves for a register access that missed MEMW_R. The walk's MEMW_A bucket, the windowed builder's MEMW_A tail and chunk jobs, and the table phase keep AlignedRow (memw_aligned.rs) instead of MemwOperation: 152 -> 48 bytes, pinned by a const assertion. The MEMW_A generator, the MEMW_A LT derivation and its BITWISE lookups read the row; the cells are the same (the 187 trace digests equal; a round-trip test over every width, memory and register).
LT, EQ, BYTEWISE, BRANCH, MUL and DVRM deduplicate their operations through a HashMap and lay their rows out in its iteration order. Each map took fresh random keys (HashMap::new), so no two builds of the same run laid these tables out alike, even in one process. Regenerating a streamed chunk in phase B (D-MEMORY A0) needs the chunk built again row for row. Every such map now hashes with tables::trace_hash's state: random keys per process, as before, shared by every map, so a table's row order is a function of its operation list. LAMBDA_VM_FIXED_TRACE_HASH=1 fixes the keys (SipHash-1-3 with zero keys) so that two processes build the same traces. The hash function is the one HashMap::new used, so the table generators cost the same, and the rows are the same multiset as before. The windowed builder tests and the streamed-collection test now compare these six tables row for row instead of as row multisets, and a new test builds one run twice in a process and compares every table, with MUL and DVRM also built from 199 distinct operations. Giving LT or DVRM its own fresh keys again fails it.
…the three-argument same_list The hash-ordered tables now hash with one state per process, so every table compares row for row and same_list lost its multiset flag.
…oof bytes Under LAMBDA_VM_FIXED_TRACE_HASH=1 and LAMBDA_VM_DETERMINISTIC_GRIND=1 two processes prove the same bytes; the digest lets a box gate compare them.
Mauro 10-02: "batch the constraints". BlockFormat::production().argue is ArgueFormat::BATCHED (one lockstep LogUp-GKR ladder per bin under 2^26 input cells, one front-loaded constraint sumcheck per group); the verifier takes the variant from its own format, never from the proof, and the production tree verifier pins it. BLOCK_WHIR_ARGUE=per-table keeps the per-table format measurable; unset, the real-block tests follow production.
… v2) (i-exec) The CPU op at 96 bytes (arg2, the branch decision and the ECALL kind derived) and MEMW_A ops kept as 48-byte aligned rows. Prover-only: the traces are the same.
…ajor The narrow kernels and their host entry points come over from the STARK side as they are there (widen_narrow_row_major, column_max_col_major, pack_col_major, math_cuda::narrow), with one addition beside the row-major widen: widen_narrow_col_major writes out[c * rows + r], the layout a table's run of columns has in the multilinear prover's resident store. The row-major path is unchanged; the pack now reads a device view, so a run inside a larger store can be packed in place. DeviceColumns::upload_tables builds a store from whole tables, each as it is held: a wide table's columns are copied up as before, a packed one crosses the bus at its packed bytes and is widened on the card into the run its columns take, so the store holds the same words at the same offsets. The packed bytes are promised for the largest table while the tables widen one at a time in stream order. pack_run packs one table's run and downloads only the packed bytes; download brings a store back, for the tests that compare two of them.
…em on the host only when they read them there NarrowColumns keeps a table's raw words column by column, each column little-endian at the width of its largest word (1, 2, 4 or 8 bytes) - the stark crate's NarrowMain layout - so widening gives back the committed words bit for bit and no proof byte can depend on it. TraceData holds its columns wide or narrow: install_narrow takes packed columns of the table's shape (Goldilocks only, whose elements are their raw words) and lets the field elements go; columns() widens a narrow table once, the first time a host reader asks, and keeps that copy until drop_widened. host_widens counts those widenings. The readers that used to take &[Mle<F>] only to learn the shape, or to hand the device a host copy it did not read when the columns were resident, now take HostColumns (a table) or HostColumn (one column, what a stacked polynomial is made of): the width and height always, the field elements on demand. gpu::column_factors takes the shape; upload_factors_from_columns, evaluate_many_base and the reduce's card session read the resident run when the card holds one and widen only otherwise; the opening builds its host parts only without a resident store; claim_reduce::prove and the stacked commit, revive and opening are generic over the column type. Every call with field columns is unchanged in what it does. gpu::upload_tables puts whole tables on the card, a narrow one widened there; gpu::pack_resident packs a table's run of a resident store on the card. Tests: the host pack and widen round trip at every width boundary, a wrong width map widens to other words, only Goldilocks widens, packed parts of the wrong shape are refused; a trace held narrow proves the same bytes as held wide and is refused packed columns of another shape or field. On a card (narrow_device): the column-major widen at every width boundary against the host pack, the card's pack of a resident run = the host's, and a store uploaded from packed and wide tables holds the wide words.
…ed on the card after its commit
BlockOptions::narrow (production Narrowing::CARD) says how phase A holds a
group's columns once committed. On the card, every table of at least 2^16
cells is packed from its run of the group's resident store (each column at
the bytes its words need) by a thread of its own, and installed once the
next group's columns are up: the committer is phase A's critical path
(FAST 425: every group after the first waits 0 s for its tables), and the
pack's download overlaps that upload. A table's field elements go when its
packed columns are installed. Narrowing::Host packs every table on the host
(a test's, or a host without a card); Narrowing::Wide keeps today's.
Phase B uploads each group as it is held - a narrow table at its packed
bytes, widened on the card into the store - and hands the revive and the
opening the group's columns as ColumnOf handles, which read a table's field
elements on the host only for a host path. A host widening is dropped when
the group's opening ends. The words are the committed ones bit for bit, so
the proof is the same byte for byte; a widening that went wrong is refused
when the opening reads a path from the kept tree top.
BlockStamps reports a BLOCK NARROW line: the tables and cells packed, the
bytes before and after, the committer's wait for the pack and the packer's
own time, and the narrow tables phase B widened on the host.
Deviations::narrow_width_map breaks the first narrow table's width map
between the phases. The real-block harnesses take BLOCK_WHIR_NARROW=
wide|card|host; the test helpers pack every table on a card
(Card { min_cells: 0 }), so a box run walks the narrow path at a test's
size.
…ap is refused narrow_storage_moves_no_byte_of_the_proof proves the same traces with every group held wide and held narrow (packed on the host), under the per-table and the batched argue: both verify, every table is packed, the host widens them without a card, and the bytes are equal when the grind is deterministic. a_streamed_block_held_narrow_proves_and_verifies does the same for the build streamed into phase A. a_wrong_narrow_width_map_is_refused_by_the_kept_tree breaks one width map between the phases: the prover refuses with RecomputedCodewordMismatch. a_wrong_narrow_width_map_is_refused_on_the_card (box, cuda) does both on a card: every table packed there, the honest proof verifies, the broken width map is refused.
…eights KECCAK and ECSM were one table each in the block format, with no height bound of their own beyond the 2^27 table cap. A keccak-heavy block at the gas limit makes about 2^21 permutation calls: one KECCAK table that tall stacks into eight polynomials of a 2^27 stack, against a group budget of three. The block prover now cuts KECCAK into tables of at most 2^18 rows and ECSM into tables of at most 2^17 (BlockOptions::keccak_rows_log2 and ::ecsm_rows_log2), at the same two places it cuts KECCAK_RND and ECDAS; at those heights each table's columns fit one polynomial of the stack. A KECCAK or ECSM row is one whole call, so a cut falls between calls. No constraint of either table reads another row, and a call reaches its rounds (KECCAK_RND), its double/add steps and scalar bits (ECDAS), and its memory only through buses keyed by its timestamp. The block frame now takes up to 2^12 tables of each (validate_block_counts lifts the one-table bound for both) and refuses a KECCAK table stated over 2^18 rows or an ECSM table over 2^17 (chunked_table_ranges names four ranges in AIR order: KECCAK, KECCAK_RND, ECSM, ECDAS). Every other verifier keeps both to one table. A block whose KECCAK fits 2^18 rows and ECSM 2^17 (every block measured so far) builds the same tables and the same proof. Tests: the count bounds and the four height caps (host), the ranges pinned against the AIR names, KECCAK and ECSM cut at one and two rows a table in the whole-run and the streamed builds, whole calls swapped across tables verify while swapped timestamps are refused, each table stated over its cap is refused at the frame with the cap's error, and both prove through the block's tree. The real-block harnesses take BLOCK_WHIR_KECCAK_LOG2 and BLOCK_WHIR_ECSM_LOG2 to force a split, and print the chunked heights.
…olynomial holds The height-cap test reads the cap constants, so a lifted KECCAK or ECSM cap passed it. Each of the four caps is the tallest power of two at which the table's columns fill at most one polynomial of the format's 2^27 stack (KECCAK 511 x 2^18, KECCAK_RND 1,480 x 2^16, ECSM 667 x 2^17, ECDAS 521 x 2^17); the new test checks that against the AIRs' widths, so lifting either new cap fails it.
…NECHAIN B1) prover/src/tests/block_onechain_tests.rs, host builds only (the card's grouped round 0 is stage B3): - REV-B2-JUDGE B1-1 (a): REV-B2-A's frame probe under Indexed. A table stated at 2^27 alone in its group stacks into one polynomial per column, and the verifier's split cuts them into runs of at most four (38 -> nine of 4 and one of 2), each within the index and two-adicity bounds. - B1-5 at the block: up to 2, 3 and 4 polynomials a group, RPX and P1, an Indexed block proves and verifies with grouped chains, its phase-A roots are the PerPoly block's, and neither mode verifies under the other. Also the BaseGrouped arm in the hash-metrics bench test's exhaustive match and two clone_on_copy lints in whir_group_tests (make lint was red at 435205a under the hash-metrics feature).
D-ONECHAIN stage B2. Under PerPoly (production) every emitted row, its order and every arena word are unchanged; a run of one polynomial under Indexed is today's chain row for row. - ChainShape carries the config's GroupFold and a run's tree count: chains() is the host's index_chains (one split for the emitter, the arena and the W3 plan), run(P') the run's shape, messages(r) = k0 + m at round 0, bound() counts the index. - emit_verify_grouped (emit_verify_weighted's body, now emit_chain): round 0 draws k0 + m challenges, rho last; eq(rho, p) from the sampled rho wires (emit_eq_index, never hinted, absent slots have none); at each query ONE draw feeds every tree's verify_opening, the successor's leaf and slot, and the fold; each tree against its root by position; the blocks combined (emit_combine) from exactly the felts each tree's leaf hashed, then folded as one 2^k0 block. The weight is sum_p eq(rho, p) w_p at alphas[..k0] || alphas[k0+m..]. - emit_stacked_verify_chains / emit_group_walk_chains / stacked_verify_cost / hint_group_chains_shaped / push_chains walk the runs; the block leaf passes each chain its run's slice of the carried (or derived prepared) roots. RoundStorage, round_words and push_round_words lay out a grouped round 0: k0 + m messages, the trees' caps in tree order, then per query each tree's block and path. - Closed forms: openings, caps and query rows per tree, the combine and eq rows, k0 + m sumcheck rounds. Tests (lfm/whir_onechain_tests.rs, REV-B2-JUDGE B2-1..B2-6): host-accepted grouped openings execute (RPX and P1 C2; P' = 2, 3, 4, a [4, 1] split, mixed heights, R = 1); the threaded schedule is the host's; the closed forms are the emitted counts; the base-lift pin in every slot (NotBaseWord); every forgery the host refuses has no execution; each in-guest reading of the index is load-bearing (test-only emitter faults); the production run of three opens for 14,845 permutations (design: 14,845). Box tier: the leaves over an Indexed block (whir_block_tests, host builds) and the median census by emission (whir_p1_census_tests::whir_onechain_census, B2-7).
ChainShape::run refuses a run whose virtual domain H_{n+m+lb} would pass
Goldilocks' two-adicity, the in-guest side of the host's runtime refusal
(REV-B2-JUDGE B1-1); tested.
The lead's #1013 lesson: a refusal test must fail at its intended check, not on any error. The stacked layer reported every chain refusal as ColumnOpeningRejected, and the in-guest negatives only saw the machine refuse. Host (multilinear): - stacked_eval::verify_traced returns the refusal with its cause (Refusal::{Before, Chain { chain, poly, cause }}); verify is it mapped exactly as before (a shape refusal by name, any other chain refusal as the chain's first polynomial's ColumnOpeningRejected), so PerPoly's verdicts and errors do not move. - whir_round::verify_grouped reports a round-0 tree's failed opening as the new Error::GroupedOpeningRejected { query, tree } (appended last). - whir_group_tests assert the cause of every negative: the tree and query of each tampered, swapped, moved or combination-preserving block; the fold for rho swapped in the combine, the final check for the weight's faults; the per-polynomial count for a grouped proof under PerPoly; the cap check or the root for a proof read under another cap. Guest (prover): - whir_chain::onechain_steps (test only, recording only, nothing emitted): marks of each emitted check (chain, sumcheck, out-of-domain and grinds, each tree's opening, the successor's, the fold, the final check); failed_at names the check whose assert failed. - whir_onechain_tests name each forgery's refusal on the serial schedule (program order, the host's) and require the machine to refuse at the check the host does; the faults refuse at round 0's first fold or the final check; B1-2's shapes refuse as another arena length. Forgery fixtures run eight queries and every forgery applies to every case (no silent skip): the one-round case at blowup 2^6, P1's at 2^10. - block_whir::verify_block_whir_traced carries the multilinear refusal (verify_block_whir_with is it, as a bool). The block tests assert the other mode's verifier refuses by the statement (the two statements are one bit apart, the fold word's 47) before any opening's shape, and the Indexed leaf refuses a forged round-0 value at tree 1's opening of that chain, as the host does at that chain's opening.
…B1+B2) Seven tests from the B1+B2 code review (REV-OC-B), renamed; no production code. Host (whir_group_tests): - round_zero_trees_cannot_trade_caps: trees 0 and 1 trading their caps (binary cap) or owner paths (Poseidon1's 4-ary cap) is refused at tree 0's cap, CapRejected. - successor_blocks_follow_the_schedule_not_the_proof: successor blocks at a final round 0, and none at a round with a successor, are each refused by name. Guest (lfm/whir_onechain_tests): - round_zero_caps_are_bound_per_tree_in_guest: cap words traded in the arena have no execution; the failing assert is in the chain's cap authentication. - grouped_chains_open_against_constant_roots: the block leaf's prepared wiring at the stacked level (roots as program constants, each chain its run's slice): honest arenas execute; exchanged constants and a forged value are refused at tree 0's and tree 1's openings. - emitted_program_digests (ignored instrument): digests of the emitted programs, to compare a test build with one whose cfg(test) emitter statements are compiled out. Block tier (lfm/whir_block_tests, ignored, host builds): - a_grouped_prepared_stack_verifies_and_its_leaf_refuses_a_forgery: at a stack of 2^8 a prepared stack is opened by one chain of three. - a_split_prepared_stack_refuses_a_forgery_in_its_last_chain: at 2^7 it splits [4, 1] and the last chain is forged. Each forgery is refused by the host at that stack's opening and by the leaf at the tree's opening, under RPX and Poseidon1.
Three checks of a grouped chain had no test that fails without them: the suite stayed green with any one of them removed. - Each round-0 tree bound to the root at its own position through its cap. For every slot in turn, tree 0 included, a stack whose slot commits another polynomial is claimed against the stated roots: the host refuses at that tree's cap (uncapped: at its first opening), and the leaf has no execution. Trading two trees' caps breaks two bindings at once and cannot show either alone. - The grouped round 0's successor bound to the root the round absorbed. A successor cap node off its root is refused at the cap by the host, and in guest where that cap is authenticated, node by node. - A one-round grouped chain's folds compared with the final value, on the host and in guest. Also holds the test build to the program a release build emits: the grouped leg's instruction stream is pinned for four cases (RPX and Poseidon1, runs [4, 1] and [4, 2]) in the library's unit tests and in an integration test built against the library without cfg(test). Test code only.
The in-guest test of the grouped round 0's successor cap gains a first case with a cap of eight nodes, unground. The successor is opened at most six times (three as round 0's successor, three as round 1's current tree), so some cap node is met by no opening whatever the draw: an arena that moves such a node is refused only where the cap is authenticated. Under a two-node cap every node is usually met by a query, and a later opening would refuse the forgery as well. Test code only.
…N B1+B2) The block path admits its proof family's base only (Poseidon1 at the 4-ary cap): a block format under RPX is refused before anything is proved. The four block-tier tests of the grouped chain looped over RPX and Poseidon1; they now run at hash_pin::BLOCK_BASE alone: - tests::block_onechain_tests::an_indexed_block_round_trips_and_refuses_the_other_mode - lfm::whir_block_tests::an_indexed_blocks_leaves_execute_and_refuse_a_grouped_forgery - lfm::whir_block_tests::a_grouped_prepared_stack_verifies_and_its_leaf_refuses_a_forgery - lfm::whir_block_tests::a_split_prepared_stack_refuses_a_forgery_in_its_last_chain The stacked-level tests (lfm::whir_onechain_tests) keep both hashes: they emit the opening leg directly, below the block path. Test code only; the rest of the diff is the loop bodies' indentation.
… split (D-ONECHAIN B1) The round trip asserted every grouped run within 2..=group_polys. A run is cut by index_chains at four whatever the group budget is, and a table that alone needs more than the budget is a group of its own, so a run of four occurs at two and at three polynomials a group. A box run failed there, at its first turn. In its place, each printed before it is asserted: per group, the proof's chain widths are index_chains of the frame's own layout, ones included; a group's stack exceeds the budget only where the group is one table; the per-polynomial proof holds one chain a polynomial; and over the three budgets a grouped run of every width from two to four occurs. The roots' equality, each mode's own verdict and the statements' differing bytes are printed before their asserts. Test code only.
The order's weights summed a contiguous slice of the run's shapes, but those are in AIR order and a group lists its tables' AIR indices, which phase A's packer does not keep contiguous. Every group was weighed by some other run of tables: on the median block the rest read as 8 % of the rebuilt cells, where its groups hold 28 % (and a third of phase B's time), so phase B took 17 of the 24 rest groups in its first 21 places and waited on the rest's regenerator (ULTRA 127: 3.1-3.4 s a run). Each group now weighs its own tables (`group_cells`), and the order readout reads the same weights. An ordering-only fix: the order is outside the transcript (each group proves on its own fork S_post || g) and any-order identity is pinned, so proofs are byte-identical; each class is still taken in group order. Tests: weights of non-contiguous groups and of contiguous ones with a stray index; with the slice put back both fail.
…(N4a) With LAMBDA_VM_BLOCK_REGEN and LAMBDA_VM_BLOCK_SPILL both unset, the production block now regenerates: whenever auto arms, the streamed chunks are dropped and rebuilt in phase B (the adaptive N2b window) instead of written, and the spill tier stays for the rest. A knob that is set keeps the meaning it had, as #1013's 1a98537: a set regen is read when the prove starts, and an unset regen beside a set spill is off, so scripts that pin the spill behave as before. Options a caller builds keep None as off. Measured with REGEN=auto: p90 disk 23 against 83 GiB, whole +0.05 s; T200-R disk 60 against 354 GiB, prove +0.3 %, takers never waited; the phase-A peak lower at both. Every prove now says the mode and the knobs (BLOCK REGEN mode). No proof byte depends on the mode.
With both LAMBDA_VM_BLOCK_SPILL and LAMBDA_VM_BLOCK_REGEN unset, BlockOptions::production() now pairs regeneration `auto` (N4a) with the spill policy NoDisk: `off` whenever live regeneration runs, so a table `auto` moves off the host is dropped when phase B can build it again (the streamed chunks, and KECCAK_RND's and LT's rest tables since N3) and stays otherwise, with no store opened. Without live regeneration (chunks not generated packed) NoDisk is `auto`, so no block loses its relief. Either knob set keeps the meaning it had, as #1013's 1a98537: a set spill reads as ever, an unset spill beside a set regen is `auto`. The BLOCK REGEN mode line is printed once the policy is resolved and ends with `no disk` when it runs with none. LAMBDA_VM_BLOCK_SPILL_RESERVE_PER_G (GiB per G cells committed, default 1.65 = today) is a measurement knob for the reserve retune D-FINISH S4 names; the default is unchanged until a gate reads the median at defaults. The proof's bytes are the same under every policy (tests under the deterministic grind).
…sh's end (N4c)
ULTRA 123 read the no-disk default (N4a + N3 + N4b) at +3.1 % on the
median and +2.1 % on p90. With the rest's KECCAK_RND and LT dropped too,
88 of 90 median groups are rebuilt, so no plain groups head phase B. Both
regenerators started 0.04 s before phase B, so its first rebuilt groups
waited for their start-up: about 2.5 s in each window, the whole phase-B
excess.
When live regeneration has armed (or drops every table) by the finish's
end, both regenerators now start there, on a thread of their own outside
phase A's (regen::EarlyStart), about 3.4 s before phase B at both block
sizes:
- the rest's regenerator starts at once and takes each table's fate as
phase A decides it: BlockRegen::await_rank(class, rank) gives the slot
once the drop thread makes it, Kept once the table is held by its group's
class or gone to the store, or Kept once phase A's end closes the
decisions; nothing is skipped before its fate is known. Tables it waits
for are counted on its line. (The rest's last groups commit after the
finish's end, so a final plan would only exist at phase A's end.)
- the streamed chunks' regenerator starts once every streamed chunk's fate
is known (their groups commit first), from that final plan; phase B checks
it against phase A's drops and the layout.
Every deposit still goes through its slot's digest. A waiter returns
Stopped as soon as its class's window closes, and an early start dropped
before phase B (the prove failed) closes every window and joins.
Knob LAMBDA_VM_BLOCK_REGEN_EARLY (on; 0 starts with phase B as before).
New line BLOCK REGEN head start.
Tests: the decision feed (a table dropped after the wait began, a held
table, kept only once phase A ends, Stopped when the window closes);
both regenerators started early with each rest drop held back prove the
bytes of regeneration off, as does the start with phase B; phase A
failing right after the finish returns its error and never hangs.
Mutation: an undecided table taken as kept is refused ("the regenerator
did not bring its columns back"), never proved.
…til phase B (N4c) The early start fills both windows at the finish's end, where T200's phase-A peak sits (087: 69.11 GiB). Until phase B starts, each window now holds only what the host's room allows: N2b's reading (VmHWM less the resident set net of the windows' deposits, less the 4 GiB margin) with no floor, split four to one between the streamed chunks' window and the rest's, each capped at its phase-B size, moved in whole-GiB steps. With no room, or no /proc reading, each window admits only its frontier. When phase B starts, an open gate gives each window its phase-B size and the streamed one its own pacer. So filling ahead of phase B never lifts the run's peak. The head-start line reports the most each window held before phase B and the readings. Tests: early_aheads at room, at no room and capped; the early-start tests prove the same bytes with the windows sized so.
…nish's end (N4c) With phase B's interleave weighing each group by its own tables, the rest's groups are spread over phase B. A replay of ULTRA 127's median runs puts the rest's regenerator about 2.7 times ahead of its takers when it starts with phase B (its waits 0.0 s at the median and p90, and at +50 % KECCAK_RND cost), so its early start, fed each table's fate as phase A committed it, is dropped. The rest's regenerator starts with phase B from phase A's final drops, as N3 built it. The streamed chunks' regenerator keeps its start at the finish's end (127: its takers' waits 2.5 s -> 0.00 s), each chunk's fate through BlockRegen::await_rank. Its window alone is sized by the host's room until phase B (VmHWM - (VmRSS - deposits) - 4 GiB, capped at its phase-B size); the rest's window is not touched before phase B. Removed: the rest's fed source and its feeder, the recorder's rest tags by family, BlockRegen::dropped_of, and the early pacer's rest window. The test deviation that held drops back now holds the streamed ones: the liveness test (phase A failing right after the finish, the streamed regenerator waiting on fates) returns its error and never hangs. The head-start line says which regenerator started early.
one_byte_windows_take_each_class_in_its_order and a_mixed_boundary_group_drops_one_class_only waited a fixed 600 s for their regenerating prove. Since the block path proves under the format's base (RPX) rather than the knob's keccak default, every block prove in the suite is slower, and on the laptop the one-byte prove ran past 600 s and then completed (phase B never waited on the regenerator). A hang never returns, so the guard now waits ten times the plain prove the test timed before it, and at least ten minutes: it still finds a hang, and no longer fails a complete prove on a slower host.
…s are blessed constants Every LFM program's artifacts carry the roots of the two static preprocessed tables. The legacy pin reads them from blessed constants; a Poseidon1 configuration computed them once per process: bitwise's 2^20 rows by 11 columns interpolated, extended and committed on the host, about a second in a prover and, with two threads missing the cache together, about two in every verify-block. `static_commitment_p1` holds the two roots at the one key the block's proof family asks for (blowup 4, coset 3, row pairs), generated by `compute_static_commitments --hash p1`. Any other key under Poseidon1 is still computed, the legacy pin keeps its own constants, and no proof byte moves: each constant is the value computed until now. Drift tests recompute both constants. One more test takes each way off the key (another blowup, another coset, one row, the legacy pin) and shows the computation is taken there, and that a configuration's cap does not enter a root.
The key test covered every off-key case on keccak_rc and said bitwise's dispatch was the same code. It is a second function with the same conditions, so one case is now recomputed for bitwise too: at the other coset the wrapper returns the computation, not the blessed constant.
…s key The key test took the legacy pin and Poseidon1 at blowup 4, coset 3, and no configuration that is neither. One now does, for both tables: under RPO the wrappers return the computation, not either family's constant. The test first checks that RPO is neither family's hash, so a change of pin asks for another third here instead of passing silently.
…purges A block that proves with no disk notes memory pressure from the walk's-end forecast of the finish's growth, which is the growth of tables that stay. It then drops them, and on a host with room its resident set never comes near the line the forecast crossed: on a 118 GiB host the p90 block peaks at 67 GiB against a line of 92, and still purged at phase A's end and at the base's end. Those two purges, and the page faults phase B takes after the first, cost it 4.1 s of 180 (ZEN5 096: 180.38 against 176.25 s). In no-disk mode `auto` now purges at its two points only when VmRSS plus the regeneration window's ceiling (16 GiB) reaches the block's pressure line, 85 % of the host target; otherwise it prints why it skipped. A 64 GiB host keeps both purges, for p90 and for the median (VmRSS 36.1 and 45.6, 33.1 and 33.2 GiB, against a line of 45.9): without the one at phase A's end it admits 4 leaf programs beside the base where it admitted 24 and proves 2.2 s slower. A resident set that cannot be read purges. A block with a store, a LAMBDA_VM_ALLOC_PURGE list, the tree's level purges and the watermark are as they were. LAMBDA_VM_ALLOC_PURGE_ROOM=off purges at both points whenever memory is short, as before. No proof byte moves: a purge returns freed pages to the OS. Tests: the decision under room at every setting and point; the six boundaries ZEN5 096 read, at both targets; at the line there is no room; no reading is no room; a block with a store has no line; the line is the pressure share and not the target.
The reason for blessing lfm/p1_commit.rs said the block path never names the module. The block's nodes run transcript_replay's Poseidon1 arms, which read it; what holds, and what the block-path scan enforces, is that the block's own five files never name it. The entry also names the static tables' generator, which has used the configuration since the Poseidon1 roots were blessed. Text of a reason string only: no test's logic moves.
The module doc gave the cost of the two boundary purges on a host with room as "both purges and their page faults, 4.1 s". The 4.1 s is right, and it is the purges' own time: 2.4 s at phase A's end and 1.6 s at the base's (ZEN5 099: 2.38-2.47 s and 1.62-1.65 s; 4.07 s between the rule and the rule before). The page faults are not supported, and the commit that brought the rule said the same of phase B in its message. What moves phase B is the number of leaf programs the tree proves beside the base. A block that skips the purges keeps 13 of 26 there, because the tree's budget counts the pages the allocator still holds: phase B is 3.4 s shorter and the tree 2.7 s longer. With the budget's share raised so that all 26 stay beside the base, and no purge, phase B is as long as with the purges (87.27 s against 87.28 and 86.81; 83.78 and 83.58 with 13). Comment only: no code line changes.
With no disk, phase B rebuilds the tables phase A dropped. The streamed chunks' regenerator was started at the finish's end, 0.58 s before phase B at the median, and its first chunk needs about a second: phase B's first rebuilt group waited for it (0.16-0.64 s at the median, measured on a 118 GiB host with one RTX 5090). Its thread now goes up where the streamed chunks end and the finish begins, whether or not regeneration has armed. It already waits for every streamed chunk's fate (`await_rank` answers nothing for a table the arm may still drop), so it starts the regenerator as soon as the arm's drop-back and the streamed groups' commits have decided them, while the finish still runs. Until the finish's end its window holds only its frontier, so it does not rebuild beside the finish; after it the host's room sizes the window, as before. If phase A ends unarmed, every table is kept and nothing starts. LAMBDA_VM_BLOCK_REGEN_EARLY: unset is the new start, `finish` the start as it was, `0` the start with phase B. The head-start line gives the regenerator's own start and its thread's. No proof byte moves: which tables are rebuilt is phase A's decision as before, each rebuilt table is checked against the digest taken at its drop, and phase A's end still refuses a regenerator whose ranks are not its final drops. Tests: the knob's three values and the line; the early window keeps its frontier until the finish's end and takes the room after. The proving tests of the early start cover the three starts; they run on a card.
When phase B rebuilds both classes it interleaved them from the start: one streamed group, then the rest's first. The rest's regenerator starts with phase B, from phase A's final drops, and its first group's tables take it about a second and a half. At the median its first group waited 0.31-0.45 s (measured on a 118 GiB host with one RTX 5090), and with the streamed chunks' regenerator ready at phase B's start it would be reached sooner and wait longer. Phase B now takes the groups it rebuilds nothing of, then the first LAMBDA_VM_BLOCK_REGEN_LEAD streamed groups (default 4; 0 is the plain interleave), then the others of both classes interleaved by their cells as before. Each class's groups stay in group order, the order its regenerator deposits in. The order line says how many streamed groups came ahead of the rest's first. No proof byte moves: the proof does not depend on the order phase B takes its groups in (the no-disk default's interleaved order proves the bytes of the group order). Tests: the order for a lead, for none, for a block with no rest group and for a lead past the class; every group once and each class in order over random blocks; the knob.
The streamed chunks' regenerator now has its thread up before phase A has dropped anything. With each streamed chunk's drop held back by 100 ms it asks for fates that are not decided yet: it must wait for every one, rebuild exactly what was dropped and leave the proof's bytes those of regeneration off. A fate answered "kept" before it is decided leaves ranks out, and phase A's end refuses that prove.
The planner started one plain thread a leaf the moment the statement arrived and took none of the proving phase's groups until every program was emitted. Nothing needs a leaf's program before the base ends except the early leaf, and while thirteen emitters ran the phase's first groups cost 0.61 s each against 0.45 s after them (median block, one run). Now W3_LEAF_EMITTERS programs (default 3) are emitted at a time, in leaf order, beside the planner's loop over the groups; each is published as it is ready, leaf 0 first, and the planner joins the emitters before it returns, so the tree still finds every admitted leaf published. W3_LEAF_EMITTERS=0 is the control: one thread a leaf, all at once, the first group taken after the last program. Scheduling only: a leaf's program is a function of the plan, and no proof byte or program id moves. The effect on the block's time is not measured yet.
…ob, off by default) A block cuts KECCAK_RND, KECCAK, ECSM and ECDAS at a fixed number of rows. The count comes from the block's first version, which built each table whole, padded to a power of two, and copied it apart: the pieces past the last real row hold padding rows alone. A mainnet block of 96,384 keccak permutations builds 64 KECCAK_RND tables of 2^16 rows where 36 hold a real row. CutPadding names the two rules: WholeRun (the default: the tables the split makes, as before) and LastTable (stop at the table that holds the last real row). LastTable's tables are WholeRun's first ones, word for word. A padding row of the four families has every bus multiplicity at zero, so the tables left out add nothing to the bus; the verifier takes whatever count the statement states and is not changed. - CutPadding::tables is the one count: the finish's announced counts, its build jobs, the monolithic finish and the tables built again in phase B (the same function with the same settings) all read StreamSkip::cut_padding. - LAMBDA_VM_BLOCK_CUT_PADDING=run|last, read once by the windowed builder; unset is run, any other value refuses the build. A table built whole and split by the block afterwards (the comparison arms) keeps the whole run's padding. - The streamed finish prints a BLOCK CUT PADDING line: the rule and, per cut family, the tables built and how many of them hold a real row. Tests, none proving: the count against a model of the split; the default and the knob's two words; for each family, wide, packed after the build and written packed, LastTable's tables are the split's first ones and every table left out has all its multiplicity columns at zero, the check itself shown to refuse a cut one table short and one table long; windowed builds of the fixtures against the frozen table phase, every other table unchanged; the header, the stream and phase B's jobs count the same tables under either rule; the knob off builds the listed tables on two pinned programs.
…hort is refused on the bus Proving tests for a block whose cut tables stop at the last real row (CutPadding::LastTable): - test_keccak_multi (per table and batched) and test_ecsm_multi (batched) proved with the whole run padded and cut at the last real row: the verifier accepts both, the second with one table fewer of each cut family the program has. - The same blocks with a family's last table, which holds real rows, also left out (KECCAK_RND per table and batched; KECCAK, ECSM and ECDAS batched): the refusal is BusImbalance, and with the balance check off the verifier accepts the same proof, so the balance is its only objection. - The streamed prove under the process's rule (LAMBDA_VM_BLOCK_CUT_PADDING): KECCAK_RND's count is the rule's with the finish monolithic, with the finish streamed, and under live regeneration, which builds each dropped KECCAK_RND table again with no mismatch. Run once per value of the variable.
Some comments added with the last changes cited a measurement by the name of the run that took it. Each now states the figure and what it was measured on (the block, the host's memory or cores, the card). Comment lines only: no code line is added, removed or moved.
Draft
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.
One WHIR (multilinear) proof per block, with no epochs (the prove-and-retire / VADCOP shape). This is a second prover next to #1010's epoch-based one. #1010 stays the reference and this branch does not touch it. The branch starts from #1010's head
f3d359998; compare againstf3d359998to see only this work.What it is
prover::block_whir::prove_block_whir/verify_block_whir. The whole block is oneMultiProof:(z, α, β)are drawn once.S_post ‖ g: upload again, argue (LogUp-GKR), re-encode with the NTT alone, open. The dropped tree levels are rebuilt from the queried cosets and checked against the kept nodes.WindowedTraceBuilder(noepoch/windowed-builder, also used by STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1013). Every full chunk of CPU, MEMW_R, MEMW_A, MEMW, LOAD, LT, SHIFT and STORE is laid out and committed as soon as it exists.⚠ Format changes (all approved by Mauro, 10-02 (§7 on 10-05), but §6, the port of #1013's S0a; for the cryptography team's end review; ledger rows W-0…W-6, W-8, W-9 in CRYPTO-REVIEW.md)
The block is one WHIR proof (
BlockWhirProof, oneMultiProof) over all of the block's tables.z, α, β.S_post ‖ g).Every parameter below is a verifier-side constant. None is read from the proof.
BLOCK_GROUP_POLYSBLOCK_MAX_GROUPSBLOCK_MAX_TABLE_VARSBLOCK_MAX_KECCAK_RNDBLOCK_KECCAK_RND_MAX_VARSBLOCK_MAX_ECDASBLOCK_ECDAS_MAX_VARSBLOCK_MAX_KECCAKBLOCK_KECCAK_MAX_VARSBLOCK_MAX_ECSMBLOCK_ECSM_MAX_VARSArgueFormat::BATCHEDPRODUCTION_WHIR_GRIND_BITSLEAF_PERMS_CAP,BLOCK_FAN_IN1. The group partition is in the statement
What. The streamed prover packs tables into groups in the order their chunks complete. It writes the partition (per group, its tables' indices) into the statement. The transcript absorbs it before any root.
Verifier. The verifier checks:
It then rebuilds every group's stack layout itself.
Security. Proven bits are unchanged.
BLOCK_MAX_TABLE_VARS(change 3), so no partition can make a chain taller than 2^27.Proof bytes vary from run to run. Arrival order depends on thread timing, and the hash-ordered tables' row order (LT, EQ, BYTEWISE, BRANCH, MUL, DVRM) follows a hash state that is random per process. Mauro, 10-02: "the proof not having the same bytes it's fine, they never had the same bytes anyways".
LAMBDA_VM_FIXED_TRACE_HASH=1(fixed hash keys) andLAMBDA_VM_DETERMINISTIC_GRIND=1. Under both, two processes prove the same bytes (FAST 423).2. Prepared openings, one stack per group (approved by Mauro, 10-02: "it's a standard technique, go ahead")
In short. This is #1010's existing prepared DECODE opening, carried into the block format. It is extended to the dense genesis pages and stacked per group.
What. The recursion guest cannot evaluate DECODE's five 2^20 preprocessed columns, nor the dense genesis pages (millions of rows), on its own. So each group's prepared tables (DECODE and the dense genesis pages chosen by
genesis_stack_plan) have their leading preprocessed columns stacked into one commitment.verify_block_whir, andverify_block_tree's plan. The prover's own roots only shortcut the prover's side.z.Negatives, on a guest with two dense genesis pages. Each is refused by the host verifier and by the recursion leaf, and admitted when the openings are skipped (the mutation):
Wrongly committed stacks are refused at the roots block: two pages swapped, a page holding another page's columns, another program's DECODE.
Cost. One stack per group costs +0.14 s of base (FAST 419; one commitment per table cost +0.49 s) and saves 0.57 s of recursion, by taking 90 k permutations out of the leaves.
BlockFormat::prepared = falseturns the openings off, for measurement only.3. ECDAS split; table heights capped
What.
split_ecdas).(round, op).Why. At a median block, one ECDAS table (2^19) argues on a 12 GiB tree. At p90 (2^20) it stacks into five polynomials on a 24 GiB tree.
Gates. FAST 534 and 535: the negatives, 3 mutations caught.
4. Recursion leaf: the leaf cap (G3) and the inverse share (W1)
LEAF_PERMS_CAP. With no fixed leaf count, it adds leaves while the heaviest leaf is over the cap. The block's own partition is unchanged.p · ediv(1, q), which has no satisfying assignment for q = 0 whatever p is. The formerediv(p, q)left the share free at p = q = 0 (≈ 2^-160 under GKR soundness).LFM_WHIR_SHARE_INVERSE=0keeps the former form.5. Batched argue, on by default since d9d0ac5 (
BlockFormat::argue = ArgueFormat::BATCHED; Mauro, 10-02: "batch the constraints")What. Each group's tables are argued together on the group's fork:
Verifier side. The verifier derives the bins from the statement's shapes and its own cap; the variant is never read from the proof. The proof carries one
BatchedArgueper group inBlockWhirProof.argues, andproof.tablesis empty under this format. The recursion leaves verify the same format (lane i-batch2, N-4).Security at the block's measured inputs (D-BATCH §3.2's formulas; re-checked at FAST 424's inputs: |T| ≤ 60 tables a group, ≤ 51 trees a bin, ≤ 2^28.87 input cells, bus messages ≤ 204 elements, N_C ≤ 413, D_max 4, n ≤ 21; L = 300):
Every term is above the WHIR phase, so the proof minimum is unchanged: 128.946 under the campaign accounting (130.393 for the WHIR fold under the calculator of record).
Measured.
The knob.
ArgueFormat::PerTable(one argument per table, the format before) stays measurable, withBLOCK_WHIR_ARGUE=per-tablein the real-block tests. At the per-table format, the MultiProof, the prepared openings and the partition are digest-equal to the head before the batched merge (BIG 393).6. KECCAK and ECSM split; their heights capped (the port of #1013's S0a; landed at 3cfffe2; approved by the lead under Mauro's any-block target, Mauro to confirm)
What.
split_keccak,split_ecsm), as ECDAS is at 2^17.Why. A keccak-heavy block at the gas limit makes about 2^21 permutation calls. One KECCAK table that tall stacks into eight polynomials of 2^27, against a group budget of three. At its cap each table fits one polynomial.
Bytes. Every block measured so far has KECCAK ≤ 2^17 rows and ECSM ≤ 2^11, so it builds the same tables and the same proof (FAST 830: digest 07d1bd43… unchanged on block 25368371).
Gates. FAST 830: KECCAK and ECSM forced into 4 tables each on block 25368371 prove and verify, base and tree; the count and height negatives; mutation C caught.
7. WHIR grinding 20 → 18 bits, queries 112 → 114 (landed at cbfa7d7; approved by Mauro, 10-05)
What.
Each WHIR chain grinds 18 bits instead of 20 before each round's queries, and opens 114 queries instead of 112 to buy the two bits back. At every block height (15 to 27 variables) Q =
num_queries(2, rounds, 128, 18).One site,
multilinear_prove::chain_config_under. Every side of the block reaches it throughBlockFormat::chain_config:verify_block_whir;The statement absorbs Q and the grind bits.
Scope: the block's WHIR proofs only. The recursion tree's own leaf, node and top proofs are STARK proofs (FRI) and keep their own grind.
It ports WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 32.62 s #1010's grind 18 (landed there at 2687e48). Opt-out:
LAMBDA_VM_ZF_WHIR_GRIND_BITS=20, which reproduces the previous proofs byte for byte.Security. The calculator of record: Johnson bound, η = 1/300, the fold proof of work unground.
Measured behind the knob, on c55dec4, one binary per box:
Bytes. The proof bytes change. Under the fixed trace hash and deterministic grind, the 1× top proof's digest is b9b4826d… at the default and 0da6fea9… under
=20. Both trees are accepted (ULTRA 015, at this head).Known limit. The leaf cap (279,000 permutations) sits above 2^18 LFM_HASH rows, and 18 bits moves every leaf about 1.7 % closer to that doubling.
Gates.
zf_format::; the chain refusals and the legacy-bytes KAT; the pins the flip moved; the host block refusal.One-binary A/B: the block tree vs #1010's epoch tree (FAST 416, noepoch/whir @ 454dad6)
Eight arms E B B E E B B E on the same binary (md5 checked after every arm):
Base, block 25368371 on FAST (each run against #1010's epoch base on the same binary)
Recursion (W3): proved and verified on block 25368371 (FAST 413)
lfm::whir_block::WhirBlockPlan) runs the host verifier's own statement checks, derives every shape from the AIR and every prepared root from the program, and never reads a proof. It prices each group in permutations and partitions the groups over the leaves (heaviest first, onto the least-loaded leaf).block_node, shared byte for byte with STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1013). They check the id, the state and the output equal across children, add the shares, and the top asserts zero.verify_block_tree(elf, statement, top)derives the top program and checks the top proof against it. Every preset is pinned inside: the base's options, the block format, the tree's options, the leaf rule and the fan-in.Every proof was verified by the harness off the clock. The base is unchanged by the emission running beside phase B (20.98 s vs 21.18 s in the arms without the change).
Next:
Base levers after the A/B (FAST 417–422, each S/P on one binary)
Memory (D-MEMORY, with i-mem / i-mem2)
drop_streamed_ops)layout_ahead, D-EXEC)Narrow trace storage (M4, lane i-m4)
Prover-only; the proof bytes are the same. The proof digest equals the head's before M4 (07d1bd43…) under the fixed trace hash + deterministic grind (FAST 820).
BlockOptions::narrow, productionNarrowing::CARD).Max RSS, wide → narrow (BIG 560; the traces themselves shrink about 4×, e.g. 24.89 → 6.20 GiB at 1×):
Before M4, #1014 ran out of memory at 3.03× (i-m4); the narrow line puts the 120.7 GiB edge near 5.5–6×.
Phase A uploads the next group beside the commit (3′, lane i-noepoch-w2)
Prover-only; the proof bytes are the same (digest 07d1bd43… at the default, FAST 832; the commits, their order and their bytes are unchanged).
BlockOptions::upload_ahead, on by default;BLOCK_WHIR_UPLOAD_AHEAD=0is the control).Streamed chunks laid out on three threads (E3 on by default, lane i-noepoch-w2)
Prover-only; the proof bytes are the same (digest 07d1bd43… at the default, FAST 838 and 835; the groups, their order and the packing are unchanged).
layout_workers0 → 3 inBlockOptions::production(59e9890). The streamed chunks are laid out on three threads, with at most K + 1 = 3 of them unpacked at a time (layout_ahead, E3), instead of on the builder's one layout thread.BLOCK_WHIR_LAYOUT_WORKERS=0(the inline layout, 01da99f's) is the control.Phase B's kept-top paths re-hashed in parallel (lever 1, lane i-noepoch-w2)
Prover-only; the proof bytes are the same (digest 07d1bd43… at the default, FAST 840 at 733557a — 82f9046 adds only the knob; the paths, their order and the refusal are unchanged).
BLOCK_WHIR_REHASH_SERIAL=1restores the serial re-hash (a measurement knob).BLOCK OPEN SPLIT(withLAMBDA_VM_BASE_SPLIT=1), phase B's openings by host stage and the kept-top gather and re-hash seconds.The tree's leaves execute while their artifacts are built (R2-i, lane i-noepoch-w2)
Prover-only; the proofs are the same (the top proof's digest equal with and without it, 0da6fea9…, under the fixed trace hash + deterministic grind, FAST 841).
prove_tree_pipelined, the way a prover would run it; there is no production tree driver yet). Its level 0 built all three leaves' artifacts first (three serial card holds, 0.24 s at 1×) and only then let the leaves execute and fill their traces (0.46 s until the first was ready) while the card sat idle (nsys timeline, FAST 839). Execution and fill do not read the artifacts; only the prove does.lfm_proveis now cut into its host half (lfm_execute_and_fill) and its card half (LfmFilled::prove, which asserts the artifacts' hasher is the one the traces were filled for), in the same order; the harness builds the artifacts on a thread of their own and each leaf executes and fills beside them, waiting for its artifacts only to prove.W3_EXEC_BESIDE_ARTIFACTS=0is the control (the order before).A second whole-block field. The W3 readout prints, on the clock between the base and the tree, the block report, a second block frame for the LT heights and the group tables: 0.13–0.14 s at 1× that a prover would not spend. "Whole block" keeps its meaning (every number above includes them); from d169edd on,
W3 RECURSIONalso prints "whole excl. harness readouts" beside it (at d169edd, FAST 841 + 842 pooled: whole 18.54 s, whole excl. harness readouts 18.40 s).The rest laid out in waves; KECCAK_RND built as its tables (W, lane i-m4b)
Prover-only; the proof bytes are the same (digest 07d1bd43… under the fixed trace hash + deterministic grind: FAST 854 at 0bcc1fc, and FAST 855's W arm on this head's code).
BlockOptions::rest_layout_bytes). The tables, their order and the groups are the same (test).split_keccak_rndthen copied into its 2^16-row tables. At 4.13× that copy was a second peak of the same height. The finish now builds the 2^16-row tables directly (BlockOptions::finish_keccak_rnd_chunks), the same tables as the split's (test), and hands none out during the windows, which would change the groups.BLOCK_WHIR_REST_LAYOUT=allandBLOCK_WHIR_KR_FINISH_CHUNKS=0restore the old behaviour.pack_rest_as_laid_out) was re-measured with the waves: no gain (+0.01 s), so it stays off.LAMBDA_VM_BLOCK_MEMLOG=1(off by default). It prints the host memory term by term every half second and at each phase mark, with the line at jemalloc's active peak and each arena's bytes (those two in the lib tests, which install jemalloc).BLOCK REST TABLES(each rest table's layout end and group) and each group's commit start (from@).The tree's first finished leaf executes during phase B (R2-ii, lane i-noepoch-w2)
Prover-only; the proofs are the same (base digest 07d1bd43… and the top proof's digest equal with and without it, 0da6fea9… — FAST 841's — under the fixed trace hash + deterministic grind: FAST 845, and FAST 847 again on fac261f, the merge with W).
block_prove_on_forks_observed; the old entry point passes a no-op, and the proof is the same with any observer). A leaf's arena is built from its groups' words alone (group_arena_words+leaf_arena;block_leaf_arenais built from them). The W3 harness turns the groups into words as they arrive; the first leaf whose groups are all opened while another group is still to come (leaf 0 = groups 1, 5, 6 at 1×, done after group 6) executes and fills its traces on a thread of its own, about 1.5 s before the base ends, and the tree picks it up. Off the clock its arena is checked against the finished proof's.W3_LEAF_DURING_PHASE_B=0is the control.The finish's tables packed as they are built (b2, lane i-m4b)
Prover-only; the proof bytes are the same (digest 07d1bd43… under the fixed trace hash + deterministic grind at both arms: FAST 855 at 0cebe16, and FAST 857 again on 365e3ab, the merge with R2-ii).
BlockOptions::pack_finished). KECCAK_RND is packed in waves of four 2^16-row tables. Phase A takes those tables as narrow columns and uploads them as they are, so the card packs only the groups' other tables. Nothing packed is transposed.BLOCK REST PACKEDcounts the rest: 187 of 191 tables at 4.13×).BLOCK_WHIR_PACK_FINISHED=0restores W.Measurement posture: the card's pool now retains freed memory (baseline shift)
Not a prover change; the proofs are the same (the top proof's digest 0da6fea9… under both postures, fixed trace hash + deterministic grind, FAST 848; 351a773 adds only a test readout).
LAMBDA_VM_MEMPOOL_RELEASE_MB=0. The card's stream-ordered memory pool then hands its freed blocks back to the driver at each synchronize, so a VRAM sampler's total − free reads the live working set. The code default is to retain every freed block (DEFAULT_MEMPOOL_RELEASE_THRESHOLD_BYTES = u64::MAXin math-cuda), and that is what a prover runs.cuStreamSynchronizeandcuMemAllocAsyncwhile the card was idle was ≈ 0.25 s in phase A, 0.42 s in phase B (0.26 s of it in the encode: one 18–33 ms gap per group) and 0.34 s in the tree.W3 DEVICEline).The prover refuses a partition over the group maximum (lane i-m4b)
Prover-only; the proof bytes are the same (digest 07d1bd43…, FAST 858). The prover now refuses a block over
BlockFormat::max_groupsas its groups close, with the verifier's ownInvalidTableCountserror, instead of proving a block the verifier refuses. The median block (90 groups against the cap of 64) spent its whole ≈ 190 s base on such a proof (BIG 564). A test covers both prover paths, with a mutation for each check.The tree's nodes built beside it, each level proving as its nodes arrive (A, lane i-noepoch-w2)
Prover-only (the W3 harness); the proofs are the same (the top proof's digest 0da6fea9… — FAST 841's — with and without it, under the fixed trace hash + deterministic grind: FAST 889, and FAST 891 again after the permit fix below).
prove_tree_pipelined) built every node program and its artifacts on one thread, level after level, and level 0 joined that thread before any node proved, so level 1 waited for the whole tree's builds. At the median block, 32 s of serial node builds after the leaves' artifacts held level 0 open 10.7 s past its last leaf proof, and the card sat idle 11.4 s before level 1 (BIG 565 / 568's card trace). At 2.66× the wait was 1.5–2.1 s (FAST 888).W3_EMIT_THREADSthreads (default 4, the builder's own: on the global pool a prover's join can steal an emission and leave the card idle, STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1013's FAST 454), and the builder's thread builds each one's artifacts as it arrives. Each node proves once its children are proved and its slot is filled. A builder that stops fails every empty slot.W3_NODE_PIPE=0is the control.Published<T>serves as the slot; there is no per-group early emission (W3 builds every leaf's artifacts up front, so a level's programs are emitted together); and there is no host-only thread marking (WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014 has none).build_levels, an emit on the pool and a finish on the builder's thread). A toy tree at the median's shape puts the serial builder's programs in their slots whatever order a pool finishes them in, and finishes every node off the rayon workers (test; finishing on a worker, or publishing in arrival order, fails it). A failed build fails every slot above it without a hang (test).WhirBlockPlan::programsbuilds as rayon jobs.W3_NODE_TIMES=1(off by default). It prints, per program, when it was built, taken and proved; its prove's split (execute, fill, the prove net of the card wait, the wait); and each node level's start against the level below. The stamps are taken either way and cost a handful ofInstantreads.prove_tree_pipelinedis its de-facto driver. With the slots it now has a driver's shape: leaves from phase B, nodes from slots, levels as their nodes and children arrive. Moving it out of the test harness is a landing item for STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1013 and WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014 both.Each node proved as soon as its own children are (C, lane i-noepoch-w2)
Prover-only (the W3 harness); the proofs are the same (the 1× top proof's digest with and without it, under the fixed trace hash + deterministic grind, both trees accepted: 0da6fea9…, BIG 614 — the digest FAST 841 / 889 / 891 printed).
siblingsworkers proves every program of the tree, leaves and nodes, taking them in topological order (the leaves, then each node level) and publishing each proof into a slot of its own. A node waits only for its own children's slots, so it executes, fills and proves while the rest of the level below still proves.W3_DATAFLOW=0is the control: each node waits for its whole level below (tested never to start early).W3 LEVELwall now runs from the level below's last proof to its own.The held tables spill to disk, by default only when memory is short (lane i-m4b)
Prover-only; the proof bytes are the same: one shape and one proof digest (07d1bd43…) across the base and all four policies, under the fixed trace hash + deterministic grind (BIG 616). The 1× top proof's digest (0da6fea9…) and the tree verify at the landed merge (ULTRA 006).
crypto/stark/src/spill.rs) is imported byte-identical; it digests each slot and writes it with O_DIRECT on a writer thread, off the committer's path. A table moves only when the writer's queue has room at that moment, so the committer never waits.SpillFailed). After a group's opening its packed columns are let go.LAMBDA_VM_BLOCK_SPILL=auto(the default) |off|always|<GiB>(a resident budget).autospills a table when the host's bytes plus a reserve plus the table would pass the target.LAMBDA_VM_BLOCK_SPILL_TARGET_GIB, else min(the cgroup's limit, v2 or v1;MemTotal) − 10 GiB.BLOCK SPILLprints the policy, the slots and bytes, the writer's split, the read-back, and the largest host readingautodecided on.always(BIG 566, single runs): 22.74 GiB spilled, max RSS 52.53 → 40.12 GiB, whole block +1.15 s, drivers waited 0 s.auto(BIG 567): by default nothing spills (target 110.7 GiB). Under a 35 GiB target it spills 21.21 GiB, max RSS 41.04 GiB, +2.91 s.autospilled 26.98 GiB from group 44 of 90, and the tree verified.auto, O =off, S =always, T =autoat a 47.5 GiB target):autorun spilled 0 in both jobs, at 110.7 GiB and under the 47.5 GiB target (FAST's limit, kept as a real test on BIG's 128 GiB; the host read 25.3–26.8 GiB across those runs);alwaysandautoat 0 spilled within noise.autospilled 0 (host at most 27.65 GiB).Phase 4 counts the BITWISE lookups in slices (#1013's c369843, lane i-m4b)
Prover-only; the bytes are the same:
What changes:
LAMBDA_VM_P4_SLICED=0keeps the whole-source collectors. The knob is WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014's only, for the A/B.Measured: the median, BIG 619. One binary on the cap-128 exploration head, N P P N,
autospill on. N runs the whole-source path, P the slices.auto)autospills only once memory is short.The top executes each child's share as that child is proved, while a child is still unproved (B and B′, lane i-noepoch-w2)
Prover-only (the W3 harness and the LFM executor); the proofs are the same. The 1× top digest is 0da6fea9… with the streamed top on and off, under the fixed trace hash + deterministic grind, and both trees are accepted (BIG 617; ULTRA 008 and 009; ULTRA 011 at c55dec4 itself).
executor::StreamedExecutionruns an execution as its arena groups land: each instruction runs in the wave in which the last group it reads lands (each instruction's group set comes from one forward pass), in the level schedule's order, into the slot the schedule gives it. So the witness isexecute's, word for word.finishchecks that every instruction ran once. Dropping the group propagation (ReadBeforeWrite) or re-running instructions across waves (DoubleWrite) fails them.W3_STREAM_TOP, on by default), so only the last child's share and the fill remain after the last child.W3 TOP EXECUTEprints which. A test covers a program ready before the last child (streamed), after it (whole), and a failed child (whole, which then reports the failure). Inverting the predicate fails the test.A node executes from its program; only its prove waits for its artifacts (b′, lane i-noepoch-w2)
Prover-only (the W3 harness); the proofs are the same: the 1× top digest is 0da6fea9… with
W3_EXEC_EARLYon and off, under the fixed trace hash + deterministic grind, and both trees are accepted (ULTRA 012; ULTRA 013 at 806c211 itself).node_flow). The traces are filled under the block hasher, which the node artifacts are built for. The top's streaming choice (B′) is taken when the top can execute.W3_EXEC_EARLY=0is the control.LfmProveError::HasherMismatch, naming both) instead of failing a release assert, and the streamed execution's finish returns an error instead of asserting its public words' capacity. Each has a test and a mutation.