feat(prover,executor): keccak sponge-absorb accelerator (ECALL -4, one row per rate block) - #912
Draft
MauroToscano wants to merge 6 commits into
Draft
feat(prover,executor): keccak sponge-absorb accelerator (ECALL -4, one row per rate block)#912MauroToscano wants to merge 6 commits into
MauroToscano wants to merge 6 commits into
Conversation
New syscall keccak_absorb_blocks(state_ptr, data_ptr, n_blocks) at a7 = u64::MAX - 3 (spec ECALL -4). Per block k the executor XORs the 136-byte rate block into lanes 0..17 (little-endian dwords) and applies keccak-f[1600]; lanes 17..25 are untouched by the XOR. Padding stays guest-side: the chip only ever sees whole rate blocks. Rejections mirror the existing accelerator guards and are chosen so the prover chip's addressing model is total over accepted inputs: - both pointers 8-aligned, n_blocks > 0; - LAST byte of each region bounded against u64 overflow (checked_mul / checked_add) and against low-limb overflow ((addr % 2^32) + last_off < 2^32) - the chip models per-dword addresses as base_lo + offset with no carry into the high limb, like the ECSM operands (ecsm_addr_ok is generalized to accel_addr_low_limb_ok); - state/data regions disjoint: the trace builder issues the state read and every message read at the ecall timestamp, so an overlap would put two MEMW ops on one (address, timestamp) pair, which the memory argument cannot order (same rationale as EcsmOperandOverlap). The log carries state_addr/data_addr (src2/dst); n_blocks is recovered from x12 by the trace builder like the ECSM operand addresses. Tests: sponge differential vs tiny_keccak (n = 1, 2, 3, 5, 8, 13 with seeded pseudo-random state+data), a chained XOR+keccak_f1600 replay against the executor's own permutation, and one rejection test per guard including both boundary sides of the low-limb check.
…for keccak absorb - syscalls: keccak_absorb_blocks(&mut [u64; 25], &[u8], n_blocks) guest wrapper (a7 = u64::MAX - 3). Documents the whole-rate-blocks contract (padding stays with the caller via keccak_permute) and debug-asserts the length and 8-byte data alignment the executor enforces. - cli --cycles: accel tuple (keccak, ecsm) -> (keccak, keccak_absorb, ecsm) with a 'KeccakAbsorb calls' line, via the existing accelerator_of confirmation path. - spec/about_ecalls.typ: register ECALL -4 (-3 is taken by the in-flight BLAKE3 accelerator PR).
…race-builder wiring New table KECCAK_SPONGE (one row per absorbed 136-byte block, 690 main cols, 216 bus interactions, 7 transition constraints): - Row k of a call receives the running state over a new self-referential KeccakSponge chain bus (BusId 32), commits the block bytes, XORs them into lanes 0..17 via 136 ByteAlu[XOR] sends (which also range-check both operands and pin the outputs), passes lanes 17..25 through unchanged, and round-trips the absorbed state through the untouched KECCAK_RND chip over the Keccak bus (round 0 -> 24). - Bookends: the first row receives the ECALL, reads x10/x11/x12 and MEMW-reads the 25-lane state at ts; the last row MEMW-writes it back at ts+1 via write-only 16-element tuples (mu_first/mu_last flags). Soundness (the per-block keying landmine): all n permutations of one call share ONE CPU timestamp, and with the old (ts, round, state) Keccak-bus tuple a malicious prover could swap two blocks' permutation outputs with every tuple still appearing once per side - the bus BALANCES. Fix: the Keccak bus payload gains a seq element. The classic core chip sends seq = 0; sponge row k sends seq = k; KECCAK_RND carries seq through untouched like ts (new SEQ column, 1480 -> 1481 cols). SEQ is pinned by mu_first*SEQ = 0 plus the chain sender's SEQ+1, and the chain cannot wrap the field, so every permutation of a call gets a unique (ts, seq) key on both Keccak-bus legs. The chain also carries (n, state_ptr, block_base); the last row pins N_LO = SEQ+1 AND N_HI = 0 (word-level, closing the N = n + p mod-p alias), so a call has exactly x12 rows. Full attack walkthrough in the module docs. This extends the Keccak bus wire format: proof-breaking, noted for the PR. All eval constraints are degree <= 2 with IS_BIT on mu, mu_first, mu_last. Addressing deviates from the brief's keccak.rs DWordHL pointer apparatus in favor of the ECSM low-limb idiom (addresses as base_lo + offset linear elements; executor guarantees low-limb room; lying provers only produce unmatchable Memw/Memory tokens): the keccak apparatus would cost ~168 extra main cols + ~84 aux cols per BLOCK and defeat the accelerator's purpose. s_addr stays byte-decomposed (with AreBytes + the &7 alignment lookup) so alignment is enforced in-chip. Trace builder: collect_keccak_sponge_ops lowers one ecall into n sponge rows + n KeccakRoundOperations (seq = block index, input = absorbed state) + MEMW ops mirroring the chip send-for-send; the per-permutation round replay of collect_bitwise_from_keccak is factored into push_keccak_round_bitwise and shared with the new collect_bitwise_from_keccak_sponge; KECCAK_RC multiplicities now count classic + sponge permutations. FIXED_TABLE_COUNT 10 -> 11 (with the PR #871 always-on-AIR cost caveat), VmAirs/Traces wired in matching order on both prover and verifier lists. Tests: constraint-set exactly-once/degree/folder-capture agreement (check_table), program + device interpreter lists, a full sender <-> collector multiset-equality test (chip + KECCAK_RND sends evaluated off the generated traces vs the collector, over a 3-block call plus an n = 1 call), a no-IS_HALF guard, and an e2e prove+verify of a 3-block absorb asm guest cross-checked against a tiny-keccak sponge replay.
…leanup scripts/gen_keccak_absorb_bench.sh builds a sponge-saturated guest: CALLS absorb ecalls of DATA_BLOCKS blocks each (all blocks through ONE ecall, ~7-cycle loop body), state chained across calls, output committed, count-gated via the CLI's 'KeccakAbsorb calls' line. KECCAK_SPONGE commits one row per block, so sweep points are CALLS x DATA_BLOCKS = 2^k. Smoke-tested: 4x8 build executes with 'KeccakAbsorb calls: 4' at 691 cycles. Also: cargo fmt over the touched packages and an allow(too_many_arguments) on the sponge memw_read helper (mirrors the ecsm.rs helper it copies).
The debug-checks legend stopped at ID 21, silently omitting Keccak(22) through GlobalMemory(31) and the new KeccakSponge(32); print the full range (TryFrom skips the reserved gaps).
…nd pin KECCAK_SPONGE KECCAK_RND is 1481 columns since the seq permutation key was added (carried through untouched for KECCAK_SPONGE, like timestamp); also pin the new KECCAK_SPONGE table at 690 columns in the same guard.
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.
What
A keccak sponge-absorb accelerator: new syscall
keccak_absorb_blocks(state_ptr, data_ptr, n_blocks)ata7 = u64::MAX - 3(spec ECALL-4;-3is taken by the in-flight BLAKE3 PR #903), backed by a newKECCAK_SPONGEtable with one row per absorbed 136-byte rate block. Per blockk:state[0..17] ^= block_k(lanewise LE dwords), then keccak-f[1600] via the existing, untouchedKECCAK_RNDround chip. Lanes 17..25 pass through the XOR unchanged. Padding stays guest-side: the guest hashes the final10*1-padded partial block through the classic per-permutation syscall, so the chip only ever sees whole rate blocks — no variable-length padding in-circuit.Why: the guest-glue cost argument
Today the keccak accelerator does one permutation per ecall on a 200-byte in-place state, and the guest does the sponge in software: per 136-byte rate block it loads 17 message dwords, XORs them into the state and stores back — roughly 70–100 instructions of glue. In this VM a guest cycle costs on the order of ~250–300 committed cell-equivalents (CPU row + register MEMW rows + range-check/decode satellites + their aux columns), so that glue is ~15–25k cell-equivalents per permutation — a 20–35% tax on the ~73k-cell permutation itself, paid on every keccak256 in an ethrex block (trie nodes, tx hashes). PR #847 already attacked the same glue in guest software for the recursion verifier (−40% cycles); this PR moves the absorb into the chip.
What one
KECCAK_SPONGErow costs instead: 690 main columns + ~108 aux columns (216 interactions) ≈ ~1k committed cells per block, replacing the ~15–25k of glue — the permutation's own 24KECCAK_RNDrows are unchanged. A loop of absorb calls costs ~7 guest cycles per call regardless of block count.⚠ Block-level win pending flamegraph measurement of the sponge-glue share. The per-permutation arithmetic above is not a block-level claim: the EC-campaign lesson (PR #871) is to measure the target's share of total cells before claiming a workload win. A flamegraph of a real ethrex block quantifying the sponge-glue share should gate any headline number.
Soundness: per-block keying (the swap attack)
All
npermutations of one absorb call share one CPU timestamp (the ecall is a single cycle). The Keccak bus tuple was(ts, round, state)and the round chip echoests. With two blocks at the samets, a malicious prover could hand block A's permutation output to block B's row and vice versa — every tuple still appears exactly once per side, so the bus balances, and the chain equality between rows only forces some consistent assignment of outputs to rows, not the right one.Fix (this PR):
seqelement: the classic core chip sendsseq = 0, sponge rowksendsseq = k, andKECCAK_RNDcarriesseqthrough untouched like it carriests(newSEQcolumn, 1480 → 1481 columns);KeccakSponge, BusId 32) is keyed(ts, seq)too:μ_first·SEQ = 0anchors a call atSEQ = 0, the chain sender emitsSEQ + 1, and a chain cannot wrap the field — so the permutations of one call carry distinct(ts, seq)keys on both Keccak-bus legs and the swap unbalances the bus;(n, state_ptr, block_base); the last row pinsN_LO = SEQ + 1andN_HI = 0(word-level, which closes theN = n + pmod-p alias), so a call has exactlyx12rows; exactly-one-first-row and no-fork/no-merge fall out of the Ecall token and the memory argument.The full attack walkthrough and the chain-shape argument are in the
prover/src/tables/keccak_sponge.rsmodule docs. (This is the multi-permutation-per-ecall instance of DESIGN §7 item 10 from the BLAKE3 work.)⚠ Wire-format break
The
seqelement changes the Keccak bus tuple for all proofs (the classic chip sends a constant 0), andKECCAK_RNDgains a column: old proofs do not verify against this branch and vice versa.FIXED_TABLE_COUNTgoes 10 → 11.Always-on-AIR caveat / ABBA needed
KECCAK_SPONGEis an always-on table: every proof pays a near-empty AIR (min 4 rows × 690 main cols + ~108 aux cols) even when the workload never absorbs. This is the PR #871 regression shape (three near-empty always-on AIRs cost +25% prove time / +25% peak heap there). A/bench-abbarun on a sponge-free workload (fib / ethrex) must gate the merge, plus a run on a keccak-heavy workload to measure the win.scripts/gen_keccak_absorb_bench.sh CALLS DATA_BLOCKS out.elfbuilds a sponge-saturated guest (each ecall absorbs DATA_BLOCKS blocks; CALLS-iteration loop), count-gated via the CLI's newKeccakAbsorb callsline.Design deviations from the classic keccak chip
base_lo + offsetlinear elements; the executor guarantees(base % 2^32) + last_offset < 2^32for both regions (same guard as the ECSM operands). Fail-closed: a drifted base only produces Memw/Memory tokens with no matching PAGE/REGISTER cell. Rationale: the keccak apparatus costs 100 pointer columns + 100 IS_HALF sends per row — fine at one row per call, but it would roughly double this chip's per-block cost.state_ptrstays byte-decomposed (AreBytes +&7alignment lookup) so alignment is enforced in-chip.ts, the last row 25 write-only lane writes atts + 1(16-element Memw tuples; the MEMW table materializesold). A single combined read+write op per lane (keccak.rs style) is impossible here because no single row holds both the pre-call and the final state. The executor rejects state/data overlap so every(address, timestamp)pair stays unique.n_blocksis recovered from the x12 register state in the trace builder (the CPU log's two free slots carry the state/data addresses), like the ECSM operand addresses.Executor guards
Both pointers 8-aligned;
n_blocks > 0; the LAST byte of each region checked against u64 overflow (checked_mul/checked_add) and low-limb overflow; state/data regions disjoint (an overlap would put two MEMW ops on one(address, timestamp)pair, which the memory argument cannot order — same rationale asEcsmOperandOverlap).Tests
tiny_keccak(n = 1, 2, 3, 5, 8, 13, seeded pseudo-random state+data), a chained XOR+keccak_f1600replay, one rejection test per guard incl. both sides of the low-limb boundary — 12/12;check_table), program + device interpreter differentials, and a full sender ↔ collector multiset equality (chip + KECCAK_RND ByteAlu/AreBytes sends evaluated off the generated traces vscollect_bitwise_from_keccak_sponge, over a 3-block call plus an n = 1 call) + a no-IS_HALF guard;test_prove_elfs_keccak_absorb— asm guest absorbs 3 blocks in one ecall, committed state cross-checked against a tiny-keccak sponge replay, then full prove + verify;prove_elfs_testssuite green (102 passed);make lintclippy passes (×4 feature combos) clean.Not in this PR
constraint_program_device_tests; box validation pending).