fix: F-2026-18140 | [Dual Defense] Underpriced Ed25519 Raw-Message Precompile Gas - #346
Merged
Conversation
verifyEd25519RawMessage charged a flat 4000 gas no matter how long the message was, while ed25519.Verify hashes the whole slice (~58us at 32B, ~922us at 1MB). Charge 4000 + 12 per 32-byte word instead, matching the SHA-256 precompile's per-word rate, and hard-cap the message at 128 KiB since a view method can be looped from memory without re-paying calldata. verifyEd25519 stays flat: it always verifies the 66-byte hex form of a bytes32 digest, so its cost cannot vary with the calldata.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
F-2026-18140 — Underpriced Ed25519 Raw-Message Precompile Gas
precompiles/usigverifiercharged a flat 4000 gas forverifyEd25519RawMessage(bytes,bytes,bytes)regardless of message length, while
Runcallsed25519.Verify(pub, message, sig)over the wholeslice. Ed25519 hashes the entire message, so CPU scales with length and gas did not.
Measured on a live node (median of 9 reps): ~58 µs at 32 B → ~922 µs at 1 MB — 16x the work for the
same 4000 gas. Because the method is
view, the realistic abuse is a contract holding one largemessage in memory and looping
STATICCALLs: the calldata is paid once, the verification repeatsat the flat price.
Fix — dual defense
1. Price the message per 32-byte word
12gas per 32-byte word is the rate the EVMSHA-256precompile charges, and Ed25519's marginalcost past the fixed curve arithmetic is the same kind of hashing work. Below ~8 KiB the flat portion
still dominates, which is why the base stays at 4000.
len(message)RequiredGasreceives the raw calldata, so the length is recovered straight out of the ABI head/tail(
rawMessageLen) with bounds-checked word reads — nobig.Int, no decoding, and it cannot panic onmalformed input. Calldata that does not parse prices at the base; a declared length too large for a
uint64prices at the cap, so lying about the size is not the cheap path.2. Hard-cap the message at 128 KiB
MaxEd25519MessageBytes = 128 * 1024; anything larger reverts withmessage too large. On afee-exempt
viewpath a price curve alone is not the defence — a hard limit is. 128 KiB matches thegateway payload cap from F-2026-18146 so there is one size limit to reason about, not two.
Net effect on the abuse shape: at a 100M block gas limit, a cap-sized message buys 1,881
verifications per block instead of 25,000 — a 13x reduction, and messages past 128 KiB cannot be
verified at all.
Why only the raw method changes
VerifyEd25519GasandVerifyEd25519RawMessageBaseGasnow deliberately diverge:abi.jsondeclaresverifyEd25519's message argument asbytes32, andquery.gotype-assertsargs[1].([32]byte)— a fixed-size array the caller cannot lengthen."0x" + hex(msgDigest), i.e. exactly 66 bytes on every call, whatever thecalldata contains.
So the legacy method's verification cost genuinely is constant, and a flat 4000 remains the honest
price. Only the raw method verifies caller-sized bytes.
Tests
precompiles/usigverifier/gas_test.go(new):TestRequiredGas_RawMessageScalesWithMessageLength— exact price at 0 / 1 / 32 / 33 B / 1 / 8 / 64 / 128 KiBTestRequiredGas_RawMessageIsStrictlyIncreasing— every extra word costs moreTestRequiredGas_SmallMessagesKeepASaneCost— ordinary calls stay within a rounding error of the old priceTestRequiredGas_AboveCapIsClampedNotUnbounded— oversized prices at the cap, never aboveTestRequiredGas_LegacyMethodStaysFlat— the divergence, incl. an oversizedpubKeyTestRequiredGas_MalformedCalldataIsPanicFreeAndBounded— 9 malformed calldatas: no panic, bounded chargeTestVerifyEd25519RawMessage_RejectsOversizedMessage/_AcceptsMessageAtCap— the cap is inclusiveTestRun_OversizedMessageReverts— the same cap throughRun, on real ABI calldataTestLargeMessageLoopIsGasProhibitive— the loop shape: ≥10x fewer verifications per block than the flat price boughtBenchmarks
BenchmarkVerifyEd25519RawMessage(per-size, reportinggas/opandgas/us) andBenchmarkRequiredGas. On an M1 the benchmark reproduces the reported curve:gas/usrising with size means the new schedule slightly over-charges large messages relative toCPU — intentional, the per-word rate follows the SHA-256 precedent and errs toward deterrence.
Mutation check
With the fix reverted behaviourally (flat price restored, cap check removed) and the tests kept,
8 of the 9 new tests fail (19 assertions across subtests). The oversized-message tests fail on
their value assertion before reaching
require.Error, i.e. the pre-fix code really did verify amessage past the cap and return
true:TestRequiredGas_LegacyMethodStaysFlatis the one that still passes — correctly, since the legacyprice is unchanged by design.
Notes
VerifyEd25519RawMessageGasis renamed toVerifyEd25519RawMessageBaseGas: it is no longer thegas cost, only its fixed part. No caller in the tree referenced it (grep over the repo:
precompiles/usigverifieronly).params.
precompiles/usigverifier/README.md(gas table, size cap, the divergence) andapp/README.md.