Skip to content

Commit a73d89d

Browse files
s1naallformless
authored andcommitted
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 e38c766 commit a73d89d

4 files changed

Lines changed: 60 additions & 20 deletions

File tree

internal/ethapi/api.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,17 +1151,12 @@ func (api *BlockChainAPI) SimulateV1(ctx context.Context, opts simOpts, blockNrO
11511151
if state == nil || err != nil {
11521152
return nil, err
11531153
}
1154-
gasCap := api.b.RPCGasCap()
1155-
if gasCap == 0 {
1156-
gasCap = gomath.MaxUint64
1157-
}
11581154
sim := &simulator{
1159-
b: api.b,
1160-
state: state,
1161-
base: base,
1162-
chainConfig: api.b.ChainConfig(),
1163-
// Each tx and all the series of txes shouldn't consume more gas than cap
1164-
gp: new(core.GasPool).AddGas(gasCap),
1155+
b: api.b,
1156+
state: state,
1157+
base: base,
1158+
chainConfig: api.b.ChainConfig(),
1159+
budget: newGasBudget(api.b.RPCGasCap()),
11651160
traceTransfers: opts.TraceTransfers,
11661161
validate: opts.Validation,
11671162
fullTx: opts.ReturnFullTransactions,

internal/ethapi/api_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"errors"
2626
"fmt"
2727
"maps"
28-
"math"
2928
"math/big"
3029
"os"
3130
"path/filepath"
@@ -2544,7 +2543,7 @@ func TestSimulateV1ChainLinkage(t *testing.T) {
25442543
state: stateDB,
25452544
base: baseHeader,
25462545
chainConfig: backend.ChainConfig(),
2547-
gp: new(core.GasPool).AddGas(math.MaxUint64),
2546+
budget: newGasBudget(0),
25482547
traceTransfers: false,
25492548
validate: false,
25502549
fullTx: false,
@@ -2629,7 +2628,7 @@ func TestSimulateV1TxSender(t *testing.T) {
26292628
state: stateDB,
26302629
base: baseHeader,
26312630
chainConfig: backend.ChainConfig(),
2632-
gp: new(core.GasPool).AddGas(math.MaxUint64),
2631+
budget: newGasBudget(0),
26332632
traceTransfers: false,
26342633
validate: false,
26352634
fullTx: true,

internal/ethapi/simulate.go

Lines changed: 49 additions & 6 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

@@ -168,14 +169,47 @@ func (m *simChainHeadReader) GetVerifiedBlockByHash(hash common.Hash) *types.Hea
168169
return m.Backend.Chain().GetVerifiedBlockByHash(hash)
169170
}
170171

172+
// gasBudget tracks the remaining gas allowed across all simulated blocks.
173+
// It enforces the RPC-level gas cap to prevent DoS.
174+
type gasBudget struct {
175+
remaining uint64
176+
}
177+
178+
// newGasBudget creates a gas budget with the given cap.
179+
// A cap of 0 is treated as unlimited.
180+
func newGasBudget(cap uint64) *gasBudget {
181+
if cap == 0 {
182+
cap = math.MaxUint64
183+
}
184+
return &gasBudget{remaining: cap}
185+
}
186+
187+
// cap returns the given gas value clamped to the remaining budget.
188+
func (b *gasBudget) cap(gas uint64) uint64 {
189+
if gas > b.remaining {
190+
return b.remaining
191+
}
192+
return gas
193+
}
194+
195+
// consume deducts the given amount from the budget.
196+
// Returns an error if the amount exceeds the remaining budget.
197+
func (b *gasBudget) consume(amount uint64) error {
198+
if amount > b.remaining {
199+
return fmt.Errorf("RPC gas cap exhausted: need %d, remaining %d", amount, b.remaining)
200+
}
201+
b.remaining -= amount
202+
return nil
203+
}
204+
171205
// simulator is a stateful object that simulates a series of blocks.
172206
// it is not safe for concurrent use.
173207
type simulator struct {
174208
b Backend
175209
state *state.StateDB
176210
base *types.Header
177211
chainConfig *params.ChainConfig
178-
gp *core.GasPool
212+
budget *gasBudget
179213
traceTransfers bool
180214
validate bool
181215
fullTx bool
@@ -261,6 +295,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
261295
}
262296
var (
263297
gasUsed, blobGasUsed uint64
298+
gp = new(core.GasPool).AddGas(blockContext.GasLimit)
264299
txes = make([]*types.Transaction, len(block.Calls))
265300
callResults = make([]simCallResult, len(block.Calls))
266301
receipts = make([]*types.Receipt, len(block.Calls))
@@ -309,7 +344,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
309344
sim.state.SetTxContext(txHash, i)
310345
// EoA check is always skipped, even in validation mode.
311346
msg := call.ToMessage(header.BaseFee, !sim.validate)
312-
result, err := applyMessageWithEVM(ctx, evm, msg, timeout, sim.gp)
347+
result, err := applyMessageWithEVM(ctx, evm, msg, timeout, gp)
313348
if err != nil {
314349
txErr := txValidationError(err)
315350
return nil, nil, nil, txErr
@@ -324,6 +359,13 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
324359
gasUsed += result.UsedGas
325360
receipts[i] = core.MakeReceipt(evm, result, sim.state, blockContext.BlockNumber, common.Hash{}, blockContext.Time, tx, gasUsed, root)
326361
blobGasUsed += receipts[i].BlobGasUsed
362+
363+
// Make sure the gas cap is still enforced. It's only for
364+
// internally protection.
365+
if err := sim.budget.consume(result.UsedGas); err != nil {
366+
return nil, nil, nil, err
367+
}
368+
327369
logs := tracer.Logs()
328370
callRes := simCallResult{ReturnValue: result.Return(), Logs: logs, GasUsed: hexutil.Uint64(result.UsedGas)}
329371
if result.Failed() {
@@ -400,10 +442,11 @@ func (sim *simulator) sanitizeCall(call *TransactionArgs, state vm.StateDB, head
400442
if *gasUsed+uint64(*call.Gas) > blockContext.GasLimit {
401443
return &blockGasLimitReachedError{fmt.Sprintf("block gas limit reached: %d >= %d", gasUsed, blockContext.GasLimit)}
402444
}
403-
if err := call.CallDefaults(sim.gp.Gas(), header.BaseFee, sim.chainConfig.ChainID); err != nil {
404-
return err
405-
}
406-
return nil
445+
// Clamp to the cross-block gas budget.
446+
gas := sim.budget.cap(uint64(*call.Gas))
447+
call.Gas = (*hexutil.Uint64)(&gas)
448+
449+
return call.CallDefaults(0, header.BaseFee, sim.chainConfig.ChainID)
407450
}
408451

409452
func (sim *simulator) activePrecompiles(base *types.Header) vm.PrecompiledContracts {

internal/ethapi/simulate_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ func TestSimulateSanitizeBlockOrder(t *testing.T) {
8080
err: "block timestamps must be in order: 72 <= 72",
8181
},
8282
} {
83-
sim := &simulator{base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp}}
83+
sim := &simulator{
84+
base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp},
85+
budget: newGasBudget(0),
86+
}
8487
res, err := sim.sanitizeChain(tc.blocks)
8588
if err != nil {
8689
if err.Error() == tc.err {

0 commit comments

Comments
 (0)