internal/ethapi: skip tx gas limit check for calls (#32641)

This disables the tx gaslimit cap for eth_call and related RPC operations.

I don't like how this fix works. Ideally we'd be checking the tx
gaslimit somewhere else, like in the block validator, or any other place
that considers block transactions. Doing the check in StateTransition
means it affects all possible ways of executing a message.

The challenge is finding a place for this check that also triggers
correctly in tests where it is wanted. So for now, we are just combining
this with the EOA sender check for transactions. Both are disabled for
call-type messages.
This commit is contained in:
Felix Lange 2025-09-19 13:29:17 +02:00 committed by GitHub
parent b9e2eb5944
commit 2b3d617e04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 22 additions and 18 deletions

View file

@ -164,8 +164,12 @@ type Message struct {
// or the state prefetching. // or the state prefetching.
SkipNonceChecks bool SkipNonceChecks bool
// When SkipFromEOACheck is true, the message sender is not checked to be an EOA. // When set, the message is not treated as a transaction, and certain
SkipFromEOACheck bool // transaction-specific checks are skipped:
//
// - From is not verified to be an EOA
// - GasLimit is not checked against the protocol defined tx gaslimit
SkipTransactionChecks bool
} }
// TransactionToMessage converts a transaction into a Message. // TransactionToMessage converts a transaction into a Message.
@ -182,7 +186,7 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In
AccessList: tx.AccessList(), AccessList: tx.AccessList(),
SetCodeAuthorizations: tx.SetCodeAuthorizations(), SetCodeAuthorizations: tx.SetCodeAuthorizations(),
SkipNonceChecks: false, SkipNonceChecks: false,
SkipFromEOACheck: false, SkipTransactionChecks: false,
BlobHashes: tx.BlobHashes(), BlobHashes: tx.BlobHashes(),
BlobGasFeeCap: tx.BlobGasFeeCap(), BlobGasFeeCap: tx.BlobGasFeeCap(),
} }
@ -320,7 +324,12 @@ func (st *stateTransition) preCheck() error {
msg.From.Hex(), stNonce) msg.From.Hex(), stNonce)
} }
} }
if !msg.SkipFromEOACheck { isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time)
if !msg.SkipTransactionChecks {
// Verify tx gas limit does not exceed EIP-7825 cap.
if isOsaka && msg.GasLimit > params.MaxTxGas {
return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit)
}
// Make sure the sender is an EOA // Make sure the sender is an EOA
code := st.state.GetCode(msg.From) code := st.state.GetCode(msg.From)
_, delegated := types.ParseDelegation(code) _, delegated := types.ParseDelegation(code)
@ -354,7 +363,6 @@ func (st *stateTransition) preCheck() error {
} }
} }
// Check the blob version validity // Check the blob version validity
isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time)
if msg.BlobHashes != nil { if msg.BlobHashes != nil {
// The to field of a blob tx type is mandatory, and a `BlobTx` transaction internally // The to field of a blob tx type is mandatory, and a `BlobTx` transaction internally
// has it as a non-nillable value, so any msg derived from blob transaction has it non-nil. // has it as a non-nillable value, so any msg derived from blob transaction has it non-nil.
@ -398,10 +406,6 @@ func (st *stateTransition) preCheck() error {
return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From) return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From)
} }
} }
// Verify tx gas limit does not exceed EIP-7825 cap.
if isOsaka && msg.GasLimit > params.MaxTxGas {
return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit)
}
return st.buyGas() return st.buyGas()
} }

View file

@ -984,7 +984,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
return nil, err return nil, err
} }
var ( var (
msg = args.ToMessage(blockContext.BaseFee, true, true) msg = args.ToMessage(blockContext.BaseFee, true)
tx = args.ToTransaction(types.LegacyTxType) tx = args.ToTransaction(types.LegacyTxType)
traceConfig *TraceConfig traceConfig *TraceConfig
) )

View file

@ -699,15 +699,15 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S
} else { } else {
gp.AddGas(globalGasCap) gp.AddGas(globalGasCap)
} }
return applyMessage(ctx, b, args, state, header, timeout, gp, &blockCtx, &vm.Config{NoBaseFee: true}, precompiles, true) return applyMessage(ctx, b, args, state, header, timeout, gp, &blockCtx, &vm.Config{NoBaseFee: true}, precompiles)
} }
func applyMessage(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, timeout time.Duration, gp *core.GasPool, blockContext *vm.BlockContext, vmConfig *vm.Config, precompiles vm.PrecompiledContracts, skipChecks bool) (*core.ExecutionResult, error) { func applyMessage(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, timeout time.Duration, gp *core.GasPool, blockContext *vm.BlockContext, vmConfig *vm.Config, precompiles vm.PrecompiledContracts) (*core.ExecutionResult, error) {
// Get a new instance of the EVM. // Get a new instance of the EVM.
if err := args.CallDefaults(gp.Gas(), blockContext.BaseFee, b.ChainConfig().ChainID); err != nil { if err := args.CallDefaults(gp.Gas(), blockContext.BaseFee, b.ChainConfig().ChainID); err != nil {
return nil, err return nil, err
} }
msg := args.ToMessage(header.BaseFee, skipChecks, skipChecks) msg := args.ToMessage(header.BaseFee, true)
// Lower the basefee to 0 to avoid breaking EVM // Lower the basefee to 0 to avoid breaking EVM
// invariants (basefee < feecap). // invariants (basefee < feecap).
if msg.GasPrice.Sign() == 0 { if msg.GasPrice.Sign() == 0 {
@ -858,7 +858,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
if err := args.CallDefaults(gasCap, header.BaseFee, b.ChainConfig().ChainID); err != nil { if err := args.CallDefaults(gasCap, header.BaseFee, b.ChainConfig().ChainID); err != nil {
return 0, err return 0, err
} }
call := args.ToMessage(header.BaseFee, true, true) call := args.ToMessage(header.BaseFee, true)
// Run the gas estimation and wrap any revertals into a custom return // Run the gas estimation and wrap any revertals into a custom return
estimate, revert, err := gasestimator.Estimate(ctx, call, opts, gasCap) estimate, revert, err := gasestimator.Estimate(ctx, call, opts, gasCap)
@ -1301,7 +1301,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH
statedb := db.Copy() statedb := db.Copy()
// Set the accesslist to the last al // Set the accesslist to the last al
args.AccessList = &accessList args.AccessList = &accessList
msg := args.ToMessage(header.BaseFee, true, true) msg := args.ToMessage(header.BaseFee, true)
// Apply the transaction with the access list tracer // Apply the transaction with the access list tracer
tracer := logger.NewAccessListTracer(accessList, addressesToExclude) tracer := logger.NewAccessListTracer(accessList, addressesToExclude)

View file

@ -287,7 +287,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
tracer.reset(txHash, uint(i)) tracer.reset(txHash, uint(i))
sim.state.SetTxContext(txHash, i) sim.state.SetTxContext(txHash, i)
// EoA check is always skipped, even in validation mode. // EoA check is always skipped, even in validation mode.
msg := call.ToMessage(header.BaseFee, !sim.validate, true) msg := call.ToMessage(header.BaseFee, !sim.validate)
result, err := applyMessageWithEVM(ctx, evm, msg, timeout, sim.gp) result, err := applyMessageWithEVM(ctx, evm, msg, timeout, sim.gp)
if err != nil { if err != nil {
txErr := txValidationError(err) txErr := txValidationError(err)

View file

@ -443,7 +443,7 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int,
// core evm. This method is used in calls and traces that do not require a real // core evm. This method is used in calls and traces that do not require a real
// live transaction. // live transaction.
// Assumes that fields are not nil, i.e. setDefaults or CallDefaults has been called. // Assumes that fields are not nil, i.e. setDefaults or CallDefaults has been called.
func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck, skipEoACheck bool) *core.Message { func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *core.Message {
var ( var (
gasPrice *big.Int gasPrice *big.Int
gasFeeCap *big.Int gasFeeCap *big.Int
@ -491,7 +491,7 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck, skipEoA
BlobHashes: args.BlobHashes, BlobHashes: args.BlobHashes,
SetCodeAuthorizations: args.AuthorizationList, SetCodeAuthorizations: args.AuthorizationList,
SkipNonceChecks: skipNonceCheck, SkipNonceChecks: skipNonceCheck,
SkipFromEOACheck: skipEoACheck, SkipTransactionChecks: true,
} }
} }