fix(tn-reth): price blsVerify on the raw calldata length before the decode (#1332) - #1333
Open
MavenRain wants to merge 1 commit into
Open
fix(tn-reth): price blsVerify on the raw calldata length before the decode (#1332)#1333MavenRain wants to merge 1 commit into
MavenRain wants to merge 1 commit into
Conversation
…ecode (#1332) The precompile checked a flat BLS_VERIFY_GAS_COST floor before the ABI decode, then computed the real charge from the decoded message length. The floor bounded how many decodes a block could buy, not how large each one was. The caller pays calldata gas and memory expansion for the bytes it sends, but those pay for the transaction data and the caller's own memory, not for the precompile's allocation and copy of the same bytes on every executing node. Every other input-length precompile prices its own input; blsVerify priced only the decoded message, so the precompile's own charge stayed at the 150,000 floor however large the signature or pubkey it copied. handle_bls_verify now computes bls_verify_gas_cost over the raw argument length and checks it once, before any byte is decoded. The rate is unchanged at 12 gas per 32-byte word, the SHA256 precompile rate and four times the EVM copy rate. The message cap stays after the decode, because it bounds the hash-to-curve input and not the copy. The ConsensusRegistry proof-of-possession call moves from 150,048 to 150,180 gas and still fits its forwarded budget. Closes #1332 Signed-off-by: Onyeka Obi <softwareengineerasaservant@isurvivable.cv>
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.
Closes #1332.
Problem
blsVerifychecked a flatBLS_VERIFY_GAS_COSTfloor of 150,000 gas before the ABI decode, thencomputed the real charge from the decoded
messagelength. The floor is a constant. The work itguards is not.
The decode copies all three
bytesfields out of calldata into ownedBytes.signatureandpubkeyare unboundedbytes, soMAX_BLS_VERIFY_MESSAGE_LENnever bounded that copy.What this is, exactly, is a rate gap the precompile owns. The caller does pay for the bytes it
sends: calldata is 4 gas per zero byte and 16 per non-zero byte, so 1 MiB of zero bytes already
costs the caller about 4.2M gas, plus the memory expansion for the buffer it staticcalls from.
Those charges pay for the transaction data and for the caller's own memory. They do not pay for
the precompile's own allocation and copy of the same bytes on every executing node. Every other
input-length precompile prices that itself:
identitycharges 3 gas per word,sha25612,ripemd160120.blsVerifypriced only the decoded message, so the precompile's own charge stayedat the 150,000 floor however large the bytes it copied, and the post-decode charge was the same
150,000 plus a few words of message.
No attacker is needed to reach this. Any contract that forwards a large buffer to the precompile
hits it. The precompile address is warm and a
STATICCALLforwards all remaining gas by default,so a normally funded transaction already carries enough gas to clear the floor and then hand the
node an arbitrarily large buffer to copy.
Root cause
The gate ordering was cheap-first, but the cheap gate was the wrong quantity. #1215 added the floor
specifically so that
gas_usedstayed byte-identical for every accepted call, which meant the onlygate that could run before the decode had to be a constant. A constant bounds how many decodes a
block can buy. It does not bound how large each decode is. The size-dependent charge ran after the
copy it was meant to pay for.
Fix
handle_bls_verifynow computesbls_verify_gas_cost(calldata.len())and checksgas_limitagainst it once, before any byte isdecoded. This is the only gas gate in the function. Each of the three
bytesfields lies insidethe raw arguments, so each copy is at most the raw argument length and the three copies together
are at most three times it:
abi_decode_rawis the non-validating decoder, so all three heads maypoint at one tail. The raw length is known before the decode, and 12 gas per word is four times
the EVM's 3-gas copy rate, so the surcharge covers even the three-way aliased copy with margin.
crates/tn-reth/src/evm/bls_precompile/mod.rs:bls_verify_gas_costtakescalldata_leninstead of
message_len. The formula is unchanged: base plusBLS_VERIFY_PER_WORD_GAS_COSTper32-byte word, all checked. It stays total, so an overflow the arithmetic cannot represent is
metered as
OutOfGasrather than a panic. That case is unreachable for any calldata a block cancarry.
SHA256precompile rate for the same variable-length hashing, and it is four times the EVM's3-gas-per-word copy rate (
CALLDATACOPY, theidentityprecompile). The accounting is per classof word, not one rate charged twice.
signatureandpubkeywords are unbounded and are neverhashed, so their 12 gas pays for a copy that costs at most 9 gas per word even when the three
heads alias. Message words pay the same 12 gas for
expand_message_xmd, which is theSHA256precompile's own rate for that hashing; the copy of the message on top of it is bounded by the
4096-byte cap, at most 139 raw words at 3 gas per word, under 1,300 gas even three-way aliased,
which the 150,000-gas base absorbs. Under the old message-priced charge a message word paid this
same 12 gas, so no call pays less than it did before.
MAX_BLS_VERIFY_MESSAGE_LENas a raw ceiling. That would reject canonical calls: the canonicalenvelope around a message adds 352 bytes (three head words of 96, the signature tail of 32 plus
64, the pubkey tail of 32 plus 96, the message length word of 32), so a 4096-byte raw ceiling
refuses every canonical message above 3744 bytes. fix(tn-reth): gate blsVerify on the O(1) gas floor before decoding calldata #1215 also recorded that a raw ceiling turns
today's
Ok(false)for an oversized pubkey into a halt. Pricing the length instead makes thecopy pay for itself, the way
sha256andidentityprice their input length without capping it.attached. It bounds the hash-to-curve input of a generic primitive. It was never the thing that
bounded the copy.
crates/tn-reth/README.md: the "for a flat 150,000 gas" sentence was already stale afterper-word pricing landed in tn-reth: meter blsVerify by message length and cap it #1051. It now reads 150,000 gas plus 12 gas per 32-byte word of the
ABI-encoded arguments, charged before the arguments are decoded.
handle_bls_verify,bls_verify_gas_cost,BLS_VERIFY_GAS_COST,BLS_VERIFY_PER_WORD_GAS_COST, andMAX_BLS_VERIFY_MESSAGE_LEN, including the reason a rawceiling was rejected.
Threat model
The price is now fixed by the raw bytes before anything is parsed, so it is attacker-independent: a
caller cannot shape calldata that decodes cheaply but copies expensively, because the charge is read
off the length and not off the decode. The check is still cheap-first, and now the cheap quantity
is the one that scales with the work.
Observable change
gas_usedfollows the raw argument length. TheConsensusRegistryproof-of-possession call (a48-byte signature, a 96-byte pubkey, a 119-byte message, 480 argument bytes, 15 words) goes from
150,048 gas to 150,180 gas. Its Solidity
staticcallforwards all remaining gas, so it isunaffected.
A call funded at or above the new raw-priced cost behaves exactly as before: same decode, same cap,
same verification, same accepted set. A call funded in the band between the old message-priced cost
and the new raw-priced cost is now
OutOfGas. An under-funded call isOutOfGasbefore the decodewhatever its bytes hold. An over-cap message with the cost funded is still
Other.#1215 chose the floor to keep
gas_usedbyte-identical. This PR takes the trade the reviewer askedfor: a
gas_usedchange in exchange for a charge that actually bounds the work.Testing
Unit:
cargo +1.94 test -p tn-reth --lib evm::bls_precompile -j 2gives 17 passed, 0 failed, 0ignored.
Integration:
cargo +1.94 test -p tn-reth --test it -j 2 -- bls_precompilegives 11 passed, 0failed, 0 ignored.
crates/tn-reth/tests/it/bls_precompile_props.rsis unchanged: its 1,000,000gas
VERIFY_GASstill funds the new charge, and itsSTATICCALLrelay test proves the registrypath at the new price.
Format:
cargo +nightly fmt -p tn-reth -- --checkis clean.Lint:
cargo +nightly clippy -p tn-reth --all-features --all-targets --no-deps -- -D warningsisclean.
New and reworked tests:
dispatch_meters_by_raw_calldata_lengthholds a 32-byte message fixed and varies only bytes theold charge ignored: a 368-byte
signature, a 416-bytepubkey, and 320 trailing bytes thedecoder discards. Each delta is exactly 10 words of surcharge. All four calls, the canonical one
included, cost the same under the old pricing.
dispatch_charges_raw_calldata_before_decodereplacesdispatch_gas_floor_precedes_decode. Acanonically encoded 1 MiB message is 32,779 argument words. Funded at
BLS_VERIFY_GAS_COST, theold floor let it through to the decoder. It is now
OutOfGas. Funded at the charge it reachesthe decode and the cap, which is the positive control.
dispatch_verify_valid_returns_truefunds the proof-of-possession call at exactlyBLS_VERIFY_GAS_COST + 15 * BLS_VERIFY_PER_WORD_GAS_COSTand assertsgas_usedequals it.dispatch_prices_aliased_heads_by_raw_lengthis the aliasing case.abi_decode_rawis thenon-validating decoder, so three
bytesheads of0x60over one 32-byte tail decode into threeequal fields: 160 argument bytes, 96 bytes copied. The call is
Ok(false)andgas_usedisBLS_VERIFY_GAS_COST + 5 * BLS_VERIFY_PER_WORD_GAS_COST, the charge on the five raw words, whichat four times the EVM copy rate covers the tripled copy.
Mutation testing, both polarities observed. Both runs predate
dispatch_prices_aliased_heads_by_raw_length, so they count the 16 tests of that suite:origin/main'shandle_bls_verifybody spliced back in with the new tests kept: 10 passed, 6failed. The failures are
dispatch_meters_by_raw_calldata_length,dispatch_charges_raw_calldata_before_decode,dispatch_verify_valid_returns_true,dispatch_verify_invalid_returns_false_not_revert,dispatch_verify_out_of_gas, anddispatch_rejects_oversized_message. Restored: 16 passed, 0 failed.gas_limit >= *costflipped togas_limit > *cost: 12 passed, 4 failed, includingdispatch_verify_valid_returns_true, which funds at exactly the cost. Restored: 16 passed, 0failed.