Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions internal/ethapi/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"os"
"path/filepath"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
45 changes: 41 additions & 4 deletions internal/ethapi/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"time"

Expand Down Expand Up @@ -150,14 +151,47 @@ 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 {
b Backend
state *state.StateDB
base *types.Header
chainConfig *params.ChainConfig
gasRemaining uint64
budget *gasBudget
traceTransfers bool
validate bool
fullTx bool
Expand Down Expand Up @@ -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)}
Expand Down Expand Up @@ -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)
}

Expand Down
5 changes: 2 additions & 3 deletions internal/ethapi/simulate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package ethapi

import (
"math"
"math/big"
"testing"

Expand Down Expand Up @@ -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 {
Expand Down