From b86cca23a89d426165dcc9e1f822ff9abfe1d17f Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Mon, 4 May 2026 10:49:12 +0200 Subject: [PATCH 01/10] core: implement EIP-8037: state creation gas cost increase --- cmd/evm/internal/t8ntool/transaction.go | 3 +- core/bench_test.go | 3 +- core/bintrie_witness_test.go | 6 +- core/chain_makers.go | 2 +- core/evm.go | 34 +-- core/gaspool.go | 55 ++++- core/state/statedb.go | 28 +++ core/state/statedb_hooked.go | 8 + core/state_processor.go | 6 +- core/state_transition.go | 245 +++++++++++++++------ core/state_transition_test.go | 105 ++++++--- core/txpool/validation.go | 3 +- core/vm/contract.go | 50 ++++- core/vm/contracts.go | 1 - core/vm/contracts_fuzz_test.go | 2 +- core/vm/contracts_test.go | 8 +- core/vm/eips.go | 18 +- core/vm/evm.go | 246 ++++++++++++--------- core/vm/gas_table.go | 271 ++++++++++++++++++++--- core/vm/gas_table_test.go | 12 +- core/vm/gascosts.go | 273 ++++++++++++++++++++---- core/vm/instructions.go | 61 +++--- core/vm/interface.go | 8 + core/vm/interpreter.go | 16 +- core/vm/interpreter_test.go | 4 +- core/vm/jump_table.go | 6 +- core/vm/operations_acl.go | 119 ++++++++++- core/vm/runtime/runtime.go | 22 +- eth/tracers/js/tracer_test.go | 2 +- eth/tracers/logger/logger_test.go | 2 +- params/protocol_params.go | 7 + tests/state_test.go | 6 +- tests/transaction_test_util.go | 4 +- 33 files changed, 1260 insertions(+), 376 deletions(-) diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index ca19ae3386..26ae11e0fb 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -133,7 +133,8 @@ func Transaction(ctx *cli.Context) error { } // Check intrinsic gas rules := chainConfig.Rules(common.Big0, true, 0) - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) + gasCostPerStateByte := core.CostPerStateByte(&types.Header{}, chainConfig) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, gasCostPerStateByte) if err != nil { r.Error = err results = append(results, r) diff --git a/core/bench_test.go b/core/bench_test.go index 65179c54d4..d49062af06 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -89,7 +89,8 @@ func genValueTx(nbytes int) func(int, *BlockGen) { data := make([]byte, nbytes) return func(i int, gen *BlockGen) { toaddr := common.Address{} - cost, _ := IntrinsicGas(data, nil, nil, false, false, false, false, false) + gasCostPerStateByte := CostPerStateByte(gen.header, gen.cm.config) + cost, _ := IntrinsicGas(data, nil, nil, false, params.Rules{}, gasCostPerStateByte) signer := gen.Signer() gasPrice := big.NewInt(0) if gen.header.BaseFee != nil { diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go index b49ac83bb5..e151a72801 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -64,12 +64,12 @@ var ( func TestProcessUBT(t *testing.T) { var ( code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`) - intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true, false) + intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) // A contract creation that calls EXTCODECOPY in the constructor. Used to ensure that the witness // will not contain that copied data. // Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985 codeWithExtCodeCopy = common.FromHex(`0x60806040526040516100109061017b565b604051809103906000f08015801561002c573d6000803e3d6000fd5b506000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561007857600080fd5b5060008067ffffffffffffffff8111156100955761009461024a565b5b6040519080825280601f01601f1916602001820160405280156100c75781602001600182028036833780820191505090505b50905060008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690506020600083833c81610101906101e3565b60405161010d90610187565b61011791906101a3565b604051809103906000f080158015610133573d6000803e3d6000fd5b50600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061029b565b60d58061046783390190565b6102068061053c83390190565b61019d816101d9565b82525050565b60006020820190506101b86000830184610194565b92915050565b6000819050602082019050919050565b600081519050919050565b6000819050919050565b60006101ee826101ce565b826101f8846101be565b905061020381610279565b925060208210156102435761023e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8360200360080261028e565b831692505b5050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600061028582516101d9565b80915050919050565b600082821b905092915050565b6101bd806102aa6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f566852414610030575b600080fd5b61003861004e565b6040516100459190610146565b60405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381ca91d36040518163ffffffff1660e01b815260040160206040518083038186803b1580156100b857600080fd5b505afa1580156100cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100f0919061010a565b905090565b60008151905061010481610170565b92915050565b6000602082840312156101205761011f61016b565b5b600061012e848285016100f5565b91505092915050565b61014081610161565b82525050565b600060208201905061015b6000830184610137565b92915050565b6000819050919050565b600080fd5b61017981610161565b811461018457600080fd5b5056fea2646970667358221220a6a0e11af79f176f9c421b7b12f441356b25f6489b83d38cc828a701720b41f164736f6c63430008070033608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ab5ed15014602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea26469706673582212203a14eb0d5cd07c277d3e24912f110ddda3e553245a99afc4eeefb2fbae5327aa64736f6c63430008070033608060405234801561001057600080fd5b5060405161020638038061020683398181016040528101906100329190610063565b60018160001c6100429190610090565b60008190555050610145565b60008151905061005d8161012e565b92915050565b60006020828403121561007957610078610129565b5b60006100878482850161004e565b91505092915050565b600061009b826100f0565b91506100a6836100f0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156100db576100da6100fa565b5b828201905092915050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b610137816100e6565b811461014257600080fd5b50565b60b3806101536000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806381ca91d314602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea26469706673582212209bff7098a2f526de1ad499866f27d6d0d6f17b74a413036d6063ca6a0998ca4264736f6c63430008070033`) - intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, true, true, true, false) + intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) signer = types.LatestSigner(testUBTChainConfig) testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain @@ -201,7 +201,7 @@ func TestProcessParentBlockHash(t *testing.T) { if isUBT { chainConfig = testUBTChainConfig } - vmContext := NewEVMBlockContext(header, nil, new(common.Address)) + vmContext := NewEVMBlockContext(header, &BlockChain{chainConfig: chainConfig}, new(common.Address)) evm := vm.NewEVM(vmContext, statedb, chainConfig, vm.Config{}) ProcessParentBlockHash(header.ParentHash, evm, bal.NewConstructionBlockAccessList()) } diff --git a/core/chain_makers.go b/core/chain_makers.go index 2e856b5161..cb78739882 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(nil, config, tx) + b.addTx(&BlockChain{chainConfig: b.cm.config}, 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 73e4c01a99..6e3938da5b 100644 --- a/core/evm.go +++ b/core/evm.go @@ -68,21 +68,31 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common } return vm.BlockContext{ - CanTransfer: CanTransfer, - Transfer: Transfer, - GetHash: GetHashFn(header, chain), - Coinbase: beneficiary, - BlockNumber: new(big.Int).Set(header.Number), - Time: header.Time, - Difficulty: new(big.Int).Set(header.Difficulty), - BaseFee: baseFee, - BlobBaseFee: blobBaseFee, - GasLimit: header.GasLimit, - Random: random, - SlotNum: slotNum, + CanTransfer: CanTransfer, + Transfer: Transfer, + GetHash: GetHashFn(header, chain), + Coinbase: beneficiary, + BlockNumber: new(big.Int).Set(header.Number), + Time: header.Time, + Difficulty: new(big.Int).Set(header.Difficulty), + BaseFee: baseFee, + BlobBaseFee: blobBaseFee, + GasLimit: header.GasLimit, + Random: random, + SlotNum: slotNum, + CostPerStateByte: CostPerStateByte(header, chain.Config()), } } +// 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 14f5abd93c..82323aafc4 100644 --- a/core/gaspool.go +++ b/core/gaspool.go @@ -27,6 +27,11 @@ type GasPool struct { remaining uint64 initial uint64 cumulativeUsed uint64 + + // After 8037 Block gas used is + // max(cumulativeRegular, cumulativeState). + cumulativeRegular uint64 + cumulativeState uint64 } // NewGasPool initializes the gasPool with the given amount. @@ -47,6 +52,19 @@ func (gp *GasPool) SubGas(amount uint64) error { return nil } +// CheckGasAmsterdam performs the EIP-8037 per-tx 2D block-inclusion check: +// the worst-case regular contribution must fit in the regular dimension and +// the worst-case state contribution must fit in the state dimension +func (gp *GasPool) CheckGasAmsterdam(regularReservation, stateReservation uint64) error { + if gp.initial-gp.cumulativeRegular < regularReservation { + return ErrGasLimitReached + } + if gp.initial-gp.cumulativeState < stateReservation { + return ErrGasLimitReached + } + return nil +} + // ReturnGas adds the refunded gas back to the pool and updates // the cumulative gas usage accordingly. func (gp *GasPool) ReturnGas(returned uint64, gasUsed uint64) error { @@ -68,20 +86,43 @@ func (gp *GasPool) ReturnGas(returned uint64, gasUsed uint64) error { 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. +func (gp *GasPool) ChargeGasAmsterdam(txRegular, txState, receiptGasUsed uint64) error { + gp.cumulativeRegular += txRegular + gp.cumulativeState += txState + gp.cumulativeUsed += receiptGasUsed + + blockUsed := max(gp.cumulativeRegular, gp.cumulativeState) + if gp.initial < blockUsed { + return fmt.Errorf("%w: block gas overflow: initial %d, used %d (regular: %d, state: %d)", + ErrGasLimitReached, gp.initial, blockUsed, gp.cumulativeRegular, gp.cumulativeState) + } + // For tx inclusion, we only check if the regular dimension fits. + gp.remaining = gp.initial - gp.cumulativeRegular + return nil +} + // Gas returns the amount of gas remaining in the pool. func (gp *GasPool) Gas() uint64 { return gp.remaining } -// CumulativeUsed returns the amount of cumulative consumed gas (refunded included). +// CumulativeUsed returns the cumulative gas consumed for receipt tracking. func (gp *GasPool) CumulativeUsed() uint64 { return gp.cumulativeUsed } // Used returns the amount of consumed gas. func (gp *GasPool) Used() uint64 { + if gp.cumulativeRegular > 0 || gp.cumulativeState > 0 { + // After 8037 we return max(sum_regular, sum_state) + return max(gp.cumulativeRegular, gp.cumulativeState) + } if gp.initial < gp.remaining { - panic("gas used underflow") + panic(fmt.Sprintf("gas used underflow: %v %v", gp.initial, gp.remaining)) } return gp.initial - gp.remaining } @@ -89,9 +130,11 @@ func (gp *GasPool) Used() uint64 { // Snapshot returns the deep-copied object as the snapshot. func (gp *GasPool) Snapshot() *GasPool { return &GasPool{ - initial: gp.initial, - remaining: gp.remaining, - cumulativeUsed: gp.cumulativeUsed, + initial: gp.initial, + remaining: gp.remaining, + cumulativeUsed: gp.cumulativeUsed, + cumulativeRegular: gp.cumulativeRegular, + cumulativeState: gp.cumulativeState, } } @@ -100,6 +143,8 @@ func (gp *GasPool) Set(other *GasPool) { gp.initial = other.initial gp.remaining = other.remaining gp.cumulativeUsed = other.cumulativeUsed + gp.cumulativeRegular = other.cumulativeRegular + gp.cumulativeState = other.cumulativeState } func (gp *GasPool) String() string { diff --git a/core/state/statedb.go b/core/state/statedb.go index 1c49d46020..5e25cce5c8 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -683,6 +683,34 @@ 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 98d01343a4..8b44751583 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -59,6 +59,14 @@ 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 5690a152e7..9af1682ce8 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -290,7 +290,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), common.U2560) + _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, 0), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } @@ -319,7 +319,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), common.U2560) + _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, 0), common.U2560) if err != nil { panic(err) } @@ -360,7 +360,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), common.U2560) + ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000, 0), 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 51c5836892..7ac66b69f1 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -68,13 +68,27 @@ func (result *ExecutionResult) Revert() []byte { } // IntrinsicGas computes the 'intrinsic gas' for a message with the given data. -func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860, isAmsterdam bool) (vm.GasCosts, error) { +func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation bool, rules params.Rules, costPerStateByte uint64) (vm.GasCosts, error) { // Set the starting gas for the raw transaction - var gas uint64 - if isContractCreation && isHomestead { - gas = params.TxGasContractCreation + var gas vm.GasCosts + if isContractCreation && rules.IsHomestead { + if rules.IsAmsterdam { + gas.RegularGas = params.TxGas + params.CreateGasAmsterdam + gas.StateGas = params.AccountCreationSize * costPerStateByte + } else { + gas.RegularGas = params.TxGasContractCreation + } } else { - gas = params.TxGas + gas.RegularGas = params.TxGas + } + // Add gas for authorizations + if authList != nil { + if rules.IsAmsterdam { + gas.RegularGas += uint64(len(authList)) * params.TxAuthTupleRegularGas + gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte + } else { + gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas + } } dataLen := uint64(len(data)) // Bump the required gas by the amount of transactional data @@ -85,59 +99,56 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set // Make sure we don't exceed uint64 for all data combinations nonZeroGas := params.TxDataNonZeroGasFrontier - if isEIP2028 { + if rules.IsIstanbul { nonZeroGas = params.TxDataNonZeroGasEIP2028 } - if (math.MaxUint64-gas)/nonZeroGas < nz { + if (math.MaxUint64-gas.RegularGas)/nonZeroGas < nz { return vm.GasCosts{}, ErrGasUintOverflow } - gas += nz * nonZeroGas + gas.RegularGas += nz * nonZeroGas - if (math.MaxUint64-gas)/params.TxDataZeroGas < z { + if (math.MaxUint64-gas.RegularGas)/params.TxDataZeroGas < z { return vm.GasCosts{}, ErrGasUintOverflow } - gas += z * params.TxDataZeroGas + gas.RegularGas += z * params.TxDataZeroGas - if isContractCreation && isEIP3860 { + if isContractCreation && rules.IsShanghai { lenWords := toWordSize(dataLen) - if (math.MaxUint64-gas)/params.InitCodeWordGas < lenWords { + if (math.MaxUint64-gas.RegularGas)/params.InitCodeWordGas < lenWords { return vm.GasCosts{}, ErrGasUintOverflow } - gas += lenWords * params.InitCodeWordGas + gas.RegularGas += lenWords * params.InitCodeWordGas } } if accessList != nil { addresses := uint64(len(accessList)) storageKeys := uint64(accessList.StorageKeys()) - if (math.MaxUint64-gas)/params.TxAccessListAddressGas < addresses { + if (math.MaxUint64-gas.RegularGas)/params.TxAccessListAddressGas < addresses { return vm.GasCosts{}, ErrGasUintOverflow } - gas += addresses * params.TxAccessListAddressGas - if (math.MaxUint64-gas)/params.TxAccessListStorageKeyGas < storageKeys { + gas.RegularGas += addresses * params.TxAccessListAddressGas + if (math.MaxUint64-gas.RegularGas)/params.TxAccessListStorageKeyGas < storageKeys { return vm.GasCosts{}, ErrGasUintOverflow } - gas += storageKeys * params.TxAccessListStorageKeyGas + gas.RegularGas += storageKeys * params.TxAccessListStorageKeyGas // EIP-7981: access list data is charged in addition to the base charge. - if isAmsterdam { + if rules.IsAmsterdam { const ( addressCost = common.AddressLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte storageKeyCost = common.HashLength * params.TxCostFloorPerToken7976 * params.TxTokenPerNonZeroByte ) - if (math.MaxUint64-gas)/addressCost < addresses { + if (math.MaxUint64-gas.RegularGas)/addressCost < addresses { return vm.GasCosts{}, ErrGasUintOverflow } - gas += addresses * addressCost - if (math.MaxUint64-gas)/storageKeyCost < storageKeys { + gas.RegularGas += addresses * addressCost + if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys { return vm.GasCosts{}, ErrGasUintOverflow } - gas += storageKeys * storageKeyCost + gas.RegularGas += storageKeys * storageKeyCost } } - if authList != nil { - gas += uint64(len(authList)) * params.CallNewAccountGas - } - return vm.GasCosts{RegularGas: gas}, nil + return gas, nil } // FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). @@ -416,18 +427,19 @@ func (st *stateTransition) buyGas() error { if have, want := st.state.GetBalance(st.msg.From), balanceCheck; have.Cmp(want) < 0 { return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) } - if err := st.gp.SubGas(st.msg.GasLimit); err != nil { - return err + + // After Amsterdam we limit the regular gas to 16k, 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) } + st.initialBudget = vm.NewGasBudget(limit, st.msg.GasLimit-limit) + st.gasRemaining = st.initialBudget.Copy() if st.evm.Config.Tracer.HasGasHook() { empty := vm.GasBudget{} - initial := vm.NewGasBudget(st.msg.GasLimit) - st.evm.Config.Tracer.EmitGasChange(empty.AsTracing(), initial.AsTracing(), tracing.GasChangeTxInitialBalance) + st.evm.Config.Tracer.EmitGasChange(empty.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxInitialBalance) } - st.gasRemaining = vm.NewGasBudget(st.msg.GasLimit) - st.initialBudget = st.gasRemaining.Copy() - st.state.SubBalance(st.msg.From, mgval, tracing.BalanceDecreaseGasBuy) return nil } @@ -453,7 +465,7 @@ func (st *stateTransition) preCheck() error { 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 isOsaka && !isAmsterdam && msg.GasLimit > params.MaxTxGas { + if !isAmsterdam && 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 @@ -559,8 +571,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { contractCreation = msg.To == nil floorDataGas uint64 ) + // Check clauses 4-5, subtract intrinsic gas if everything is correct - cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) + cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules, st.evm.Context.CostPerStateByte) if err != nil { return nil, err } @@ -571,17 +584,65 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if st.evm.Config.Tracer.HasGasHook() { st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) } - // Gas limit suffices for the floor data cost (EIP-7623) + + // 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 + } + } + + // Compute the floor data cost (EIP-7623), needed for both Prague and Amsterdam validation. if rules.IsPrague { floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList) if err != nil { return nil, err } if msg.GasLimit < floorDataGas { - return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, 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()) + } + // 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 { st.evm.AccessEvents.AddTxOrigin(msg.From) @@ -617,7 +678,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { vmerr error // vm errors do not effect consensus and are therefore not assigned to err ) if contractCreation { - ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value) + var result vm.GasBudget + ret, _, result, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value) + st.gasRemaining.Absorb(result) } else { // Increment the nonce for the next transaction. st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) @@ -626,7 +689,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(&auth) + _ = st.applyAuthorization(rules, &auth) } } @@ -638,9 +701,32 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok { st.state.AddAddressToAccessList(addr) } - // Execute the transaction's call. - ret, st.gasRemaining, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value) + var result vm.GasBudget + ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) + st.gasRemaining.Absorb(result) + } + 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) + } + } } // Record the gas used excluding gas refunds. This value represents the actual @@ -648,33 +734,53 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { peakGasUsed := st.gasUsed() // Compute refund counter, capped to a refund quotient. - st.gasRemaining.Refund(st.calcRefund()) + st.gasRemaining.RefundRegular(st.calcRefund()) if rules.IsPrague { // After EIP-7623: Data-heavy transactions pay the floor gas. 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) + /* + 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) } } if peakGasUsed < floorDataGas { peakGasUsed = floorDataGas } } - // Return gas to the user - st.returnGas() - // Return gas to the gas pool + returned := st.returnGas() if rules.IsAmsterdam { - // Refund is excluded for returning - err = st.gp.ReturnGas(st.initialBudget.RegularGas-peakGasUsed, st.gasUsed()) + // EIP-8037: 2D gas accounting for Amsterdam. + // 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. + var txState uint64 + if st.gasRemaining.UsedStateGas > 0 { + txState = uint64(st.gasRemaining.UsedStateGas) + } + txRegular := max(st.gasRemaining.UsedRegularGas, floorDataGas) + if err := st.gp.ChargeGasAmsterdam(txRegular, txState, st.gasUsed()); err != nil { + return nil, err + } } else { - // Refund is included for returning - err = st.gp.ReturnGas(st.gasRemaining.RegularGas, st.gasUsed()) - } - if err != nil { - return nil, err + if err = st.gp.ReturnGas(returned, st.gasUsed()); err != nil { + return nil, err + } } effectiveTip := msg.GasPrice if rules.IsLondon { @@ -744,7 +850,7 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio } // applyAuthorization applies an EIP-7702 code delegation to the state. -func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) error { +func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization) error { authority, err := st.validateAuthorization(auth) if err != nil { return err @@ -753,25 +859,36 @@ func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) // If the account already exists in state, refund the new account cost // charged in the intrinsic calculation. if st.state.Exist(authority) { - st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) + if rules.IsAmsterdam { + // EIP-8037: refund account creation state gas to the reservoir. + st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + } else { + st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) + } } + prevDelegation, isDelegated := types.ParseDelegation(st.state.GetCode(authority)) + // Update nonce and account code. st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization) if auth.Address == (common.Address{}) { // Delegation to zero address means clear. - st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear) + if isDelegated { + st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear) + } return nil } - // Otherwise install delegation to auth.Address. - st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) + // install delegation to auth.Address if the delegation changed + if !isDelegated || auth.Address != prevDelegation { + st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) + } return nil } // calcRefund computes refund counter, capped to a refund quotient. -func (st *stateTransition) calcRefund() vm.GasBudget { +func (st *stateTransition) calcRefund() uint64 { var refund uint64 if !st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) { // Before EIP-3529: refunds were capped to gasUsed / 2 @@ -789,12 +906,13 @@ func (st *stateTransition) calcRefund() vm.GasBudget { st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxRefunds) } - return vm.NewGasBudget(refund) + return refund } // returnGas returns ETH for remaining gas, // exchanged at the original rate. -func (st *stateTransition) returnGas() { +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) @@ -804,6 +922,7 @@ func (st *stateTransition) returnGas() { after.RegularGas = 0 st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxLeftOverReturned) } + return gas } // gasUsed returns the amount of gas used up by the state transition. diff --git a/core/state_transition_test.go b/core/state_transition_test.go index 8aab016123..be2de7f511 100644 --- a/core/state_transition_test.go +++ b/core/state_transition_test.go @@ -155,50 +155,50 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028 bool isEIP3860 bool isAmsterdam bool - want uint64 + want vm.GasCosts }{ { name: "frontier/empty-call", - want: params.TxGas, + want: vm.GasCosts{RegularGas: params.TxGas}, }, { name: "frontier/contract-creation-pre-homestead", creation: true, isHomestead: false, // pre-homestead, contract creation still uses TxGas - want: params.TxGas, + want: vm.GasCosts{RegularGas: params.TxGas}, }, { name: "homestead/contract-creation", creation: true, isHomestead: true, - want: params.TxGasContractCreation, + want: vm.GasCosts{RegularGas: params.TxGasContractCreation}, }, { name: "frontier/non-zero-data", data: bytes.Repeat([]byte{0xff}, 100), // 100 nz bytes * 68 (frontier) - want: params.TxGas + 100*params.TxDataNonZeroGasFrontier, + want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataNonZeroGasFrontier}, }, { name: "istanbul/non-zero-data", data: bytes.Repeat([]byte{0xff}, 100), isEIP2028: true, // 100 nz bytes * 16 (post-EIP2028) - want: params.TxGas + 100*params.TxDataNonZeroGasEIP2028, + want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataNonZeroGasEIP2028}, }, { name: "istanbul/zero-data", data: bytes.Repeat([]byte{0x00}, 100), isEIP2028: true, // 100 zero bytes * 4 - want: params.TxGas + 100*params.TxDataZeroGas, + want: vm.GasCosts{RegularGas: params.TxGas + 100*params.TxDataZeroGas}, }, { name: "istanbul/mixed-data", data: append(bytes.Repeat([]byte{0x00}, 50), bytes.Repeat([]byte{0xff}, 50)...), isEIP2028: true, - want: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028, + want: vm.GasCosts{RegularGas: params.TxGas + 50*params.TxDataZeroGas + 50*params.TxDataNonZeroGasEIP2028}, }, { name: "shanghai/init-code-word-gas", @@ -208,7 +208,7 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028: true, isEIP3860: true, // TxGasContractCreation + 64 zero bytes * 4 + 2 words * 2 - want: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, + want: vm.GasCosts{RegularGas: params.TxGasContractCreation + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas}, }, { name: "shanghai/init-code-non-multiple-of-32", @@ -217,7 +217,7 @@ func TestIntrinsicGas(t *testing.T) { isHomestead: true, isEIP2028: true, isEIP3860: true, - want: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas, + want: vm.GasCosts{RegularGas: params.TxGasContractCreation + 33*params.TxDataZeroGas + 2*params.InitCodeWordGas}, }, { name: "berlin/access-list", @@ -227,7 +227,7 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, // 2 addrs * 2400 + 3 keys * 1900 - want: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas, + want: vm.GasCosts{RegularGas: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas}, }, { name: "amsterdam/access-list-extra-cost", @@ -238,9 +238,9 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028: true, isAmsterdam: true, // base access-list charge + EIP-7981 extra - want: params.TxGas + + want: vm.GasCosts{RegularGas: params.TxGas + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas + - 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost, + 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost}, }, { name: "prague/auth-list", @@ -250,8 +250,54 @@ func TestIntrinsicGas(t *testing.T) { {Address: addr1}, }, isEIP2028: true, - // 3 auths * 25000 - want: params.TxGas + 3*params.CallNewAccountGas, + // 3 auths * 25000 (pre-Amsterdam: CallNewAccountGas per auth tuple) + want: vm.GasCosts{RegularGas: params.TxGas + 3*params.CallNewAccountGas}, + }, + { + name: "amsterdam/contract-creation-empty", + creation: true, + isHomestead: true, + isEIP2028: true, + isAmsterdam: true, + // EIP-8037: creation regular gas is TxGas + CreateGasAmsterdam (not TxGasContractCreation), + // and account-creation cost is moved to state gas. + want: vm.GasCosts{ + RegularGas: params.TxGas + params.CreateGasAmsterdam, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, + }, + { + name: "amsterdam/contract-creation-init-code", + data: bytes.Repeat([]byte{0x00}, 64), // 2 words of init code + creation: true, + isHomestead: true, + isEIP2028: true, + isEIP3860: true, // Shanghai gates init-code word gas + isAmsterdam: true, + want: vm.GasCosts{ + RegularGas: params.TxGas + params.CreateGasAmsterdam + + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, + }, + { + name: "amsterdam/contract-creation-with-access-list", + data: bytes.Repeat([]byte{0xff}, 32), // 1 word of non-zero init code + accessList: types.AccessList{ + {Address: addr1, StorageKeys: []common.Hash{key1}}, + }, + creation: true, + isHomestead: true, + isEIP2028: true, + isEIP3860: true, + isAmsterdam: true, + want: vm.GasCosts{ + RegularGas: params.TxGas + params.CreateGasAmsterdam + + 32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas + + 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, }, { name: "amsterdam/combined", @@ -264,23 +310,34 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, isAmsterdam: true, - want: params.TxGas + - 100*params.TxDataNonZeroGasEIP2028 + - 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + - 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + - 1*params.CallNewAccountGas, + // EIP-8037 splits the auth-tuple charge into regular + state gas: + // regular: TxAuthTupleRegularGas (7500) per auth + // state: (AuthorizationCreationSize + AccountCreationSize) * CostPerStateByte per auth + want: vm.GasCosts{ + RegularGas: params.TxGas + + 100*params.TxDataNonZeroGasEIP2028 + + 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + + 1*params.TxAuthTupleRegularGas, + StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + rules := params.Rules{ + IsHomestead: tt.isHomestead, + IsIstanbul: tt.isEIP2028, + IsShanghai: tt.isEIP3860, + IsAmsterdam: tt.isAmsterdam, + } got, err := IntrinsicGas(tt.data, tt.accessList, tt.authList, - tt.creation, tt.isHomestead, tt.isEIP2028, tt.isEIP3860, tt.isAmsterdam) + tt.creation, rules, params.CostPerStateByte) if err != nil { t.Fatalf("unexpected error: %v", err) } - want := vm.GasCosts{RegularGas: tt.want} - if got != want { - t.Fatalf("gas mismatch: got %+v, want %+v", got, want) + if got != tt.want { + t.Fatalf("gas mismatch: got %+v, want %+v", got, tt.want) } }) } diff --git a/core/txpool/validation.go b/core/txpool/validation.go index c87bba31ac..439d5c68ff 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -125,7 +125,8 @@ 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 - intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) + gasCostPerStateByte := core.CostPerStateByte(head, opts.Config) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, gasCostPerStateByte) if err != nil { return err } diff --git a/core/vm/contract.go b/core/vm/contract.go index 45c879c80f..03360ebaff 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -42,6 +42,8 @@ type Contract struct { IsDeployment bool IsSystemCall bool + // Gas carries the unified gas state for this frame: running balance, + // reservoir, and per-frame usage accumulators. See GasBudget. Gas GasBudget value *uint256.Int } @@ -113,7 +115,6 @@ func (c *Contract) GetOp(n uint64) OpCode { if n < uint64(len(c.Code)) { return OpCode(c.Code[n]) } - return STOP } @@ -125,9 +126,10 @@ func (c *Contract) Caller() common.Address { return c.caller } -// UseGas attempts the use gas and subtracts it and returns true on success -func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.GasChangeReason) (ok bool) { - prior, ok := c.Gas.Charge(cost) +// chargeRegular deducts regular gas only, with tracer integration. +// Returns false on OOG. Delegates the arithmetic to GasBudget.ChargeRegular. +func (c *Contract) chargeRegular(r uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) bool { + prior, ok := c.Gas.ChargeRegular(r) if !ok { return false } @@ -137,15 +139,45 @@ func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.G return true } -// RefundGas refunds the leftover gas budget back to the contract. -func (c *Contract) RefundGas(refund GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) { - prior, changed := c.Gas.Refund(refund) - if !changed { - return +// chargeState deducts state gas (spilling into regular when the reservoir is +// exhausted), with tracer integration. Returns false on OOG. +func (c *Contract) chargeState(s uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) bool { + prior, ok := c.Gas.ChargeState(s) + if !ok { + return false } if logger.HasGasHook() && reason != tracing.GasChangeIgnored { logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) } + return true +} + +// 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) { + prior := c.Gas + c.Gas.Absorb(child) + if logger.HasGasHook() && reason != tracing.GasChangeIgnored { + logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) + } +} + +// forwardGas drains `regular` regular gas and the entire state reservoir +// from this contract's running budget and returns the initial GasBudget for +// a child frame. The caller's UsedRegularGas is bumped by the forwarded +// amount so that the absorb-on-return path correctly reclaims the unused +// portion. Thin wrapper around GasBudget.Forward with tracer integration. +// +// Caller must ensure `regular` is no larger than the running balance (the +// opcode's dynamic gas table is expected to validate that before invoking +// the opcode handler). +func (c *Contract) forwardGas(regular uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) GasBudget { + prior := c.Gas + child := c.Gas.Forward(regular) + if logger.HasGasHook() && reason != tracing.GasChangeIgnored { + logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) + } + return child } // Address returns the contracts address diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 71cfdbc527..306fa0dbc3 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -266,7 +266,6 @@ func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address comm gasCost := p.RequiredGas(input) prior, ok := gas.Charge(GasCosts{RegularGas: gasCost}) if !ok { - gas.Exhaust() return nil, gas, ErrOutOfGas } if logger.HasGasHook() { diff --git a/core/vm/contracts_fuzz_test.go b/core/vm/contracts_fuzz_test.go index 988cdb91f2..4d28df6a6a 100644 --- a/core/vm/contracts_fuzz_test.go +++ b/core/vm/contracts_fuzz_test.go @@ -37,7 +37,7 @@ func FuzzPrecompiledContracts(f *testing.F) { return } inWant := string(input) - RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas), nil, params.Rules{}) + RunPrecompiledContract(nil, p, a, input, NewGasBudget(gas, 0), nil, params.Rules{}) if inHave := string(input); inWant != inHave { t.Errorf("Precompiled %v modified input data", a) } diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index e7841c8552..c6975bd0a6 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -100,7 +100,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) { in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}); err != nil { + if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}); err != nil { t.Error(err) } else if common.Bytes2Hex(res) != test.Expected { t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res)) @@ -122,7 +122,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) { gas := test.Gas - 1 t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}) + _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}) if err.Error() != "out of gas" { t.Errorf("Expected error [out of gas], got [%v]", err) } @@ -139,7 +139,7 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(test.Name, func(t *testing.T) { - _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas), nil, params.Rules{}) + _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, NewGasBudget(gas, 0), nil, params.Rules{}) if err.Error() != test.ExpectedError { t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err) } @@ -170,7 +170,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) { start := time.Now() for bench.Loop() { copy(data, in) - res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas), nil, params.Rules{}) + res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, NewGasBudget(reqGas, 0), nil, params.Rules{}) } elapsed := uint64(time.Since(start)) if elapsed < 1 { diff --git a/core/vm/eips.go b/core/vm/eips.go index 33af8fd4fd..0fbf27519a 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -43,6 +43,7 @@ var activators = map[int]func(*JumpTable){ 7939: enable7939, 8024: enable8024, 7843: enable7843, + 8037: enable8037, } // EnableEIP enables the given EIP on the config. @@ -377,7 +378,7 @@ func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, er code := evm.StateDB.GetCode(addr) paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64()) consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(code)), false, scope.Contract.Gas.RegularGas) - scope.Contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeUnspecified) + scope.Contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas } @@ -403,7 +404,7 @@ func opPush1EIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // advanced past this boundary. contractAddr := scope.Contract.Address() consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas) - scope.Contract.UseGas(GasCosts{RegularGas: wanted}, evm.Config.Tracer, tracing.GasChangeUnspecified) + scope.Contract.chargeRegular(wanted, evm.Config.Tracer, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas } @@ -430,7 +431,7 @@ func makePushEIP4762(size uint64, pushByteSize int) executionFunc { if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall { contractAddr := scope.Contract.Address() consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas.RegularGas) - scope.Contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeUnspecified) + scope.Contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas } @@ -590,3 +591,14 @@ func enable7843(jt *JumpTable) { maxStack: maxStack(0, 1), } } + +// enable8037 enables the multidimensional-metering as specified in EIP-8037. +func enable8037(jt *JumpTable) { + jt[CREATE].constantGas = params.CreateGasAmsterdam + jt[CREATE].dynamicGas = gasCreateEip8037 + jt[CREATE2].constantGas = params.CreateGasAmsterdam + jt[CREATE2].dynamicGas = gasCreate2Eip8037 + jt[CALL].dynamicGas = gasCallEIP8037 + jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037 + jt[SSTORE].dynamicGas = gasSStore8037 +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 832306b9a0..19a7c1368e 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -67,6 +67,8 @@ type BlockContext struct { BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price) Random *common.Hash // Provides information for PREVRANDAO SlotNum uint64 // Provides information for SLOTNUM + + CostPerStateByte uint64 // CostPerByte for new state after EIP-8037 } // TxContext provides the EVM with information about a transaction. @@ -245,23 +247,25 @@ func isSystemCall(caller common.Address) bool { // parameters. It also handles any necessary value transfer required and takse // the necessary steps to create accounts and reverses the state in case of an // execution error or failed value transfer. -func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) { +func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) { // Capture the tracer start/end events in debug mode if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, CALL, caller, addr, input, gas.RegularGas, value.ToBig()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) - }(gas.RegularGas) + evm.captureBegin(evm.depth, CALL, caller, addr, input, gas, value.ToBig()) + defer func(startGas GasBudget) { + evm.captureEnd(evm.depth, startGas, result, ret, err) + }(gas) } + initialStateGas := gas.StateGas + // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { - return nil, gas, ErrDepth + return nil, gas.Preserved(), ErrDepth } syscall := isSystemCall(caller) // Fail if we're trying to transfer more than the available balance. if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { - return nil, gas, ErrInsufficientBalance + return nil, gas.Preserved(), ErrInsufficientBalance } snapshot := evm.StateDB.Snapshot() p, isPrecompile := evm.precompile(addr) @@ -275,16 +279,15 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // hash leaf to the access list, then account creation will proceed unimpaired. // Thus, only pay for the creation of the code hash leaf here. wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false) - if _, ok := gas.Charge(GasCosts{RegularGas: wgas}); !ok { + if _, ok := gas.ChargeRegular(wgas); !ok { evm.StateDB.RevertToSnapshot(snapshot) - gas.Exhaust() - return nil, gas, ErrOutOfGas + return nil, gas.ExitHalt(initialStateGas), ErrOutOfGas } } if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() { // Calling a non-existing account, don't do anything. - return nil, gas, nil + return nil, gas.Preserved(), nil } evm.StateDB.CreateAccount(addr) } @@ -296,7 +299,11 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } if isPrecompile { - ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + var stateDB StateDB + if evm.chainRules.IsAmsterdam { + stateDB = evm.StateDB + } + ret, gas, err = RunPrecompiledContract(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) @@ -312,21 +319,17 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } } // When an error was returned by the EVM or when setting the creation code - // above we revert to the snapshot and consume any gas remaining. Additionally, - // when we're in homestead this also counts for code storage gas errors. + // above we revert to the snapshot. gasFromExec below handles the + // regular-gas burn on halt. 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) } - gas.Exhaust() } - // TODO: consider clearing up unused snapshots: - //} else { - // evm.StateDB.DiscardSnapshot(snapshot) } - return ret, gas, err + return ret, gas.ExitFromErr(err, initialStateGas), err } // CallCode executes the contract associated with the addr with the given input @@ -336,30 +339,33 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // // CallCode differs from Call in the sense that it executes the given address' // code with the caller as context. -func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) { +func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas.RegularGas, value.ToBig()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) - }(gas.RegularGas) + evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas, value.ToBig()) + defer func(startGas GasBudget) { + evm.captureEnd(evm.depth, startGas, result, ret, err) + }(gas) } + initialStateGas := gas.StateGas + // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { - return nil, gas, ErrDepth + return nil, gas.Preserved(), ErrDepth } // Fail if we're trying to transfer more than the available balance - // Note although it's noop to transfer X ether to caller itself. But - // if caller doesn't have enough balance, it would be an error to allow - // over-charging itself. So the check here is necessary. if !evm.Context.CanTransfer(evm.StateDB, caller, value) { - return nil, gas, ErrInsufficientBalance + return nil, gas.Preserved(), ErrInsufficientBalance } var snapshot = evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + var stateDB StateDB + if evm.chainRules.IsAmsterdam { + stateDB = evm.StateDB + } + ret, gas, err = RunPrecompiledContract(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,10 +380,9 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } - gas.Exhaust() } } - return ret, gas, err + return ret, gas.ExitFromErr(err, initialStateGas), err } // DelegateCall executes the contract associated with the addr with the given input @@ -385,28 +390,31 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt // // DelegateCall differs from CallCode in the sense that it executes the given address' // code with the caller as context and the caller is set to the caller of the caller. -func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, leftOverGas GasBudget, err error) { +func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas GasBudget, value *uint256.Int) (ret []byte, result GasBudget, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { // DELEGATECALL inherits value from parent call - evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas.RegularGas, value.ToBig()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) - }(gas.RegularGas) + evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas, value.ToBig()) + defer func(startGas GasBudget) { + evm.captureEnd(evm.depth, startGas, result, ret, err) + }(gas) } + initialStateGas := gas.StateGas + // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { - return nil, gas, ErrDepth + return nil, gas.Preserved(), ErrDepth } var snapshot = evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + var stateDB StateDB + if evm.chainRules.IsAmsterdam { + stateDB = evm.StateDB + } + ret, gas, err = RunPrecompiledContract(stateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) } else { - // Initialise a new contract and make initialise the delegate values - // - // Note: The value refers to the original value from the parent call. contract := NewContract(originCaller, caller, value, gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) ret, err = evm.Run(contract, input, false) @@ -418,27 +426,28 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } - gas.Exhaust() } } - return ret, gas, err + return ret, gas.ExitFromErr(err, initialStateGas), err } // StaticCall executes the contract associated with the addr with the given input // as parameters while disallowing any modifications to the state during the call. // Opcodes that attempt to perform such modifications will result in exceptions // instead of performing the modifications. -func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas GasBudget) (ret []byte, leftOverGas GasBudget, err error) { +func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas GasBudget) (ret []byte, result GasBudget, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas.RegularGas, nil) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) - }(gas.RegularGas) + evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas, nil) + defer func(startGas GasBudget) { + evm.captureEnd(evm.depth, startGas, result, ret, err) + }(gas) } + initialStateGas := gas.StateGas + // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { - return nil, gas, ErrDepth + return nil, gas.Preserved(), ErrDepth } // We take a snapshot here. This is a bit counter-intuitive, and could probably be skipped. // However, even a staticcall is considered a 'touch'. On mainnet, static calls were introduced @@ -454,16 +463,14 @@ 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 { - ret, gas, err = RunPrecompiledContract(evm.StateDB, p, addr, input, gas, evm.Config.Tracer, evm.chainRules) + var stateDB StateDB + if evm.chainRules.IsAmsterdam { + stateDB = evm.StateDB + } + ret, gas, err = RunPrecompiledContract(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. contract := NewContract(caller, addr, new(uint256.Int), gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) - - // When an error was returned by the EVM or when setting the creation code - // above we revert to the snapshot and consume any gas remaining. Additionally - // when we're in Homestead this also counts for code storage gas errors. ret, err = evm.Run(contract, input, true) gas = contract.Gas } @@ -473,41 +480,47 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) } - gas.Exhaust() } } - return ret, gas, err + return ret, gas.ExitFromErr(err, initialStateGas), err } // create creates a new contract using code as deployment code. -func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, leftOverGas GasBudget, err error) { - if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, typ, caller, address, code, gas.RegularGas, value.ToBig()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas.RegularGas, ret, err) - }(gas.RegularGas) - } +func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, result GasBudget, err error) { // Depth check execution. Fail if we're trying to execute above the // limit. + var nonce uint64 if evm.depth > int(params.CallCreateDepth) { - return nil, common.Address{}, gas, ErrDepth + err = ErrDepth + } else if !evm.Context.CanTransfer(evm.StateDB, caller, value) { + err = ErrInsufficientBalance + } else { + nonce = evm.StateDB.GetNonce(caller) + if nonce+1 < nonce { + err = ErrNonceUintOverflow + } } - if !evm.Context.CanTransfer(evm.StateDB, caller, value) { - return nil, common.Address{}, gas, ErrInsufficientBalance + initialStateGas := gas.StateGas + + if err == nil { + evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator) } - nonce := evm.StateDB.GetNonce(caller) - if nonce+1 < nonce { - return nil, common.Address{}, gas, ErrNonceUintOverflow + if evm.Config.Tracer != nil { + evm.captureBegin(evm.depth, typ, caller, address, code, gas, value.ToBig()) + defer func(startGas GasBudget) { + evm.captureEnd(evm.depth, startGas, result, ret, err) + }(gas) + } + if err != nil { + return nil, common.Address{}, gas.Preserved(), err } - evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator) // Charge the contract creation init gas in verkle mode if evm.chainRules.IsEIP4762 { statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas) prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas}) if !ok { - gas.Exhaust() - return nil, common.Address{}, gas, ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(initialStateGas), ErrOutOfGas } if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck) @@ -528,11 +541,13 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) { + halt := gas.ExitHalt(initialStateGas) if evm.Config.Tracer.HasGasHook() { - evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution) + evm.Config.Tracer.EmitGasChange(gas.AsTracing(), halt.AsTracing(), tracing.GasChangeCallFailedExecution) } - gas.Exhaust() - return nil, common.Address{}, gas, ErrContractAddressCollision + // EIP-8037 collision rule: the state reservoir is fully preserved on + // address collision while regular gas is burnt. + return nil, common.Address{}, halt, ErrContractAddressCollision } // Create a new account on the state only if the object was not present. // It might be possible the contract code is deployed to a pre-existent @@ -554,8 +569,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.chainRules.IsEIP4762 { consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas) if consumed < wanted { - gas.Exhaust() - return nil, common.Address{}, gas, ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(initialStateGas), ErrOutOfGas } prior, _ := gas.Charge(GasCosts{RegularGas: consumed}) if evm.Config.Tracer.HasGasHook() { @@ -574,13 +588,21 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value contract.IsDeployment = true ret, err = evm.initNewContract(contract, address) + + // Special case: ErrCodeStoreOutOfGas pre-Homestead does NOT roll back + // state and gas is preserved (i.e., treated as success). if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - contract.UseGas(GasCosts{RegularGas: contract.Gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallFailedExecution) + if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { + evm.Config.Tracer.OnGasChange(contract.Gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) + } } + return ret, address, contract.Gas.ExitFromErr(err, initialStateGas), err } - return ret, address, contract.Gas, err + // Either success, or pre-Homestead ErrCodeStoreOutOfGas (gas preserved). + // Both packaged as a success-form GasBudget. + return ret, address, contract.Gas.ExitSuccess(), err } // initNewContract runs a new contract's creation code, performs checks on the @@ -591,27 +613,43 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b return ret, err } - // Check whether the max code size has been exceeded, assign err if the case. - if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { - return ret, err - } - + // Check prefix before gas calculation. // Reject code starting with 0xEF if EIP-3541 is enabled. if len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon { return ret, ErrInvalidCode } - - if !evm.chainRules.IsEIP4762 { - createDataGas := uint64(len(ret)) * params.CreateDataGas - if !contract.UseGas(GasCosts{RegularGas: createDataGas}, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { + if evm.chainRules.IsEIP4762 { + consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas) + contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) + if len(ret) > 0 && (consumed < wanted) { + return ret, ErrCodeStoreOutOfGas + } + if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { + return ret, err + } + } else if evm.chainRules.IsAmsterdam { + // Check max code size BEFORE charging gas so over-max code + // does not consume state gas (which would inflate tx_state). + if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { + return ret, err + } + // Charge regular gas (hash cost) before state gas (code-deposit cost). + regularCost := toWordSize(uint64(len(ret))) * params.Keccak256WordGas + if !contract.chargeRegular(regularCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { + return ret, ErrCodeStoreOutOfGas + } + stateCost := uint64(len(ret)) * evm.Context.CostPerStateByte + if !contract.chargeState(stateCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { return ret, ErrCodeStoreOutOfGas } } else { - consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas.RegularGas) - contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) - if len(ret) > 0 && (consumed < wanted) { + createDataCost := uint64(len(ret)) * params.CreateDataGas + if !contract.chargeRegular(createDataCost, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { return ret, ErrCodeStoreOutOfGas } + if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { + return ret, err + } } if len(ret) > 0 { @@ -621,7 +659,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b } // Create creates a new contract using code as deployment code. -func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasBudget, err error) { +func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) { contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller)) return evm.create(caller, code, gas, value, contractAddr, CREATE) } @@ -630,7 +668,7 @@ func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value // // The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:] // instead of the usual sender-and-nonce-hash as the address where the contract is initialized at. -func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasBudget, err error) { +func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) { inithash := crypto.Keccak256Hash(code) contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:]) return evm.create(caller, code, gas, endowment, contractAddr, CREATE2) @@ -668,22 +706,20 @@ func (evm *EVM) resolveCodeHash(addr common.Address) common.Hash { // ChainConfig returns the environment's chain configuration func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig } -func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to common.Address, input []byte, startGas uint64, value *big.Int) { +func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to common.Address, input []byte, startGas GasBudget, value *big.Int) { tracer := evm.Config.Tracer if tracer.OnEnter != nil { - tracer.OnEnter(depth, byte(typ), from, to, input, startGas, value) + tracer.OnEnter(depth, byte(typ), from, to, input, startGas.RegularGas, value) } if tracer.HasGasHook() { - initial := NewGasBudget(startGas) - tracer.EmitGasChange(tracing.Gas{}, initial.AsTracing(), tracing.GasChangeCallInitialBalance) + tracer.EmitGasChange(tracing.Gas{}, startGas.AsTracing(), tracing.GasChangeCallInitialBalance) } } -func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret []byte, err error) { +func (evm *EVM) captureEnd(depth int, startGas GasBudget, leftOverGas GasBudget, ret []byte, err error) { tracer := evm.Config.Tracer - if leftOverGas != 0 && tracer.HasGasHook() { - leftover := NewGasBudget(leftOverGas) - tracer.EmitGasChange(leftover.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned) + if !leftOverGas.IsZero() && tracer.HasGasHook() { + tracer.EmitGasChange(leftOverGas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned) } var reverted bool if err != nil { @@ -693,7 +729,7 @@ func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret [ reverted = false } if tracer.OnExit != nil { - tracer.OnExit(depth, ret, startGas-leftOverGas, VMErrorFromErr(err), reverted) + tracer.OnExit(depth, ret, startGas.RegularGas-leftOverGas.RegularGas, VMErrorFromErr(err), reverted) } } diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 046311f9cc..dca4358fb2 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -291,10 +291,19 @@ var ( gasMLoad = pureMemoryGascost gasMStore8 = pureMemoryGascost gasMStore = pureMemoryGascost - gasCreate = pureMemoryGascost ) +func gasCreate(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } + return pureMemoryGascost(evm, contract, stack, mem, memorySize) +} + func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } gas, err := memoryGasCost(mem, memorySize) if err != nil { return GasCosts{}, err @@ -313,6 +322,9 @@ func gasCreate2(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memoryS } func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } gas, err := memoryGasCost(mem, memorySize) if err != nil { return GasCosts{}, err @@ -331,7 +343,11 @@ func gasCreateEip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m } return GasCosts{RegularGas: gas}, nil } + func gasCreate2Eip3860(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } gas, err := memoryGasCost(mem, memorySize) if err != nil { return GasCosts{}, err @@ -352,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 @@ -365,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 @@ -384,17 +400,17 @@ var ( gasStaticCall = makeCallVariantGasCost(gasStaticCallIntrinsic) ) -func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc { +func makeCallVariantGasCost(intrinsicFunc intrinsicGasFunc) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { intrinsic, err := intrinsicFunc(evm, contract, stack, mem, memorySize) if err != nil { return GasCosts{}, err } - evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic.RegularGas, stack.back(0)) + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic, stack.back(0)) if err != nil { return GasCosts{}, err } - gas, overflow := math.SafeAdd(intrinsic.RegularGas, evm.callGasTemp) + gas, overflow := math.SafeAdd(intrinsic, evm.callGasTemp) if overflow { return GasCosts{}, ErrGasUintOverflow } @@ -402,19 +418,19 @@ func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc { } } -func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { var ( gas uint64 transfersValue = !stack.back(2).IsZero() address = common.Address(stack.back(1).Bytes20()) ) if evm.readOnly && transfersValue { - return GasCosts{}, ErrWriteProtection + return 0, ErrWriteProtection } // Stateless check memoryGas, err := memoryGasCost(mem, memorySize) if err != nil { - return GasCosts{}, err + return 0, err } var transferGas uint64 if transfersValue && !evm.chainRules.IsEIP4762 { @@ -422,12 +438,12 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m } var overflow bool if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow { - return GasCosts{}, ErrGasUintOverflow + return 0, ErrGasUintOverflow } // Terminate the gas measurement if the leftover gas is not sufficient, // it can effectively prevent accessing the states in the following steps. if contract.Gas.RegularGas < gas { - return GasCosts{}, ErrOutOfGas + return 0, ErrOutOfGas } // Stateful check var stateGas uint64 @@ -439,56 +455,87 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m stateGas += params.CallNewAccountGas } if gas, overflow = math.SafeAdd(gas, stateGas); overflow { - return GasCosts{}, ErrGasUintOverflow + return 0, ErrGasUintOverflow } - return GasCosts{RegularGas: gas}, nil + return gas, nil } -func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +// gasCallIntrinsic8037 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) { + var ( + gas uint64 + transfersValue = !stack.back(2).IsZero() + ) + if evm.readOnly && transfersValue { + return 0, ErrWriteProtection + } memoryGas, err := memoryGasCost(mem, memorySize) if err != nil { - return GasCosts{}, err + return 0, err + } + var transferGas uint64 + if transfersValue && !evm.chainRules.IsEIP4762 { + transferGas = params.CallValueTransferGas + } + var overflow bool + if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow { + return 0, ErrGasUintOverflow + } + return gas, nil +} + +func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + memoryGas, err := memoryGasCost(mem, memorySize) + if err != nil { + return 0, err } var ( - gas uint64 - overflow bool + gas uint64 + overflow bool + transfersValue = !stack.back(2).IsZero() ) - if stack.back(2).Sign() != 0 && !evm.chainRules.IsEIP4762 { - gas += params.CallValueTransferGas + if transfersValue { + if !evm.chainRules.IsEIP4762 { + gas += params.CallValueTransferGas + } } if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { - return GasCosts{}, ErrGasUintOverflow + return 0, ErrGasUintOverflow } - return GasCosts{RegularGas: gas}, nil + return gas, nil } -func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { gas, err := memoryGasCost(mem, memorySize) if err != nil { - return GasCosts{}, err + return 0, err } - return GasCosts{RegularGas: gas}, nil + return gas, nil } -func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { gas, err := memoryGasCost(mem, memorySize) if err != nil { - return GasCosts{}, err + return 0, err } - return GasCosts{RegularGas: gas}, nil + return gas, nil } func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { if evm.readOnly { return GasCosts{}, ErrWriteProtection } - var gas uint64 // EIP150 homestead gas reprice fork: if evm.chainRules.IsEIP150 { gas = params.SelfdestructGasEIP150 - var address = common.Address(stack.back(0).Bytes20()) + if gas > contract.Gas.RegularGas { + return GasCosts{RegularGas: gas}, nil + } + var address = common.Address(stack.back(0).Bytes20()) if evm.chainRules.IsEIP158 { // if empty and transfers value if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { @@ -504,3 +551,169 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me } return GasCosts{RegularGas: gas}, nil } + +func gasCreateEip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } + gas, err := memoryGasCost(mem, memorySize) + if err != nil { + return GasCosts{}, err + } + size, overflow := stack.back(2).Uint64WithOverflow() + if overflow { + return GasCosts{}, ErrGasUintOverflow + } + if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil { + return GasCosts{}, err + } + // Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow + words := (size + 31) / 32 + wordGas := params.InitCodeWordGas * words + stateGas := params.AccountCreationSize * evm.Context.CostPerStateByte + return GasCosts{RegularGas: gas + wordGas, StateGas: stateGas}, nil +} + +func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } + gas, err := memoryGasCost(mem, memorySize) + if err != nil { + return GasCosts{}, err + } + size, overflow := stack.back(2).Uint64WithOverflow() + if overflow { + return GasCosts{}, ErrGasUintOverflow + } + if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil { + return GasCosts{}, err + } + // Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow + words := (size + 31) / 32 + // 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 +} + +// gasCall8037 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) { + var ( + gas GasCosts + transfersValue = !stack.back(2).IsZero() + address = common.Address(stack.back(1).Bytes20()) + ) + if evm.chainRules.IsEIP158 { + if transfersValue && evm.StateDB.Empty(address) { + gas.StateGas += params.AccountCreationSize * evm.Context.CostPerStateByte + } + } else if !evm.StateDB.Exist(address) { + gas.StateGas += params.AccountCreationSize * evm.Context.CostPerStateByte + } + return gas, nil +} + +func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } + var ( + gas GasCosts + address = common.Address(stack.peek().Bytes20()) + ) + if !evm.StateDB.AddressInAccessList(address) { + // If the caller cannot afford the cost, this change will be rolled back + evm.StateDB.AddAddressToAccessList(address) + gas.RegularGas = params.ColdAccountAccessCostEIP2929 + } + // Check we have enough regular gas before we add the address to the BAL + if contract.Gas.RegularGas < gas.RegularGas { + return gas, nil + } + // if empty and transfers value + if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { + gas.StateGas += params.AccountCreationSize * evm.Context.CostPerStateByte + } + return gas, nil +} + +func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + if evm.readOnly { + return GasCosts{}, ErrWriteProtection + } + // If we fail the minimum gas availability invariant, fail (0) + if contract.Gas.RegularGas <= params.SstoreSentryGasEIP2200 { + return GasCosts{}, errors.New("not enough gas for reentrancy sentry") + } + // Gas sentry honoured, do the actual gas calculation based on the stored value + var ( + y, x = stack.back(1), stack.peek() + slot = common.Hash(x.Bytes32()) + current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) + cost GasCosts + ) + // Check slot presence in the access list + if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + cost = GasCosts{RegularGas: params.ColdSloadCostEIP2929} + // If the caller cannot afford the cost, this change will be rolled back + evm.StateDB.AddSlotToAccessList(contract.Address(), slot) + } + value := common.Hash(y.Bytes32()) + + if current == value { // noop (1) + // EIP 2200 original clause: + // return params.SloadGasEIP2200, nil + return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // SLOAD_GAS + } + 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, + }, nil + } + if value == (common.Hash{}) { // delete slot (2.1.2b) + evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + } + // EIP-2200 original clause: + // return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2) + return GasCosts{RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929}, nil // write existing slot (2.1.2) + } + if original != (common.Hash{}) { + if current == (common.Hash{}) { // recreate slot (2.2.1.1) + evm.StateDB.SubRefund(params.SstoreClearsScheduleRefundEIP3529) + } else if value == (common.Hash{}) { // delete slot (2.2.1.2) + evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + } + } + if original == value { + if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1) + // EIP-8037 point (2): refund state gas directly to the reservoir + // at the SSTORE restoration point (0→x→0 in same tx); not to the + // refund counter, which is capped at gas_used/5. + contract.Gas.RefundState(params.StorageCreationSize * evm.Context.CostPerStateByte) + + // Regular portion of the refund still goes through the refund counter. + evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929) + } else { // reset to original existing slot (2.2.2.2) + // EIP 2200 Original clause: + // evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200) + // - SSTORE_RESET_GAS redefined as (5000 - COLD_SLOAD_COST) + // - SLOAD_GAS redefined as WARM_STORAGE_READ_COST + // Final: (5000 - COLD_SLOAD_COST) - WARM_STORAGE_READ_COST + evm.StateDB.AddRefund((params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) - params.WarmStorageReadCostEIP2929) + } + } + // EIP-2200 original clause: + //return params.SloadGasEIP2200, nil // dirty update (2.2) + return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // dirty update (2.2) +} diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go index 16ce651a7d..803ef31101 100644 --- a/core/vm/gas_table_test.go +++ b/core/vm/gas_table_test.go @@ -97,12 +97,12 @@ func TestEIP2200(t *testing.T) { Transfer: func(StateDB, common.Address, common.Address, *uint256.Int, *params.Rules) {}, } evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}}) - initialGas := NewGasBudget(tt.gaspool) - _, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int)) + initialGas := NewGasBudget(tt.gaspool, 0) + _, result, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int)) if !errors.Is(err, tt.failure) { t.Errorf("test %d: failure mismatch: have %v, want %v", i, err, tt.failure) } - if used := leftOver.Used(initialGas); used != tt.used { + if used := result.Used(initialGas); used != tt.used { t.Errorf("test %d: gas used mismatch: have %v, want %v", i, used, tt.used) } if refund := evm.StateDB.GetRefund(); refund != tt.refund { @@ -157,12 +157,12 @@ func TestCreateGas(t *testing.T) { } evm := NewEVM(vmctx, statedb, chainConfig, config) - initialGas := NewGasBudget(uint64(testGas)) - ret, leftOver, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int)) + initialGas := NewGasBudget(uint64(testGas), 0) + ret, result, err := evm.Call(common.Address{}, address, nil, initialGas.Copy(), new(uint256.Int)) if err != nil { return false } - gasUsed = leftOver.Used(initialGas) + gasUsed = result.Used(initialGas) if len(ret) != 32 { t.Fatalf("test %d: expected 32 bytes returned, have %d", i, len(ret)) } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index ed938ae41f..0865a0982d 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -22,9 +22,8 @@ import ( "github.com/ethereum/go-ethereum/core/tracing" ) -// GasCosts denotes a vector of gas costs in the -// multidimensional metering paradigm. It represents the cost -// charged by an individual operation. +// GasCosts denotes a vector of gas costs in the multidimensional metering +// paradigm. It represents the cost charged by an individual operation. type GasCosts struct { RegularGas uint64 StateGas uint64 @@ -40,67 +39,263 @@ func (g GasCosts) String() string { return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas) } -// GasBudget denotes a vector of remaining gas allowances available -// for EVM execution in the multidimensional metering paradigm. -// Unlike GasCosts which represents the price of an operation, -// GasBudget tracks how much gas is left to spend. +// GasBudget is the unified gas-state structure used throughout the EVM. +// It carries two pairs of fields: +// +// - RegularGas / StateGas: the running balance during execution, or the +// leftover balance the caller must absorb after a sub-call. +// - UsedRegularGas / UsedStateGas: per-frame accumulators tracking gross +// consumption. UsedStateGas is signed so it can be decremented by inline +// state-gas refunds (e.g., SSTORE 0->A->0). +// +// The same struct serves three roles: +// +// - During execution: Charge / ChargeRegular / ChargeState / RefundState +// and RefundRegular mutate the running balance and the usage accumulators +// in lockstep. +// +// - At frame exit: Preserved / ExitSuccess / ExitRevert / ExitHalt / +// ExitFromErr 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 +// budget into its own running budget. type GasBudget struct { - RegularGas uint64 // The leftover gas for execution and state gas usage - StateGas uint64 // The state gas reservoir + RegularGas uint64 // remaining regular-gas balance (or leftover for caller to absorb) + StateGas uint64 // remaining state-gas reservoir (or leftover for caller to absorb) + UsedRegularGas uint64 // gross regular gas consumed in this frame + UsedStateGas int64 // signed net state-gas consumed in this frame } -// NewGasBudget creates a GasBudget with the given initial regular gas allowance. -func NewGasBudget(gas uint64) GasBudget { - return GasBudget{RegularGas: gas} +// NewGasBudget initializes a fresh GasBudget for execution / forwarding, +// with both usage accumulators set to zero. +func NewGasBudget(regular, state uint64) GasBudget { + return GasBudget{RegularGas: regular, StateGas: state} } -// Used returns the amount of regular gas consumed so far. +// Used returns the total scalar gas consumed relative to an initial budget +// (= (initial.regular + initial.state) − (current.regular + current.state)). +// This is the payment scalar (EIP-8037's tx_gas_used_before_refund). +// +// TODO(rjl493456442) the total used gas can be calculated via g.UsedRegularGas +// and g.UsedStateGas. func (g GasBudget) Used(initial GasBudget) uint64 { - return initial.RegularGas - g.RegularGas + return (initial.RegularGas + initial.StateGas) - (g.RegularGas + g.StateGas) } -// Exhaust sets all remaining gas to zero, preserving the initial amount -// for usage tracking. -func (g *GasBudget) Exhaust() { - g.RegularGas = 0 - g.StateGas = 0 +// Copy returns a deep copy of the budget. +func (g GasBudget) Copy() GasBudget { + return g } -func (g *GasBudget) Copy() GasBudget { - return GasBudget{RegularGas: g.RegularGas, StateGas: g.StateGas} -} - -// String returns a visual representation of the gas budget vector. +// String returns a visual representation of the budget. func (g GasBudget) String() string { - return fmt.Sprintf("<%v,%v>", g.RegularGas, g.StateGas) + return fmt.Sprintf("<%v,%v,used=<%v,%v>>", g.RegularGas, g.StateGas, g.UsedRegularGas, g.UsedStateGas) } -// CanAfford reports whether the budget has sufficient gas to cover the cost. +// CanAfford reports whether the running balance can cover the given cost. +// State-gas charges that exceed the reservoir spill into regular gas. func (g GasBudget) CanAfford(cost GasCosts) bool { - return g.RegularGas >= cost.RegularGas + if g.RegularGas < cost.RegularGas { + return false + } + if cost.StateGas > g.StateGas { + spillover := cost.StateGas - g.StateGas + if spillover > g.RegularGas-cost.RegularGas { + return false + } + } + return true } -// Charge deducts the given gas cost from the budget. It returns the -// pre-charge budget and false if the budget does not have sufficient -// gas to cover the cost. +// Charge deducts a combined regular+state cost from the running balance and +// updates the usage accumulators. State-gas in excess of the reservoir spills +// into regular_gas. func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) { prior := *g - if g.RegularGas < cost.RegularGas { + if !g.CanAfford(cost) { return prior, false } + // Charge regular gas g.RegularGas -= cost.RegularGas - return prior, true -} + g.UsedRegularGas += cost.RegularGas -// Refund adds the given gas budget back. It returns the pre-refund budget -// and whether the budget was actually changed. -func (g *GasBudget) Refund(other GasBudget) (GasBudget, bool) { - prior := *g - g.RegularGas += other.RegularGas - return prior, g.RegularGas != prior.RegularGas + // Charge state gas + if cost.StateGas > g.StateGas { + spillover := cost.StateGas - g.StateGas + g.StateGas = 0 + g.RegularGas -= spillover + } else { + g.StateGas -= cost.StateGas + } + g.UsedStateGas += int64(cost.StateGas) + return prior, true } // AsTracing converts the GasBudget into the tracing-facing Gas vector. func (g GasBudget) AsTracing() tracing.Gas { return tracing.Gas{Regular: g.RegularGas, State: g.StateGas} } + +// ChargeRegular is a convenience that deducts a regular-only cost. +func (g *GasBudget) ChargeRegular(r uint64) (GasBudget, bool) { + return g.Charge(GasCosts{RegularGas: r}) +} + +// ChargeState is a convenience that deducts a state-only cost (spills to +// regular when the reservoir is exhausted). Returns false on OOG. +func (g *GasBudget) ChargeState(s uint64) (GasBudget, bool) { + return g.Charge(GasCosts{StateGas: s}) +} + +// IsZero returns an indicator if the gas budget has been exhausted. +func (g *GasBudget) IsZero() bool { + return g.RegularGas == 0 && g.StateGas == 0 +} + +// RefundState applies an inline state-gas refund (e.g., SSTORE 0->A->0). +// The reservoir is credited and the signed usage counter is decremented +// in lockstep, preserving the per-frame invariant: +// +// StateGas + UsedStateGas == initialStateGas + spillover_so_far +// +// which the revert path relies on for the correct gross refund. +func (g *GasBudget) RefundState(s uint64) { + g.StateGas += s + g.UsedStateGas -= int64(s) +} + +// RefundRegular applies an inline regular-gas refund. +func (g *GasBudget) RefundRegular(s uint64) { + g.RegularGas += s + g.UsedRegularGas -= s +} + +// Forward drains `regular` regular gas and the entire state reservoir from +// the parent's running budget and returns the initial GasBudget for a child +// frame. The parent's UsedRegularGas is bumped by the forwarded amount so +// that the absorb-on-return path correctly reclaims the unused portion. +// +// Used by frame boundaries where the regular forward has NOT been pre- +// deducted: tx-level dispatch (state_transition) and CREATE / CREATE2. The +// CALL family pre-deducts the forward via the dynamic gas table for tracer- +// reporting reasons and therefore constructs its child budget directly. +// +// Caller must ensure `regular` does not exceed the running balance and +// apply any EIP-150 1/64 retention before calling Forward. +func (g *GasBudget) Forward(regular uint64) GasBudget { + g.RegularGas -= regular + g.UsedRegularGas += regular + + child := GasBudget{ + RegularGas: regular, + StateGas: g.StateGas, + } + g.StateGas = 0 + return child +} + +// ForwardAll forwards the parent's full remaining budget (both regular and +// state) to a child frame. Equivalent to Forward(g.RegularGas) — used at +// the tx boundary where there is no 1/64 retention. +func (g *GasBudget) ForwardAll() GasBudget { + return g.Forward(g.RegularGas) +} + +// ============================================================================ +// Exit-form constructors. These take a post-execution running budget and +// produce a new GasBudget in "leftover form" — the value the caller should +// absorb to update its own state. +// ============================================================================ + +// Preserved produces a leftover form with the running balance preserved and +// usage zeroed. Use this for pre-execution validation failures (depth, +// balance, EIP-158 zero-value-to-nonexistent) where no execution actually +// occurred. +func (g GasBudget) Preserved() GasBudget { + return GasBudget{ + RegularGas: g.RegularGas, + StateGas: g.StateGas, + // UsedRegularGas / UsedStateGas stay at zero. + } +} + +// ExitSuccess produces the leftover form for a successful frame. Inline +// state-gas refunds have already been folded into StateGas / UsedStateGas +// during execution; the running budget IS the exit budget on success. +func (g GasBudget) ExitSuccess() GasBudget { + return g +} + +// ExitRevert produces the leftover form for a REVERT exit. Per EIP-8037, +// the gross state-gas charged in the failing subtree is refunded to the +// caller's reservoir via the signed-counter formula +// +// leftover.StateGas = StateGas + UsedStateGas +// +// and UsedStateGas is zeroed because the state effect is routed through +// StateGas. UsedRegularGas is unchanged. +func (g GasBudget) ExitRevert() GasBudget { + reservoir := int64(g.StateGas) + g.UsedStateGas + if reservoir < 0 { + // Defensive: invariant guarantees non-negativity. Clamping prevents + // a hypothetical bug from sign-extending to a huge uint64. + reservoir = 0 + } + return GasBudget{ + RegularGas: g.RegularGas, + StateGas: uint64(reservoir), + UsedRegularGas: g.UsedRegularGas, + UsedStateGas: 0, + } +} + +// ExitHalt produces the leftover form for an exceptional halt. Remaining +// regular gas is accounted as burned; the reservoir resets to initialStateGas +// (the value the caller held at the call site). UsedStateGas is zeroed +// because state-gas effects do not persist on halt. +func (g GasBudget) ExitHalt(initialStateGas uint64) GasBudget { + return GasBudget{ + RegularGas: 0, + StateGas: initialStateGas, + UsedRegularGas: g.UsedRegularGas + g.RegularGas, + UsedStateGas: 0, + } +} + +// ExitFromErr dispatches on err to the appropriate exit-form constructor +// for the post-evm.Run path: +// +// - err == nil → ExitSuccess +// - err == ErrExecutionReverted → ExitRevert +// - any other err → ExitHalt +// +// 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 { + switch { + case err == nil: + return g.ExitSuccess() + case err == ErrExecutionReverted: + return g.ExitRevert() + default: + return g.ExitHalt(initialStateGas) + } +} + +// Absorb merges a sub-call's leftover GasBudget into this (caller's) running +// budget. The caller's UsedRegularGas is reclaimed by the unused forwarded +// regular gas (which was pre-charged in full at call entry); the state +// reservoir is overwritten with the child's leftover; and the child's signed +// net state-gas usage is added to the caller's accumulator. +// +// Invariant maintained by all callers: at the moment of this call, the +// caller's UsedRegularGas already accounts for the FULL forwarded regular +// gas (as if the child had consumed all of it). On halt, child.RegularGas +// is 0 so the reclaim is a no-op. +func (g *GasBudget) Absorb(child GasBudget) { + g.UsedRegularGas -= child.RegularGas + g.RegularGas += child.RegularGas + g.StateGas = child.StateGas + g.UsedStateGas += child.UsedStateGas +} diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 4b05092cc7..0f5274857c 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -647,25 +647,21 @@ func opSwap16(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - if evm.readOnly { - return nil, ErrWriteProtection - } var ( value = scope.Stack.pop() offset, size = scope.Stack.pop(), scope.Stack.pop() input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) - gas = scope.Contract.Gas.RegularGas + forward = scope.Contract.Gas.RegularGas ) if evm.chainRules.IsEIP150 { - gas -= gas / 64 + forward -= forward / 64 } // reuse size int for stackvalue stackvalue := size - scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation) - - res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, NewGasBudget(gas), &value) + child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation) + res, addr, result, suberr := evm.Create(scope.Contract.Address(), input, child, &value) // Push item on the stack based on the returned error. If the ruleset is // homestead we must check for CodeStoreOutOfGasError (homestead only // rule) and treat as an error, if the ruleset is frontier we must @@ -679,8 +675,10 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } scope.Stack.push(&stackvalue) - scope.Contract.RefundGas(returnGas, 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) + } if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer return res, nil @@ -690,24 +688,21 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { - if evm.readOnly { - return nil, ErrWriteProtection - } var ( endowment = scope.Stack.pop() offset, size = scope.Stack.pop(), scope.Stack.pop() salt = scope.Stack.pop() input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) - gas = scope.Contract.Gas.RegularGas + forward = scope.Contract.Gas.RegularGas ) // Apply EIP150 - gas -= gas / 64 - scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation2) + forward -= forward / 64 + // reuse size int for stackvalue stackvalue := size - res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, NewGasBudget(gas), - &endowment, &salt) + child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation2) + res, addr, result, suberr := evm.Create2(scope.Contract.Address(), input, child, &endowment, &salt) // Push item on the stack based on the returned error. if suberr != nil { stackvalue.Clear() @@ -715,7 +710,10 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { stackvalue.SetBytes(addr.Bytes()) } scope.Stack.push(&stackvalue) - scope.Contract.RefundGas(returnGas, 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) + } if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer @@ -743,7 +741,12 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if !value.IsZero() { gas += params.CallStipend } - ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value) + + // Regular gas for the forward was already pre-deducted by the dynamic + // gas table (see makeCallVariantGasCallEIP*); only the state reservoir + // needs to be handed off to the child here. + childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) + ret, result, err := evm.Call(scope.Contract.Address(), toAddr, args, childBudget, &value) if err != nil { temp.Clear() @@ -755,7 +758,7 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.RefundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -777,7 +780,8 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { gas += params.CallStipend } - ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, NewGasBudget(gas), &value) + childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) + ret, result, err := evm.CallCode(scope.Contract.Address(), toAddr, args, childBudget, &value) if err != nil { temp.Clear() } else { @@ -788,7 +792,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.RefundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -806,7 +810,8 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, NewGasBudget(gas), scope.Contract.value) + childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) + ret, result, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, childBudget, scope.Contract.value) if err != nil { temp.Clear() } else { @@ -816,8 +821,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(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.RefundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -835,7 +839,8 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - ret, returnGas, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, NewGasBudget(gas)) + childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) + ret, result, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, childBudget) if err != nil { temp.Clear() } else { @@ -846,7 +851,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(returnGas, 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 a9938c2a28..8c67a553c4 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -71,6 +71,14 @@ 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/interpreter.go b/core/vm/interpreter.go index 3994327247..ca33670163 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -174,7 +174,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte // associated costs. contractAddr := contract.Address() consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas.RegularGas) - contract.UseGas(GasCosts{RegularGas: consumed}, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) + contract.chargeRegular(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) if consumed < wanted { return nil, ErrOutOfGas } @@ -192,10 +192,8 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack} } // for tracing: this gas consumption event is emitted below in the debug section. - if contract.Gas.RegularGas < cost { + if !contract.chargeRegular(cost, nil, tracing.GasChangeIgnored) { return nil, ErrOutOfGas - } else { - contract.Gas.RegularGas -= cost } // All ops with a dynamic memory usage also has a dynamic gas cost. @@ -224,11 +222,13 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte if err != nil { return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err) } - // for tracing: this gas consumption event is emitted below in the debug section. - if contract.Gas.RegularGas < dynamicCost.RegularGas { + // EIP-8037: charge regular gas before state gas. The state charge + // is a no-op when dynamicCost.StateGas == 0 (e.g., pre-Amsterdam). + if !contract.chargeRegular(dynamicCost.RegularGas, nil, tracing.GasChangeIgnored) { + return nil, ErrOutOfGas + } + if !contract.chargeState(dynamicCost.StateGas, nil, tracing.GasChangeIgnored) { return nil, ErrOutOfGas - } else { - contract.Gas.RegularGas -= dynamicCost.RegularGas } } diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go index 868cb12d04..42530b83b7 100644 --- a/core/vm/interpreter_test.go +++ b/core/vm/interpreter_test.go @@ -55,7 +55,7 @@ func TestLoopInterrupt(t *testing.T) { timeout := make(chan bool) go func(evm *EVM) { - _, _, err := evm.Call(common.Address{}, address, nil, NewGasBudget(math.MaxUint64), new(uint256.Int)) + _, _, err := evm.Call(common.Address{}, address, nil, NewGasBudget(math.MaxUint64, 0), new(uint256.Int)) errChannel <- err }(evm) @@ -85,7 +85,7 @@ func BenchmarkInterpreter(b *testing.B) { value = uint256.NewInt(0) stack = newStackForTesting() mem = NewMemory() - contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas), nil) + contract = NewContract(common.Address{}, common.Address{}, value, NewGasBudget(startGas, 0), nil) ) stack.push(uint256.NewInt(123)) stack.push(uint256.NewInt(123)) diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 82fc43ec13..9ea8349e3a 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -23,8 +23,9 @@ import ( ) type ( - executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error) - gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (GasCosts, error) // last parameter is the requested memory size as a uint64 + executionFunc func(pc *uint64, evm *EVM, callContext *ScopeContext) ([]byte, error) + gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (GasCosts, error) // last parameter is the requested memory size as a uint64 + 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) ) @@ -97,6 +98,7 @@ func newAmsterdamInstructionSet() JumpTable { instructionSet := newOsakaInstructionSet() enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode) enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE) + enable8037(&instructionSet) // EIP-8037 (State creation gas cost increase) return validate(instructionSet) } diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 86ac262a93..6ddb54c94a 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -168,7 +168,7 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g evm.StateDB.AddAddressToAccessList(addr) // Charge the remaining difference here already, to correctly calculate available // gas for call - if !contract.UseGas(GasCosts{RegularGas: coldCost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + if !contract.chargeRegular(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } } @@ -245,6 +245,10 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { return GasCosts{}, ErrOutOfGas } } + if gas > contract.Gas.RegularGas { + return GasCosts{RegularGas: gas}, nil + } + // if empty and transfers value if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { gas += params.CreateBySelfdestructGas @@ -262,6 +266,8 @@ var ( gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic) gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic) gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic) + + innerGasCallEIP8037 = makeCallVariantGasCallEIP8037(gasCallIntrinsic8037, gasCall8037) ) func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { @@ -276,7 +282,15 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem return innerGasCallEIP7702(evm, contract, stack, mem, memorySize) } -func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { +func gasCallEIP8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + transfersValue := !stack.back(2).IsZero() + if evm.readOnly && transfersValue { + return GasCosts{}, ErrWriteProtection + } + return innerGasCallEIP8037(evm, contract, stack, mem, memorySize) +} + +func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( eip2929Cost uint64 @@ -295,7 +309,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { // Charge the remaining difference here already, to correctly calculate // available gas for call - if !contract.UseGas(GasCosts{RegularGas: eip2929Cost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + if !contract.chargeRegular(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } } @@ -312,7 +326,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { // Terminate the gas measurement if the leftover gas is not sufficient, // it can effectively prevent accessing the states in the following steps. // It's an essential safeguard before any stateful check. - if contract.Gas.RegularGas < intrinsicCost.RegularGas { + if contract.Gas.RegularGas < intrinsicCost { return GasCosts{}, ErrOutOfGas } @@ -324,13 +338,13 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { evm.StateDB.AddAddressToAccessList(target) eip7702Cost = params.ColdAccountAccessCostEIP2929 } - if !contract.UseGas(GasCosts{RegularGas: eip7702Cost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } } // Calculate the gas budget for the nested call. The costs defined by // EIP-2929 and EIP-7702 have already been applied. - evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost.RegularGas, stack.back(0)) + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost, stack.back(0)) if err != nil { return GasCosts{}, err } @@ -339,6 +353,10 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { // part of the dynamic gas. This will ensure it is correctly reported to // tracers. contract.Gas.RegularGas += eip2929Cost + eip7702Cost + // Undo the RegularGasUsed increments from the direct UseGas charges, + // since this gas will be re-charged via the returned cost. + contract.Gas.UsedRegularGas -= eip2929Cost + contract.Gas.UsedRegularGas -= eip7702Cost // Aggregate the gas costs from all components, including EIP-2929, EIP-7702, // the CALL opcode itself, and the cost incurred by nested calls. @@ -349,7 +367,94 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow { return GasCosts{}, ErrGasUintOverflow } - if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost.RegularGas); overflow { + if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost); overflow { + return GasCosts{}, ErrGasUintOverflow + } + if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow { + return GasCosts{}, ErrGasUintOverflow + } + return GasCosts{RegularGas: totalCost}, nil + } +} + +// makeCallVariantGasCallEIP8037 creates a call gas function for Amsterdam (EIP-8037). +// 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 { + return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + var ( + eip2929Cost uint64 + eip7702Cost uint64 + addr = common.Address(stack.back(1).Bytes20()) + ) + // EIP-2929 cold access check. + if !evm.StateDB.AddressInAccessList(addr) { + evm.StateDB.AddAddressToAccessList(addr) + eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + if !contract.chargeRegular(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + return GasCosts{}, ErrOutOfGas + } + } + + // Compute intrinsic cost (memory + transfer, no new account creation). + intrinsicCost, err := intrinsicFunc(evm, contract, stack, mem, memorySize) + if err != nil { + return GasCosts{}, err + } + + // 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) { + return GasCosts{}, ErrOutOfGas + } + + // EIP-7702 delegation check. + if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok { + if evm.StateDB.AddressInAccessList(target) { + eip7702Cost = params.WarmStorageReadCostEIP2929 + } else { + evm.StateDB.AddAddressToAccessList(target) + eip7702Cost = params.ColdAccountAccessCostEIP2929 + } + if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + return GasCosts{}, ErrOutOfGas + } + } + + // Compute and charge state gas (new account creation) AFTER regular gas. + stateGas, err := stateGasFunc(evm, contract, stack, mem, memorySize) + 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 { + return GasCosts{}, ErrOutOfGas + } + } + + // Calculate the gas budget for the nested call (63/64 rule). + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, 0, stack.back(0)) + if err != nil { + return GasCosts{}, err + } + + // 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 + + // Aggregate total cost. + var ( + overflow bool + totalCost uint64 + ) + if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow { + return GasCosts{}, ErrGasUintOverflow + } + if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost); overflow { return GasCosts{}, ErrGasUintOverflow } if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow { diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index 4fafdf3a50..34dec1bd4a 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -144,15 +144,15 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) { // set the receiver's (the executing contract) code for execution. cfg.State.SetCode(address, code, tracing.CodeChangeUnspecified) // Call the code with the given configuration. - ret, leftOverGas, err := vmenv.Call( + ret, result, err := vmenv.Call( cfg.Origin, common.BytesToAddress([]byte("contract")), input, - vm.NewGasBudget(cfg.GasLimit), + vm.NewGasBudget(cfg.GasLimit, 0), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { - cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err) + cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err) } return ret, cfg.State, err } @@ -179,16 +179,16 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) { // - reset transient storage(eip 1153) cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil) // Call the code with the given configuration. - code, address, leftOverGas, err := vmenv.Create( + code, address, result, err := vmenv.Create( cfg.Origin, input, - vm.NewGasBudget(cfg.GasLimit), + vm.NewGasBudget(cfg.GasLimit, 0), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { - cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err) + cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err) } - return code, address, leftOverGas.RegularGas, err + return code, address, result.RegularGas, err } // Call executes the code given by the contract's address. It will return the @@ -213,15 +213,15 @@ func Call(address common.Address, input []byte, cfg *Config) ([]byte, uint64, er statedb.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.ActivePrecompiles(rules), nil) // Call the code with the given configuration. - ret, leftOverGas, err := vmenv.Call( + ret, result, err := vmenv.Call( cfg.Origin, address, input, - vm.NewGasBudget(cfg.GasLimit), + vm.NewGasBudget(cfg.GasLimit, 0), uint256.MustFromBig(cfg.Value), ) if cfg.EVMConfig.Tracer != nil && cfg.EVMConfig.Tracer.OnTxEnd != nil { - cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - leftOverGas.RegularGas}, err) + cfg.EVMConfig.Tracer.OnTxEnd(&types.Receipt{GasUsed: cfg.GasLimit - result.RegularGas}, err) } - return ret, leftOverGas.RegularGas, err + return ret, result.RegularGas, err } diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go index 6570d73575..2fefa46492 100644 --- a/eth/tracers/js/tracer_test.go +++ b/eth/tracers/js/tracer_test.go @@ -55,7 +55,7 @@ func runTrace(tracer *tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCo gasLimit uint64 = 31000 startGas uint64 = 10000 value = uint256.NewInt(0) - contract = vm.NewContract(common.Address{}, common.Address{}, value, vm.NewGasBudget(startGas), nil) + contract = vm.NewContract(common.Address{}, common.Address{}, value, vm.NewGasBudget(startGas, 0), nil) ) evm.SetTxContext(vmctx.txCtx) contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x1, 0x0} diff --git a/eth/tracers/logger/logger_test.go b/eth/tracers/logger/logger_test.go index decdf588e1..73868d22e0 100644 --- a/eth/tracers/logger/logger_test.go +++ b/eth/tracers/logger/logger_test.go @@ -47,7 +47,7 @@ func TestStoreCapture(t *testing.T) { var ( logger = NewStructLogger(nil) evm = vm.NewEVM(vm.BlockContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger.Hooks()}) - contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), vm.NewGasBudget(100000), nil) + contract = vm.NewContract(common.Address{}, common.Address{}, new(uint256.Int), vm.NewGasBudget(100000, 0), nil) ) contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)} var index common.Hash diff --git a/params/protocol_params.go b/params/protocol_params.go index 3e36b83547..b7dcd0b81b 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -88,6 +88,7 @@ const ( LogTopicGas uint64 = 375 // Multiplied by the * of the LOG*, per LOG transaction. e.g. LOG0 incurs 0 * c_txLogTopicGas, LOG4 incurs 4 * c_txLogTopicGas. CreateGas uint64 = 32000 // Once per CREATE operation & contract-creation transaction. Create2Gas uint64 = 32000 // Once per CREATE2 operation + CreateGasAmsterdam uint64 = 9000 // Regular gas portion of CREATE in Amsterdam (EIP-8037); state gas is charged separately. CreateNGasEip4762 uint64 = 1000 // Once per CREATEn operations post-verkle SelfdestructRefundGas uint64 = 24000 // Refunded following a selfdestruct operation. MemoryGas uint64 = 3 // Times the address of the (highest referenced byte in memory + 1). NOTE: referencing happens on read, write and in instructions such as RETURN and CALL. @@ -100,6 +101,7 @@ const ( TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702 + TxAuthTupleRegularGas uint64 = 7500 // Per auth tuple regular gas specified in EIP-8037 // These have been changed during the course of the chain CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction. @@ -196,6 +198,11 @@ const ( // the bound has a small safety margin for system-contract accesses that // don't consume block gas. BALItemCost uint64 = 2000 + + AccountCreationSize = 112 + StorageCreationSize = 32 + AuthorizationCreationSize = 23 + CostPerStateByte = 1174 ) // Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation diff --git a/tests/state_test.go b/tests/state_test.go index cf1d4bce4c..f6ae90c21f 100644 --- a/tests/state_test.go +++ b/tests/state_test.go @@ -325,10 +325,10 @@ func runBenchmark(b *testing.B, t *StateTest) { b.StartTimer() start := time.Now() - initialGas := vm.NewGasBudget(msg.GasLimit) + initialGas := vm.NewGasBudget(msg.GasLimit, 0) // Execute the message. - _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), msg.Value) + _, result, err := evm.Call(sender.Address(), *msg.To, msg.Data, initialGas.Copy(), msg.Value) if err != nil { b.Error(err) return @@ -337,7 +337,7 @@ func runBenchmark(b *testing.B, t *StateTest) { b.StopTimer() elapsed += uint64(time.Since(start)) refund += state.StateDB.GetRefund() - gasUsed += leftOverGas.Used(initialGas) + gasUsed += result.Used(initialGas) state.StateDB.RevertToSnapshot(snapshot) } diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 572c109f1e..88745dabaa 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -80,8 +80,8 @@ func (tt *TransactionTest) Run() error { if err != nil { return } - // Intrinsic gas - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, rules.IsAmsterdam) + // Intrinsic cost + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, 0) if err != nil { return } From 5963fc8408aba77d4b4fe1aaf5faa38ac1c1effe Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Mon, 18 May 2026 11:19:25 +0800 Subject: [PATCH 02/10] 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 } From 39fb6df9515b9a8cb3d507dc90a81c94919f7dd0 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 20 May 2026 15:18:03 +0800 Subject: [PATCH 03/10] core: absorb marius's fix from the baldev-7 branch --- core/blockchain.go | 6 +++++ core/state_transition.go | 57 +++++++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 7b5a910b7a..eb4ea7ebc1 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2301,6 +2301,12 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation stats.CrossValidation = xvtime // The time spent on stateless cross validation + // Attach the computed block access list so it gets persisted alongside the + // block. The validator has already verified the hash matches the header. + if res.Bal != nil && block.AccessList() == nil { + block = block.WithAccessListUnsafe(res.Bal.ToEncodingObj()) + } + // Write the block to the chain and get the status. var status WriteStatus if config.WriteState { diff --git a/core/state_transition.go b/core/state_transition.go index b026bbcbf9..1314fdcf4b 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -781,21 +781,28 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { st.gasRemaining.RefundRegular(st.calcRefund()) if rules.IsPrague { - // 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. + // EIP-7623 floor: tx_gas_used_after_refund = max(used, calldata_floor). + // Drain the leftover gas budget — regular first, then state — to bring + // gasUsed up to the floor. State must be drained too because a failed + // contract-creation top-level refund (line ~770) can move otherwise-spent + // gas back into the state reservoir, leaving RegularGas too small to + // satisfy the floor on its own. if used := st.gasUsed(); used < floorDataGas { - prior, _ := st.gasRemaining.ChargeRegular(floorDataGas - used) + prior := st.gasRemaining + need := floorDataGas - used + if take := min(need, st.gasRemaining.RegularGas); take > 0 { + st.gasRemaining.RegularGas -= take + st.gasRemaining.UsedRegularGas += take + need -= take + } + if take := min(need, st.gasRemaining.StateGas); take > 0 { + st.gasRemaining.StateGas -= take + st.gasRemaining.UsedStateGas += int64(take) + need -= take + } + if need > 0 { + return nil, fmt.Errorf("insufficient gas for floor cost, remaining: %d", need) + } if st.evm.Config.Tracer.HasGasHook() { st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxDataFloor) } @@ -901,7 +908,6 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se if err != nil { return err } - // If the account already exists in state, refund the new account cost // charged in the intrinsic calculation. if st.state.Exist(authority) { @@ -912,24 +918,33 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) } } - prevDelegation, isDelegated := types.ParseDelegation(st.state.GetCode(authority)) + if rules.IsAmsterdam { + // EIP-8037: also refund the auth-base state gas when no new delegation + // indicator bytes are written. Two cases: + // - the authority already has a delegation (overwrite in place); or + // - the auth is no-op (auth.Address == 0). + // In both cases the 23 delegation bytes are reused, so the auth-base + // portion of the intrinsic state gas is refilled. + if isDelegated || auth.Address == (common.Address{}) { + st.gasRemaining.RefundState(params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte) + } + } // Update nonce and account code. st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization) + + // Delegation to zero address means clear. if auth.Address == (common.Address{}) { - // Delegation to zero address means clear. if isDelegated { st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear) } return nil } - - // install delegation to auth.Address if the delegation changed + // Install delegation to auth.Address if the delegation changed if !isDelegated || auth.Address != prevDelegation { st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) } - return nil } @@ -958,7 +973,7 @@ func (st *stateTransition) calcRefund() uint64 { // 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 := uint256.NewInt(gas) remaining.Mul(remaining, st.msg.GasPrice) st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) From ce259970916ac7aeff9ddb5900e0c4627a67d0dd Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 20 May 2026 16:27:14 +0800 Subject: [PATCH 04/10] core/state: improve authorization state gas refund --- core/state/state_object.go | 24 +++++++++++ core/state/statedb.go | 12 ++++++ core/state/statedb_hooked.go | 4 ++ core/state_transition.go | 83 ++++++++++++++++++++++++++++++------ core/vm/interface.go | 5 +++ 5 files changed, 114 insertions(+), 14 deletions(-) diff --git a/core/state/state_object.go b/core/state/state_object.go index ce456e7668..2165fad57a 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -572,6 +572,30 @@ func (s *stateObject) Code() []byte { return code } +// GetCommittedCode returns the contract code committed at the start of the +// current execution, ignoring any intra-execution SetCode modifications. +// Used by EIP-7702 authorization application to make refund decisions +// relative to the originally-committed delegation, matching the spirit of +// GetCommittedState for storage slots. +func (s *stateObject) GetCommittedCode() []byte { + // The account did not exist at the start of the current execution. + if s.origin == nil { + return nil + } + hash := common.BytesToHash(s.origin.CodeHash) + if hash == types.EmptyCodeHash { + return nil + } + // If the code has not been touched in this execution, the live cache + // already holds the committed code. + if !s.dirtyCode { + return s.Code() + } + // Code was modified within the current execution. Reach for the on-disk + // blob keyed by the origin hash. + return s.db.reader.Code(s.address, hash) +} + // CodeSize returns the size of the contract code associated with this object, // or zero if none. This method is an almost mirror of Code, but uses a cache // inside the database to avoid loading codes seen recently. diff --git a/core/state/statedb.go b/core/state/statedb.go index 1c49d46020..15c175fd30 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -397,6 +397,18 @@ func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { return common.Hash{} } +// GetCommittedCode returns the contract code committed at the start of the +// current execution, ignoring any in-progress SetCode mutations. Returns +// nil when the account had no code (or did not exist) prior to this +// execution. +func (s *StateDB) GetCommittedCode(addr common.Address) []byte { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.GetCommittedCode() + } + return nil +} + // GetState retrieves the value associated with the specific key. func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { stateObject := s.getStateObject(addr) diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 98d01343a4..184629ea41 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -75,6 +75,10 @@ func (s *hookedStateDB) GetCode(addr common.Address) []byte { return s.inner.GetCode(addr) } +func (s *hookedStateDB) GetCommittedCode(addr common.Address) []byte { + return s.inner.GetCommittedCode(addr) +} + func (s *hookedStateDB) GetCodeSize(addr common.Address) int { return s.inner.GetCodeSize(addr) } diff --git a/core/state_transition.go b/core/state_transition.go index 1314fdcf4b..b8ca6714fa 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -744,12 +744,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) // Apply EIP-7702 authorizations. - if msg.SetCodeAuthorizations != nil { - for _, auth := range msg.SetCodeAuthorizations { - // Note errors are ignored, we simply skip invalid authorizations here. - st.applyAuthorization(rules, &auth) - } - } + st.applyAuthorizations(rules, msg.SetCodeAuthorizations) // Perform convenience warming of sender's delegation target. Although the // sender is already warmed in Prepare(..), it's possible a delegation to @@ -902,8 +897,48 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio return authority, nil } +// applyAuthorizations applies every EIP-7702 code delegation in the tx and, +// under EIP-8037, reconciles the per-authority creation budget so that the +// total AuthorizationCreationSize state gas charged matches the net change +// in on-disk delegation bytes. +// +// Invalid authorizations are silently skipped (their auth-base intrinsic +// state gas remains charged, matching the pre-existing behavior). +func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) { + if len(auths) == 0 { + return + } + // Under EIP-8037 each authority can be billed at most one + // AuthorizationCreationSize. applyAuthorization records authorities it + // has billed; we reconcile after the loop by refunding any creation that + // was billed but whose final delegation state in this tx ended up empty + // (e.g., 0→a→0). + var billed map[common.Address]struct{} + if rules.IsAmsterdam { + billed = make(map[common.Address]struct{}) + } + for _, auth := range auths { + // Errors are ignored — invalid authorizations are simply skipped. + st.applyAuthorization(rules, &auth, billed) + } + // End-of-loop reconciliation: a billed creation whose authority is no + // longer delegated wrote zero net bytes to disk, so the auth-base + // intrinsic state gas should not be retained. + for authority := range billed { + if _, isDelegated := types.ParseDelegation(st.state.GetCode(authority)); !isDelegated { + st.gasRemaining.RefundState(params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte) + } + } +} + // applyAuthorization applies an EIP-7702 code delegation to the state. -func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization) error { +// +// authBilledCreations, when non-nil, tracks the set of authorities for which +// this tx has been billed one AuthorizationCreationSize charge (the per- +// authority "first creation" budget). The caller is expected to do an +// end-of-loop pass over this set and refund any entry whose final delegation +// state ended up empty. +func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, authBilledCreations map[common.Address]struct{}) error { authority, err := st.validateAuthorization(auth) if err != nil { return err @@ -920,14 +955,34 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se } prevDelegation, isDelegated := types.ParseDelegation(st.state.GetCode(authority)) if rules.IsAmsterdam { - // EIP-8037: also refund the auth-base state gas when no new delegation - // indicator bytes are written. Two cases: - // - the authority already has a delegation (overwrite in place); or - // - the auth is no-op (auth.Address == 0). - // In both cases the 23 delegation bytes are reused, so the auth-base - // portion of the intrinsic state gas is refilled. - if isDelegated || auth.Address == (common.Address{}) { + // EIP-8037: refund the auth-base state gas unless this auth is the + // first one in this tx to write delegation bytes to an authority + // whose committed code was empty. Refund when ANY of: + // + // - the authority was already delegated at the start of the tx + // (the 23 bytes are already accounted for in committed state, + // and any auth against it just re-writes them); + // + // - the auth is no-op / clearing (auth.Address == 0) — no bytes + // are written in this step at all; + // + // - we have already billed a creation for this authority in + // this tx (per-authority creation budget is 1). + // + // Modeling it this way mirrors the SSTORE "reset to original" + // pattern (EIP-2200 / EIP-3529) and avoids both the undercount in + // a→0→b (committed had delegation, second auth missed the refund) + // and the overcount in 0→a→0→c (each later auth was previously + // billed as a fresh creation). The remaining 0→a→0 case — a + // creation is billed and then undone within the same auth list — + // is handled by the caller's end-of-loop adjustment over + // authBilledCreations. + _, committedDelegated := types.ParseDelegation(st.state.GetCommittedCode(authority)) + _, alreadyBilled := authBilledCreations[authority] + if committedDelegated || alreadyBilled || auth.Address == (common.Address{}) { st.gasRemaining.RefundState(params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte) + } else { + authBilledCreations[authority] = struct{}{} } } diff --git a/core/vm/interface.go b/core/vm/interface.go index a9938c2a28..dc897f0c69 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -42,6 +42,11 @@ type StateDB interface { GetCodeHash(common.Address) common.Hash GetCode(common.Address) []byte + // GetCommittedCode returns the contract code at the start of the current + // execution, ignoring any in-progress SetCode mutations. Returns nil when + // the account had no code prior to this execution. + GetCommittedCode(common.Address) []byte + // SetCode sets the new code for the address, and returns the previous code, if any. SetCode(common.Address, []byte, tracing.CodeChangeReason) []byte GetCodeSize(common.Address) int From bf69a15de68bfac5f7e2a8faab24fca7016ac7ce Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Thu, 21 May 2026 11:33:12 +0800 Subject: [PATCH 05/10] core/vm: change exit halt --- core/vm/evm.go | 28 +++++++++------------------- core/vm/gascosts.go | 23 ++++++++++++++++------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 76d6978a78..a16e9c8cc2 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -255,8 +255,6 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g evm.captureEnd(evm.depth, startGas, result, ret, err) }(gas) } - initialStateGas := gas.StateGas - // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { return nil, gas.Preserved(), ErrDepth @@ -281,7 +279,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false) if _, ok := gas.ChargeRegular(wgas); !ok { evm.StateDB.RevertToSnapshot(snapshot) - return nil, gas.ExitHalt(initialStateGas), ErrOutOfGas + return nil, gas.ExitHalt(), ErrOutOfGas } } @@ -316,7 +314,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, initialStateGas) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -345,8 +343,6 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt evm.captureEnd(evm.depth, startGas, result, ret, err) }(gas) } - initialStateGas := gas.StateGas - // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { return nil, gas.Preserved(), ErrDepth @@ -370,7 +366,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, initialStateGas) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -398,8 +394,6 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, evm.captureEnd(evm.depth, startGas, result, ret, err) }(gas) } - initialStateGas := gas.StateGas - // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { return nil, gas.Preserved(), ErrDepth @@ -417,7 +411,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, initialStateGas) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -443,8 +437,6 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b evm.captureEnd(evm.depth, startGas, result, ret, err) }(gas) } - initialStateGas := gas.StateGas - // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { return nil, gas.Preserved(), ErrDepth @@ -472,7 +464,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, initialStateGas) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { @@ -499,8 +491,6 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value err = ErrNonceUintOverflow } } - initialStateGas := gas.StateGas - if err == nil { evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator) } @@ -519,7 +509,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas) prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas}) if !ok { - return nil, common.Address{}, gas.ExitHalt(initialStateGas), ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas } if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck) @@ -540,7 +530,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) { - halt := gas.ExitHalt(initialStateGas) + halt := gas.ExitHalt() if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(gas.AsTracing(), halt.AsTracing(), tracing.GasChangeCallFailedExecution) } @@ -568,7 +558,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.chainRules.IsEIP4762 { consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas) if consumed < wanted { - return nil, common.Address{}, gas.ExitHalt(initialStateGas), ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(), ErrOutOfGas } prior, _ := gas.Charge(GasCosts{RegularGas: consumed}) if evm.Config.Tracer.HasGasHook() { @@ -593,7 +583,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { evm.StateDB.RevertToSnapshot(snapshot) - exit := contract.Gas.Exit(err, initialStateGas) + exit := contract.Gas.Exit(err) if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution) diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 83d148b6ce..63269a071b 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -251,13 +251,22 @@ func (g GasBudget) ExitRevert() GasBudget { } // ExitHalt produces the leftover form for an exceptional halt. Remaining -// regular gas is accounted as burned; the reservoir resets to initialStateGas -// (the value the caller held at the call site). UsedStateGas is zeroed -// because state-gas effects do not persist on halt. -func (g GasBudget) ExitHalt(initialStateGas uint64) GasBudget { +// regular gas is burned into UsedRegularGas; the state dimension follows the +// same revert-style formula as ExitRevert because the spec routes both halt +// and revert through incorporate_child_on_error: +// +// parent.state_gas_left = parent + child.state_gas_used + child.state_gas_left +// +// which in our model means returning `StateGas + UsedStateGas` to the parent +// and zeroing the per-frame counter. +func (g GasBudget) ExitHalt() GasBudget { + reservoir := int64(g.StateGas) + g.UsedStateGas + if reservoir < 0 { + reservoir = 0 + } return GasBudget{ RegularGas: 0, - StateGas: initialStateGas, + StateGas: uint64(reservoir), UsedRegularGas: g.UsedRegularGas + g.RegularGas, UsedStateGas: 0, } @@ -272,14 +281,14 @@ 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) Exit(err error, initialStateGas uint64) GasBudget { +func (g GasBudget) Exit(err error) GasBudget { switch { case err == nil: return g.ExitSuccess() case err == ErrExecutionReverted: return g.ExitRevert() default: - return g.ExitHalt(initialStateGas) + return g.ExitHalt() } } From 6e4f5710f926d37e16c61555a5bd21e4f95b49de Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Thu, 21 May 2026 16:14:49 +0200 Subject: [PATCH 06/10] core/vm: use up gas _after_ the call --- core/vm/gascosts.go | 26 ++++++++++++++------------ core/vm/instructions.go | 29 +++++++++++++++++------------ 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 63269a071b..0ae36ba126 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -173,8 +173,7 @@ func (g *GasBudget) RefundRegular(s uint64) { // Forward drains `regular` regular gas and the entire state reservoir from // the parent's running budget and returns the initial GasBudget for a child -// frame. The parent's UsedRegularGas is bumped by the forwarded amount so -// that the absorb-on-return path correctly reclaims the unused portion. +// frame. // // Used by frame boundaries where the regular forward has NOT been pre- // deducted: tx-level dispatch (state_transition) and CREATE / CREATE2. The @@ -185,7 +184,6 @@ func (g *GasBudget) RefundRegular(s uint64) { // apply any EIP-150 1/64 retention before calling Forward. func (g *GasBudget) Forward(regular uint64) GasBudget { g.RegularGas -= regular - g.UsedRegularGas += regular child := GasBudget{ RegularGas: regular, @@ -293,18 +291,22 @@ func (g GasBudget) Exit(err error) GasBudget { } // Absorb merges a sub-call's leftover GasBudget into this (caller's) running -// budget. The caller's UsedRegularGas is reclaimed by the unused forwarded -// regular gas (which was pre-charged in full at call entry); the state -// reservoir is overwritten with the child's leftover; and the child's signed -// net state-gas usage is added to the caller's accumulator. +// budget. // -// Invariant maintained by all callers: at the moment of this call, the -// caller's UsedRegularGas already accounts for the FULL forwarded regular -// gas (as if the child had consumed all of it). On halt, child.RegularGas -// is 0 so the reclaim is a no-op. +// - RegularGas: the child's leftover regular gas flows back to the caller. +// - UsedRegularGas: the child's own gross regular-gas consumption is added +// to the caller's accumulator. Combined with the caller having NOT +// pre-bumped UsedRegularGas by the forwarded amount, this matches the +// spec's escrow_subcall_regular_gas + incorporate_child_* pattern: only +// opcode charges count towards regular_gas_used, never state-gas +// spillover or escrowed forwards. +// - StateGas: the reservoir is overwritten by the child's leftover (spec's +// incorporate_child_on_success / on_error formula for state_gas_left). +// - UsedStateGas: the child's signed net state-gas usage is added to the +// caller's accumulator (spec's incorporate child gas). func (g *GasBudget) Absorb(child GasBudget) { - g.UsedRegularGas -= child.RegularGas g.RegularGas += child.RegularGas + g.UsedRegularGas += child.UsedRegularGas g.StateGas = child.StateGas g.UsedStateGas += child.UsedStateGas } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index d85fea5ee6..417c7efab9 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -743,6 +743,12 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if !value.IsZero() { gas += params.CallStipend } + // Escrow pattern (spec: escrow_subcall_regular_gas): undo the full + // forwarded gas — including the value-transfer stipend — from + // UsedRegularGas. The stipend is a free loan to the child and must + // not count toward the caller's regular gas usage; the child's own + // opcode charges are re-added via Absorb on return. + scope.Contract.Gas.UsedRegularGas -= gas // Regular gas for the forward was already pre-deducted by the dynamic // gas table (see makeCallVariantGasCallEIP*); only the state reservoir @@ -762,18 +768,10 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } 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) - } - } + // EIP-8037: CALL does not refund the new-account state_gas on + // ErrDepth/ErrInsufficientBalance — the spec only reclaims the forwarded + // reservoir; the state_gas charged for new account creation stays in + // state_gas_used (see Amsterdam spec call() / generic_call()). evm.returnData = ret return ret, nil @@ -794,6 +792,9 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if !value.IsZero() { gas += params.CallStipend } + // Escrow pattern: undo the full forwarded gas (including stipend) from + // UsedRegularGas; the child's own opcode charges come back via Absorb. + scope.Contract.Gas.UsedRegularGas -= gas childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) ret, result, err := evm.CallCode(scope.Contract.Address(), toAddr, args, childBudget, &value) @@ -825,6 +826,8 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) + // Undo the forwarded gas from UsedRegularGas to prevent double-charging. + scope.Contract.Gas.UsedRegularGas -= gas childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) ret, result, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, childBudget, scope.Contract.value) if err != nil { @@ -854,6 +857,8 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) + // Undo the forwarded gas from UsedRegularGas to prevent double-charging. + scope.Contract.Gas.UsedRegularGas -= gas childBudget := NewGasBudget(gas, scope.Contract.Gas.StateGas) ret, result, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, childBudget) if err != nil { From 05f242939fe72af4dab83a50d829e1adad92ccf6 Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Thu, 21 May 2026 16:15:49 +0200 Subject: [PATCH 07/10] core/vm: put account in BAL before transfer Might not actually be needed if spec is changed, see https://github.com/ethereum/EIPs/pull/11699 --- core/vm/evm.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/vm/evm.go b/core/vm/evm.go index a16e9c8cc2..22f5c7ebe3 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -261,6 +261,12 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } syscall := isSystemCall(caller) + // EIP-7928: per the Amsterdam spec, delegation resolution happens before + // the value-transfer check, so the delegated-to must appear in the BAL + // even when the call later reverts with ErrInsufficientBalance. Touch the + // target's code here (a no-op for non-delegated accounts) to record it. + evm.resolveCode(addr) + // Fail if we're trying to transfer more than the available balance. if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas.Preserved(), ErrInsufficientBalance @@ -347,6 +353,12 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt if evm.depth > int(params.CallCreateDepth) { return nil, gas.Preserved(), ErrDepth } + + // EIP-7928: per the Amsterdam spec, delegation resolution happens before + // the value-transfer check, so the delegated-to must appear in the BAL + // even when the call later reverts with ErrInsufficientBalance. + evm.resolveCode(addr) + // Fail if we're trying to transfer more than the available balance if !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas.Preserved(), ErrInsufficientBalance From 88c11627ea050ece8d2440cfc2d4ec52a733396c Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Thu, 21 May 2026 16:16:13 +0200 Subject: [PATCH 08/10] tests: add BAL specific tests --- build/checksums.txt | 5 +++++ build/ci.go | 18 ++++++++++++++++++ tests/block_test.go | 15 ++++++++++++--- tests/init_test.go | 7 ++++--- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/build/checksums.txt b/build/checksums.txt index 454efa93c4..d6a9f2b803 100644 --- a/build/checksums.txt +++ b/build/checksums.txt @@ -5,6 +5,11 @@ # https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0 a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz +# version:spec-tests-bal v7.2.0 +# https://github.com/ethereum/execution-specs/releases +# https://github.com/ethereum/execution-specs/releases/download/tests-bal%40v7.2.0 +fc1d9ae174cdd5db789068839999e6f83666cc79f7dac36e973d7616d9a2e2cf fixtures_bal.tar.gz + # version:golang 1.25.10 # https://go.dev/dl/ 20cf04a92e5af99748e341bc8996fa28090c9ac98765fa115ec5ddf41d7af41d go1.25.10.src.tar.gz diff --git a/build/ci.go b/build/ci.go index 173a3280ce..8948c2230e 100644 --- a/build/ci.go +++ b/build/ci.go @@ -164,6 +164,9 @@ var ( // This is where the tests should be unpacked. executionSpecTestsDir = "tests/spec-tests" + + // This is where the bal-specific release of the tests should be unpacked. + executionSpecTestsBALDir = "tests/spec-tests-bal" ) var GOBIN, _ = filepath.Abs(filepath.Join("build", "bin")) @@ -402,6 +405,7 @@ func doTest(cmdline []string) { // Get test fixtures. if !*short { downloadSpecTestFixtures(csdb, *cachedir) + downloadBALSpecTestFixtures(csdb, *cachedir) } // Configure the toolchain. @@ -467,6 +471,20 @@ func downloadSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string return filepath.Join(cachedir, base) } +// downloadBALSpecTestFixtures downloads and extracts the bal-specific execution-spec-tests fixtures. +func downloadBALSpecTestFixtures(csdb *download.ChecksumDB, cachedir string) string { + ext := ".tar.gz" + base := "fixtures_bal" + archivePath := filepath.Join(cachedir, base+ext) + if err := csdb.DownloadFileFromKnownURL(archivePath); err != nil { + log.Fatal(err) + } + if err := build.ExtractArchive(archivePath, executionSpecTestsBALDir); err != nil { + log.Fatal(err) + } + return filepath.Join(cachedir, base) +} + // doCheckGenerate ensures that re-generating generated files does not cause // any mutations in the source file tree. func doCheckGenerate() { diff --git a/tests/block_test.go b/tests/block_test.go index 0f087967bb..31bb4f9d36 100644 --- a/tests/block_test.go +++ b/tests/block_test.go @@ -82,8 +82,17 @@ func TestBlockchain(t *testing.T) { // TestExecutionSpecBlocktests runs the test fixtures from execution-spec-tests. func TestExecutionSpecBlocktests(t *testing.T) { - if !common.FileExist(executionSpecBlockchainTestDir) { - t.Skipf("directory %s does not exist", executionSpecBlockchainTestDir) + testExecutionSpecBlocktests(t, executionSpecBlockchainTestDir) +} + +// TestExecutionSpecBlocktestsBAL runs the BAL release test fixtures from execution-spec-tests. +func TestExecutionSpecBlocktestsBAL(t *testing.T) { + testExecutionSpecBlocktests(t, executionSpecBALBlockchainTestDir) +} + +func testExecutionSpecBlocktests(t *testing.T, testDir string) { + if !common.FileExist(testDir) { + t.Skipf("directory %s does not exist", testDir) } bt := new(testMatcher) @@ -97,7 +106,7 @@ func TestExecutionSpecBlocktests(t *testing.T) { bt.skipLoad(`dynamicAccountOverwriteEmpty_Paris`) bt.skipLoad(`create2collisionStorageParis`) - bt.walk(t, executionSpecBlockchainTestDir, func(t *testing.T, name string, test *BlockTest) { + bt.walk(t, testDir, func(t *testing.T, name string, test *BlockTest) { execBlockTest(t, bt, test) }) } diff --git a/tests/init_test.go b/tests/init_test.go index b933c9808c..20c9e8e1c9 100644 --- a/tests/init_test.go +++ b/tests/init_test.go @@ -41,9 +41,10 @@ var ( transactionTestDir = filepath.Join(baseDir, "TransactionTests") rlpTestDir = filepath.Join(baseDir, "RLPTests") difficultyTestDir = filepath.Join(baseDir, "BasicTests") - executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests") - executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests") - executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests") + executionSpecBlockchainTestDir = filepath.Join(".", "spec-tests", "fixtures", "blockchain_tests") + executionSpecBALBlockchainTestDir = filepath.Join(".", "spec-tests-bal", "fixtures", "blockchain_tests") + executionSpecStateTestDir = filepath.Join(".", "spec-tests", "fixtures", "state_tests") + executionSpecTransactionTestDir = filepath.Join(".", "spec-tests", "fixtures", "transaction_tests") benchmarksDir = filepath.Join(".", "evm-benchmarks", "benchmarks") ) From 27c8f4a19f8b65fb77af44a2a8dc3c5b23ac73f0 Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Thu, 21 May 2026 18:43:14 +0200 Subject: [PATCH 09/10] beacon/engine: fix marshalling --- beacon/engine/ed_codec.go | 7 +++---- beacon/engine/types.go | 20 +++++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/beacon/engine/ed_codec.go b/beacon/engine/ed_codec.go index 02a1fd3805..83603616a7 100644 --- a/beacon/engine/ed_codec.go +++ b/beacon/engine/ed_codec.go @@ -10,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/types/bal" ) var _ = (*executableDataMarshaling)(nil) @@ -36,7 +35,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -87,7 +86,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + BlockAccessList *hexutil.Bytes `json:"blockAccessList,omitempty"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -165,7 +164,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { e.SlotNumber = (*uint64)(dec.SlotNumber) } if dec.BlockAccessList != nil { - e.BlockAccessList = dec.BlockAccessList + e.BlockAccessList = *dec.BlockAccessList } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 60d564b877..c5414f0a07 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -17,6 +17,7 @@ package engine import ( + "bytes" "fmt" "math/big" "slices" @@ -24,8 +25,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" ) @@ -101,7 +103,7 @@ type ExecutableData struct { BlobGasUsed *uint64 `json:"blobGasUsed"` ExcessBlobGas *uint64 `json:"excessBlobGas"` SlotNumber *uint64 `json:"slotNumber,omitempty"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"` } // JSON type overrides for executableData. @@ -314,13 +316,14 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H } // If Amsterdam is enabled, data.BlockAccessList is always non-nil, - // even for empty blocks with no state transitions. + // even for empty blocks with no state transitions. The wire format is + // the RLP-encoded access list; the header hash is keccak256(rlp). // // If Amsterdam is not enabled yet, blockAccessListHash is expected // to be nil. var blockAccessListHash *common.Hash if data.BlockAccessList != nil { - hash := data.BlockAccessList.Hash() + hash := crypto.Keccak256Hash(data.BlockAccessList) blockAccessListHash = &hash } header := &types.Header{ @@ -372,7 +375,14 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types. BlobGasUsed: block.BlobGasUsed(), ExcessBlobGas: block.ExcessBlobGas(), SlotNumber: block.SlotNumber(), - BlockAccessList: block.AccessList(), + } + // Per Engine API spec (Amsterdam): blockAccessList is the RLP-encoded + // access list, serialized as a hex string. Encode it to bytes here. + if al := block.AccessList(); al != nil { + var buf bytes.Buffer + if err := rlp.Encode(&buf, al); err == nil { + data.BlockAccessList = buf.Bytes() + } } // Add blobs. From 3f0c06c527e37d7cb5aa004ca2798b2a533e904c Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Thu, 21 May 2026 20:57:34 +0200 Subject: [PATCH 10/10] cmd: add --bal.executionmode flag (still noop) --- cmd/geth/main.go | 1 + cmd/utils/flags.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 850e26d161..0f88dbbcf1 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -95,6 +95,7 @@ var ( utils.BinTrieGroupDepthFlag, utils.LightKDFFlag, utils.EthRequiredBlocksFlag, + utils.BALExecutionModeFlag, utils.LegacyWhitelistFlag, // deprecated utils.CacheFlag, utils.CacheDatabaseFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index c41cf4ee40..ad70d48c2d 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -243,6 +243,11 @@ var ( Usage: "Comma separated block number-to-hash mappings to require for peering (=)", Category: flags.EthCategory, } + BALExecutionModeFlag = &cli.StringFlag{ + Name: "bal.executionmode", + Usage: "EIP-7928 block-access-list execution mode (no-op placeholder)", + Category: flags.EthCategory, + } BloomFilterSizeFlag = &cli.Uint64Flag{ Name: "bloomfilter.size", Usage: "Megabytes of memory allocated to bloom-filter for pruning",