From 5963fc8408aba77d4b4fe1aaf5faa38ac1c1effe Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Mon, 18 May 2026 11:19:25 +0800 Subject: [PATCH] core: improve EIP-8037 implementations --- cmd/evm/internal/t8ntool/transaction.go | 3 +- core/bench_test.go | 3 +- core/chain_makers.go | 2 +- core/evm.go | 11 +- core/gaspool.go | 21 +- core/state/statedb.go | 28 --- core/state/statedb_hooked.go | 8 - core/state_processor.go | 30 ++- core/state_transition.go | 291 ++++++++++++++---------- core/tracing/hooks.go | 4 + core/txpool/validation.go | 12 +- core/vm/contract.go | 14 +- core/vm/contracts.go | 2 +- core/vm/evm.go | 69 +++--- core/vm/gas_table.go | 41 ++-- core/vm/gascosts.go | 6 +- core/vm/instructions.go | 33 ++- core/vm/interface.go | 8 - core/vm/jump_table.go | 3 + core/vm/operations_acl.go | 23 +- params/protocol_params.go | 7 +- tests/transaction_test_util.go | 2 +- 22 files changed, 333 insertions(+), 288 deletions(-) diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index 26ae11e0fb..9eb1bdbf5f 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -133,8 +133,7 @@ func Transaction(ctx *cli.Context) error { } // Check intrinsic gas rules := chainConfig.Rules(common.Big0, true, 0) - gasCostPerStateByte := core.CostPerStateByte(&types.Header{}, chainConfig) - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, gasCostPerStateByte) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte) if err != nil { r.Error = err results = append(results, r) diff --git a/core/bench_test.go b/core/bench_test.go index d49062af06..fe66aeae0d 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -89,8 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) { data := make([]byte, nbytes) return func(i int, gen *BlockGen) { toaddr := common.Address{} - gasCostPerStateByte := CostPerStateByte(gen.header, gen.cm.config) - cost, _ := IntrinsicGas(data, nil, nil, false, params.Rules{}, gasCostPerStateByte) + cost, _ := IntrinsicGas(data, nil, nil, false, params.Rules{}, params.CostPerStateByte) signer := gen.Signer() gasPrice := big.NewInt(0) if gen.header.BaseFee != nil { diff --git a/core/chain_makers.go b/core/chain_makers.go index cb78739882..2e856b5161 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -168,7 +168,7 @@ func (b *BlockGen) AddTxWithChain(bc *BlockChain, tx *types.Transaction) { // been set, the block's coinbase is set to the zero address. // The evm interpreter can be customized with the provided vm config. func (b *BlockGen) AddTxWithVMConfig(tx *types.Transaction, config vm.Config) { - b.addTx(&BlockChain{chainConfig: b.cm.config}, config, tx) + b.addTx(nil, config, tx) } // GetBalance returns the balance of the given address at the generated block. diff --git a/core/evm.go b/core/evm.go index 6e3938da5b..fdea63e469 100644 --- a/core/evm.go +++ b/core/evm.go @@ -80,19 +80,10 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common GasLimit: header.GasLimit, Random: random, SlotNum: slotNum, - CostPerStateByte: CostPerStateByte(header, chain.Config()), + CostPerStateByte: params.CostPerStateByte, } } -// CostPerStateByte computes the cost per one byte of state creation -// after EIP-8037. -func CostPerStateByte(header *types.Header, config *params.ChainConfig) uint64 { - if !config.IsAmsterdam(header.Number, header.Time) { - return 0 - } - return params.CostPerStateByte -} - // NewEVMTxContext creates a new transaction context for a single transaction. func NewEVMTxContext(msg *Message) vm.TxContext { ctx := vm.TxContext{ diff --git a/core/gaspool.go b/core/gaspool.go index 82323aafc4..2fb6416795 100644 --- a/core/gaspool.go +++ b/core/gaspool.go @@ -42,9 +42,9 @@ func NewGasPool(amount uint64) *GasPool { } } -// SubGas deducts the given amount from the pool if enough gas is +// CheckGasLegacy deducts the given amount from the pool if enough gas is // available and returns an error otherwise. -func (gp *GasPool) SubGas(amount uint64) error { +func (gp *GasPool) CheckGasLegacy(amount uint64) error { if gp.remaining < amount { return ErrGasLimitReached } @@ -65,31 +65,24 @@ func (gp *GasPool) CheckGasAmsterdam(regularReservation, stateReservation uint64 return nil } -// ReturnGas adds the refunded gas back to the pool and updates +// ChargeGasLegacy adds the refunded gas back to the pool and updates // the cumulative gas usage accordingly. -func (gp *GasPool) ReturnGas(returned uint64, gasUsed uint64) error { +func (gp *GasPool) ChargeGasLegacy(returned uint64, gasUsed uint64) error { if gp.remaining > math.MaxUint64-returned { return fmt.Errorf("%w: remaining: %d, returned: %d", ErrGasLimitOverflow, gp.remaining, returned) } - // The returned gas calculation differs across forks. - // - // - Pre-Amsterdam: - // returned = purchased - remaining (refund included) - // - // - Post-Amsterdam: - // returned = purchased - gasUsed (refund excluded) + // returned = purchased - remaining (refund included) gp.remaining += returned // gasUsed = max(txGasUsed - gasRefund, calldataFloorGasCost) - // regardless of Amsterdam is activated or not. gp.cumulativeUsed += gasUsed return nil } // ChargeGasAmsterdam calculates the new remaining gas in the pool after the // execution of a message. Previously we subtracted and re-added gas to the -// gaspool. After Amsterdam we only check if we can include the transaction and charge the -// gaspool at the end. +// gaspool. After Amsterdam we only check if we can include the transaction +// and charge the gaspool at the end. func (gp *GasPool) ChargeGasAmsterdam(txRegular, txState, receiptGasUsed uint64) error { gp.cumulativeRegular += txRegular gp.cumulativeState += txState diff --git a/core/state/statedb.go b/core/state/statedb.go index 5e25cce5c8..1c49d46020 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -683,34 +683,6 @@ func (s *StateDB) IsNewContract(addr common.Address) bool { return obj.newContract } -// SameTxSelfDestructs returns the addresses that were both created and -// self-destructed in the current transaction (EIP-6780). -func (s *StateDB) SameTxSelfDestructs() []common.Address { - var out []common.Address - for addr, obj := range s.stateObjects { - if obj.newContract && obj.selfDestructed { - out = append(out, addr) - } - } - return out -} - -// NewStorageSlotCount returns the number of storage slots that were written -// to a non-zero value in the current transaction on the given account. -func (s *StateDB) NewStorageSlotCount(addr common.Address) int { - obj, ok := s.stateObjects[addr] - if !ok { - return 0 - } - var count int - for _, v := range obj.dirtyStorage { - if v != (common.Hash{}) { - count++ - } - } - return count -} - // Copy creates a deep, independent copy of the state. // Snapshots of the copied state cannot be applied to the copy. func (s *StateDB) Copy() *StateDB { diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 8b44751583..98d01343a4 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -59,14 +59,6 @@ func (s *hookedStateDB) IsNewContract(addr common.Address) bool { return s.inner.IsNewContract(addr) } -func (s *hookedStateDB) SameTxSelfDestructs() []common.Address { - return s.inner.SameTxSelfDestructs() -} - -func (s *hookedStateDB) NewStorageSlotCount(addr common.Address) int { - return s.inner.NewStorageSlotCount(addr) -} - func (s *hookedStateDB) GetBalance(addr common.Address) *uint256.Int { return s.inner.GetBalance(addr) } diff --git a/core/state_processor.go b/core/state_processor.go index 9af1682ce8..d0ce45078e 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -277,9 +277,15 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList defer tracer.OnSystemCallEnd() } } + // SYSTEM_MAX_SSTORES_PER_CALL = 16 is the upper bound on the number of + // new storage slots a single system call is expected to write. + // + // This value matches MAX_WITHDRAWAL_REQUESTS_PER_BLOCK (EIP-7002), the + // largest per-block bound across the existing system contracts. + stateBudget := params.SystemMaxSStoresPerCall * evm.Context.CostPerStateByte * params.StorageCreationSize msg := &Message{ From: params.SystemAddress, - GasLimit: 30_000_000, + GasLimit: 30_000_000 + stateBudget, GasPrice: uint256.NewInt(0), GasFeeCap: uint256.NewInt(0), GasTipCap: uint256.NewInt(0), @@ -290,7 +296,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) - _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, 0), common.U2560) + _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, stateBudget), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } @@ -306,9 +312,15 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList * defer tracer.OnSystemCallEnd() } } + // SYSTEM_MAX_SSTORES_PER_CALL = 16 is the upper bound on the number of + // new storage slots a single system call is expected to write. + // + // This value matches MAX_WITHDRAWAL_REQUESTS_PER_BLOCK (EIP-7002), the + // largest per-block bound across the existing system contracts. + stateBudget := params.SystemMaxSStoresPerCall * evm.Context.CostPerStateByte * params.StorageCreationSize msg := &Message{ From: params.SystemAddress, - GasLimit: 30_000_000, + GasLimit: 30_000_000 + stateBudget, GasPrice: uint256.NewInt(0), GasFeeCap: uint256.NewInt(0), GasTipCap: uint256.NewInt(0), @@ -319,7 +331,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList * evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) - _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, 0), common.U2560) + _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, stateBudget), common.U2560) if err != nil { panic(err) } @@ -348,9 +360,15 @@ func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.E defer tracer.OnSystemCallEnd() } } + // SYSTEM_MAX_SSTORES_PER_CALL = 16 is the upper bound on the number of + // new storage slots a single system call is expected to write. + // + // This value matches MAX_WITHDRAWAL_REQUESTS_PER_BLOCK (EIP-7002), the + // largest per-block bound across the existing system contracts. + stateBudget := params.SystemMaxSStoresPerCall * evm.Context.CostPerStateByte * params.StorageCreationSize msg := &Message{ From: params.SystemAddress, - GasLimit: 30_000_000, + GasLimit: 30_000_000 + stateBudget, GasPrice: uint256.NewInt(0), GasFeeCap: uint256.NewInt(0), GasTipCap: uint256.NewInt(0), @@ -360,7 +378,7 @@ func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.E evm.StateDB.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex) evm.StateDB.AddAddressToAccessList(addr) - ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, 0), common.U2560) + ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, stateBudget), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } diff --git a/core/state_transition.go b/core/state_transition.go index 7ac66b69f1..b026bbcbf9 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto/kzg4844" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -375,6 +376,24 @@ func (st *stateTransition) to() common.Address { return *st.msg.To } +// buyGas pre-pays gas from the sender's balance and initializes the +// transaction's gas budget. It is invoked at the tail of preCheck. +// +// The balance requirement is the worst-case ETH the tx may need to lock +// up: `msg.GasLimit × max(msg.GasPrice, msg.GasFeeCap) + msg.Value`, +// plus `blobGas × msg.BlobGasFeeCap` under Cancun. Insufficient balance +// returns ErrInsufficientFunds. After the check, the sender is actually +// debited `msg.GasLimit × msg.GasPrice` (plus `blobGas × blobBaseFee` +// under Cancun), the cap-vs-tip differential is settled at tx end. +// +// The gas budget is seeded into both `initialBudget` (frozen snapshot +// for tx-end accounting) and `gasRemaining` (live running balance): +// +// - Pre-Amsterdam: one-dimensional regular budget equal to +// `msg.GasLimit`; the state-gas reservoir is zero. +// - Amsterdam+ (EIP-8037): two-dimensional budget. Regular gas is +// capped at `MaxTxGas` (EIP-7825, 16_777_216); any excess from +// `msg.GasLimit` above that cap becomes the state-gas reservoir. func (st *stateTransition) buyGas() error { mgval := new(uint256.Int).SetUint64(st.msg.GasLimit) _, overflow := mgval.MulOverflow(mgval, st.msg.GasPrice) @@ -428,7 +447,7 @@ func (st *stateTransition) buyGas() error { return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) } - // After Amsterdam we limit the regular gas to 16k, the data gas to the transaction limit + // After Amsterdam we limit the regular gas to 16M, the data gas to the transaction limit limit := st.msg.GasLimit if st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) { limit = min(st.msg.GasLimit, params.MaxTxGas) @@ -437,13 +456,32 @@ func (st *stateTransition) buyGas() error { st.gasRemaining = st.initialBudget.Copy() if st.evm.Config.Tracer.HasGasHook() { - empty := vm.GasBudget{} - st.evm.Config.Tracer.EmitGasChange(empty.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxInitialBalance) + st.evm.Config.Tracer.EmitGasChange(tracing.Gas{}, st.gasRemaining.AsTracing(), tracing.GasChangeTxInitialBalance) } + // Deduct the gas cost from the sender's balance st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy) return nil } +// preCheck performs all pre-execution validation that does not require +// the EVM to run, then ends by calling buyGas to lock in the gas budget. +// It returns a consensus error if any of the following fail: +// +// - Sender nonce matches state and is not at 2^64-1 (EIP-2681). +// - EIP-7825 per-tx gas-limit cap on Osaka chains pre-Amsterdam +// (the cap also bounds the regular dimension after Amsterdam, but +// it is enforced there via the two-dimensional budget in buyGas). +// - EIP-3607 sender-is-EOA, allowing accounts whose only code is an +// EIP-7702 delegation designator. +// - EIP-1559 fee-cap, tip-cap and base-fee constraints (London+). +// - Blob-tx structural checks: non-nil `To`, non-empty hash list, +// valid KZG versioned hashes, count below `BlobTxMaxBlobs` (Osaka+). +// - Blob fee-cap not below the current blob base fee (Cancun+). +// - EIP-7702 set-code-tx shape: non-nil `To` and non-empty +// authorization list. +// +// The SkipNonceChecks / SkipTransactionChecks / NoBaseFee flags bypass +// subsets of these checks for simulation paths (eth_call, eth_estimateGas). func (st *stateTransition) preCheck() error { // Only check transactions that are not fake msg := st.msg @@ -461,8 +499,10 @@ func (st *stateTransition) preCheck() error { msg.From.Hex(), stNonce) } } - isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time) - isAmsterdam := st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) + var ( + isOsaka = st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time) + isAmsterdam = st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) + ) if !msg.SkipTransactionChecks { // Verify tx gas limit does not exceed EIP-7825 cap. if !isAmsterdam && isOsaka && msg.GasLimit > params.MaxTxGas { @@ -539,28 +579,72 @@ func (st *stateTransition) preCheck() error { return st.buyGas() } -// execute will transition the state by applying the current message and -// returning the evm execution result with following fields. -// -// - used gas: total gas used (including gas being refunded) -// - returndata: the returned data from evm -// - concrete execution error: various EVM errors which abort the execution, e.g. -// ErrOutOfGas, ErrExecutionReverted -// -// However if any consensus issue encountered, return the error directly with -// nil evm execution result. -func (st *stateTransition) execute() (*ExecutionResult, error) { - // First check this message satisfies all consensus rules before - // applying the message. The rules include these clauses - // - // 1. the nonce of the message caller is correct - // 2. caller has enough balance to cover transaction fee(gaslimit * gasprice) - // 3. the amount of gas required is available in the block - // 4. the purchased gas is enough to cover intrinsic usage - // 5. there is no overflow when calculating intrinsic gas - // 6. caller has enough balance to cover asset transfer for **topmost** call +// reserveBlockGasBudget checks if the remaining gas budget in the block pool is +// sufficient for including this transaction. +func (st *stateTransition) reserveBlockGasBudget(rules params.Rules, gasLimit uint64, intrinsicCost vm.GasCosts) error { + var err error + if rules.IsAmsterdam { + // EIP-8037 per-tx 2D block-inclusion check. For each dimension, + // the worst-case contribution is tx.gas minus the other + // dimension's intrinsic (capped at MaxTxGas for the regular + // dimension). + regularReservation := gasLimit + if regularReservation > intrinsicCost.StateGas { + regularReservation -= intrinsicCost.StateGas + } else { + regularReservation = 0 + } + regularReservation = min(regularReservation, params.MaxTxGas) - // Check clauses 1-3, buy gas if everything is correct + stateReservation := gasLimit + if stateReservation > intrinsicCost.RegularGas { + stateReservation -= intrinsicCost.RegularGas + } else { + stateReservation = 0 + } + err = st.gp.CheckGasAmsterdam(regularReservation, stateReservation) + } else { + err = st.gp.CheckGasLegacy(gasLimit) + } + return err +} + +// execute transitions the state by applying the current message and +// returns the EVM execution result with the following fields: +// +// - used gas: total gas used, including gas refunded +// - peak used gas: maximum gas used before applying refunds +// - returndata: data returned by the EVM +// - execution error: EVM-level errors that abort execution, such as +// ErrOutOfGas or ErrExecutionReverted +// +// If a consensus error is encountered, it is returned directly with a +// nil EVM execution result. +func (st *stateTransition) execute() (*ExecutionResult, error) { + // The state-transition pipeline below runs in stages. Each stage may + // abort with a consensus error before the EVM is invoked: + // + // 1. preCheck: nonce, fee-cap, blob and EIP-7702 structural + // checks; ends by calling buyGas to debit the + // sender and seed the two-dimensional gas budget + // (EIP-8037). + // 2. Intrinsic: charges the intrinsic regular + state cost from + // the running budget with overflow detection. + // 3. Block pool: per-dimension inclusion reservation against the + // block gas pool (two-dimensional after Amsterdam, + // EIP-8037). + // 4. Floor pre: EIP-7623 calldata floor must fit in the gas allowance. + // 5. Top-call: run the top-most call, ensuring sender can cover + // the value transfer of the top call frame; init-code + // size respects the cap. + // + // After the EVM has run, the result path applies EIP-8037 state-gas + // refunds, the EIP-3529 regular-refund cap, and the EIP-7623 scalar + // floor (`tx_gas_used = max(tx_gas_used_after_refund, floor)`), + // returns leftover gas to the sender, settles the block pool and + // pays the coinbase tip. + + // Stage 1: validate the message and pre-pay gas. if err := st.preCheck(); err != nil { return nil, err } @@ -572,7 +656,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { floorDataGas uint64 ) - // Check clauses 4-5, subtract intrinsic gas if everything is correct + // Stage 2: charge intrinsic gas (with overflow detection inside + // IntrinsicGas). Under Amsterdam the cost is two-dimensional and + // Charge debits both regular and state in one step. cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules, st.evm.Context.CostPerStateByte) if err != nil { return nil, err @@ -585,62 +671,33 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) } - // Check if we have enough gas in the block - if rules.IsAmsterdam { - // EIP-8037 per-tx 2D block-inclusion check. For each dimension, - // the worst-case contribution is tx.gas minus the other - // dimension's intrinsic (capped at MaxTxGas for the regular - // dimension). - regularReservation := msg.GasLimit - if regularReservation > cost.StateGas { - regularReservation -= cost.StateGas - } else { - regularReservation = 0 - } - regularReservation = min(regularReservation, params.MaxTxGas) - - stateReservation := msg.GasLimit - if stateReservation > cost.RegularGas { - stateReservation -= cost.RegularGas - } else { - stateReservation = 0 - } - if err := st.gp.CheckGasAmsterdam(regularReservation, stateReservation); err != nil { - return nil, err - } - } else { - if err := st.gp.SubGas(msg.GasLimit); err != nil { - return nil, err - } + // Stage 3: reserve this tx's share of the block gas pool. Under + // Amsterdam this is a two-dimensional per-tx inclusion check; before + // Amsterdam it is a single scalar subtraction. + if err := st.reserveBlockGasBudget(rules, msg.GasLimit, cost); err != nil { + return nil, err } - // Compute the floor data cost (EIP-7623), needed for both Prague and Amsterdam validation. + // Stage 4: validate the EIP-7623 calldata floor against the gas limit. + // The floor inflates the total gas usage at tx end, so the gas limit + // must be sufficient to cover that. if rules.IsPrague { floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList) if err != nil { return nil, err } + // Make sure the transaction has sufficient gas allowance to + // pay the floor cost. if msg.GasLimit < floorDataGas { return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, msg.GasLimit, floorDataGas) } - } - - if rules.IsAmsterdam { - if cost.Sum() > msg.GasLimit { - return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, msg.GasLimit, cost.Sum()) + // In Amsterdam, the transaction gas limit is allowed to exceed + // params.MaxTxGas, but the calldata floor cost is capped by it. + if rules.IsAmsterdam { + if max(cost.RegularGas, floorDataGas) > params.MaxTxGas { + return nil, fmt.Errorf("%w: regular intrisic cost %v, floor: %v", ErrFloorDataGas, cost.RegularGas, floorDataGas) + } } - // RegularGas must by < 16M - maxRegularGas := max(cost.RegularGas, floorDataGas) - if maxRegularGas > params.MaxTxGas { - return nil, fmt.Errorf("%w: max regular gas %d exceeds limit %d", ErrIntrinsicGas, maxRegularGas, params.MaxTxGas) - } - } - before, ok := st.gasRemaining.Charge(cost) - if !ok { - return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas) - } - if st.evm.Config.Tracer.HasGasHook() { - st.evm.Config.Tracer.EmitGasChange(before.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) } if rules.IsEIP4762 { @@ -651,7 +708,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } } - // Check clause 6 + // Stage 5: top-call affordability, the sender must still be able + // to cover the value transfer of the top frame after gas pre-pay. value := msg.Value if value == nil { value = new(uint256.Int) @@ -689,7 +747,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if msg.SetCodeAuthorizations != nil { for _, auth := range msg.SetCodeAuthorizations { // Note errors are ignored, we simply skip invalid authorizations here. - _ = st.applyAuthorization(rules, &auth) + st.applyAuthorization(rules, &auth) } } @@ -706,58 +764,44 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) st.gasRemaining.Absorb(result) } + // If this was a failed contract creation, refund the account creation costs. if rules.IsAmsterdam { - if vmerr != nil { - // If this was a contract creation, refund the account creation costs. - if contractCreation { - refund := params.AccountCreationSize * st.evm.Context.CostPerStateByte - st.gasRemaining.RefundState(refund) - } - } else { - // Compute refunds for selfdestructed slots - cpsb := st.evm.Context.CostPerStateByte - var sdRefund uint64 - for _, addr := range st.state.SameTxSelfDestructs() { - r := params.AccountCreationSize * cpsb - r += uint64(st.state.NewStorageSlotCount(addr)) * params.StorageCreationSize * cpsb - r += uint64(st.state.GetCodeSize(addr)) * cpsb - sdRefund += r - } - if sdRefund > 0 { - st.gasRemaining.RefundState(sdRefund) - } + if vmerr != nil && contractCreation { + refund := params.AccountCreationSize * st.evm.Context.CostPerStateByte + st.gasRemaining.RefundState(refund) } } // Record the gas used excluding gas refunds. This value represents the actual // gas allowance required to complete execution. peakGasUsed := st.gasUsed() + peakRegular := st.gasRemaining.UsedRegularGas // Compute refund counter, capped to a refund quotient. st.gasRemaining.RefundRegular(st.calcRefund()) if rules.IsPrague { - // After EIP-7623: Data-heavy transactions pay the floor gas. + // We can always guarantee that the initial regular gas allowance + // is sufficient to cover the floor cost. + // + // Pre-Amsterdam, there is a single dimension and gas limit is greater + // than the floor cost. + // + // Since Amsterdam: + // - If GasLimit <= 16M, the state reservoir is initialized to 0, + // and regular_gas_budget >= floor_cost always holds. + // - If GasLimit > 16M, the state reservoir is non-zero, while + // regular_gas_budget == 16M, which is still guaranteed to be + // greater than the floor cost. The extra cost should be deducted + // from the regular even the state reservoir is non-zero. if used := st.gasUsed(); used < floorDataGas { - /* - prior, _ := st.gasRemaining.Charge(vm.GasCosts{RegularGas: floorDataGas - used}) - if st.evm.Config.Tracer.HasGasHook() { - st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxDataFloor) - } - */ - prev := st.gasRemaining.RegularGas - // When the calldata floor exceeds actual gas used, any - // remaining state gas must also be consumed. - targetRemaining := (st.initialBudget.RegularGas + st.initialBudget.StateGas) - floorDataGas - st.gasRemaining.StateGas = 0 - st.gasRemaining.RegularGas = targetRemaining - if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { - t.OnGasChange(prev, st.gasRemaining.RegularGas, tracing.GasChangeTxDataFloor) + prior, _ := st.gasRemaining.ChargeRegular(floorDataGas - used) + if st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxDataFloor) } } - if peakGasUsed < floorDataGas { - peakGasUsed = floorDataGas - } + peakGasUsed = max(peakGasUsed, floorDataGas) + peakRegular = max(peakRegular, floorDataGas) } returned := st.returnGas() @@ -766,19 +810,21 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // st.gasRemaining.UsedRegularGas / UsedStateGas already include both // the intrinsic charge (from st.gasRemaining.Charge(cost) above) and // the per-frame exec contributions absorbed from evm.Call / evm.Create. - // UsedStateGas may be negative when inline refunds (SSTORE 0→x→0, - // CREATE-failure refund, 7702 auth refund, same-tx-SD refund) exceed - // intrinsic + exec state charges. Clamp at 0. + // + // UsedStateGas should never become negative in the top-most frame, since + // state gas refunds only occur when state creation is reverted within the + // same transaction, while clearing pre-existing state is never refunded. var txState uint64 - if st.gasRemaining.UsedStateGas > 0 { + if st.gasRemaining.UsedStateGas >= 0 { txState = uint64(st.gasRemaining.UsedStateGas) + } else { + log.Error("Negative top-most frame state gas usage", "amount", st.gasRemaining.UsedStateGas) } - txRegular := max(st.gasRemaining.UsedRegularGas, floorDataGas) - if err := st.gp.ChargeGasAmsterdam(txRegular, txState, st.gasUsed()); err != nil { + if err := st.gp.ChargeGasAmsterdam(peakRegular, txState, st.gasUsed()); err != nil { return nil, err } } else { - if err = st.gp.ReturnGas(returned, st.gasUsed()); err != nil { + if err = st.gp.ChargeGasLegacy(returned, st.gasUsed()); err != nil { return nil, err } } @@ -909,18 +955,15 @@ func (st *stateTransition) calcRefund() uint64 { return refund } -// returnGas returns ETH for remaining gas, -// exchanged at the original rate. +// returnGas returns ETH for remaining gas, exchanged at the original rate. func (st *stateTransition) returnGas() uint64 { gas := st.gasRemaining.RegularGas + st.gasRemaining.StateGas remaining := uint256.NewInt(st.gasRemaining.RegularGas) remaining.Mul(remaining, st.msg.GasPrice) st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) - if st.gasRemaining.RegularGas > 0 && st.evm.Config.Tracer.HasGasHook() { - after := st.gasRemaining - after.RegularGas = 0 - st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxLeftOverReturned) + if !st.gasRemaining.IsZero() && st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), tracing.Gas{}, tracing.GasChangeTxLeftOverReturned) } return gas } diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index 6ea3f7ebbf..72cbc8a298 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -472,6 +472,10 @@ const ( // transaction data. This change will always be a negative change. GasChangeTxDataFloor GasChangeReason = 19 + // GasChangeStateGasRefund represents the amount of pre-charged state gas + // refunded back to the state reservoir. + GasChangeStateGasRefund GasChangeReason = 20 + // GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as // it will be "manually" tracked by a direct emit of the gas change event. GasChangeIgnored GasChangeReason = 0xFF diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 439d5c68ff..5f8463729b 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -125,8 +125,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction has more gas than the bare minimum needed to cover // the transaction metadata - gasCostPerStateByte := core.CostPerStateByte(head, opts.Config) - intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, gasCostPerStateByte) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte) if err != nil { return err } @@ -139,9 +138,18 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types if err != nil { return err } + // Make sure the transaction has sufficient gas allowance to + // pay the floor cost. if tx.Gas() < floorDataGas { return fmt.Errorf("%w: gas %v, minimum needed %v", core.ErrFloorDataGas, tx.Gas(), floorDataGas) } + // In Amsterdam, the transaction gas limit is allowed to exceed + // params.MaxTxGas, but the calldata floor cost is capped by it. + if rules.IsAmsterdam { + if max(intrGas.RegularGas, floorDataGas) > params.MaxTxGas { + return fmt.Errorf("%w: regular intrisic cost %v, floor: %v", core.ErrFloorDataGas, intrGas.RegularGas, floorDataGas) + } + } } // Ensure the gasprice is high enough to cover the requirement of the calling pool if tx.GasTipCapIntCmp(opts.MinTip) < 0 { diff --git a/core/vm/contract.go b/core/vm/contract.go index 03360ebaff..11172af24c 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -152,9 +152,19 @@ func (c *Contract) chargeState(s uint64, logger *tracing.Hooks, reason tracing.G return true } -// RefundGas absorbs a sub-call's leftover GasBudget into this contract's gas +// refundState refunds the pre-charged state gas back to state reservoir. +func (c *Contract) refundState(s uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) { + prior := c.Gas + c.Gas.RefundState(s) + + if s != 0 && logger.HasGasHook() && reason != tracing.GasChangeIgnored { + logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) + } +} + +// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas // state. Thin wrapper around GasBudget.Absorb with tracer integration. -func (c *Contract) RefundGas(child GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) { +func (c *Contract) refundGas(child GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) { prior := c.Gas c.Gas.Absorb(child) if logger.HasGasHook() && reason != tracing.GasChangeIgnored { diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 306fa0dbc3..6908ffeba1 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -264,7 +264,7 @@ func ActivePrecompiles(rules params.Rules) []common.Address { // - any error that occurred func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address common.Address, input []byte, gas GasBudget, logger *tracing.Hooks, rules params.Rules) (ret []byte, remaining GasBudget, err error) { gasCost := p.RequiredGas(input) - prior, ok := gas.Charge(GasCosts{RegularGas: gasCost}) + prior, ok := gas.ChargeRegular(gasCost) if !ok { return nil, gas, ErrOutOfGas } diff --git a/core/vm/evm.go b/core/vm/evm.go index 19a7c1368e..76d6978a78 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -299,11 +299,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } if isPrecompile { - var stateDB StateDB - if evm.chainRules.IsAmsterdam { - stateDB = evm.StateDB - } - ret, gas, err = RunPrecompiledContract(stateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) } else { // Initialise a new contract and set the code that is to be used by the EVM. code := evm.resolveCode(addr) @@ -318,18 +314,20 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g gas = contract.Gas } } - // When an error was returned by the EVM or when setting the creation code - // above we revert to the snapshot. gasFromExec below handles the - // regular-gas burn on halt. + + // Calculate the remaining gas at the end of frame + exitGas := gas.Exit(err, initialStateGas) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) + + // Drain the leftover regular gas if unexceptional halt occurs if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution) } } } - return ret, gas.ExitFromErr(err, initialStateGas), err + return ret, exitGas, err } // CallCode executes the contract associated with the addr with the given input @@ -361,11 +359,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - var stateDB StateDB - if evm.chainRules.IsAmsterdam { - stateDB = evm.StateDB - } - ret, gas, err = RunPrecompiledContract(stateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) } else { // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. @@ -374,15 +368,20 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt ret, err = evm.Run(contract, input, false) gas = contract.Gas } + + // Calculate the remaining gas at the end of frame + exitGas := gas.Exit(err, initialStateGas) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) + + // Drain the leftover regular gas if unexceptional halt occurs if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution) } } } - return ret, gas.ExitFromErr(err, initialStateGas), err + return ret, exitGas, err } // DelegateCall executes the contract associated with the addr with the given input @@ -409,26 +408,27 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - var stateDB StateDB - if evm.chainRules.IsAmsterdam { - stateDB = evm.StateDB - } - ret, gas, err = RunPrecompiledContract(stateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) } else { contract := NewContract(originCaller, caller, value, gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) ret, err = evm.Run(contract, input, false) gas = contract.Gas } + + // Calculate the remaining gas at the end of frame + exitGas := gas.Exit(err, initialStateGas) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) + + // Drain the leftover regular gas if unexceptional halt occurs if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution) } } } - return ret, gas.ExitFromErr(err, initialStateGas), err + return ret, exitGas, err } // StaticCall executes the contract associated with the addr with the given input @@ -463,26 +463,25 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b evm.StateDB.AddBalance(addr, new(uint256.Int), tracing.BalanceChangeTouchAccount) if p, isPrecompile := evm.precompile(addr); isPrecompile { - var stateDB StateDB - if evm.chainRules.IsAmsterdam { - stateDB = evm.StateDB - } - ret, gas, err = RunPrecompiledContract(stateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) } else { contract := NewContract(caller, addr, new(uint256.Int), gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) ret, err = evm.Run(contract, input, true) gas = contract.Gas } + + // Calculate the remaining gas at the end of frame + exitGas := gas.Exit(err, initialStateGas) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), exitGas.AsTracing(), tracing.GasChangeCallFailedExecution) } } } - return ret, gas.ExitFromErr(err, initialStateGas), err + return ret, exitGas, err } // create creates a new contract using code as deployment code. @@ -593,12 +592,14 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value // state and gas is preserved (i.e., treated as success). if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { evm.StateDB.RevertToSnapshot(snapshot) + + exit := contract.Gas.Exit(err, initialStateGas) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(contract.Gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer.HasGasHook() { + evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution) } } - return ret, address, contract.Gas.ExitFromErr(err, initialStateGas), err + return ret, address, exit, err } // Either success, or pre-Homestead ErrCodeStoreOutOfGas (gas preserved). // Both packaged as a success-form GasBudget. diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index dca4358fb2..7ce9e7ca8d 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -368,7 +368,7 @@ func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, } func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { - expByteLen := uint64(((*stack.back(1)).BitLen() + 7) / 8) + expByteLen := uint64((stack.back(1).BitLen() + 7) / 8) var ( gas = expByteLen * params.ExpByteFrontier // no overflow check required. Max is 256 * ExpByte gas @@ -381,7 +381,7 @@ func gasExpFrontier(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem } func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { - expByteLen := uint64(((*stack.back(1)).BitLen() + 7) / 8) + expByteLen := uint64((stack.back(1).BitLen() + 7) / 8) var ( gas = expByteLen * params.ExpByteEIP158 // no overflow check required. Max is 256 * ExpByte gas @@ -460,10 +460,10 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m return gas, nil } -// gasCallIntrinsic8037 is the intrinsic gas calculator for CALL in Amsterdam. +// regularGasCall8037 is the intrinsic gas calculator for CALL in Amsterdam. // It computes memory expansion + value transfer gas but excludes new account // creation, which is handled as state gas by the wrapper. -func gasCallIntrinsic8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +func regularGasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { var ( gas uint64 transfersValue = !stack.back(2).IsZero() @@ -571,7 +571,10 @@ func gasCreateEip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m words := (size + 31) / 32 wordGas := params.InitCodeWordGas * words stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte - return GasCosts{RegularGas: gas + wordGas, StateGas: stateGas}, nil + return GasCosts{ + RegularGas: gas + wordGas, + StateGas: stateGas, + }, nil } func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { @@ -591,28 +594,35 @@ func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, } // Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow words := (size + 31) / 32 - // CREATE2 charges both InitCodeWordGas (EIP-3860) and Keccak256WordGas (for address hashing). + + // CREATE2 charges both InitCodeWordGas (EIP-3860) and Keccak256WordGas + // (for address hashing). wordGas := (params.InitCodeWordGas + params.Keccak256WordGas) * words stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte - return GasCosts{RegularGas: gas + wordGas, StateGas: stateGas}, nil + return GasCosts{ + RegularGas: gas + wordGas, + StateGas: stateGas, + }, nil } -// gasCall8037 is the stateful gas calculator for CALL in Amsterdam (EIP-8037). +// stateGasCall8037 is the stateful gas calculator for CALL in Amsterdam (EIP-8037). // It only returns the state-dependent gas (account creation as state gas). // Memory gas, transfer gas, and callGas are handled by gasCallStateless and // makeCallVariantGasCall. -func gasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +func stateGasCall8037(evm *EVM, contract *Contract, stack *Stack) (uint64, error) { var ( - gas GasCosts + gas uint64 transfersValue = !stack.back(2).IsZero() address = common.Address(stack.back(1).Bytes20()) ) + // TODO(rjl, marius), can EIP8037 implicitly means the EIP158 is also activated? + // It's technically possible to skip the EIP158 but very unlikely in practice. if evm.chainRules.IsEIP158 { if transfersValue && evm.StateDB.Empty(address) { - gas.StateGas += params.AccountCreationSize * evm.Context.CostPerStateByte + gas += params.AccountCreationSize * evm.Context.CostPerStateByte } } else if !evm.StateDB.Exist(address) { - gas.StateGas += params.AccountCreationSize * evm.Context.CostPerStateByte + gas += params.AccountCreationSize * evm.Context.CostPerStateByte } return gas, nil } @@ -671,14 +681,9 @@ func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo } if original == current { if original == (common.Hash{}) { // create slot (2.1.1) - // EIP-8037: Return both regular and state gas. System calls do not charge state gas. - var stateGas uint64 - if !contract.IsSystemCall { - stateGas = params.StorageCreationSize * evm.Context.CostPerStateByte - } return GasCosts{ RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929, - StateGas: stateGas, + StateGas: params.StorageCreationSize * evm.Context.CostPerStateByte, }, nil } if value == (common.Hash{}) { // delete slot (2.1.2b) diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 0865a0982d..83d148b6ce 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -55,7 +55,7 @@ func (g GasCosts) String() string { // in lockstep. // // - At frame exit: Preserved / ExitSuccess / ExitRevert / ExitHalt / -// ExitFromErr produce a new GasBudget in "leftover" form that packages +// Exit produce a new GasBudget in "leftover" form that packages // the result for the caller. // // - At absorption: the caller's Absorb method merges the child's leftover @@ -263,7 +263,7 @@ func (g GasBudget) ExitHalt(initialStateGas uint64) GasBudget { } } -// ExitFromErr dispatches on err to the appropriate exit-form constructor +// Exit dispatches on err to the appropriate exit-form constructor // for the post-evm.Run path: // // - err == nil → ExitSuccess @@ -272,7 +272,7 @@ func (g GasBudget) ExitHalt(initialStateGas uint64) GasBudget { // // Soft validation failures (occurring BEFORE evm.Run) should call Preserved // directly instead of going through this dispatcher. -func (g GasBudget) ExitFromErr(err error, initialStateGas uint64) GasBudget { +func (g GasBudget) Exit(err error, initialStateGas uint64) GasBudget { switch { case err == nil: return g.ExitSuccess() diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 0f5274857c..d85fea5ee6 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -675,7 +675,7 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } scope.Stack.push(&stackvalue) - scope.Contract.RefundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) if evm.chainRules.IsAmsterdam && suberr != nil { scope.Contract.Gas.RefundState(params.AccountCreationSize * evm.Context.CostPerStateByte) } @@ -710,11 +710,13 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { stackvalue.SetBytes(addr.Bytes()) } scope.Stack.push(&stackvalue) - scope.Contract.RefundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) - if evm.chainRules.IsAmsterdam && suberr != nil { - scope.Contract.Gas.RefundState(params.AccountCreationSize * evm.Context.CostPerStateByte) - } + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + // If the creation frame reverts or halts exceptionally, the charged state-gas + // is refilled back to the state reservoir in Amsterdam. + if evm.chainRules.IsAmsterdam && suberr != nil { + scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeStateGasRefund) + } if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer return res, nil @@ -754,11 +756,24 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { temp.SetOne() } stack.push(&temp) + if err == nil || err == ErrExecutionReverted { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) - scope.Contract.RefundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + // If the call frame reverts or halts exceptionally, the charged state-gas + // is refilled back to the state reservoir in Amsterdam. + // + // The state-gas should only be refunded if the state creation doesn't + // happens, such as ErrDepth, ErrInsufficientBalance. + // + // TODO(rjl) it's so ugly, please rework it. + if evm.chainRules.IsAmsterdam && err != nil { + if (err == ErrDepth || err == ErrInsufficientBalance) && !value.IsZero() && evm.StateDB.Empty(toAddr) { + scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeStateGasRefund) + } + } evm.returnData = ret return ret, nil @@ -792,7 +807,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -821,7 +836,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if err == nil || err == ErrExecutionReverted { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -851,7 +866,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil diff --git a/core/vm/interface.go b/core/vm/interface.go index 8c67a553c4..a9938c2a28 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -71,14 +71,6 @@ type StateDB interface { // during the current transaction. IsNewContract(addr common.Address) bool - // SameTxSelfDestructs returns addresses that were created and then - // self-destructed in the current transaction. - SameTxSelfDestructs() []common.Address - - // NewStorageSlotCount returns the number of storage slots written to a - // non-zero value in the current transaction on the given address. - NewStorageSlotCount(addr common.Address) int - // Empty returns whether the given account is empty. Empty // is defined according to EIP161 (balance = nonce = code = 0). Empty(common.Address) bool diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 9ea8349e3a..5cc5e34ced 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -28,6 +28,9 @@ type ( intrinsicGasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) // last parameter is the requested memory size as a uint64 // memorySizeFunc returns the required size, and whether the operation overflowed a uint64 memorySizeFunc func(*Stack) (size uint64, overflow bool) + + regularGasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) + stateGasFunc func(*EVM, *Contract, *Stack) (uint64, error) ) type operation struct { diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 6ddb54c94a..67056ecb65 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -267,7 +267,7 @@ var ( gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic) gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic) - innerGasCallEIP8037 = makeCallVariantGasCallEIP8037(gasCallIntrinsic8037, gasCall8037) + innerGasCallEIP8037 = makeCallVariantGasCallEIP8037(regularGasCall8037, stateGasCall8037) ) func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { @@ -381,7 +381,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { // It extends the EIP-7702 pattern with state gas handling and GasUsed tracking. // intrinsicFunc computes the regular gas (memory + transfer, no new account creation). // stateGasFunc computes the state gas (new account creation as state gas). -func makeCallVariantGasCallEIP8037(intrinsicFunc intrinsicGasFunc, stateGasFunc gasFunc) gasFunc { +func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stateGasFunc) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( eip2929Cost uint64 @@ -397,8 +397,8 @@ func makeCallVariantGasCallEIP8037(intrinsicFunc intrinsicGasFunc, stateGasFunc } } - // Compute intrinsic cost (memory + transfer, no new account creation). - intrinsicCost, err := intrinsicFunc(evm, contract, stack, mem, memorySize) + // Compute regular cost (memory + transfer, no new account creation). + regularCost, err := regularFunc(evm, contract, stack, mem, memorySize) if err != nil { return GasCosts{}, err } @@ -406,7 +406,7 @@ func makeCallVariantGasCallEIP8037(intrinsicFunc intrinsicGasFunc, stateGasFunc // Charge intrinsic cost directly (regular gas). This must happen // BEFORE state gas to prevent reservoir inflation, and also serves // as the OOG guard before stateful operations. - if !contract.chargeRegular(intrinsicCost, evm.Config.Tracer, tracing.GasChangeCallOpCode) { + if !contract.chargeRegular(regularCost, evm.Config.Tracer, tracing.GasChangeCallOpCode) { return GasCosts{}, ErrOutOfGas } @@ -424,13 +424,12 @@ func makeCallVariantGasCallEIP8037(intrinsicFunc intrinsicGasFunc, stateGasFunc } // Compute and charge state gas (new account creation) AFTER regular gas. - stateGas, err := stateGasFunc(evm, contract, stack, mem, memorySize) + stateGas, err := stateGasFunc(evm, contract, stack) if err != nil { return GasCosts{}, err } - if stateGas.StateGas > 0 { - // Charge updates contract.Gas.UsedStateGas in lockstep. - if _, ok := contract.Gas.Charge(GasCosts{StateGas: stateGas.StateGas}); !ok { + if stateGas > 0 { + if _, ok := contract.Gas.ChargeState(stateGas); !ok { return GasCosts{}, ErrOutOfGas } } @@ -443,8 +442,8 @@ func makeCallVariantGasCallEIP8037(intrinsicFunc intrinsicGasFunc, stateGasFunc // Temporarily undo direct regular charges for tracer reporting. // The interpreter will charge the returned totalCost. - contract.Gas.RegularGas += eip2929Cost + eip7702Cost + intrinsicCost - contract.Gas.UsedRegularGas -= eip2929Cost + eip7702Cost + intrinsicCost + contract.Gas.RegularGas += eip2929Cost + eip7702Cost + regularCost + contract.Gas.UsedRegularGas -= eip2929Cost + eip7702Cost + regularCost // Aggregate total cost. var ( @@ -454,7 +453,7 @@ func makeCallVariantGasCallEIP8037(intrinsicFunc intrinsicGasFunc, stateGasFunc if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow { return GasCosts{}, ErrGasUintOverflow } - if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost); overflow { + if totalCost, overflow = math.SafeAdd(totalCost, regularCost); overflow { return GasCosts{}, ErrGasUintOverflow } if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow { diff --git a/params/protocol_params.go b/params/protocol_params.go index b7dcd0b81b..69e10fa5d9 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -199,10 +199,11 @@ const ( // don't consume block gas. BALItemCost uint64 = 2000 - AccountCreationSize = 112 - StorageCreationSize = 32 + AccountCreationSize = 120 + StorageCreationSize = 64 AuthorizationCreationSize = 23 - CostPerStateByte = 1174 + CostPerStateByte = 1530 + SystemMaxSStoresPerCall = 16 ) // Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 88745dabaa..91f7d6c3ec 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -81,7 +81,7 @@ func (tt *TransactionTest) Run() error { return } // Intrinsic cost - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, 0) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte) if err != nil { return }