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 {
return nil, err
}
gasCap := api.b.RPCGasCap()
if gasCap == 0 {
gasCap = gomath.MaxUint64
}
sim := &simulator{
b: api.b,
state: state,
base: base,
chainConfig: api.b.ChainConfig(),
gasRemaining: gasCap,
budget: newGasBudget(api.b.RPCGasCap()),
traceTransfers: opts.TraceTransfers,
validate: opts.Validation,
fullTx: opts.ReturnFullTransactions,

View file

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

View file

@ -21,6 +21,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"time"
@ -150,6 +151,39 @@ func (m *simChainHeadReader) GetHeaderByHash(hash common.Hash) *types.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.
// it is not safe for concurrent use.
type simulator struct {
@ -157,7 +191,7 @@ type simulator struct {
state *state.StateDB
base *types.Header
chainConfig *params.ChainConfig
gasRemaining uint64
budget *gasBudget
traceTransfers bool
validate 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
// internally protection.
if sim.gasRemaining < result.UsedGas {
return nil, nil, nil, fmt.Errorf("gas cap reached, required: %d, remaining: %d", result.UsedGas, sim.gasRemaining)
if err := sim.budget.consume(result.UsedGas); err != nil {
return nil, nil, nil, err
}
sim.gasRemaining -= result.UsedGas
logs := tracer.Logs()
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) {
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)
}

View file

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