Skip to content

perf(crypto): wire keccak_permute ecall into merkle/transcript/grinding - #802

Merged
MauroToscano merged 7 commits into
mainfrom
perf/wire_keccak_recursion
Jul 14, 2026
Merged

MauroToscano merged 7 commits into
mainfrom
perf/wire_keccak_recursion

Conversation

@Oppen

@Oppen Oppen commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Route the STARK verifier's Keccak-256 usage (Merkle tree backends, Fiat-Shamir transcript, FRI grinding) through the keccak_permute precompile on the riscv64 guest, via a PlatformKeccak256 digest wrapper around lambda_vm_syscalls::keccak::Keccak256; falls back to software sha3 on host.
  • Excludes the riscv-only syscalls crate from the root workspace (exclude = ["syscalls"]): making it reachable from crypto/crypto via a target-gated path dependency caused Cargo to auto-adopt it as an implicit workspace member, so cargo test tried to run its bare-metal unit tests on host and aborted.
  • Adds an end-to-end guest test (keccak_precompile) exercising the ecall-backed sponge against known Keccak-256 vectors (empty, rate-1, exactly-rate, multi-block), covering the padding edge cases.

Cycle comparison (recursion guest, blowup8 preset)

before after speedup
single query 5,234,002,718 1,722,846,582 3.04x
multi query 22,494,934,472 7,687,016,693 2.93x

Test plan

  • make test-profile-recursion-single / -multi before and after (cycles above)
  • test_recursion_execute_1query — in-VM verify accepts with the ecall-backed hasher
  • new test_keccak_precompile guest test — known-answer vectors incl. padding edge cases
  • make lint
  • make test — all pass except math-cuda CUDA tests (expected: no GPU on this machine)

Route the STARK verifier's Keccak-256 usage (Merkle tree backends,
Fiat-Shamir transcript, FRI grinding) through the keccak_permute
precompile on the riscv64 guest via a new PlatformKeccak256 digest
wrapper, falling back to software sha3 on host. The guest previously
used sha3's software permutation exclusively, making it the dominant
verifier cost.

Recursion guest cycles (blowup8 preset):
- single query:  5,234,002,718 -> 1,722,846,582  (3.04x)
- multi query:  22,494,934,472 -> 7,687,016,693  (2.93x)

Also excludes the riscv-only `syscalls` crate from the root workspace:
making it reachable from crypto/crypto (a workspace member) via a
target-gated path dependency caused Cargo to auto-adopt it as an
implicit member, so `cargo test` tried to run its bare-metal unit
tests on host and aborted.

Adds an end-to-end guest test (keccak_precompile) exercising the
ecall-backed sponge against known Keccak-256 vectors, covering the
padding edge cases (empty, rate-1, exactly-rate, multi-block input).
@Oppen

Oppen commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@github-actions

Copy link
Copy Markdown

Codex Code Review

No actionable issues found in the PR diff. Static review only; builds and tests were not run per instructions.

@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review — perf(crypto): wire keccak_permute ecall into merkle/transcript/grinding

Reviewed the full diff against the surrounding code. No critical/high issues found. The change is well-scoped and the key correctness property is properly guarded.

Correctness (the property that matters): the critical requirement is that the guest (riscv, precompile-backed) and host (software sha3) produce IDENTICAL Keccak-256 output — otherwise a proof built one way would not verify the other. This holds:

  • PlatformKeccak256 correctly bridges the syscall sponge to the digest 0.10 traits (Update/FixedOutput/Reset/FixedOutputReset/HashMarker/OutputSizeUser=U32), so the Digest blanket impl applies and it is a true drop-in wherever D: Digest was expected (merkle backends, transcript, grinding).
  • finalize_into_reset correctly clones the sponge before the consuming finalize, then resets — matching sha3 finalize_reset semantics used in DefaultTranscript::sample(). The added derive(Clone) on the syscall Keccak256 is what makes this (and state()`s hasher.clone()) sound.
  • The new keccak_precompile guest test exercises the real padding edge cases (empty, rate-1 = 135B, exactly-rate = 136B, multi-block) with known-answer vectors, and the empty/abc vectors match canonical Keccak-256. Good coverage for the sponge boundary behavior.

Minor observations (non-blocking):

  • sha3 remains a normal (non-target-gated) dependency of crypto, so it is still compiled for the riscv guest even though the precompile path replaces its use on the hot path. Not a bug — just no longer strictly needed there.
  • syscalls/src/random.rs: the __getrandom_v03_custom signature change is a pure rustfmt reflow, unrelated to this PR purpose. Harmless.
  • The exclude = ["syscalls"] workspace change is clearly documented with its rationale; it only affects host-side cargo test, and the riscv-only crate is still reachable via its target-gated / guest-program path deps.

LGTM.

@github-actions

Copy link
Copy Markdown

AI Review

PR #802 · 17 changed files

⚠️ All 5 reviewers failed (see Reviewer Lanes below) — this is NOT a clean result; the review did not run.

Findings

No non-rejected structured findings were reported.

Reviewer Lanes

Lane Model Prompt Status Findings
glm openrouter/z-ai/glm-5.2 general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0
kimi openrouter/moonshotai/kimi-k2.7-code general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0
minimax minimax/MiniMax-M3 general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0
moonmath zro/minimax-m3 general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0
nemotron openrouter/nvidia/nemotron-3-ultra-550b-a55b general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0

Native Codex and Claude reviews run separately and post their own comments. They are not included in this structured provenance report.

Raw lane outputs, candidates, final issues, and model metrics are uploaded as workflow artifacts.

@diegokingston diegokingston left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Statement is using still sha3

…t sponge pattern

elf_digest still used sha3::Keccak256 directly, so the recursion guest hashed
the entire inner ELF in software instead of through the keccak_permute ecall
like every other hash site. Also add a KAT covering DefaultTranscript's real
call pattern (small non-rate-aligned updates interleaved with finalize_reset),
which the existing one-shot KAT never exercised, and drop a redundant
clone+scratch-buffer in PlatformKeccak256's finalize_into/finalize_into_reset.
@MauroToscano

Copy link
Copy Markdown
Contributor

/bench-verify

@github-actions

Copy link
Copy Markdown

⏳ Benchmark started on the bench server. The verifier bench takes ~5 min; the recursion-guest cycle comparison then adds guest builds — a few minutes when cached, up to ~1h on a cold run. The bench server is occupied until it finishes.

@MauroToscano

Copy link
Copy Markdown
Contributor

/bench-verify

@github-actions

github-actions Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

Verifier benchmark — 934ffc1031 vs main (20 pairs)

=== Verify ABBA result ===

Metric main PR Δ
Verify time 3.892s 3.893s +0.02% ⚪
Proof size 230.99 MiB 230.99 MiB +0.00% ⚪
  pairs: 20   mean A (PR): 3.893s   mean B (main): 3.892s
  [parametric] paired-t   mean +0.02%   sd 0.61%   se 0.14%
               95% CI: [-0.26%, +0.30%]   (t df=19 = 2.093)
  [robust]     median +0.14%   Wilcoxon W+=110 W-=100  p(exact)=0.8695  (z=+0.17)

  run-to-run jitter:    A CV 0.44%   B CV 0.37%        (lower = steadier)
  within-session drift: -0.31% over the run, 1st->2nd half -0.19%

⚪ INCONCLUSIVE — effect not separable from 0 at n=20 (point estimate ~+0.14%). Add pairs to resolve.

Drift-free interleaved A/B/B/A measurement. - = PR faster. Trust the verdict when paired-t and Wilcoxon agree.


Recursion guest cycles (main vs PR)

=== Recursion-guest cycle comparison — single query (blowup=2, 1 query) — deterministic to ~±100k cycles ===
REF_B (baseline) origin/main 8e504da guest=recursion-min.elf
REF_A (PR) 934ffc1 934ffc1 guest=recursion-min.elf

Metric REF_B (baseline) REF_A (PR) Δ (A-B)
Guest cycles 126.0M 99.1M -26.9M (-21.35%)
Keccak calls 0 3885 +3885

note: cycles reproduce to ~±100k (build codegen + proof nondeterminism); treat sub-100k deltas as noise, not signal.

raw (exact integer counts)
ref_b_sha=8e504da5f4bc9ef77342221d7eeca2049fe7cc5a ref_b_elf=recursion-min.elf ref_b_cycles=125968543 ref_b_keccak=0 ref_b_execute_wall_s=3
ref_a_sha=934ffc1031165e05e4a37bb0353cf4ac28f50225 ref_a_elf=recursion-min.elf ref_a_cycles=99079876 ref_a_keccak=3885 ref_a_execute_wall_s=2
delta_cycles=-26888667 delta_keccak=3885

@github-actions

Copy link
Copy Markdown

⏳ Benchmark started on the bench server. The verifier bench takes ~5 min; the recursion-guest cycle comparison then adds guest builds — a few minutes when cached, up to ~1h on a cold run. The bench server is occupied until it finishes.

program_id_from_digest still used sha3::Keccak256 (software) instead
of PlatformKeccak256, so the in-guest program-id fold bypassed the
keccak_permute accelerator. statement.rs also imported the Digest
trait via sha3 despite already using PlatformKeccak256, masking that
the guest path never touches sha3 directly.

Drop the sha3 dependency from prover in favor of digest, since
PlatformKeccak256 supplies the concrete hasher on all targets.

@MauroToscano MauroToscano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should patch sha3 crate in a fork to use sha3, then only swap the version of sha3 on the guest, not do this

…+ cleanups (#813)

* perf(crypto): route recursion program_id fold through keccak_permute + cleanups

Follow-ups to #802's keccak-syscall wiring.

- prover/recursion.rs: program_id_from_digest ran the in-guest program_id
  fold on software sha3::Keccak256 -- the one verifier-guest keccak still
  bypassing the keccak_permute ecall. Route it through PlatformKeccak256, as
  statement::elf_digest already is. Correctness-neutral (software and syscall
  Keccak-256 are identical, so check_attestation still matches), but it removes
  the last software Keccak-f from the recursion guest.
- Unify prover on digest::Digest (statement.rs used sha3::Digest, while
  grinding.rs and default_transcript.rs already use digest::Digest). No concrete
  sha3 type remains in prover, so swap its sha3 dependency for digest.
- platform_keccak.rs: replace the two hot-path `.try_into().unwrap()`s in
  finalize_into / finalize_into_reset with an infallible fixed-size copy.
- Cargo.toml: correct the exclude=["syscalls"] rationale -- the crate has no
  unit tests; host workspace builds fail because it defines a #[global_allocator]
  plus #[unsafe(no_mangle)] entrypoints/syscalls (and riscv asm!) that only link
  for riscv64im-lambda-vm-elf.

* align prover digest dep with base (drop needless default-features=false)

---------

Co-authored-by: Mario Rugiero <mrugiero@gmail.com>
@MauroToscano

Copy link
Copy Markdown
Contributor

/bench-verify

We should patch sha3 crate in a fork to use sha3, then only swap the version of sha3 on the guest, not do this

After further discussion we decided to keep it like this, lgtm

@github-actions

Copy link
Copy Markdown

⏳ Benchmark started on the bench server. The verifier bench takes ~5 min; the recursion-guest cycle comparison then adds guest builds — a few minutes when cached, up to ~1h on a cold run. The bench server is occupied until it finishes.

@MauroToscano

Copy link
Copy Markdown
Contributor

/bench

@MauroToscano
MauroToscano dismissed diegokingston’s stale review July 14, 2026 20:12

The fix has been applied

@github-actions

Copy link
Copy Markdown

Benchmark — ethrex 20 transfers (median of 3)

Table parallelism: auto (cores / 3)

Metric main PR Δ
Peak heap 73267 MB 72268 MB -999 MB (-1.4%) ⚪
Prove time 38.430s 38.278s -0.152s (-0.4%) ⚪

✅ No significant change.

🔬 Looks like a small speedup (-0.4%) — below what 3 runs can confirm. Comment /bench-abba to run the drift-free ABBA tiebreaker (paired-t CI + exact Wilcoxon). Note: it occupies the bench server for ~30–40 min.
Optional pair count: /bench-abba 32 (20 resolves ~1%, 32 for ~0.6%).

✅ Low variance (time: 1.5%, heap: 2.2%)

Commit: 6625390 · Baseline: cached · Runner: self-hosted bench

@MauroToscano
MauroToscano enabled auto-merge July 14, 2026 20:14
@MauroToscano
MauroToscano added this pull request to the merge queue Jul 14, 2026
Merged via the queue into main with commit b04c808 Jul 14, 2026
15 checks passed
@MauroToscano
MauroToscano deleted the perf/wire_keccak_recursion branch July 14, 2026 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants