internal/ethapi: fix gas cap for eth_simulateV1 (#33952)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

Fixes a regression in #33593 where a block gas limit > gasCap resulted
in more execution than the gas cap.
This commit is contained in:
Sina M 2026-03-05 02:09:07 +01:00 committed by GitHub
parent fc8c10476d
commit ce64ab44ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 46 additions and 15 deletions

View file

@ -849,16 +849,12 @@ func (api *BlockChainAPI) SimulateV1(ctx context.Context, opts simOpts, blockNrO
if state == nil || err != nil { if state == nil || err != nil {
return nil, err return nil, err
} }
gasCap := api.b.RPCGasCap()
if gasCap == 0 {
gasCap = gomath.MaxUint64
}
sim := &simulator{ sim := &simulator{
b: api.b, b: api.b,
state: state, state: state,
base: base, base: base,
chainConfig: api.b.ChainConfig(), chainConfig: api.b.ChainConfig(),
gasRemaining: gasCap, budget: newGasBudget(api.b.RPCGasCap()),
traceTransfers: opts.TraceTransfers, traceTransfers: opts.TraceTransfers,
validate: opts.Validation, validate: opts.Validation,
fullTx: opts.ReturnFullTransactions, fullTx: opts.ReturnFullTransactions,

View file

@ -24,7 +24,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"math"
"math/big" "math/big"
"os" "os"
"path/filepath" "path/filepath"
@ -2507,7 +2506,7 @@ func TestSimulateV1ChainLinkage(t *testing.T) {
state: stateDB, state: stateDB,
base: baseHeader, base: baseHeader,
chainConfig: backend.ChainConfig(), chainConfig: backend.ChainConfig(),
gasRemaining: math.MaxUint64, budget: newGasBudget(0),
traceTransfers: false, traceTransfers: false,
validate: false, validate: false,
fullTx: false, fullTx: false,
@ -2592,7 +2591,7 @@ func TestSimulateV1TxSender(t *testing.T) {
state: stateDB, state: stateDB,
base: baseHeader, base: baseHeader,
chainConfig: backend.ChainConfig(), chainConfig: backend.ChainConfig(),
gasRemaining: math.MaxUint64, budget: newGasBudget(0),
traceTransfers: false, traceTransfers: false,
validate: false, validate: false,
fullTx: true, fullTx: true,

View file

@ -21,6 +21,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"math"
"math/big" "math/big"
"time" "time"
@ -150,6 +151,39 @@ func (m *simChainHeadReader) GetHeaderByHash(hash common.Hash) *types.Header {
return header return header
} }
// gasBudget tracks the remaining gas allowed across all simulated blocks.
// It enforces the RPC-level gas cap to prevent DoS.
type gasBudget struct {
remaining uint64
}
// newGasBudget creates a gas budget with the given cap.
// A cap of 0 is treated as unlimited.
func newGasBudget(cap uint64) *gasBudget {
if cap == 0 {
cap = math.MaxUint64
}
return &gasBudget{remaining: cap}
}
// cap returns the given gas value clamped to the remaining budget.
func (b *gasBudget) cap(gas uint64) uint64 {
if gas > b.remaining {
return b.remaining
}
return gas
}
// consume deducts the given amount from the budget.
// Returns an error if the amount exceeds the remaining budget.
func (b *gasBudget) consume(amount uint64) error {
if amount > b.remaining {
return fmt.Errorf("RPC gas cap exhausted: need %d, remaining %d", amount, b.remaining)
}
b.remaining -= amount
return nil
}
// simulator is a stateful object that simulates a series of blocks. // simulator is a stateful object that simulates a series of blocks.
// it is not safe for concurrent use. // it is not safe for concurrent use.
type simulator struct { type simulator struct {
@ -157,7 +191,7 @@ type simulator struct {
state *state.StateDB state *state.StateDB
base *types.Header base *types.Header
chainConfig *params.ChainConfig chainConfig *params.ChainConfig
gasRemaining uint64 budget *gasBudget
traceTransfers bool traceTransfers bool
validate bool validate bool
fullTx bool fullTx bool
@ -318,10 +352,9 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
// Make sure the gas cap is still enforced. It's only for // Make sure the gas cap is still enforced. It's only for
// internally protection. // internally protection.
if sim.gasRemaining < result.UsedGas { if err := sim.budget.consume(result.UsedGas); err != nil {
return nil, nil, nil, fmt.Errorf("gas cap reached, required: %d, remaining: %d", result.UsedGas, sim.gasRemaining) return nil, nil, nil, err
} }
sim.gasRemaining -= result.UsedGas
logs := tracer.Logs() logs := tracer.Logs()
callRes := simCallResult{ReturnValue: result.Return(), Logs: logs, GasUsed: hexutil.Uint64(result.UsedGas), MaxUsedGas: hexutil.Uint64(result.MaxUsedGas)} callRes := simCallResult{ReturnValue: result.Return(), Logs: logs, GasUsed: hexutil.Uint64(result.UsedGas), MaxUsedGas: hexutil.Uint64(result.MaxUsedGas)}
@ -405,6 +438,10 @@ func (sim *simulator) sanitizeCall(call *TransactionArgs, state vm.StateDB, head
if remaining < uint64(*call.Gas) { if remaining < uint64(*call.Gas) {
return &blockGasLimitReachedError{fmt.Sprintf("block gas limit reached: remaining: %d, required: %d", remaining, *call.Gas)} return &blockGasLimitReachedError{fmt.Sprintf("block gas limit reached: remaining: %d, required: %d", remaining, *call.Gas)}
} }
// Clamp to the cross-block gas budget.
gas := sim.budget.cap(uint64(*call.Gas))
call.Gas = (*hexutil.Uint64)(&gas)
return call.CallDefaults(0, header.BaseFee, sim.chainConfig.ChainID) return call.CallDefaults(0, header.BaseFee, sim.chainConfig.ChainID)
} }

View file

@ -17,7 +17,6 @@
package ethapi package ethapi
import ( import (
"math"
"math/big" "math/big"
"testing" "testing"
@ -82,8 +81,8 @@ func TestSimulateSanitizeBlockOrder(t *testing.T) {
}, },
} { } {
sim := &simulator{ sim := &simulator{
base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp}, base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp},
gasRemaining: math.MaxUint64, budget: newGasBudget(0),
} }
res, err := sim.sanitizeChain(tc.blocks) res, err := sim.sanitizeChain(tc.blocks)
if err != nil { if err != nil {