Skip to content
Open
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
15 changes: 5 additions & 10 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1151,17 +1151,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(),
// Each tx and all the series of txes shouldn't consume more gas than cap
gp: new(core.GasPool).AddGas(gasCap),
b: api.b,
state: state,
base: base,
chainConfig: api.b.ChainConfig(),
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 @@ -25,7 +25,6 @@ import (
"errors"
"fmt"
"maps"
"math"
"math/big"
"os"
"path/filepath"
Expand Down Expand Up @@ -2544,7 +2543,7 @@ func TestSimulateV1ChainLinkage(t *testing.T) {
state: stateDB,
base: baseHeader,
chainConfig: backend.ChainConfig(),
gp: new(core.GasPool).AddGas(math.MaxUint64),
budget: newGasBudget(0),
traceTransfers: false,
validate: false,
fullTx: false,
Expand Down Expand Up @@ -2629,7 +2628,7 @@ func TestSimulateV1TxSender(t *testing.T) {
state: stateDB,
base: baseHeader,
chainConfig: backend.ChainConfig(),
gp: new(core.GasPool).AddGas(math.MaxUint64),
budget: newGasBudget(0),
traceTransfers: false,
validate: false,
fullTx: true,
Expand Down
55 changes: 49 additions & 6 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 @@ -168,14 +169,47 @@ func (m *simChainHeadReader) GetVerifiedBlockByHash(hash common.Hash) *types.Hea
return m.Backend.Chain().GetVerifiedBlockByHash(hash)
}

// 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
gp *core.GasPool
budget *gasBudget
traceTransfers bool
validate bool
fullTx bool
Expand Down Expand Up @@ -261,6 +295,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
}
var (
gasUsed, blobGasUsed uint64
gp = new(core.GasPool).AddGas(blockContext.GasLimit)
txes = make([]*types.Transaction, len(block.Calls))
callResults = make([]simCallResult, len(block.Calls))
receipts = make([]*types.Receipt, len(block.Calls))
Expand Down Expand Up @@ -309,7 +344,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
sim.state.SetTxContext(txHash, i)
// EoA check is always skipped, even in validation mode.
msg := call.ToMessage(header.BaseFee, !sim.validate)
result, err := applyMessageWithEVM(ctx, evm, msg, timeout, sim.gp)
result, err := applyMessageWithEVM(ctx, evm, msg, timeout, gp)
if err != nil {
txErr := txValidationError(err)
return nil, nil, nil, txErr
Expand All @@ -324,6 +359,13 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
gasUsed += result.UsedGas
receipts[i] = core.MakeReceipt(evm, result, sim.state, blockContext.BlockNumber, common.Hash{}, blockContext.Time, tx, gasUsed, root)
blobGasUsed += receipts[i].BlobGasUsed

// Make sure the gas cap is still enforced. It's only for
// internally protection.
if err := sim.budget.consume(result.UsedGas); err != nil {
return nil, nil, nil, err
}

logs := tracer.Logs()
callRes := simCallResult{ReturnValue: result.Return(), Logs: logs, GasUsed: hexutil.Uint64(result.UsedGas)}
if result.Failed() {
Expand Down Expand Up @@ -400,10 +442,11 @@ func (sim *simulator) sanitizeCall(call *TransactionArgs, state vm.StateDB, head
if *gasUsed+uint64(*call.Gas) > blockContext.GasLimit {
return &blockGasLimitReachedError{fmt.Sprintf("block gas limit reached: %d >= %d", gasUsed, blockContext.GasLimit)}
}
if err := call.CallDefaults(sim.gp.Gas(), header.BaseFee, sim.chainConfig.ChainID); err != nil {
return err
}
return nil
// 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)
}

func (sim *simulator) activePrecompiles(base *types.Header) vm.PrecompiledContracts {
Expand Down
5 changes: 4 additions & 1 deletion internal/ethapi/simulate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ func TestSimulateSanitizeBlockOrder(t *testing.T) {
err: "block timestamps must be in order: 72 <= 72",
},
} {
sim := &simulator{base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp}}
sim := &simulator{
base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp},
budget: newGasBudget(0),
}
res, err := sim.sanitizeChain(tc.blocks)
if err != nil {
if err.Error() == tc.err {
Expand Down
Loading