Perf/ecsm affine selector - #879
Conversation
|
Benchmark Results for modified programs 🚀
|
|
/bench |
Benchmark — ethrex 20 transfers (median of 3)Table parallelism: auto (cores / 3)
Commit: 9ce5c1b · Baseline: cached · Runner: self-hosted bench |
…affine-selector # Conflicts: # crypto/ethrex-crypto/src/tests/ecsm_tests.rs # executor/src/vm/instruction/execution.rs # prover/src/tables/cpu.rs # syscalls/src/syscalls.rs
|
/bench |
Brings the HINT syscall renumbering (u64::MAX - 30, off the FEXT_FMA number), the field_inv software fallback and its test seam, the HINT table GPU parity check and the ethrex-crypto host tests in CI. crypto/ethrex-crypto/src/lib.rs conflicted: the base inserted field_inv_with_oracle exactly where this branch had rewritten lincomb2_with_oracle's doc. Kept both, with the affine wording — the base's "from four x-only oracle queries" describes the reconstruction this branch removes.
The affine ecall rejected any k within 64 bytes of the input point, but the point is 64 bytes and the scalar 32, so a scalar sitting directly below the point is disjoint at a distance of 32 and was rejected anyway. Both callers build the two buffers as separate stack arrays, so whether the ecall worked at all depended on which one the guest compiler laid out first: a rustc or LTO change could turn ecrecover into a hard EcsmOperandOverlap, which has no software fallback. The guard now tests the two byte ranges, which is what trace provability actually requires — xG‖yG is read at T and k at T+1, so they only need to not touch. The doc comment claiming the distance assumption held by construction was true only for the x-only path, where both operands are 32 bytes. Adds executor coverage for the boundary case that used to fail, for a non-canonical yG and for the 64-byte address-limb span.
The column-count comment in the ECSM chip still said 668 after yR_sub_p landed (684), and scalar_mul_affine's doc claimed its input is the even-y lift of xG, which is only true on the x-only path — the affine path passes the caller's own point, which is the whole point of the function. Drops the "PoC" labels left in production paths and puts the new syscall variant in discriminant order. The new guest's Cargo.lock was generated on a tree carrying the dlmalloc allocator, so it listed dlmalloc and critical-section as lambda-vm-syscalls dependencies that the crate does not declare; the guest build passes no --locked, so it silently rewrote the file on every run and dirtied the tree after every make compile-programs. Also formats crypto/ethrex-crypto, which no CI job checks because it is not a workspace member, and sizes the ECSM memw op vector for affine rows, which push eight more ops.
The affine selector and yR canonicality had no direct tests: op_for hardcoded is_affine = false, so IS_BIT(IS_AFFINE) and AffineZeroOnPadding were only reached through the end-to-end ELF test, and nothing exercised OverflowRequired(YrLtP) at all, even though it is what keeps the published yR canonical. Adds a mixed affine / x-only trace, the isolated selector checks, a yR = p case mirroring the existing xG one, and an end-to-end test that clears IS_AFFINE on the affine row. That last one confirms the Ecall-bus anchor empirically rather than by argument: the receiver's syscall word is xonly + IS_AFFINE·(affine − xonly) and the CPU sends the guest's real a7, so a row claiming the wrong variant leaves that bus and the two it gates unbalanced.
|
/ai-review |
Codex Code ReviewNo actionable issues found in the specified PR diff. Static review only; tests/builds were not run as instructed. |
AI ReviewPR #879 · 17 changed files Findings
Status column reflects the verdict from the verifier: deepseek-verifier (openrouter/deepseek/deepseek-v4-pro). AI-002: EcsmError Display messages don't cover the affine path
Claim The Display impl for EcsmError::NotOnCurve says "ECSM xG is not a valid curve x-coordinate" and CoordinateOutOfRange says "ECSM xG must be < p". With the new affine entry point (prepare_with_y / scalar_mul_xy_with_y), NotOnCurve can also mean the full point (xG, yG) is not on the curve, and CoordinateOutOfRange can mean yG >= p. The messages are now misleading when the affine ecall rejects an input. Evidence lib.rs lines 76-85 show the unchanged Display messages. Lines 127-151 add prepare_with_y, which returns EcsmError::NotOnCurve when yG² ≢ xG³+b (mod p) and EcsmError::CoordinateOutOfRange when xg >= p() || yg >= p(). The executor's affine ecall maps these errors back to the guest (execution.rs lines 540-585). Suggested fix Update the Display strings to describe both cases, e.g. "ECSM input point is not on the curve" for NotOnCurve and "ECSM xG/yG must be < p" for CoordinateOutOfRange, or split the messages per entry point. AI-003: Repeated p() allocations in prepare_with_y
Claim The on-curve check in prepare_with_y calls p() three times, each constructing a new BigUint from P_BYTES. Evidence Line 105: Suggested fix Cache p() in a local variable: 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. |
NotOnCurve and CoordinateOutOfRange both predate the affine ecall and named only xG: "ECSM xG is not a valid curve x-coordinate" and "ECSM xG must be < p". The affine path returns the same two variants for a caller-supplied yG that fails yG² ≡ xG³ + b or is >= p, so the messages pointed at the wrong operand. Rewords them and the variant docs to cover both entry points. Also hoists the modulus in prepare_with_y: it was rebuilt from P_BYTES five times per call (two range checks plus three reductions in the on-curve test). One BigUint either way against the scalar multiplication that follows, so this is for the reader, not the clock.
|
/bench |
What
Adds an affine variant of the ECSM ecall (
ECSM_AFFINE_SYSCALL_NUMBER = u64::MAX - 11): inputxG‖yG(64 B), outputxR‖yR(64 B), against the x-only variant's 32 B in/out. AnIS_AFFINEselector column lets one prover serve both ABIs, so existing x-only consumers (
ecsm.elf, bench,ASM
test_ecsm*) keep working unchanged.With x-only output the guest could not learn
y(k·P), so it recovered it from a second queryx((k+1)·P)plus the chord-addition law — two accelerator calls and a batch of field arithmeticper point. ethrex's ecrecover now takes
yfrom the chip: two ECSM queries per 2-term linearcombination instead of four, and
solve_yis gone.Details
executor/src/vm/instruction/execution.rs):ECSM_AFFINE_SYSCALL_NUMBERgetsits own handler branch (
scalar_mul_xy_with_y). Both operand buffers are 64 B, so theaddress-limb guard spans offset 63 for
xG/xR(31 for the scalar) and thexG/kdisjointness guard widens to 64 B. Validates
0 < k < N,xG < p,yG < pand that(xG, yG)is on the curve.IS_AFFINEselector (prover/src/tables/ecsm.rs, 667 → 684 columns, 413 → 423constraints): the
yG-read (4 dwords ataddr_xG + 32, ts) andyR-write (4 dwords ataddr_xR + 32, ts+3) MEMW buses fire withmult = IS_AFFINE, so they are inert on x-only rowsand on padding. The x-only path is untouched:
yGstays the canonical even lift andyRiswitnessed only for ECDAS.
yR < p(OverflowKind::YrLtP, +16 halfword columns and its carry chain): publishingyRis what makes its representation observable, and the byte range checks only bound it below
2^256.xRwas already forced canonical byXrLtP; this gives the other half of the outputthe same treatment.
ecsm_mul_affineinsyscalls/src/syscalls.rs, andlincomb2_with_oracleincrypto/ethrex-crypto/src/lib.rsnow does two oracle queries instead of four and dropssolve_yentirely. It still needs one field inverse for the final affine addition.ecsm_affine, plus a forged-yRrejection test.Measured (ethrex bench_20, bench server, baseline = the hint branch this sits on)
3 interleaved pairs, each side with its own prover and its own guest ELF, since this touches
both. All proofs verify.
Halving the accelerator calls is the structural part; prove time moves less because ECSM/ECDAS is
a fraction of the trace. An earlier measurement of this work reported −10.2%, on a baseline that
sat 14,466 cycles above
8 × 524,288and therefore carried a ninth CPU chunk the affine sidedropped — a boundary artifact, not the change. Pairs are interleaved rather than run as
sequential blocks, because cross-session drift on that machine exceeds the effect being measured.
Soundness
Returning
ymakes two things observable that were not:yGonly throughyG² ≡ xG³ + b, which holds for both+yGand−yG. On the x-only path that freedom is invisible (x(k·P) = x(k·(−P))), but onceyRis published a witness could pick−yGand get a correctly-computed multiple of adifferent point — indistinguishable to both the AIR (an on-curve check passes either way) and
the guest (it cannot know the parity of
k·Pwithout doing the multiplication it delegated).The
yGMEMW read closes it by pinning the witnessed column to the caller's own buffer.p, so withoutYrLtPawitness could publish
yR + pwheneveryR < 2^256 − p(~2^32), and such points areconstructible:
3 | p−1makes cubing 3-to-1, so a small targetyhas a cube-root preimageabout a third of the time.
IS_AFFINEitself is pinned by theEcallreceiver, whose syscall word isxonly + IS_AFFINE·(affine − xonly); the CPU sends the reala7, so a row claiming the wrongmode leaves the bus unbalanced.
IS_BIT(IS_AFFINE)andIS_AFFINE·(1 − µ) = 0keep it a bitthat is zero on padding. The written
yRinherits its byte range checks through the ECDAS bus,which carries the coordinate byte by byte and range-checks its own columns.
Removing the reconstruction also retires an argument rather than adding one: the x-only path had
to establish that the wrong sign of
yawas excluded by the scalar guards, which is what madek = 1andk = N−1degenerate. Withysupplied by the chip those scalars are ordinary —secp256k1 has cofactor 1 and prime
N, sok·P ≠ Ofor everyk ∈ (0, N).