Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
cfdbb69
feat(hints): replace the hint ecall with a private-input hint arena
diegokingston Aug 19, 2026
ee8ee23
Merge branch 'chore/bump-ethrex-rev-finish' into feat/hint-arena
diegokingston Aug 19, 2026
7e519fd
fix(guests): bump ecrecover_hints' ethrex-crypto pin to 4f658c2b
diegokingston Aug 19, 2026
cd1d611
perf(hints): auto-record the hint arena in prove/count entry points
diegokingston Aug 19, 2026
5781ff5
chore(hints): satisfy the lint gate (fmt + clippy)
diegokingston Aug 19, 2026
521b84c
Merge remote-tracking branch 'origin/chore/bump-ethrex-rev' into feat…
jotabulacios Aug 26, 2026
3ad5f77
Update gpu_force_downgrade to the three-argument prove_with_inputs
jotabulacios Aug 26, 2026
3b77a6f
Unify ecrecover_hints' ethrex pin at 797df554
jotabulacios Aug 26, 2026
3db5708
Update the ethrex-tests call site to the three-argument Executor::new
jotabulacios Aug 27, 2026
4dcab54
Answer hint requests during the run instead of requiring a recording …
jotabulacios Aug 25, 2026
8dbfc7c
Add a driver that measures what the hint mechanism costs on real ethr…
jotabulacios Aug 25, 2026
37f4d38
Answer hint requests during a continuation too, dropping its pre-pass.
jotabulacios Aug 25, 2026
d1320f1
Drop the doc reference to the deleted resolve_hints from prove_with_i…
jotabulacios Aug 25, 2026
4ea8f08
Hook the hint request log on the guest's store instruction, not in Me…
jotabulacios Aug 25, 2026
1a61b19
Fix the gpu_force_downgrade comment for one-pass
jotabulacios Aug 27, 2026
85b0cf8
Fix the hint request order in ecrecover_hints doc
jotabulacios Aug 27, 2026
44c68ac
Fix the stale hint comment in run_with_flamegraph
jotabulacios Aug 27, 2026
7f1e4d9
Drop the two-pass hint API the guest never used
jotabulacios Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crypto/ethrex-crypto/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 43 additions & 42 deletions crypto/ethrex-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,38 +64,37 @@ impl Crypto for LambdaVmEcsmCrypto {

// ── ECDSA secp256k1 recovery via the ECSM precompile ────────────────────────

/// Obtain a 32-byte big-endian hint for `x_be` via the executor `hint` ecall
/// (the host computes the modular inverse / sqrt; the value is provable via the
/// prover's HINT table). The result is UNTRUSTED — the ecall adds no correctness
/// constraint, so every caller MUST verify it in-guest (`x·inv == 1`, `y² == x³+7`)
/// AND recompute in software on any verification failure. The hint is only ever
/// allowed to save work, never to change the answer: because the prover chooses the
/// bytes, an unverified-or-rejected-outright hint would let it steer a caller's
/// accept/reject outcome (e.g. force a valid signature to look invalid). See
/// [`scalar_inv`] / [`decompress_r`] for the fallback that closes that hole.
/// Fetch a hint for `(hint_id, x_be)` from the private-input hint arena
/// (positional — one slot per request). The request is published to the guest's
/// hint request log and the executor answers it in the same run by seeding the
/// arena slot this call then reads. When nobody answers, the slot reads back as
/// zeros, which fail the caller's in-guest verify and trigger its software
/// fallback.
///
/// The hint is UNTRUSTED — the prover chooses the arena bytes, so every caller
/// MUST verify it in-guest (`x·inv == 1`, `y² == x³+7`) AND recompute in
/// software on any verification failure. The hint is only ever allowed to save
/// work, never to change the answer: an unverified-or-rejected-outright hint
/// would let the prover steer a caller's accept/reject outcome (e.g. force a
/// valid signature to look invalid). See [`scalar_inv`] / [`decompress_r`] for
/// the fallback that closes that hole.
#[cfg(target_arch = "riscv64")]
fn get_hint(hint_id: usize, x_be: &[u8; 32]) -> [u8; 32] {
// 8-byte-aligned output buffer so the HINT table's four 8-byte writes land on the
// aligned memory path (MEMW_A) instead of the general MEMW path. An `[u8; 32]` on
// the stack is only 1-aligned, which forces the four writes onto the unaligned
// path and inflates the trace.
#[repr(C, align(8))]
struct Aligned32([u8; 32]);
let mut out = Aligned32([0u8; 32]);
lambda_vm_syscalls::syscalls::hint(hint_id, &mut out.0, x_be);
out.0
lambda_vm_syscalls::syscalls::request_hint(hint_id, x_be)
}

/// Scalar-field inverse `x⁻¹ mod n`.
///
/// On riscv64 the inverse is first requested from the untrusted `hint` ecall and
/// verified in-guest (`x·inv == 1`); **on any verification failure it is recomputed
/// in software.** `x⁻¹` exists for every `x` this is called with — the only caller,
/// `ecsm_ecrecover`, guarantees `r ≠ 0` before calling — so a failed verify can only
/// mean the host lied, and the software value is authoritative. This is what keeps
/// the result independent of the prover-chosen hint: a bad hint makes the guest do
/// more work, it can never change the answer, so it cannot turn a valid signature
/// into a recovery failure. Off-target (host) it inverts in software directly.
/// On riscv64 the inverse is read from the untrusted private-input **hint
/// arena** (positional — one slot per request) and verified in-guest
/// (`x·inv == 1`); **on any verification failure or an exhausted arena it is
/// recomputed in software.** `x⁻¹` exists for every `x` this is called with —
/// the only caller, `ecsm_ecrecover`, guarantees `r ≠ 0` before calling — so a
/// failed verify can only mean the host lied, and the software value is
/// authoritative. This is what keeps the result independent of the
/// prover-chosen hint: a bad hint makes the guest do more work, it can never
/// change the answer, so it cannot turn a valid signature into a recovery
/// failure. Off-target (host) it inverts in software directly.
fn scalar_inv(x: &Scalar) -> Option<Scalar> {
#[cfg(target_arch = "riscv64")]
{
Expand Down Expand Up @@ -133,16 +132,16 @@ where

/// Decompress R from its x-coordinate + parity.
///
/// On riscv64 the square root `y = sqrt(x³+7)` is first requested from the untrusted
/// `hint` ecall and verified in-guest (`y² == x³+7`), with parity selection; **on any
/// verification failure the point is recomputed with the software
/// `AffinePoint::decompress`.** Unlike the inverse, a failure here is *not*
/// necessarily a lying host: a genuine non-residue (an invalid signature) has no
/// root and must legitimately yield `None`. So the fallback is the authoritative
/// software decompress, which returns `Some` for a residue and `None` for a
/// non-residue regardless of the prover-chosen hint — the hint can only save work,
/// never steer the accept/reject outcome. Off-target it uses the software
/// decompress directly.
/// On riscv64 the square root `y = sqrt(x³+7)` is read from the untrusted
/// private-input **hint arena** and verified in-guest (`y² == x³+7`), with
/// parity selection; **on any verification failure or an exhausted arena the
/// point is recomputed with the software `AffinePoint::decompress`.** Unlike
/// the inverse, a failure here is *not* necessarily a lying host: a genuine
/// non-residue (an invalid signature) has no root and must legitimately yield
/// `None`. So the fallback is the authoritative software decompress, which
/// returns `Some` for a residue and `None` for a non-residue regardless of the
/// prover-chosen hint — the hint can only save work, never steer the
/// accept/reject outcome. Off-target it uses the software decompress directly.
fn decompress_r(r_bytes: &FieldBytes, y_is_odd: bool) -> Option<AffinePoint> {
#[cfg(target_arch = "riscv64")]
{
Expand Down Expand Up @@ -337,12 +336,14 @@ fn ecsm_oracle(x: &FieldElement, k: &Scalar) -> Option<FieldElement> {

/// Base-field inverse `x⁻¹ mod p`.
///
/// On riscv64 the inverse is first requested from the untrusted `hint` ecall and
/// verified in-guest (`x·inv == 1`); **on any verification failure it is recomputed
/// in software.** A bad hint can only cost the guest extra work, never change the
/// answer — it cannot steer a caller's accept/reject outcome. Off-target it inverts
/// in software directly. Returns `None` only for a genuinely non-invertible input
/// (`x = 0`), which the callers' degeneracy guards already exclude.
/// On riscv64 the inverse is read from the untrusted private-input **hint
/// arena** (positional — one slot per request) and verified in-guest
/// (`x·inv == 1`); **on any verification failure or an exhausted arena it is
/// recomputed in software.** A bad or missing hint can only cost the guest
/// extra work, never change the answer — it cannot steer a caller's
/// accept/reject outcome. Off-target it inverts in software directly. Returns
/// `None` only for a genuinely non-invertible input (`x = 0`), which the
/// callers' degeneracy guards already exclude.
#[cfg(any(target_arch = "riscv64", test))]
fn field_inv(x: &FieldElement) -> Option<FieldElement> {
#[cfg(target_arch = "riscv64")]
Expand Down
20 changes: 10 additions & 10 deletions crypto/ethrex-crypto/src/tests/hint_tests.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
//! Host tests for the untrusted-hint verify-then-fallback paths (`scalar_inv`,
//! `field_inv`, `decompress_r`).
//!
//! The guest asks the (untrusted, prover-chosen) `hint` ecall for a modular
//! inverse / square root, then verifies it in-circuit. These tests inject the
//! oracle directly — an *honest* oracle (matching the executor's `compute_hint`)
//! and a *lying* one — and assert the software fallback makes the result identical
//! either way. That is the property the whole hint design rests on: because the
//! prover chooses the hinted bytes and the ecall adds no correctness constraint, a
//! bad hint must only be able to make the guest do more work, never change its
//! accept/reject outcome. On the guest this code is `cfg(target_arch = "riscv64")`;
//! the `test` gate on `*_with_oracle` is what lets CI compile and exercise it on
//! the host.
//! The guest reads each modular inverse / square root from the untrusted,
//! prover-chosen private-input **hint arena**, then verifies it in-circuit.
//! These tests inject the oracle directly — an *honest* oracle (matching the
//! executor's `compute_hint`) and a *lying* one — and assert the software
//! fallback makes the result identical either way. That is the property the
//! whole hint design rests on: because the prover chooses the arena bytes and
//! they are unconstrained, a bad hint must only be able to make the guest do
//! more work, never change its accept/reject outcome. On the guest this code
//! is `cfg(target_arch = "riscv64")`; the `test` gate on `*_with_oracle` is
//! what lets CI compile and exercise it on the host.

use crate::*;

Expand Down
Loading
Loading