Feat/hint ecall - #876
Conversation
|
/bench |
Benchmark — ethrex 20 transfers (median of 3)Table parallelism: auto (cores / 3)
Commit: 64a22ff · Baseline: cached · Runner: self-hosted bench |
|
Benchmark Results for modified programs 🚀
|
|
/ai-review |
Codex Code Review
|
Review — Hint ecallThe mechanics are well built and the docs are unusually thorough about why each piece exists. The wins are real and the plumbing (Ecall receive + 4 MEMW write sends, CriticalThe HINT row's The This breaks the PR's stated soundness anchor. The "Soundness surface" section argues the guest's HighNothing prevents this from reaching a production proof. ( Medium
Low
Checked, no issue foundGuest-side verifies are correct: the |
AI ReviewPR #876 · 38 changed files
Findings
Status column reflects the verdict from the verifier: deepseek-verifier (openrouter/deepseek/deepseek-v4-pro). AI-002: executor/Cargo.toml k256 dep comment "BENCH ONLY" is inaccurate — it is used in production proving
Claim The dependency comment says k256 is for host-side hint computation and is 'BENCH ONLY', but Evidence
Suggested fix Drop the AI-007: Unused import in new HINT table
Claim
Evidence Line 60 of Suggested fix Remove AI-008: Disk-spill sizing omits HINT table rows
Claim
Evidence
Suggested fix Add a Reviewer Lanes
Verification Lanes
Native Codex and Claude reviews run separately and post their own comments. They are not included in this structured provenance report. Discarded candidates (4) — rejected by the verifier
Raw lane outputs, candidates, final issues, and model metrics are uploaded as workflow artifacts. |
|
/bench |
|
/bench |
|
Benchmark Results for unmodified programs 🚀
|
|
/ai-review |
Codex Code Review
|
|
/bench |
What
Adds a
Hintecall (u64::MAX - 20): the executor computes a value that is expensive in theguest and cheap to check — secp256k1 field inverse, scalar inverse, field square root — writes
it into guest memory, and the guest verifies it with ordinary constrained instructions and
recomputes it in software if the check fails. The call site is ecrecover's inversions and
root in
crypto/ethrex-crypto/src/lib.rs. AHINTtable puts the ecall's direct memory writesinto the memory argument.
Why
The flamegraph of a hinted ethrex block put field and scalar inversion plus square root at
most of the guest's cycles: each software inversion is thousands of instructions, while
checking one is a single multiplication. The ecall moves the computation to the host and keeps
the check in-circuit.
Details
executor/src/vm/instruction/execution.rs):a0selects the operation(
HINT_FIELD_INV,HINT_SCALAR_INV,HINT_FIELD_SQRT),a1anda2point at the input andoutput buffers, both 32-byte big-endian (k256's own serialization).
compute_hintuses the samek256 arithmetic the guest verifies against; a numeric failure (non-canonical input, no
inverse/sqrt) returns zeros, which the guest treats as a failed check and recomputes in
software. An unrecognized selector is rejected up front (
HintUnknownSelector) rather thanproducing a silent zero.
receives the ecall on the
Ecallbus, readsx12through the memory argument to bind theoutput address, sends the four 8-byte
MEMWwrites of the output atout_addr+0/8/16/24, andrange-checks the 32 output cells. The ecall writes guest memory directly, bypassing the
load/store decode, so without those sends the output's initial→final memory chain is
unexplained and the memory argument does not balance. The input read is deliberately not
modelled: a read leaves the value unchanged and the guest supplied the input with ordinary
stores.
syscalls/src/syscalls.rs) and the call site incrypto/ethrex-crypto/src/lib.rs:scalar_inv,decompress_rand the field inverse verify thehinted value, and fall back to a software computation if it does not check out (see Soundness
surface). The output buffer is 8-byte aligned so its writes take the aligned memory path
(
MEMW_A) rather than the general one.hint_min(one call, commits the result) andhint_multi.Compatibility
Like the ECSM accelerator (#657), this adds an always-on table, so
FIXED_TABLE_COUNTgoes10 → 11 and the recursion verifier checks 11 tables. Prover and verifier are built from the same
code and move in lockstep, so the only operational cost is regenerating existing proofs and the
pinned recursion ELFs.
Impact (ethrex 20-tx block, vm-benchmarks-1, 5 interleaved rounds, all verify ✅)
Comparison is with-hints vs without-hints: baseline =
5fd961a0, the point onmainthis branchwas cut from (the same code before the hint ecall), proven by its 10-table prover; treatment =
this branch, proven by its 11-table prover. Medians:
ECSM calls unchanged at 80 and keccak permutations at 411 on both sides: no cryptographic work
is skipped, its inversions and roots just stop costing thousands of guest cycles each. Variance
was ~1–1.5% (prove-time CV) with clean separation.
Soundness surface
The table constrains nothing about which value was hinted — that is deliberate and it is
what makes the ecall cheap. Soundness comes from two places: what the AIR constrains (where the
value lands, that it is 32 bytes, and that the multiplicity is boolean) and what the guest
enforces (that the value is correct, or else recomputed in software).
In the AIR:
out_addris bound tox12by aMEMWregister read at the ecall timestamp. The four writeaddresses come from a trace column, so without that binding the witness picks the destination
and the table is an arbitrary-memory-write gadget — something the in-guest verify cannot
contain, since the adversary just targets a different buffer.
table that writes fresh values into memory (STORE, KECCAK, ECSM, PAGE) checks its own cells;
otherwise the witness can keep the linear combination consistent while encoding field elements
outside
[0, 256)where loads and the ALU expect bytes.mu, the multiplicity gating every interaction, is bit-constrained (mu·(1−mu) = 0), matchingevery other multiplicity-column table (ECSM/ECDAS/COMMIT/STORE/MEMW_R). The
Ecallbus alonedoes not give this: its tuple carries a free timestamp column, so LogUp pins only the sum of
muover the rows sharing a tuple — a1/2 + 1/2split would satisfy it. The bit constraintmakes the argument local instead of resting on how MEMW handles its own multiplicities
downstream.
In the guest (verify-then-fallback). A hinted value is untrusted and prover-chosen, so it
may only ever save work — never change the answer. That rules out two failure modes, not one:
x·inv == 1for the inverses,y² == x³+7plus parity selection for the root. All three verify by difference rather thanct_eq, because k256 compares magnitude and normalization tags too — a naive comparison nevermatches.
"the value is invalid"; the guest recomputes in software.
scalar_invfalls back toinvert_vartime(its caller guaranteesr ≠ 0, so a failed check means the host lied);decompress_rfalls back toAffinePoint::decompress(a genuine non-residue must still yieldNone). Without this, a prover feeding garbage could turn a valid signature into a recoveryfailure — ECRECOVER returns empty — making honest and attacked executions both provable with
different state roots. An unknown selector is a hard ecall error for the same reason.
constraint. The consistency test in this PR catches an inconsistent trace, which is the
failure mode of a buggy trace builder, not of an adversary.
The guarantee is therefore a property of the program, not of the machine, and it extends to
every future call site. Constraining the value in the AIR instead (
out·in == 1) is possibleand costs rows; the guest already performs exactly that multiplication.
The ecall validates that both 32-byte operands stay inside their low 32-bit address limb,
since the tables send addresses as
[lo32, hi32]with the per-write offset added tolo32alone.