diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index ff6797f67b..41d165a423 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -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, diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index a82df440e6..62e9979d3d 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -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, diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 63513f8d66..f0c69e39a4 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -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) } diff --git a/internal/ethapi/simulate_test.go b/internal/ethapi/simulate_test.go index b6037a8f35..6a83e744de 100644 --- a/internal/ethapi/simulate_test.go +++ b/internal/ethapi/simulate_test.go @@ -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 {