From 7d5b36f86ca71d8bec741a21a5e55eb2dcb90522 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 24 Jun 2026 11:18:33 +0800 Subject: [PATCH] core: implement EIP-2780 --- cmd/evm/internal/t8ntool/transaction.go | 3 +- core/bench_test.go | 2 +- core/bintrie_witness_test.go | 4 +- core/eip2780_test.go | 208 ++++++++++++++++++++++++ core/error.go | 9 +- core/state_processor.go | 2 +- core/state_transition.go | 87 +++++++++- core/state_transition_test.go | 47 +++++- core/txpool/validation.go | 6 +- params/protocol_params.go | 7 + tests/transaction_test_util.go | 3 +- 11 files changed, 347 insertions(+), 31 deletions(-) create mode 100644 core/eip2780_test.go diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index 9eb1bdbf5f..28329e9611 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/tests" + "github.com/holiman/uint256" "github.com/urfave/cli/v2" ) @@ -133,7 +134,7 @@ 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, params.CostPerStateByte) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), r.Address, tx.To(), uint256.MustFromBig(tx.Value()), 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 fe66aeae0d..79584309da 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -89,7 +89,7 @@ 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, params.Rules{}, params.CostPerStateByte) + cost, _ := IntrinsicGas(data, nil, nil, common.Address{}, &toaddr, nil, params.Rules{}, params.CostPerStateByte) 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 e151a72801..6ddf1ce1de 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, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) + intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, common.Address{}, nil, nil, 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, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) + intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, common.Address{}, nil, nil, params.Rules{IsHomestead: true, IsIstanbul: true, IsShanghai: true}, 0) signer = types.LatestSigner(testUBTChainConfig) testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain diff --git a/core/eip2780_test.go b/core/eip2780_test.go new file mode 100644 index 0000000000..976876300d --- /dev/null +++ b/core/eip2780_test.go @@ -0,0 +1,208 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// TestEIP2780Intrinsic checks the intrinsic-gas decomposition. +func TestEIP2780Intrinsic(t *testing.T) { + var ( + from = common.HexToAddress("0x1111111111111111111111111111111111111111") + to = common.HexToAddress("0x2222222222222222222222222222222222222222") + ) + cases := []struct { + name string + to *common.Address + value *uint256.Int + want vm.GasCosts + }{ + { + name: "self-transfer", + to: &from, + value: uint256.NewInt(1), + want: vm.GasCosts{RegularGas: params.TxBaseCost2780}, // 12,000 + }, + { + name: "self-transfer/zero-value", + to: &from, + value: uint256.NewInt(0), + want: vm.GasCosts{RegularGas: params.TxBaseCost2780}, // 12,000 + }, + { + name: "zero-value call", + to: &to, + value: uint256.NewInt(0), + // TxBaseCost + ColdAccountAccess = 15,000 + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780}, + }, + { + name: "value transfer to existing EOA", + to: &to, + value: uint256.NewInt(1), + // TxBaseCost + ColdAccountAccess + TxValueCost + TransferLogCost = 21,000 + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + + params.TxValueCost2780 + params.TransferLogCost2780}, + }, + { + name: "contract creation, value = 0", + to: nil, + value: uint256.NewInt(0), + // TxBaseCost + CreateAccess = 23,000 regular, plus one account creation in state. + want: vm.GasCosts{ + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, + }, + { + name: "contract creation, value > 0", + to: nil, + value: uint256.NewInt(1), + // TxBaseCost + CreateAccess + TransferLogCost = 24,756 regular, plus account creation. + want: vm.GasCosts{ + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + params.TransferLogCost2780, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := IntrinsicGas(nil, nil, nil, from, tc.to, tc.value, rules8037, params.CostPerStateByte) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tc.want { + t.Fatalf("gas mismatch: got %+v, want %+v", got, tc.want) + } + }) + } +} + +// TestEIP2780Gas checks every "Transaction reference case" in +// the EIP-2780 specification end-to-end, asserting the two-dimensional charge +// (intrinsic + top-level + execution) recorded in the block gas pool. +func TestEIP2780Gas(t *testing.T) { + const ( + cold = params.ColdAccountAccess2780 + base = params.TxBaseCost2780 + valueCst = params.TxValueCost2780 + params.TransferLogCost2780 + ) + var ( + existingEOA = common.HexToAddress("0xe0a0000000000000000000000000000000000001") + stopContract = common.HexToAddress("0xc0de000000000000000000000000000000000001") + delegated = common.HexToAddress("0xde1e000000000000000000000000000000000001") + emptyTarget = common.HexToAddress("0x7a76000000000000000000000000000000000001") // never allocated + freshEOA = common.HexToAddress("0xbeef000000000000000000000000000000000001") // never allocated + ) + // Shared world: a funded EOA, a STOP contract and an account delegated to a + // non-existent (codeless) target. The delegation target is intentionally + // absent so resolving it executes no code. + base7702 := types.GenesisAlloc{ + existingEOA: {Balance: big.NewInt(1)}, + stopContract: {Code: []byte{0x00}}, // STOP + delegated: {Code: types.AddressToDelegation(emptyTarget)}, + } + // valueCreateTx builds a contract-creation transaction carrying value. + valueCreateTx := func(value int64) *types.Transaction { + return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{ + ChainID: cfg8037.ChainID, Nonce: 0, To: nil, Value: big.NewInt(value), + Gas: 300_000, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), + }) + } + + cases := []struct { + name string + tx *types.Transaction + wantRegular, wantState uint64 + }{ + // case 1: ETH transfer to self. + {"self-transfer", callTx(0, senderAddr, 1, 100_000, nil), base, 0}, + // case 2: no-transfer to an existing EOA. + {"zero-value/eoa", callTx(0, existingEOA, 0, 100_000, nil), base + cold, 0}, + // case 3: no-transfer to a contract. + {"zero-value/contract", callTx(0, stopContract, 0, 100_000, nil), base + cold, 0}, + // case 4: ETH transfer to an existing EOA. + {"value/eoa", callTx(0, existingEOA, 1, 100_000, nil), base + cold + valueCst, 0}, + // case 5: ETH transfer to a contract. + {"value/contract", callTx(0, stopContract, 1, 100_000, nil), base + cold + valueCst, 0}, + // case 6: no-transfer to a 7702-delegated account. + {"zero-value/delegated", callTx(0, delegated, 0, 100_000, nil), base + 2*cold, 0}, + // case 7: ETH transfer to a 7702-delegated account (no new-account charge). + {"value/delegated", callTx(0, delegated, 1, 100_000, nil), base + 2*cold + valueCst, 0}, + // case 8: ETH transfer creating a new account. + {"value/new-account", callTx(0, freshEOA, 1, 300_000, nil), base + cold + valueCst, newAccountState}, + // case 9: contract-creation transaction, value = 0. + {"create/zero-value", createTx(0, 300_000, nil), base + params.CreateAccess2780, newAccountState}, + // case 10: contract-creation transaction, value > 0. + {"create/value", valueCreateTx(1), base + params.CreateAccess2780 + params.TransferLogCost2780, newAccountState}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + res, gp, err := applyMsg(t, mkState(senderAlloc(base7702)), tc.tx) + if err != nil { + t.Fatalf("consensus error: %v", err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + if gp.cumulativeRegular != tc.wantRegular { + t.Errorf("regular gas = %d, want %d", gp.cumulativeRegular, tc.wantRegular) + } + if gp.cumulativeState != tc.wantState { + t.Errorf("state gas = %d, want %d", gp.cumulativeState, tc.wantState) + } + }) + } +} + +// TestEIP2780NewAccountFunded verifies that a value transfer creating a new +// account both materializes and funds the recipient. +func TestEIP2780NewAccountFunded(t *testing.T) { + fresh := common.HexToAddress("0xbeef000000000000000000000000000000000002") + sdb := mkState(senderAlloc(nil)) + if _, _, err := applyMsg(t, sdb, callTx(0, fresh, 1, 300_000, nil)); err != nil { + t.Fatal(err) + } + if !sdb.Exist(fresh) || sdb.GetBalance(fresh).Cmp(uint256.NewInt(1)) != 0 { + t.Fatalf("recipient not funded: exist=%v balance=%v", sdb.Exist(fresh), sdb.GetBalance(fresh)) + } +} + +// TestEIP2780InsufficientGasForCallCharge verifies that a value transfer +// creating a new account is rejected when the gas limit only covers the 21,000 +// intrinsic base and not the additional new-account state gas charged before the +// call executes. +func TestEIP2780InsufficientGasForCallCharge(t *testing.T) { + fresh := common.HexToAddress("0xbeef000000000000000000000000000000000003") + sdb := mkState(senderAlloc(nil)) + _, _, err := applyMsg(t, sdb, callTx(0, fresh, 1, 21_000, nil)) + if !errors.Is(err, ErrEIP2780CallCharge) { + t.Fatalf("expected ErrEIP2780CallCharge, got %v", err) + } + if sdb.Exist(fresh) { + t.Fatal("recipient should not be created when the call charge cannot be paid") + } +} diff --git a/core/error.go b/core/error.go index 7dd5b8a432..70785fe77e 100644 --- a/core/error.go +++ b/core/error.go @@ -66,11 +66,6 @@ var ( // have enough funds for transfer(topmost call only). ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer") - // ErrInsufficientBalanceWitness is returned if the transaction sender has enough - // funds to cover the transfer, but not enough to pay for witness access/modification - // costs for the transaction - ErrInsufficientBalanceWitness = errors.New("insufficient funds to cover witness access costs for transaction") - // ErrInsufficientFunds is returned if the total cost of executing a transaction // is higher than the balance of the user's account. ErrInsufficientFunds = errors.New("insufficient funds for gas * price + value") @@ -86,6 +81,10 @@ var ( // than required for the data floor cost. ErrFloorDataGas = errors.New("insufficient gas for floor data gas cost") + // ErrEIP2780CallCharge is returned if the transaction doesn't have sufficient + // gas to cover the cost of EIP-2780 call charge. + ErrEIP2780CallCharge = errors.New("insufficient gas for EIP-2780 call charge") + // ErrTxTypeNotSupported is returned if a transaction is not supported in the // current network configuration. ErrTxTypeNotSupported = types.ErrTxTypeNotSupported diff --git a/core/state_processor.go b/core/state_processor.go index f40aee0301..8275015c41 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -88,10 +88,10 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated blockAccessList = bal.NewConstructionBlockAccessList() ) defer evm.Release() + if jumpDestCache != nil { evm.SetJumpDestCache(jumpDestCache) } - // Run the pre-execution system calls blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time())) diff --git a/core/state_transition.go b/core/state_transition.go index 07d8055322..fe8203d5ff 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -68,16 +68,15 @@ 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 bool, rules params.Rules, costPerStateByte uint64) (vm.GasCosts, error) { +func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, from common.Address, to *common.Address, value *uint256.Int, rules params.Rules, costPerStateByte uint64) (vm.GasCosts, error) { + isContractCreation := to == nil + // Set the starting gas for the raw transaction 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 - } + if rules.IsAmsterdam { + gas = intrinsicBaseGasEIP2780(from, to, value, costPerStateByte) + } else if isContractCreation && rules.IsHomestead { + gas.RegularGas = params.TxGasContractCreation } else { gas.RegularGas = params.TxGas } @@ -151,6 +150,39 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set return gas, nil } +// intrinsicBaseGasEIP2780 computes the EIP-2780 intrinsic base cost. +func intrinsicBaseGasEIP2780(from common.Address, to *common.Address, value *uint256.Int, costPerStateByte uint64) vm.GasCosts { + var ( + isContractCreation = to == nil + isSelfTransfer = to != nil && *to == from + hasValue = value != nil && !value.IsZero() + ) + // tx.sender: signature recovery plus the sender account access and write. + gas := vm.GasCosts{RegularGas: params.TxBaseCost2780} + + // tx.to charge. + switch { + case isSelfTransfer: + // The recipient account is already accessed and written as the sender. + case isContractCreation: + gas.RegularGas += params.CreateAccess2780 + gas.StateGas += params.AccountCreationSize * costPerStateByte + default: + gas.RegularGas += params.ColdAccountAccess2780 + } + + // tx.value charge. + switch { + case !hasValue || isSelfTransfer: + // No transfer log and no recipient balance write. + case isContractCreation: + gas.RegularGas += params.TransferLogCost2780 + default: + gas.RegularGas += params.TransferLogCost2780 + params.TxValueCost2780 + } + return gas +} + // FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) (uint64, error) { var ( @@ -622,7 +654,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { contractCreation = msg.To == nil floorDataGas uint64 ) - cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules, st.evm.Context.CostPerStateByte) + cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules, st.evm.Context.CostPerStateByte) if err != nil { return nil, err } @@ -714,6 +746,10 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok { st.state.AddAddressToAccessList(addr) } + // EIP-2780: charge the transaction's top-level recipient costs. + if rules.IsAmsterdam && !st.chargeCallRecipientEIP2780(value) { + return nil, fmt.Errorf("%w: address %v", ErrEIP2780CallCharge, msg.To.Hex()) + } // Execute the transaction's call. ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) st.gasRemaining.Absorb(result) @@ -786,6 +822,39 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { }, nil } +// chargeCallRecipientEIP2780 applies the EIP-2780 transaction top-level gas costs for +// a message-call transaction, charged before any opcode executes: +// +// - if the recipient is EIP-161 empty and the transaction carries value, charge +// for account creation. +// +// - if the recipient is an EIP-7702 delegated account, resolving the delegation +// loads the target's code, charged an additional cold account access in +// regular gas. +func (st *stateTransition) chargeCallRecipientEIP2780(value *uint256.Int) bool { + var ( + cost vm.GasCosts + to = *st.msg.To + ) + if !value.IsZero() && st.state.Empty(to) { + cost.StateGas += params.AccountCreationSize * st.evm.Context.CostPerStateByte + } + if _, ok := types.ParseDelegation(st.state.GetCode(to)); ok { + cost.RegularGas += params.ColdAccountAccess2780 + } + if cost == (vm.GasCosts{}) { + return true + } + prior, ok := st.gasRemaining.Charge(cost) + if !ok { + return false + } + if st.evm.Config.Tracer.HasGasHook() { + st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas) + } + return true +} + // settleGas finalizes the per-tx gas accounting after EVM execution: // // - Snapshots the EIP-8037 block-level 2D figures (tx_regular_gas, diff --git a/core/state_transition_test.go b/core/state_transition_test.go index be2de7f511..402c5a86a1 100644 --- a/core/state_transition_test.go +++ b/core/state_transition_test.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" ) func TestFloorDataGas(t *testing.T) { @@ -155,6 +156,7 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028 bool isEIP3860 bool isAmsterdam bool + value *uint256.Int want vm.GasCosts }{ { @@ -237,8 +239,9 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, isAmsterdam: true, - // base access-list charge + EIP-7981 extra - want: vm.GasCosts{RegularGas: params.TxGas + + // EIP-2780: zero-value call base is TxBaseCost + ColdAccountAccess + // (15,000). Plus base access-list charge + EIP-7981 extra. + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas + 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost}, }, @@ -259,10 +262,10 @@ func TestIntrinsicGas(t *testing.T) { 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. + // EIP-2780: creation regular gas is TxBaseCost + CreateAccess (23,000), + // and account-creation cost is charged as state gas. want: vm.GasCosts{ - RegularGas: params.TxGas + params.CreateGasAmsterdam, + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780, StateGas: params.AccountCreationSize * params.CostPerStateByte, }, }, @@ -275,7 +278,7 @@ func TestIntrinsicGas(t *testing.T) { isEIP3860: true, // Shanghai gates init-code word gas isAmsterdam: true, want: vm.GasCosts{ - RegularGas: params.TxGas + params.CreateGasAmsterdam + + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, StateGas: params.AccountCreationSize * params.CostPerStateByte, }, @@ -292,7 +295,7 @@ func TestIntrinsicGas(t *testing.T) { isEIP3860: true, isAmsterdam: true, want: vm.GasCosts{ - RegularGas: params.TxGas + params.CreateGasAmsterdam + + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + 32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas + 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost, @@ -314,7 +317,7 @@ func TestIntrinsicGas(t *testing.T) { // regular: TxAuthTupleRegularGas (7500) per auth // state: (AuthorizationCreationSize + AccountCreationSize) * CostPerStateByte per auth want: vm.GasCosts{ - RegularGas: params.TxGas + + RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + 100*params.TxDataNonZeroGasEIP2028 + 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + @@ -322,6 +325,28 @@ func TestIntrinsicGas(t *testing.T) { StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte, }, }, + { + name: "amsterdam/value-transfer-call", + isEIP2028: true, + isAmsterdam: true, + value: uint256.NewInt(1), + // EIP-2780: TxBaseCost + ColdAccountAccess + TransferLogCost + TxValueCost = 21,000. + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + + params.TransferLogCost2780 + params.TxValueCost2780}, + }, + { + name: "amsterdam/value-bearing-contract-creation", + creation: true, + isHomestead: true, + isEIP2028: true, + isAmsterdam: true, + value: uint256.NewInt(1), + // EIP-2780: TxBaseCost + CreateAccess + TransferLogCost = 24,756, plus account-creation state gas. + want: vm.GasCosts{ + RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + params.TransferLogCost2780, + StateGas: params.AccountCreationSize * params.CostPerStateByte, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -331,8 +356,12 @@ func TestIntrinsicGas(t *testing.T) { IsShanghai: tt.isEIP3860, IsAmsterdam: tt.isAmsterdam, } + var to *common.Address + if !tt.creation { + to = &addr1 + } got, err := IntrinsicGas(tt.data, tt.accessList, tt.authList, - tt.creation, rules, params.CostPerStateByte) + common.Address{}, to, tt.value, rules, params.CostPerStateByte) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 3b30dd30ef..404e350468 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" ) var ( @@ -116,7 +117,8 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types return core.ErrTipAboveFeeCap } // Make sure the transaction is signed properly - if _, err := types.Sender(signer, tx); err != nil { + from, err := types.Sender(signer, tx) + if err != nil { return fmt.Errorf("%w: %v", ErrInvalidSender, err) } // Limit nonce to 2^64-1 per EIP-2681 @@ -125,7 +127,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 - intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), from, tx.To(), uint256.MustFromBig(tx.Value()), rules, params.CostPerStateByte) if err != nil { return err } diff --git a/params/protocol_params.go b/params/protocol_params.go index 69e10fa5d9..720c04ea27 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -103,6 +103,13 @@ const ( TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702 TxAuthTupleRegularGas uint64 = 7500 // Per auth tuple regular gas specified in EIP-8037 + // EIP-2780: resource-based intrinsic transaction gas. + TxBaseCost2780 uint64 = 12000 + ColdAccountAccess2780 uint64 = 3000 + CreateAccess2780 uint64 = 11000 + TxValueCost2780 uint64 = 4244 + TransferLogCost2780 uint64 = 1756 + // These have been changed during the course of the chain CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction. CallGasEIP150 uint64 = 700 // Static portion of gas for CALL-derivates after EIP 150 (Tangerine) diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index 91f7d6c3ec..7cf2ac66cf 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" ) // TransactionTest checks RLP decoding and sender derivation of transactions. @@ -81,7 +82,7 @@ func (tt *TransactionTest) Run() error { return } // Intrinsic cost - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte) + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), sender, tx.To(), uint256.MustFromBig(tx.Value()), rules, params.CostPerStateByte) if err != nil { return }