Skip to content

Commit ce64ab4

Browse files
authored
internal/ethapi: fix gas cap for eth_simulateV1 (#33952)
Fixes a regression in #33593 where a block gas limit > gasCap resulted in more execution than the gas cap.
1 parent fc8c104 commit ce64ab4

File tree

4 files changed

+46
-15
lines changed

4 files changed

+46
-15
lines changed

internal/ethapi/api.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -849,16 +849,12 @@ func (api *BlockChainAPI) SimulateV1(ctx context.Context, opts simOpts, blockNrO
849849
if state == nil || err != nil {
850850
return nil, err
851851
}
852-
gasCap := api.b.RPCGasCap()
853-
if gasCap == 0 {
854-
gasCap = gomath.MaxUint64
855-
}
856852
sim := &simulator{
857853
b: api.b,
858854
state: state,
859855
base: base,
860856
chainConfig: api.b.ChainConfig(),
861-
gasRemaining: gasCap,
857+
budget: newGasBudget(api.b.RPCGasCap()),
862858
traceTransfers: opts.TraceTransfers,
863859
validate: opts.Validation,
864860
fullTx: opts.ReturnFullTransactions,

internal/ethapi/api_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"encoding/json"
2525
"errors"
2626
"fmt"
27-
"math"
2827
"math/big"
2928
"os"
3029
"path/filepath"
@@ -2507,7 +2506,7 @@ func TestSimulateV1ChainLinkage(t *testing.T) {
25072506
state: stateDB,
25082507
base: baseHeader,
25092508
chainConfig: backend.ChainConfig(),
2510-
gasRemaining: math.MaxUint64,
2509+
budget: newGasBudget(0),
25112510
traceTransfers: false,
25122511
validate: false,
25132512
fullTx: false,
@@ -2592,7 +2591,7 @@ func TestSimulateV1TxSender(t *testing.T) {
25922591
state: stateDB,
25932592
base: baseHeader,
25942593
chainConfig: backend.ChainConfig(),
2595-
gasRemaining: math.MaxUint64,
2594+
budget: newGasBudget(0),
25962595
traceTransfers: false,
25972596
validate: false,
25982597
fullTx: true,

internal/ethapi/simulate.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/json"
2222
"errors"
2323
"fmt"
24+
"math"
2425
"math/big"
2526
"time"
2627

@@ -150,14 +151,47 @@ func (m *simChainHeadReader) GetHeaderByHash(hash common.Hash) *types.Header {
150151
return header
151152
}
152153

154+
// gasBudget tracks the remaining gas allowed across all simulated blocks.
155+
// It enforces the RPC-level gas cap to prevent DoS.
156+
type gasBudget struct {
157+
remaining uint64
158+
}
159+
160+
// newGasBudget creates a gas budget with the given cap.
161+
// A cap of 0 is treated as unlimited.
162+
func newGasBudget(cap uint64) *gasBudget {
163+
if cap == 0 {
164+
cap = math.MaxUint64
165+
}
166+
return &gasBudget{remaining: cap}
167+
}
168+
169+
// cap returns the given gas value clamped to the remaining budget.
170+
func (b *gasBudget) cap(gas uint64) uint64 {
171+
if gas > b.remaining {
172+
return b.remaining
173+
}
174+
return gas
175+
}
176+
177+
// consume deducts the given amount from the budget.
178+
// Returns an error if the amount exceeds the remaining budget.
179+
func (b *gasBudget) consume(amount uint64) error {
180+
if amount > b.remaining {
181+
return fmt.Errorf("RPC gas cap exhausted: need %d, remaining %d", amount, b.remaining)
182+
}
183+
b.remaining -= amount
184+
return nil
185+
}
186+
153187
// simulator is a stateful object that simulates a series of blocks.
154188
// it is not safe for concurrent use.
155189
type simulator struct {
156190
b Backend
157191
state *state.StateDB
158192
base *types.Header
159193
chainConfig *params.ChainConfig
160-
gasRemaining uint64
194+
budget *gasBudget
161195
traceTransfers bool
162196
validate bool
163197
fullTx bool
@@ -318,10 +352,9 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
318352

319353
// Make sure the gas cap is still enforced. It's only for
320354
// internally protection.
321-
if sim.gasRemaining < result.UsedGas {
322-
return nil, nil, nil, fmt.Errorf("gas cap reached, required: %d, remaining: %d", result.UsedGas, sim.gasRemaining)
355+
if err := sim.budget.consume(result.UsedGas); err != nil {
356+
return nil, nil, nil, err
323357
}
324-
sim.gasRemaining -= result.UsedGas
325358

326359
logs := tracer.Logs()
327360
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
405438
if remaining < uint64(*call.Gas) {
406439
return &blockGasLimitReachedError{fmt.Sprintf("block gas limit reached: remaining: %d, required: %d", remaining, *call.Gas)}
407440
}
441+
// Clamp to the cross-block gas budget.
442+
gas := sim.budget.cap(uint64(*call.Gas))
443+
call.Gas = (*hexutil.Uint64)(&gas)
444+
408445
return call.CallDefaults(0, header.BaseFee, sim.chainConfig.ChainID)
409446
}
410447

internal/ethapi/simulate_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package ethapi
1818

1919
import (
20-
"math"
2120
"math/big"
2221
"testing"
2322

@@ -82,8 +81,8 @@ func TestSimulateSanitizeBlockOrder(t *testing.T) {
8281
},
8382
} {
8483
sim := &simulator{
85-
base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp},
86-
gasRemaining: math.MaxUint64,
84+
base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp},
85+
budget: newGasBudget(0),
8786
}
8887
res, err := sim.sanitizeChain(tc.blocks)
8988
if err != nil {

0 commit comments

Comments
 (0)