diff --git a/core/arbitrum_hooks.go b/core/arbitrum_hooks.go index f959395b7a..ea93ebefc5 100644 --- a/core/arbitrum_hooks.go +++ b/core/arbitrum_hooks.go @@ -45,8 +45,8 @@ var InterceptRPCMessage = func( // Gets ArbOS's maximum intended gas per second var GetArbOSSpeedLimitPerSecond func(statedb *state.StateDB) (uint64, error) -// Allows ArbOS to update the gas cap so that it ignores the message's specific L1 poster costs. -var InterceptRPCGasCap = func(gascap *uint64, msg *Message, header *types.Header, statedb *state.StateDB) {} +// While processing RPC only - Ask ArbOS what are the poster costs for this message. +var RPCPostingGasHook = func(msg *Message, header *types.Header, statedb *state.StateDB) (uint64, error) { return 0, nil } // Renders a solidity error in human-readable form var RenderRPCError func(data []byte) error diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 82ac3fde53..94307f3f06 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -945,12 +945,11 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc config.BlockOverrides.Apply(&vmctx) } // Execute the trace - gasLimitNotSetByUser := args.Gas == nil - if err := args.CallDefaults(api.backend.RPCGasCap(), vmctx.BaseFee, api.backend.ChainConfig().ChainID, gasLimitNotSetByUser); err != nil { + if err := args.CallDefaults(api.backend.RPCGasCap(), vmctx.BaseFee, api.backend.ChainConfig().ChainID); err != nil { return nil, err } var ( - msg = args.ToMessage(vmctx.BaseFee, api.backend.RPCGasCap(), block.Header(), statedb, core.MessageEthcallMode, api.backend.ChainConfig().ChainID, gasLimitNotSetByUser) + msg = args.ToMessage(vmctx.BaseFee, api.backend.RPCGasCap(), block.Header(), statedb, core.MessageEthcallMode) tx = args.ToTransaction() traceConfig *TraceConfig ) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index b3e4629fc8..5ce1e3d7db 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1130,11 +1130,10 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S // Get a new instance of the EVM. var err error - gasLimitNotSetByUser := args.Gas == nil - if err = args.CallDefaults(globalGasCap, blockCtx.BaseFee, b.ChainConfig().ChainID, gasLimitNotSetByUser); err != nil { + if err = args.CallDefaults(globalGasCap, blockCtx.BaseFee, b.ChainConfig().ChainID); err != nil { return nil, err } - msg := args.ToMessage(blockCtx.BaseFee, globalGasCap, header, state, runMode, b.ChainConfig().ChainID, gasLimitNotSetByUser) + msg := args.ToMessage(blockCtx.BaseFee, globalGasCap, header, state, runMode) // Arbitrum: support NodeInterface.sol by swapping out the message if needed var res *core.ExecutionResult @@ -1296,20 +1295,20 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr ErrorRatio: gasestimator.EstimateGasErrorRatio, RunScheduledTxes: runScheduledTxes, } - gasLimitNotSetByUser := args.Gas == nil - if err := args.CallDefaults(gasCap, header.BaseFee, b.ChainConfig().ChainID, gasLimitNotSetByUser); err != nil { + if err := args.CallDefaults(gasCap, header.BaseFee, b.ChainConfig().ChainID); err != nil { return 0, err } // Run the gas estimation andwrap any revertals into a custom return // Arbitrum: this also appropriately recursively calls another args.ToMessage with increased gasCap by posterCostInL2Gas amount - call := args.ToMessage(header.BaseFee, gasCap, header, state, core.MessageGasEstimationMode, b.ChainConfig().ChainID, gasLimitNotSetByUser) + call := args.ToMessage(header.BaseFee, gasCap, header, state, core.MessageGasEstimationMode) // Arbitrum: raise the gas cap to ignore L1 costs so that it's compute-only { - gasCap, err = args.L2OnlyGasCap(header.BaseFee, gasCap, header, state, core.MessageGasEstimationMode, b.ChainConfig().ChainID, gasLimitNotSetByUser) + postingGas, err := core.RPCPostingGasHook(call, header, state) if err != nil { return 0, err } + gasCap += postingGas } estimate, revert, err := gasestimator.Estimate(ctx, call, opts, gasCap) @@ -1725,7 +1724,6 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH } // Ensure any missing fields are filled, extract the recipient and input data - gasLimitNotSetByUser := args.Gas == nil if err := args.setDefaults(ctx, b, true); err != nil { return nil, 0, nil, err } @@ -1753,7 +1751,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH statedb := db.Copy() // Set the accesslist to the last al args.AccessList = &accessList - msg := args.ToMessage(header.BaseFee, b.RPCGasCap(), header, statedb, core.MessageEthcallMode, b.ChainConfig().ChainID, gasLimitNotSetByUser) + msg := args.ToMessage(header.BaseFee, b.RPCGasCap(), header, statedb, core.MessageEthcallMode) // Apply the transaction with the access list tracer tracer := logger.NewAccessListTracer(accessList, args.from(), to, precompiles) diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 692de5d09b..197584a74e 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -76,6 +76,9 @@ type TransactionArgs struct { // This configures whether blobs are allowed to be passed. blobSidecarAllowed bool + + // was gas originally set by user + gasNotSetByUser bool } // from retrieves the transaction sender address. @@ -139,6 +142,7 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, skipGas } if args.Gas == nil { + args.gasNotSetByUser = true if skipGasEstimation { // Skip gas usage estimation if a precise gas limit is not critical, e.g., in non-transaction calls. gas := hexutil.Uint64(b.RPCGasCap()) if gas == 0 { @@ -373,9 +377,25 @@ func (args *TransactionArgs) setBlobTxSidecar(ctx context.Context) error { return nil } +func (args *TransactionArgs) setGasUsingCap(globalGasCap uint64) { + args.gasNotSetByUser = args.gasNotSetByUser || (args.Gas == nil) + if args.gasNotSetByUser { + gas := globalGasCap + if gas == 0 { + gas = uint64(math.MaxUint64 / 2) + } + args.Gas = (*hexutil.Uint64)(&gas) + } else { + if globalGasCap > 0 && globalGasCap < uint64(*args.Gas) { + log.Warn("Caller gas above allowance, capping", "requested", args.Gas, "cap", globalGasCap) + args.Gas = (*hexutil.Uint64)(&globalGasCap) + } + } +} + // CallDefaults sanitizes the transaction arguments, often filling in zero values, // for the purpose of eth_call class of RPC methods. -func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, chainID *big.Int, gasLimitNotSetByUser bool) error { +func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, chainID *big.Int) error { // Reject invalid combinations of pre- and post-1559 fee styles if args.GasPrice != nil && ((args.MaxFeePerGas != nil && args.MaxFeePerGas.ToInt().Cmp(common.Big0) != 0) || (args.MaxPriorityFeePerGas != nil && args.MaxPriorityFeePerGas.ToInt().Cmp(common.Big0) != 0)) { return errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") @@ -387,18 +407,7 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, return fmt.Errorf("chainId does not match node's (have=%v, want=%v)", have, chainID) } } - if gasLimitNotSetByUser { - gas := globalGasCap - if gas == 0 { - gas = uint64(math.MaxUint64 / 2) - } - args.Gas = (*hexutil.Uint64)(&gas) - } else { - if globalGasCap > 0 && globalGasCap < uint64(*args.Gas) { - log.Warn("Caller gas above allowance, capping", "requested", args.Gas, "cap", globalGasCap) - args.Gas = (*hexutil.Uint64)(&globalGasCap) - } - } + args.setGasUsingCap(globalGasCap) if args.Nonce == nil { args.Nonce = new(hexutil.Uint64) } @@ -427,7 +436,7 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, } // Assumes that fields are not nil, i.e. setDefaults or CallDefaults has been called. -func (args *TransactionArgs) ToMessage(baseFee *big.Int, globalGasCap uint64, header *types.Header, state *state.StateDB, runMode core.MessageRunMode, chainID *big.Int, gasLimitNotSetByUser bool) *core.Message { +func (args *TransactionArgs) ToMessage(baseFee *big.Int, globalGasCap uint64, header *types.Header, state *state.StateDB, runMode core.MessageRunMode) *core.Message { var ( gasPrice *big.Int gasFeeCap *big.Int @@ -480,28 +489,20 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, globalGasCap uint64, he SkipL1Charging: skipL1Charging, } // Arbitrum: raise the gas cap to ignore L1 costs so that it's compute-only - if state != nil { + if state != nil && !skipL1Charging { // ToMessage recurses once to allow ArbOS to intercept the result for all callers // ArbOS uses this to modify globalGasCap so that the cap will ignore this tx's specific L1 data costs - core.InterceptRPCGasCap(&globalGasCap, msg, header, state) - err := args.CallDefaults(globalGasCap, baseFee, chainID, gasLimitNotSetByUser) - // If we fail to call defaults, we should panic because it's a programming error since we've already called it once - if err != nil { - panic(fmt.Sprintf("CallDefaults failed: %v", err)) + postingGas, err := core.RPCPostingGasHook(msg, header, state) + if err == nil { + args.setGasUsingCap(globalGasCap + postingGas) + msg.GasLimit = uint64(*args.Gas) + } else { + log.Error("error reading posting gas", "err", err) } - return args.ToMessage(baseFee, globalGasCap, header, nil, runMode, chainID, gasLimitNotSetByUser) // we pass a nil to avoid another recursion } return msg } -// Raises the vanilla gas cap by the tx's l1 data costs in l2 terms. This creates a new gas cap that after -// data payments are made, equals the original vanilla cap for the remaining, L2-specific work the tx does. -func (args *TransactionArgs) L2OnlyGasCap(baseFee *big.Int, gasCap uint64, header *types.Header, state *state.StateDB, runMode core.MessageRunMode, chainID *big.Int, gasLimitNotSetByUser bool) (uint64, error) { - msg := args.ToMessage(baseFee, gasCap, header, nil, runMode, chainID, gasLimitNotSetByUser) - core.InterceptRPCGasCap(&gasCap, msg, header, state) - return gasCap, nil -} - // ToTransaction converts the arguments to a transaction. // This assumes that setDefaults has been called. func (args *TransactionArgs) ToTransaction() *types.Transaction {