Skip to content

Experimental/optimizations - #870

Draft
jotabulacios wants to merge 15 commits into
mainfrom
experimental/optimizations
Draft

Experimental/optimizations#870
jotabulacios wants to merge 15 commits into
mainfrom
experimental/optimizations

Conversation

@jotabulacios

@jotabulacios jotabulacios commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

This branch bundles the guest-side performance work that was measured together on
ethrex blocks. It is not meant to be merged as one PR — it mixes four unrelated
changes with very different review surfaces, and it is being split into separate PRs
against main. It stays open as the reference for the numbers and as the base for the
branches that hang off it.

What is in it:

  • Hint ecall + HINT table (7 commits). A syscall where the executor hands the guest
    a value that is expensive to compute and cheap to check — modular inverse, square root
    — and the guest verifies it in ordinary constrained instructions. The single call site
    is ecrecover's inversions and square root. This is the largest single win here and the
    one with an open decision: the table deliberately does not constrain the hinted value,
    so soundness rests on the guest's verify rather than on the AIR.
  • Guest keccak through the accelerator (2 commits). Vendors tiny-keccak with one
    change — keccakf calls the keccak_permute syscall — so ethrex's trie and RLP
    hashing reaches the accelerator instead of running the permutation in software.
  • Guest allocator (2 commits). dlmalloc as the default, plus an opt-in bump
    allocator behind a feature. Superseded by Perf/dlmalloc guest allocator #869, which splits the same work better.
  • Zero-copy private input (1 commit). The guest reads the input in place instead of
    copying it into a Vec.

The last five commits are correctness fixes to the hint ecall: big-endian ABI, operand
address validation, and verifying hints by difference instead of comparing bytes.

@jotabulacios

Copy link
Copy Markdown
Collaborator Author

/bench

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

Benchmark — ethrex 20 transfers (median of 3)

Table parallelism: auto (cores / 3)

Metric main PR Δ
Peak heap 50856 MB 28849 MB -22007 MB (-43.3%) 🟢
Prove time 25.309s 14.734s -10.575s (-41.8%) 🟢

🎉 Improvement detected — heap or time decreased by more than 5%.

⚠️ Prove time spread: 5.3% (15.336s / 14.734s / 14.559s)
⚠️ Heap spread: 5.4% (28025 MB / 28849 MB / 29589 MB)
Consider re-running /bench

Commit: 4b1e8c9 · Baseline: cached · Runner: self-hosted bench

@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.

Nice work — the design is sound (the HINT table's rationale for being sound is well documented, executor and prover share compute_hint, the allocator story is well thought out, and the hint guests cover multi-row + read-back chaining). A set of concrete improvement proposals, roughly ordered:

1. Make the hint ABI big-endian (drop ~8 byte-reversal loops). Every hint call currently does 4 per-byte reversals: 2 guest-side (each of scalar_inv/decompress_r/field_inv converts BE→LE on input and LE→BE on output = 6 loops) + 2 host-side in compute_hint. k256 is BE-native (to_bytes/from_bytes), so a BE ABI deletes all of them: guests pass x.to_bytes() and read the result with from_bytes directly; the host stops reversing too. Saves ~1-2k cycles per ecrecover (3 hints) and simplifies the docs. Trade-off: diverges from the ECSM ecall's LE convention — worth it since the hint's consumers are all k256-native, and an LE variant can always be added later if needed. Touch points: syscalls::hint doc, compute_hint, the three consumers, and the test guests' expectations if byte order is asserted.

2. Missing address validation in the Hint dispatch. The handler does load_u256_le(in_addr) / store_u256_le(out_addr) with no alignment/overflow checks, unlike ECSM (ecsm_addr_ok) and Keccak (8-alignment + range). The HINT table sends four doublewords at out_addr +0/8/16/24; an unaligned or low-limb-overflowing out_addr breaks the table's assumptions. Suggest the same guard (out_addr % 8 == 0, (addr % 2^32) + max_offset < 2^32 for both addresses) plus a negative test guest — the current hint_min/hint_multi don't exercise rejection.

3. RecoveryHint vs hint-ecall: pick one. There's parallel unmerged work (preloaded hints via the private input, with a [len][rkyv][n×97B hints] fixture frame) that this PR's on-demand ecall supersedes. The on-demand design is strictly simpler (no fixture format change, no host precompute plumbing). Suggest documenting the decision in the PR and dropping the preloaded path + fixture tail + the fixture-tooling changes; also note the guest's rkyv::from_bytes will reject framed fixtures, so the two formats can't coexist.

4. Use ct_eq instead of to_bytes comparisons. field_inv ((*x * inv).to_bytes() == ONE.to_bytes()) and decompress_r (y2.to_bytes() != rhs.to_bytes()) each pay 2 full normalizations + a byte compare; scalar_inv already uses ct_eq. A few hundred cycles per call, free.

5. Stale import. crypto/ethrex-crypto/src/lib.rs:19 use ethrex_crypto::keccak::keccak_hash is unused on riscv64 builds (warning); cfg-gate or delete.

6. collect_hint_ops reads the hint input byte-by-byte (32 map reads) — switch to 4 × read_bytes(addr, 8) (symmetry with the write loop, fewer map ops).

Process/minor:

  • "BENCH ONLY" markers are everywhere; if hint-then-verify is here to stay (the numbers say yes), decide before merging whether to keep them or document the pattern as permanent.
  • Consider documenting an allocation convention for future hint ids (e.g. per-curve ranges) in the enum docs.
  • The red "Run benchmarks for modified programs" check is a workflow script bug (cmp misuse + missing head_programs/*.elf because the branch adds new guests without a base counterpart) — not a code problem; everything else is green.

Verified as good (no action): the table's mu gating/padding, not modeling the input read (the documented soundness argument holds), the shared compute_hint between executor and prover, the sqrt parity + from_encoded_point backstop, the tiny-keccak vendoring approach, and the dlmalloc-backed-by-bump design.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

Benchmark Results for modified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
head ecsm 2.1 ± 0.2 2.0 2.5 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head hashmap 70.9 ± 1.3 69.4 73.0 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head keccak 87.3 ± 1.6 85.9 91.2 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head syscall_commit 66.1 ± 1.2 64.8 68.1 1.00

@jotabulacios

Copy link
Copy Markdown
Collaborator Author

/bench

@diegokingston

Copy link
Copy Markdown
Collaborator

Follow-up on my earlier review — item 4 (use ct_eq instead of to_bytes comparisons) was wrong; do NOT apply it. I implemented it and measured the result: k256's FieldElement::ct_eq compares the internal representations limb-wise (self.0.ct_eq(&other.0), arithmetic/field.rs:341) with no normalization, so a mul-chain product that is 1 (mod p) but held in a lazy, non-canonical representation compares unequal to FieldElement::ONE. In practice: field_inv's verify rejected a mathematically-correct hinted inverse (verified mod p off-VM), lincomb2 returned None, and ecrecover silently fell back to the software lincomb — 1-tx went from ~946k to ~2.7M cycles while still passing correctness checks. The canonical to_bytes() == to_bytes() compare in the current code is the correct verify, not just a slower one — keep it. (Scalar's ct_eq happens to work, and y2.ct_eq(&rhs) happened to pass, but both are representation-luck, not a guarantee.)

The rest of the review items are implemented on branch perf/hint-ecall-review-fixes (1 commit): big-endian hint ABI (deletes the ~8 byte-reversal loops per call — 1-tx 945,875 → 916,372 cycles with byte-identical output), hint-dispatch address validation (alignment + low-limb overflow, mirroring the ECSM guard), batched input reads in collect_hint_ops, and the stale keccak_hash import gated. Verified with 14/14 ethrex-crypto tests, 3/3 hint prove tests, and the three ethrex fixtures (640,133 / 916,372 / 1,876,730 cycles).

@diegokingston

Copy link
Copy Markdown
Collaborator

/bench

@jotabulacios jotabulacios mentioned this pull request Jul 28, 2026
@diegokingston

Copy link
Copy Markdown
Collaborator

/bench

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.

2 participants