From 07dd8e052f50420f1fa29878d9c285fbfc5c0dc6 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Mon, 29 Jun 2026 11:04:30 +0800 Subject: [PATCH 01/44] core: implement EIP-2780 (#35212) Implements https://eips.ethereum.org/EIPS/eip-2780 --------- Co-authored-by: Marius van der Wijden --- cmd/evm/internal/t8ntool/transaction.go | 11 +- core/bench_test.go | 2 +- core/bintrie_witness_test.go | 4 +- core/eip2780_test.go | 214 ++++++++++++++++++++++++ core/eip8037_test.go | 10 +- core/error.go | 5 - core/state_processor.go | 2 +- core/state_transition.go | 119 +++++++++++-- core/state_transition_test.go | 62 +++++-- core/txpool/validation.go | 13 +- params/protocol_params.go | 7 + tests/transaction_test_util.go | 9 +- 12 files changed, 407 insertions(+), 51 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..7207cad41d 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" ) @@ -132,8 +133,14 @@ func Transaction(ctx *cli.Context) error { r.Address = sender } // Check intrinsic gas + value, overflow := uint256.FromBig(tx.Value()) + if overflow { + // A 256-bit overflow is reported by the field validation below; use a + // non-zero placeholder so intrinsic gas is still computed and reported. + value = uint256.NewInt(1) + } 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(), value, rules, params.CostPerStateByte) if err != nil { r.Error = err results = append(results, r) @@ -147,7 +154,7 @@ func Transaction(ctx *cli.Context) error { } // For Prague txs, validate the floor data gas. if rules.IsPrague { - floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) + floorDataGas, err := core.FloorDataGas(rules, r.Address, tx.To(), value, tx.Data(), tx.AccessList()) 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 5d82fc1df8..2cf7972cf8 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..d9545923ff --- /dev/null +++ b/core/eip2780_test.go @@ -0,0 +1,214 @@ +// 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 ( + "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, whose gas limit only covers the 21,000 intrinsic base +// and not the additional new-account state gas charged before the call executes, +// halts out of gas. The transaction stays valid (no consensus error) but +// execution fails and the recipient is not created. +func TestEIP2780InsufficientGasForCallCharge(t *testing.T) { + fresh := common.HexToAddress("0xbeef000000000000000000000000000000000003") + sdb := mkState(senderAlloc(nil)) + res, _, err := applyMsg(t, sdb, callTx(0, fresh, 1, 21_000, nil)) + if err != nil { + t.Fatalf("transaction should remain valid: %v", err) + } + if res.Err != vm.ErrOutOfGas { + t.Fatalf("expected out of gas, got %v", res.Err) + } + if res.UsedGas != 21_000 { + t.Fatalf("expected used gas, got %v", res.UsedGas) + } + if sdb.Exist(fresh) { + t.Fatal("recipient should not be created when the call charge cannot be paid") + } +} diff --git a/core/eip8037_test.go b/core/eip8037_test.go index 8626c0b090..6d506cba96 100644 --- a/core/eip8037_test.go +++ b/core/eip8037_test.go @@ -191,7 +191,7 @@ var ( // A creation tx's intrinsic gas pre-charges one account creation as state gas. func TestCreateTxIntrinsicChargesAccountUnconditionally(t *testing.T) { - cost, err := IntrinsicGas(nil, nil, nil, true, rules8037, params.CostPerStateByte) + cost, err := IntrinsicGas(nil, nil, nil, common.Address{}, nil, nil, rules8037, params.CostPerStateByte) if err != nil { t.Fatal(err) } @@ -325,7 +325,7 @@ func clearSlots(addr common.Address, n int) (types.GenesisAlloc, []byte) { // tx_gas_used_before_refund (peak) exceeds the post-refund gas used. func TestGasUsedBeforeRefund(t *testing.T) { c := common.HexToAddress("0xc1ea0") - alloc, _ := clearSlots(c, 1) + alloc, _ := clearSlots(c, 4) res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, nil)) if err != nil { t.Fatal(err) @@ -351,8 +351,8 @@ func TestRefundCappedAt20Percent(t *testing.T) { // The EIP-7623 calldata floor is applied after the refund. func TestRefundCalldataFloorAfterRefund(t *testing.T) { data := make([]byte, 1000) // all-zero calldata: floor dominates a bare call - floor, _ := FloorDataGas(rules8037, data, nil) to := common.HexToAddress("0xeeee") + floor, _ := FloorDataGas(rules8037, senderAddr, &to, new(uint256.Int), data, nil) res, _, err := applyMsg(t, mkState(senderAlloc(nil)), callTx(0, to, 0, 1_000_000, data)) if err != nil { t.Fatal(err) @@ -367,7 +367,7 @@ func TestRefundFloorNegatesRefund(t *testing.T) { c := common.HexToAddress("0xc1ea1") alloc, _ := clearSlots(c, 1) data := make([]byte, 1000) - floor, _ := FloorDataGas(rules8037, data, nil) + floor, _ := FloorDataGas(rules8037, senderAddr, &c, new(uint256.Int), data, nil) res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, data)) if err != nil { t.Fatal(err) @@ -474,7 +474,7 @@ var delegate8037 = common.HexToAddress("0xde1e8a7e") // Intrinsic gas pre-charges the worst-case (account + indicator) per auth. func TestAuthIntrinsicWorstCase(t *testing.T) { - cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, false, rules8037, params.CostPerStateByte) + cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, common.Address{}, &delegate8037, nil, rules8037, params.CostPerStateByte) if err != nil { t.Fatal(err) } diff --git a/core/error.go b/core/error.go index 7dd5b8a432..26b007f9d9 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") diff --git a/core/state_processor.go b/core/state_processor.go index 48a8da4062..5b81abef6f 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 bbeb163b16..3b1642e0b9 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -68,16 +68,19 @@ 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 + if rules.IsAmsterdam { + gas.RegularGas = intrinsicBaseGasEIP2780(from, to, value) + if isContractCreation { + // New-account creation is charged as state gas (EIP-8037). gas.StateGas = params.AccountCreationSize * costPerStateByte - } else { - gas.RegularGas = params.TxGasContractCreation } + } else if isContractCreation && rules.IsHomestead { + gas.RegularGas = params.TxGasContractCreation } else { gas.RegularGas = params.TxGas } @@ -151,8 +154,41 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set return gas, nil } +// intrinsicBaseGasEIP2780 computes the regular-gas portion of the EIP-2780 +// intrinsic base cost: the per-resource decomposition of the legacy flat 21,000. +func intrinsicBaseGasEIP2780(from common.Address, to *common.Address, value *uint256.Int) uint64 { + 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 := params.TxBaseCost2780 + + // tx.to charge. + switch { + case isSelfTransfer: + // The recipient account is already accessed and written as the sender. + case isContractCreation: + gas += params.CreateAccess2780 + default: + gas += params.ColdAccountAccess2780 + } + + // tx.value charge. + switch { + case !hasValue || isSelfTransfer: + // No transfer log and no recipient balance write. + case isContractCreation: + gas += params.TransferLogCost2780 + default: + gas += 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) { +func FloorDataGas(rules params.Rules, from common.Address, to *common.Address, value *uint256.Int, data []byte, accessList types.AccessList) (uint64, error) { var ( tokens uint64 tokenCost uint64 @@ -198,12 +234,19 @@ func FloorDataGas(rules params.Rules, data []byte, accessList types.AccessList) tokenCost = params.TxCostFloorPerToken } + // The floor is anchored to the transaction base cost. Under EIP-2780 that + // base is the per-resource decomposition (the same one used by the intrinsic + // gas), so the floor never undercuts the transaction's own base. + floorBase := params.TxGas + if rules.IsAmsterdam { + floorBase = intrinsicBaseGasEIP2780(from, to, value) + } // Check for overflow - if (math.MaxUint64-params.TxGas)/tokenCost < tokens { + if (math.MaxUint64-floorBase)/tokenCost < tokens { return 0, ErrGasUintOverflow } // Minimum gas required for a transaction based on its data tokens (EIP-7623). - return params.TxGas + tokens*tokenCost, nil + return floorBase + tokens*tokenCost, nil } // toWordSize returns the ceiled word size required for init code payment calculation. @@ -614,7 +657,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 } @@ -629,7 +672,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // 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) + floorDataGas, err = FloorDataGas(rules, msg.From, msg.To, msg.Value, msg.Data, msg.AccessList) if err != nil { return nil, err } @@ -705,9 +748,16 @@ 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, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) - st.gasRemaining.Absorb(result) + // EIP-2780: charge the transaction's top-level recipient costs. If the + // budget cannot cover the charge, the top frame halts out of gas. + if rules.IsAmsterdam && !st.chargeCallRecipientEIP2780(value) { + vmerr = vm.ErrOutOfGas + st.gasRemaining = st.gasRemaining.ExitHalt() + } else { + // 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) + } } // Settle down the gas usage and refund the ETH back if any remaining @@ -754,6 +804,47 @@ 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 non-existent 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 + ) + // This runs in the topmost frame before any bytecode executes, so unlike the + // execution-level checks which must use StateDB.Empty because SELFDESTRUCT can + // leave a transient EIP-161-empty account, no empty account can exist here, and + // !Exist is equivalent to Empty. + if !value.IsZero() && !st.state.Exist(to) { + cost.StateGas += params.AccountCreationSize * st.evm.Context.CostPerStateByte + } + if _, ok := types.ParseDelegation(st.state.GetCode(to)); ok { + // EIP-2780: The tx.sender, tx.to, and (where applicable) delegation-target + // charges above are always at the cold rate. + // + // The delegation-target is already warmed before, no double warming here. + 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..ace43aa13a 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) { @@ -73,21 +74,22 @@ func TestFloorDataGas(t *testing.T) { { name: "amsterdam/empty", amsterdam: true, - want: params.TxGas, + // EIP-2780 anchors the floor to the reduced base cost. + want: params.TxBaseCost2780, }, { name: "amsterdam/data-only", amsterdam: true, data: bytes.Repeat([]byte{0x00}, 1024), // post-amsterdam: every byte = 4 tokens regardless of value - want: params.TxGas + 1024*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + want: params.TxBaseCost2780 + 1024*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, }, { name: "amsterdam/data-non-zero", amsterdam: true, data: bytes.Repeat([]byte{0xff}, 1024), // same as zero data post-amsterdam - want: params.TxGas + 1024*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + want: params.TxBaseCost2780 + 1024*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, }, { name: "amsterdam/access-list-addresses-only", @@ -97,7 +99,7 @@ func TestFloorDataGas(t *testing.T) { {Address: addr2}, }, // 2 * 20 bytes * 4 tokens/byte * 16 cost/token - want: params.TxGas + 2*common.AddressLength*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + want: params.TxBaseCost2780 + 2*common.AddressLength*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, }, { name: "amsterdam/access-list-with-storage-keys", @@ -106,7 +108,7 @@ func TestFloorDataGas(t *testing.T) { {Address: addr1, StorageKeys: []common.Hash{key1, key2}}, }, // 1 addr * 20 * 4 + 2 keys * 32 * 4 = 80 + 256 = 336 tokens * 16 - want: params.TxGas + (1*common.AddressLength+2*common.HashLength)*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, + want: params.TxBaseCost2780 + (1*common.AddressLength+2*common.HashLength)*params.TxTokenPerNonZeroByte*params.TxCostFloorPerToken7976, }, { name: "amsterdam/mixed", @@ -117,13 +119,13 @@ func TestFloorDataGas(t *testing.T) { {Address: addr2, StorageKeys: []common.Hash{key1, key2}}, }, // data: 100*4 = 400; addrs: 2*20*4 = 160; keys: 3*32*4 = 384; total = 944 * 16 - want: params.TxGas + (100*params.TxTokenPerNonZeroByte+2*common.AddressLength*params.TxTokenPerNonZeroByte+3*common.HashLength*params.TxTokenPerNonZeroByte)*params.TxCostFloorPerToken7976, + want: params.TxBaseCost2780 + (100*params.TxTokenPerNonZeroByte+2*common.AddressLength*params.TxTokenPerNonZeroByte+3*common.HashLength*params.TxTokenPerNonZeroByte)*params.TxCostFloorPerToken7976, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rules := params.Rules{IsAmsterdam: tt.amsterdam} - got, err := FloorDataGas(rules, tt.data, tt.accessList) + got, err := FloorDataGas(rules, addr1, &addr1, new(uint256.Int), tt.data, tt.accessList) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -155,6 +157,7 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028 bool isEIP3860 bool isAmsterdam bool + value *uint256.Int want vm.GasCosts }{ { @@ -237,8 +240,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 +263,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 +279,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 +296,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 +318,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 +326,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 +357,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 4651c06b3e..b53e3ee2bf 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,16 +117,22 @@ 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 if tx.Nonce()+1 < tx.Nonce() { return core.ErrNonceMax } + // Sanity check for extremely large numbers (supported by RLP or RPC) + value, overflow := uint256.FromBig(tx.Value()) + if overflow { + return core.ErrInsufficientFunds + } // 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(), value, rules, params.CostPerStateByte) if err != nil { return err } @@ -134,7 +141,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction can cover floor data gas. if rules.IsPrague { - floorDataGas, err := core.FloorDataGas(rules, tx.Data(), tx.AccessList()) + floorDataGas, err := core.FloorDataGas(rules, from, tx.To(), value, tx.Data(), tx.AccessList()) if err != nil { return err } diff --git a/params/protocol_params.go b/params/protocol_params.go index 84ded9df48..7416eb8cff 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..010c31324b 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,11 @@ func (tt *TransactionTest) Run() error { return } // Intrinsic cost - cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules, params.CostPerStateByte) + value, overflow := uint256.FromBig(tx.Value()) + if overflow { + return sender, hash, 0, errors.New("value exceeds 256 bits") + } + cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), sender, tx.To(), value, rules, params.CostPerStateByte) if err != nil { return } @@ -92,7 +97,7 @@ func (tt *TransactionTest) Run() error { if rules.IsPrague { var floorDataGas uint64 - floorDataGas, err = core.FloorDataGas(rules, tx.Data(), tx.AccessList()) + floorDataGas, err = core.FloorDataGas(rules, sender, tx.To(), value, tx.Data(), tx.AccessList()) if err != nil { return } From eccd239a297e8477333fcd3e363399e1a5575d37 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Mon, 29 Jun 2026 17:12:34 +0800 Subject: [PATCH 02/44] trie: introduce UpdateBatch (#32448) This feature is an optimization used in the BAL, mostly for experimental purpose. --------- Co-authored-by: jwasinger Co-authored-by: MariusVanDerWijden --- core/state/database.go | 6 +++ trie/bintrie/trie.go | 29 ++++++++++ trie/secure_trie.go | 46 ++++++++++++++++ trie/tracer.go | 19 +++++-- trie/transitiontrie/transition.go | 29 ++++++++++ trie/trie.go | 89 +++++++++++++++++++++++++++++++ trie/trie_test.go | 54 +++++++++++++++++++ 7 files changed, 269 insertions(+), 3 deletions(-) diff --git a/core/state/database.go b/core/state/database.go index 3b1e627f28..1001a4b4cf 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -107,12 +107,18 @@ type Trie interface { // in the trie with provided address. UpdateAccount(address common.Address, account *types.StateAccount, codeLen int) error + // UpdateAccountBatch attempts to update a list of accounts in the batch manner. + UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, codeLengths []int) error + // UpdateStorage associates key with value in the trie. If value has length zero, // any existing value is deleted from the trie. The value bytes must not be modified // by the caller while they are stored in the trie. If a node was not found in the // database, a trie.MissingNodeError is returned. UpdateStorage(addr common.Address, key, value []byte) error + // UpdateStorageBatch attempts to update a list of storages in the batch manner. + UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error + // DeleteAccount abstracts an account deletion from the trie. DeleteAccount(address common.Address) error diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go index 7b05e68df9..e8b3021284 100644 --- a/trie/bintrie/trie.go +++ b/trie/bintrie/trie.go @@ -474,3 +474,32 @@ func (t *BinaryTrie) PrefetchStorage(addr common.Address, keys [][]byte) error { func (t *BinaryTrie) Witness() map[string][]byte { return t.tracer.Values() } + +// UpdateStorageBatch updates a list of storage slots sequentially. +func (t *BinaryTrie) UpdateStorageBatch(address common.Address, keys [][]byte, values [][]byte) error { + if len(keys) != len(values) { + return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values)) + } + for i, key := range keys { + if err := t.UpdateStorage(address, key, values[i]); err != nil { + return err + } + } + return nil +} + +// UpdateAccountBatch updates a list of accounts sequentially. +func (t *BinaryTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, codeLens []int) error { + if len(addresses) != len(accounts) { + return fmt.Errorf("addresses and accounts length mismatch: %d != %d", len(addresses), len(accounts)) + } + if len(addresses) != len(codeLens) { + return fmt.Errorf("addresses and code length mismatch: %d != %d", len(addresses), len(codeLens)) + } + for i, addr := range addresses { + if err := t.UpdateAccount(addr, accounts[i], codeLens[i]); err != nil { + return err + } + } + return nil +} diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 4d03ca45f0..9f6cea9790 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -210,6 +210,29 @@ func (t *StateTrie) UpdateStorage(_ common.Address, key, value []byte) error { return nil } +// UpdateStorageBatch attempts to update a list storages in the batch manner. +func (t *StateTrie) UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error { + var ( + hkeys = make([][]byte, 0, len(keys)) + evals = make([][]byte, 0, len(values)) + ) + for _, key := range keys { + hk := crypto.Keccak256(key) + if t.preimages != nil { + t.secKeyCache[common.Hash(hk)] = common.CopyBytes(key) + } + hkeys = append(hkeys, hk) + } + for _, val := range values { + data, err := rlp.EncodeToBytes(val) + if err != nil { + return err + } + evals = append(evals, data) + } + return t.trie.UpdateBatch(hkeys, evals) +} + // UpdateAccount will abstract the write of an account to the secure trie. func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccount, _ int) error { hk := crypto.Keccak256(address.Bytes()) @@ -226,6 +249,29 @@ func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccoun return nil } +// UpdateAccountBatch attempts to update a list accounts in the batch manner. +func (t *StateTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, _ []int) error { + var ( + hkeys = make([][]byte, 0, len(addresses)) + values = make([][]byte, 0, len(accounts)) + ) + for _, addr := range addresses { + hk := crypto.Keccak256(addr.Bytes()) + if t.preimages != nil { + t.secKeyCache[common.Hash(hk)] = addr.Bytes() + } + hkeys = append(hkeys, hk) + } + for _, acc := range accounts { + data, err := rlp.EncodeToBytes(acc) + if err != nil { + return err + } + values = append(values, data) + } + return t.trie.UpdateBatch(hkeys, values) +} + func (t *StateTrie) UpdateContractCode(_ common.Address, _ common.Hash, _ []byte) error { return nil } diff --git a/trie/tracer.go b/trie/tracer.go index 04122d1384..042fa468bf 100644 --- a/trie/tracer.go +++ b/trie/tracer.go @@ -33,12 +33,10 @@ import ( // while the latter is inserted/deleted in order to follow the rule of trie. // This tool can track all of them no matter the node is embedded in its // parent or not, but valueNode is never tracked. -// -// Note opTracer is not thread-safe, callers should be responsible for handling -// the concurrency issues by themselves. type opTracer struct { inserts map[string]struct{} deletes map[string]struct{} + lock sync.RWMutex } // newOpTracer initializes the tracer for capturing trie changes. @@ -53,6 +51,9 @@ func newOpTracer() *opTracer { // in the deletion set (resurrected node), then just wipe it from // the deletion set as it's "untouched". func (t *opTracer) onInsert(path []byte) { + t.lock.Lock() + defer t.lock.Unlock() + if _, present := t.deletes[string(path)]; present { delete(t.deletes, string(path)) return @@ -64,6 +65,9 @@ func (t *opTracer) onInsert(path []byte) { // in the addition set, then just wipe it from the addition set // as it's untouched. func (t *opTracer) onDelete(path []byte) { + t.lock.Lock() + defer t.lock.Unlock() + if _, present := t.inserts[string(path)]; present { delete(t.inserts, string(path)) return @@ -73,12 +77,18 @@ func (t *opTracer) onDelete(path []byte) { // reset clears the content tracked by tracer. func (t *opTracer) reset() { + t.lock.Lock() + defer t.lock.Unlock() + clear(t.inserts) clear(t.deletes) } // copy returns a deep copied tracer instance. func (t *opTracer) copy() *opTracer { + t.lock.RLock() + defer t.lock.RUnlock() + return &opTracer{ inserts: maps.Clone(t.inserts), deletes: maps.Clone(t.deletes), @@ -87,6 +97,9 @@ func (t *opTracer) copy() *opTracer { // deletedList returns a list of node paths which are deleted from the trie. func (t *opTracer) deletedList() [][]byte { + t.lock.RLock() + defer t.lock.RUnlock() + paths := make([][]byte, 0, len(t.deletes)) for path := range t.deletes { paths = append(paths, []byte(path)) diff --git a/trie/transitiontrie/transition.go b/trie/transitiontrie/transition.go index 3e5511be9e..d939e804e3 100644 --- a/trie/transitiontrie/transition.go +++ b/trie/transitiontrie/transition.go @@ -144,6 +144,19 @@ func (t *TransitionTrie) UpdateStorage(address common.Address, key []byte, value return t.overlay.UpdateStorage(address, key, v) } +// UpdateStorageBatch attempts to update a list storages in the batch manner. +func (t *TransitionTrie) UpdateStorageBatch(address common.Address, keys [][]byte, values [][]byte) error { + if len(keys) != len(values) { + return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values)) + } + for i, key := range keys { + if err := t.UpdateStorage(address, key, values[i]); err != nil { + return err + } + } + return nil +} + // UpdateAccount abstract an account write to the trie. func (t *TransitionTrie) UpdateAccount(addr common.Address, account *types.StateAccount, codeLen int) error { // NOTE: before the rebase, this was saving the state root, so that OpenStorageTrie @@ -152,6 +165,22 @@ func (t *TransitionTrie) UpdateAccount(addr common.Address, account *types.State return t.overlay.UpdateAccount(addr, account, codeLen) } +// UpdateAccountBatch attempts to update a list accounts in the batch manner. +func (t *TransitionTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, codeLens []int) error { + if len(addresses) != len(accounts) { + return fmt.Errorf("address and accounts length mismatch: %d != %d", len(addresses), len(accounts)) + } + if len(addresses) != len(codeLens) { + return fmt.Errorf("address and code length mismatch: %d != %d", len(addresses), len(codeLens)) + } + for i, addr := range addresses { + if err := t.UpdateAccount(addr, accounts[i], codeLens[i]); err != nil { + return err + } + } + return nil +} + // DeleteStorage removes any existing value for key from the trie. If a node was not // found in the database, a trie.MissingNodeError is returned. func (t *TransitionTrie) DeleteStorage(addr common.Address, key []byte) error { diff --git a/trie/trie.go b/trie/trie.go index 1ef2c2f1a6..0026c6048c 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -480,6 +480,95 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error } } +// parallelUpdateThreshold is the minimum number of entries a batch must contain +// before UpdateBatch processes them concurrently; smaller batches are applied +// sequentially to avoid the goroutine overhead. +const parallelUpdateThreshold = 4 + +// UpdateBatch updates a batch of entries concurrently. +func (t *Trie) UpdateBatch(keys [][]byte, values [][]byte) error { + // Short circuit if the trie is already committed and unusable. + if t.committed { + return ErrCommitted + } + if len(keys) != len(values) { + return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values)) + } + // Insert the entries sequentially if there are not too many + // trie nodes in the trie. + fn, ok := t.root.(*fullNode) + if !ok || len(keys) < parallelUpdateThreshold { + return t.updateSequential(keys, values) + } + // Group the entries by the first nibble of the (hex) key, so that each group + // can be applied to a distinct child of the root independently. + var ( + ikeys = make(map[byte][][]byte) + ivals = make(map[byte][][]byte) + deleted [17]bool // child positions receiving at least one deletion + eg errgroup.Group + ) + for i, key := range keys { + hkey := keybytesToHex(key) + ikeys[hkey[0]] = append(ikeys[hkey[0]], hkey) + ivals[hkey[0]] = append(ivals[hkey[0]], values[i]) + if len(values[i]) == 0 { + deleted[hkey[0]] = true + } + } + // If the root may be collapsed after applying the updates, fallback + // to the sequential mode to eliminate the additional complexity. + survivors := 0 + for i, child := range &fn.Children { + if child != nil && !deleted[i] { + survivors++ + } + } + if survivors < 2 { + return t.updateSequential(keys, values) + } + // Execute the trie updates in concurrent mode + fn.flags = t.newFlag() + for pos, ks := range ikeys { + eg.Go(func() error { + vs := ivals[pos] + prefix := []byte{pos} + for i, k := range ks { + if len(vs[i]) != 0 { + _, n, err := t.insert(fn.Children[pos], prefix, k[1:], valueNode(vs[i])) + if err != nil { + return err + } + fn.Children[pos] = n + } else { + _, n, err := t.delete(fn.Children[pos], prefix, k[1:]) + if err != nil { + return err + } + fn.Children[pos] = n + } + } + return nil + }) + } + if err := eg.Wait(); err != nil { + return err + } + t.unhashed += len(keys) + t.uncommitted += len(keys) + return nil +} + +// updateSequential applies the given entries to the trie one by one. +func (t *Trie) updateSequential(keys [][]byte, values [][]byte) error { + for i, key := range keys { + if err := t.Update(key, values[i]); err != nil { + return err + } + } + return nil +} + // MustDelete is a wrapper of Delete and will omit any encountered error but // just print out an error message. func (t *Trie) MustDelete(key []byte) { diff --git a/trie/trie_test.go b/trie/trie_test.go index 3661933e22..949f381f07 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -1580,3 +1580,57 @@ func BenchmarkTrieSeqPrefetch(b *testing.B) { } } } + +func TestUpdateBatch(t *testing.T) { + testUpdateBatch(t, []kv{ + {k: []byte("do"), v: []byte("verb")}, + {k: []byte("ether"), v: []byte("wookiedoo")}, + {k: []byte("horse"), v: []byte("stallion")}, + {k: []byte("shaman"), v: []byte("horse")}, + {k: []byte("doge"), v: []byte("coin")}, + {k: []byte("dog"), v: []byte("puppy")}, + }) + + var entries []kv + for i := 0; i < 256; i++ { + entries = append(entries, kv{k: testrand.Bytes(32), v: testrand.Bytes(32)}) + } + testUpdateBatch(t, entries) +} + +func testUpdateBatch(t *testing.T, entries []kv) { + var ( + base = NewEmpty(nil) + keys [][]byte + vals [][]byte + ) + for _, entry := range entries { + base.Update(entry.k, entry.v) + keys = append(keys, entry.k) + vals = append(vals, entry.v) + } + for i := 0; i < 10; i++ { + k, v := testrand.Bytes(32), testrand.Bytes(32) + base.Update(k, v) + keys = append(keys, k) + vals = append(vals, v) + } + + cmp := NewEmpty(nil) + if err := cmp.UpdateBatch(keys, vals); err != nil { + t.Fatalf("Failed to update batch, %v", err) + } + + // Traverse the original tree, the changes made on the copy one shouldn't + // affect the old one + for _, key := range keys { + v1, _ := base.Get(key) + v2, _ := cmp.Get(key) + if !bytes.Equal(v1, v2) { + t.Errorf("Unexpected data, key: %v, want: %v, got: %v", key, v1, v2) + } + } + if base.Hash() != cmp.Hash() { + t.Errorf("Hash mismatch: want %x, got %x", base.Hash(), cmp.Hash()) + } +} From 11524ba075eea76dab4fd1c11bba79a869350efd Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 29 Jun 2026 20:59:12 +0800 Subject: [PATCH 03/44] core/rawdb: fix freezer metadata log field ordering (#35246) --- core/rawdb/freezer_meta.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/rawdb/freezer_meta.go b/core/rawdb/freezer_meta.go index 3cda9ae45c..03c26f7daf 100644 --- a/core/rawdb/freezer_meta.go +++ b/core/rawdb/freezer_meta.go @@ -112,7 +112,7 @@ func decodeV2(file *os.File) *freezerTableMeta { return nil } if o.Offset > math.MaxInt64 { - log.Error("Invalid flushOffset %d in freezer metadata", o.Offset, "file", file.Name()) + log.Error("Invalid flushOffset %d in freezer metadata", "offset", o.Offset, "file", file.Name()) return nil } return &freezerTableMeta{ From a0568b1dbfdebb2db967283d44cdef3b83f41013 Mon Sep 17 00:00:00 2001 From: rayoo Date: Mon, 29 Jun 2026 06:41:33 -0700 Subject: [PATCH 04/44] cmd/geth: release account iterator in listEIP7610EligibleAccounts (#35250) listEIP7610EligibleAccounts opens an account iterator and never releases it. --- cmd/geth/snapshot.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index 8fedc21c27..c0258df68c 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -1054,6 +1054,7 @@ func listEIP7610EligibleAccounts(ctx *cli.Context) error { log.Error("Failed to get account iterator", "err", err) return err } + defer iter.Release() var ( start = time.Now() accounts []common.Address From 9700b5b10eee6ea3e0f71eaa2da3875e25047c7b Mon Sep 17 00:00:00 2001 From: Chase Wright Date: Mon, 29 Jun 2026 08:55:42 -0500 Subject: [PATCH 05/44] cmd/devp2p: support dual-stack discovery listener (#35220) `devp2p discv4 listen` / `discv5 listen` is the supported replacement for the removed bootnode tool, but it bound IPv4-only and `-extaddr` took a single address, so it couldn't run a dual-stack bootnode. This binds the listener dual-stack (falling back to IPv4-only where IPv6 is unavailable) and lets `-extaddr` take a comma-separated IPv4/IPv6 pair. A single node can then advertise both `ip` and `ip6` in its ENR over one UDP port: ``` devp2p discv4 listen --nodekey --addr [::]:30301 \ --extaddr 203.0.113.10:30301,[2001:db8::1]:30301 ``` The fallback IP is only derived from the listener when no `-extaddr` is given, so a v4- or v6-only `-extaddr` no longer leaks a loopback entry. All addresses must share one UDP port (single socket). --- cmd/devp2p/discv4cmd.go | 60 ++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/cmd/devp2p/discv4cmd.go b/cmd/devp2p/discv4cmd.go index 84c7ef0c44..1089210c57 100644 --- a/cmd/devp2p/discv4cmd.go +++ b/cmd/devp2p/discv4cmd.go @@ -126,7 +126,7 @@ var ( } extAddrFlag = &cli.StringFlag{ Name: "extaddr", - Usage: "UDP endpoint announced in ENR. You can provide a bare IP address or IP:port as the value of this flag.", + Usage: "UDP endpoint announced in ENR. You can provide a bare IP address or IP:port as the value of this flag. Provide a comma-separated pair to announce both an IPv4 and an IPv6 endpoint.", } crawlTimeoutFlag = &cli.DurationFlag{ Name: "timeout", @@ -344,36 +344,60 @@ func parseExtAddr(spec string) (ip net.IP, port int, ok bool) { func listen(ctx *cli.Context, ln *enode.LocalNode) *net.UDPConn { addr := ctx.String(listenAddrFlag.Name) + extAddr := ctx.String(extAddrFlag.Name) + var ( + socket net.PacketConn + err error + ) if addr == "" { - addr = "0.0.0.0:0" + // Dual-stack socket, falling back to IPv4-only where IPv6 is unavailable. + if socket, err = net.ListenPacket("udp", "[::]:0"); err != nil { + socket, err = net.ListenPacket("udp", "0.0.0.0:0") + } + } else { + socket, err = net.ListenPacket("udp", addr) } - socket, err := net.ListenPacket("udp4", addr) if err != nil { exit(err) } - // Configure UDP endpoint in ENR from listener address. + // Configure the ENR endpoint from the listener address, but only without an + // explicit -extaddr: otherwise we'd announce a fallback IP for an address + // family the user didn't specify (e.g. loopback IPv4 on an IPv6-only node). usocket := socket.(*net.UDPConn) uaddr := socket.LocalAddr().(*net.UDPAddr) - if uaddr.IP.IsUnspecified() { - ln.SetFallbackIP(net.IP{127, 0, 0, 1}) - } else { - ln.SetFallbackIP(uaddr.IP) + if extAddr == "" { + if uaddr.IP.IsUnspecified() { + ln.SetFallbackIP(net.IP{127, 0, 0, 1}) + } else { + ln.SetFallbackIP(uaddr.IP) + } } ln.SetFallbackUDP(uaddr.Port) - // If an ENR endpoint is set explicitly on the command-line, override - // the information from the listening address. Note this is careful not - // to set the UDP port if the external address doesn't have it. - extAddr := ctx.String(extAddrFlag.Name) + // Override with explicit -extaddr address(es). A static IP is set per family, + // and all specs share one UDP port because the node has a single socket. if extAddr != "" { - ip, port, ok := parseExtAddr(extAddr) - if !ok { - exit(fmt.Errorf("-%s: invalid external address %q", extAddrFlag.Name, extAddr)) + var extPort int + for spec := range strings.SplitSeq(extAddr, ",") { + spec = strings.TrimSpace(spec) + if spec == "" { + continue + } + ip, port, ok := parseExtAddr(spec) + if !ok { + exit(fmt.Errorf("-%s: invalid external address %q", extAddrFlag.Name, spec)) + } + ln.SetStaticIP(ip) + if port != 0 { + if extPort != 0 && port != extPort { + exit(fmt.Errorf("-%s: all addresses must announce the same UDP port, got %d and %d", extAddrFlag.Name, extPort, port)) + } + extPort = port + } } - ln.SetStaticIP(ip) - if port != 0 { - ln.SetFallbackUDP(port) + if extPort != 0 { + ln.SetFallbackUDP(extPort) } } From 68671a453079b6fc2b8ec88a567545ddc361a8d7 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 30 Jun 2026 08:58:43 +0800 Subject: [PATCH 06/44] core: implement EIP-8246 (#35219) EIP: https://eips.ethereum.org/EIPS/eip-8246 Supersedes #35218 --- core/bal_test.go | 56 ++++++++++++ core/eip8246_test.go | 105 +++++++++++++++++++++ core/state/statedb.go | 172 ++++++++++++++++++++--------------- core/state/statedb_hooked.go | 14 +-- core/state_transition.go | 6 -- core/types/log.go | 14 --- core/vm/instructions.go | 19 ++-- core/vm/interface.go | 1 - 8 files changed, 278 insertions(+), 109 deletions(-) create mode 100644 core/eip8246_test.go diff --git a/core/bal_test.go b/core/bal_test.go index 6b79f86c43..84579ca564 100644 --- a/core/bal_test.go +++ b/core/bal_test.go @@ -946,6 +946,62 @@ func TestBALSelfDestructPreExistingContract(t *testing.T) { } } +// TestBALSelfDestructToSelfKeepsBalance: under EIP-8246 a freshly created +// contract that self-destructs to itself keeps its balance (it is not burnt and +// the account is not removed). The surviving balance-only account must therefore +// be recorded in the BAL with its preserved balance. +func TestBALSelfDestructToSelfKeepsBalance(t *testing.T) { + env := newBALTestEnv(nil) + // Init code: ADDRESS SELFDESTRUCT — the contract self-destructs to itself + // during its own creation transaction (satisfying EIP-6780's same-tx rule). + // ADDRESS (0x30) ; SELFDESTRUCT (0xff) + init := []byte{0x30, 0xff} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(100), 1_000_000, 0, init)) + }) + + created := receipts[0].ContractAddress + cc := assertPresent(t, b, created) + // EIP-8246: balance preserved (not burnt), account survives -> the BAL must + // record the created address with its retained balance. + if len(cc.BalanceChanges) != 1 || cc.BalanceChanges[0].PostBalance.Uint64() != 100 { + t.Fatalf("self-destruct-to-self must preserve balance 100 in the BAL: %+v", cc.BalanceChanges) + } +} + +// TestBALSelfDestructToSelfPrefundedUnchanged: a pre-funded address onto which a +// contract is deployed and which self-destructs to itself in the same +// transaction. Under EIP-8246 the account survives with its balance unchanged, +// so the BAL must list it only as an access (no balance/nonce/code change). +func TestBALSelfDestructToSelfPrefundedUnchanged(t *testing.T) { + // The contract address created by the sender's nonce-0 transaction; it is + // pre-funded in genesis (balance only: nonce 0, no code, no storage), which + // EIP-7610 permits as a deployment target. + key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + created := crypto.CreateAddress(crypto.PubkeyToAddress(key.PublicKey), 0) + + env := newBALTestEnv(types.GenesisAlloc{ + created: {Balance: big.NewInt(77)}, + }) + // Init code: ADDRESS SELFDESTRUCT, deployed with zero value so the balance is + // untouched (stays at the pre-funded 77). + init := []byte{0x30, 0xff} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 1_000_000, 0, init)) + }) + + if receipts[0].ContractAddress != created { + t.Fatalf("unexpected created address: have %x want %x", receipts[0].ContractAddress, created) + } + aa := assertPresent(t, b, created) + // EIP-8246: balance preserved and equal to the pre-transaction value, so no + // balance change; nonce and code end where they started (0 / empty). The + // account is only read, with an empty change set. + assertEmpty(t, aa) +} + // ============================== Mid-tx balance round-trip ============================== // TestBALMidTxBalanceRoundTrip: when an address's balance changes during a diff --git a/core/eip8246_test.go b/core/eip8246_test.go new file mode 100644 index 0000000000..2fd39e3d44 --- /dev/null +++ b/core/eip8246_test.go @@ -0,0 +1,105 @@ +// 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 ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" +) + +// TestEIP8246SelfdestructNoBurn verifies that, once EIP-8246 is active +// (Amsterdam), a contract that is created and self-destructs to itself within +// the same transaction keeps its balance instead of burning it: the account +// survives as a balance-only account (no code, zero nonce, balance preserved). +// +// https://eips.ethereum.org/EIPS/eip-8246 +func TestEIP8246SelfdestructNoBurn(t *testing.T) { + var ( + key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr1 = crypto.PubkeyToAddress(key1.PublicKey) + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + value = big.NewInt(1_000_000) + // Init code: ADDRESS (0x30) ; SELFDESTRUCT (0xff). The created contract + // self-destructs to itself during its own creation transaction. + initcode = common.FromHex("30ff") + ) + // TODO: drop this hacky Amsterdam config initialization once the final + // Amsterdam config is available (mirrors TestEthTransferLogs). + config.AmsterdamTime = new(uint64) + blobConfig := *config.BlobScheduleConfig + blobConfig.Amsterdam = blobConfig.Osaka + config.BlobScheduleConfig = &blobConfig + + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + addr1: {Balance: newGwei(1_000_000_000)}, + }, + } + // The contract created by addr1's first (nonce 0) transaction. + created := crypto.CreateAddress(addr1, 0) + + db, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + tx := types.MustSignNewTx(key1, signer, &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 0, + To: nil, // contract creation + Gas: 1_000_000, + GasFeeCap: newGwei(5), + GasTipCap: newGwei(5), + Value: value, + Data: initcode, + }) + b.AddTx(tx) + }) + + chain, err := NewBlockChain(db, gspec, engine, nil) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + // Read the post-state of the generated block directly. InsertChain is avoided + // on purpose: it would additionally verify the EIP-7928 block access list, + // which the chain-generation harness on this branch does not yet populate + // consistently — an orthogonal concern to the EIP-8246 state semantics under + // test here. + state, err := chain.StateAt(blocks[0].Header()) + if err != nil { + t.Fatalf("failed to obtain block state: %v", err) + } + // EIP-8246: the self-destructed, freshly-created contract keeps its balance + // rather than burning it, so the account survives. + if got := state.GetBalance(created); got.ToBig().Cmp(value) != 0 { + t.Errorf("created account balance = %v, want %v (EIP-8246: balance must be preserved, not burned)", got, value) + } + // It survives as a balance-only account: nonce reset to 0 and no code. + if got := state.GetNonce(created); got != 0 { + t.Errorf("created account nonce = %d, want 0", got) + } + if got := state.GetCodeSize(created); got != 0 { + t.Errorf("created account code size = %d, want 0 (code must be cleared)", got) + } +} diff --git a/core/state/statedb.go b/core/state/statedb.go index 1c49d46020..176445a575 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -764,50 +764,15 @@ func (s *StateDB) GetRefund() uint64 { return s.refund } -type removedAccountWithBalance struct { - address common.Address - balance *uint256.Int -} - -// LogsForBurnAccounts returns the eth burn logs for accounts scheduled for -// removal which still have positive balance. The purpose of this function is -// to handle a corner case of EIP-7708 where a self-destructed account might -// still receive funds between sending/burning its previous balance and actual -// removal. In this case the burning of these remaining balances still need to -// be logged. -// Specification EIP-7708: https://eips.ethereum.org/EIPS/eip-7708 -// -// This function should only be invoked at the transaction boundary, specifically -// before the Finalise. -func (s *StateDB) LogsForBurnAccounts() []*types.Log { - var list []removedAccountWithBalance - for addr := range s.journal.mutations { - if obj, exist := s.stateObjects[addr]; exist && obj.selfDestructed && !obj.Balance().IsZero() { - list = append(list, removedAccountWithBalance{ - address: obj.address, - balance: obj.Balance(), - }) - } - } - if list == nil { - return nil - } - sort.Slice(list, func(i, j int) bool { - return list[i].address.Cmp(list[j].address) < 0 - }) - logs := make([]*types.Log, len(list)) - for i, acct := range list { - logs[i] = types.EthBurnLog(acct.address, acct.balance) - } - return logs -} - // Finalise finalises the state by removing the destructed objects and clears // the journal as well as the refunds. Finalise, however, will not push any updates // into the tries just yet. Only IntermediateRoot or Commit will do that. func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccessList { + if s.stateAccessList != nil { + return s.finaliseAmsterdam(deleteEmptyObjects) + } addressesToPrefetch := make([]common.Address, 0, len(s.journal.mutations)) - for addr, state := range s.journal.mutations { + for addr := range s.journal.mutations { obj, exist := s.stateObjects[addr] if !exist { // RIPEMD160 (0x03) gets an extra dirty marker for a historical @@ -831,46 +796,103 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccess if _, ok := s.stateObjectsDestruct[obj.address]; !ok { s.stateObjectsDestruct[obj.address] = obj } - // Aggregate the account mutation into the block-level accessList - // if Amsterdam has been activated. - if s.stateAccessList != nil { - // Notably, if the account is deleted during the transaction, - // its pre-transaction nonce, code, and storage must be empty. - // - // EIP-6780 restricts self-destruct to contracts deployed within - // the same transaction, while EIP-7610 rejects deployments to - // destinations with non-empty storage, non-zero nonce and non-empty - // code. - // - // Therefore, when an account is deleted, its pre-transaction nonce - // code and storage is guaranteed to be empty, leaving nothing to - // clean up here. - balance := uint256.NewInt(0) - if state.balanceSet && balance.Cmp(state.balance) != 0 { - s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance) - } - } } else { - // Aggregate the account mutation into the block-level accessList - // if Amsterdam has been activated. - if s.stateAccessList != nil { - balance := obj.Balance() - if state.balanceSet && balance.Cmp(state.balance) != 0 { - s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance) - } - nonce := obj.Nonce() - if state.nonceSet && nonce != state.nonce { - s.stateAccessList.NonceChange(addr, s.blockAccessIndex, nonce) - } - if state.codeSet { - if code := obj.Code(); !bytes.Equal(code, state.code) { - s.stateAccessList.CodeChange(addr, s.blockAccessIndex, code) - } - } - } obj.finalise() s.markUpdate(addr) } + addressesToPrefetch = append(addressesToPrefetch, addr) // Copy needed for closure + } + if s.prefetcher != nil && len(addressesToPrefetch) > 0 { + if err := s.prefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, addressesToPrefetch, nil, false); err != nil { + log.Error("Failed to prefetch addresses", "addresses", len(addressesToPrefetch), "err", err) + } + } + // Invalidate journal because reverting across transactions is not allowed. + s.clearJournalAndRefund() + + return nil +} + +func (s *StateDB) recordAccessListChanges(addr common.Address, state *journalMutationState) { + var ( + balance = uint256.NewInt(0) + nonce uint64 + ) + obj := s.stateObjects[addr] // nil when the account was removed + if obj != nil { + balance, nonce = obj.Balance(), obj.Nonce() + } + if state.balanceSet && balance.Cmp(state.balance) != 0 { + s.stateAccessList.BalanceChange(s.blockAccessIndex, addr, balance) + } + if state.nonceSet && nonce != state.nonce { + s.stateAccessList.NonceChange(addr, s.blockAccessIndex, nonce) + } + if state.codeSet { + var code []byte + if obj != nil { + code = obj.Code() + } + if !bytes.Equal(code, state.code) { + s.stateAccessList.CodeChange(addr, s.blockAccessIndex, code) + } + } +} + +// finaliseAmsterdam is the Amsterdam-and-later variant of Finalise. +func (s *StateDB) finaliseAmsterdam(deleteEmptyObjects bool) *bal.ConstructionBlockAccessList { + addressesToPrefetch := make([]common.Address, 0, len(s.journal.mutations)) + for addr, state := range s.journal.mutations { + obj, exist := s.stateObjects[addr] + if !exist { + // RIPEMD160 (0x03) gets an extra dirty marker for a historical + // mainnet consensus exception (at block 1714175, in tx + // 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2) + // around empty-account touch/revert handling. + // + // That marker survives journal revert, so the account may remain in + // s.journal.mutations even though its state object was rolled + // back and no longer exists. In that case there is nothing to + // finalise or delete, so ignore it here. + continue + } + switch { + case obj.selfDestructed: + // EIP-8264: accounts marked for self-destruction, instead of + // being deleted, are modified as follows: + // - nonce is reset to 0, + // - balance is unchanged, + // - code is cleared, + // - all storage is cleared + if !obj.Balance().IsZero() { + o := newObject(s, obj.address, obj.origin) + o.setBalance(new(uint256.Int).Set(obj.Balance())) + s.setStateObject(o) + s.markUpdate(addr) + } else { + delete(s.stateObjects, obj.address) + s.markDelete(addr) + if _, ok := s.stateObjectsDestruct[obj.address]; !ok { + s.stateObjectsDestruct[obj.address] = obj + } + } + + case deleteEmptyObjects && obj.empty(): + // EIP-161: a touched, empty account is removed. + delete(s.stateObjects, obj.address) + s.markDelete(addr) + if _, ok := s.stateObjectsDestruct[obj.address]; !ok { + s.stateObjectsDestruct[obj.address] = obj + } + + default: + obj.finalise() + s.markUpdate(addr) + } + // Aggregate the resulting account metadata change + // into the block-level access list. + s.recordAccessListChanges(addr, state) + // At this point, also ship the address off to the precacher. The precacher // will start loading tries, and when the change is eventually committed, // the commit-phase will be a lot faster diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 98d01343a4..73e8253311 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -230,10 +230,6 @@ func (s *hookedStateDB) AddLog(log *types.Log) { } } -func (s *hookedStateDB) LogsForBurnAccounts() []*types.Log { - return s.inner.LogsForBurnAccounts() -} - func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlockAccessList { if s.hooks.OnBalanceChange == nil && s.hooks.OnNonceChangeV2 == nil && s.hooks.OnNonceChange == nil && s.hooks.OnCodeChangeV2 == nil && s.hooks.OnCodeChange == nil { // Short circuit if no relevant hooks are set. @@ -256,18 +252,24 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) *bal.ConstructionBlock return bytes.Compare(selfDestructedAddrs[i][:], selfDestructedAddrs[j][:]) < 0 }) + // EIP-8246 (Amsterdam) removes the SELFDESTRUCT burn: a self-destructed + // account that retains a non-zero balance is preserved as a balance-only + // account rather than removed, so its balance is no longer burnt. + burnsBalance := s.inner.stateAccessList == nil + for _, addr := range selfDestructedAddrs { obj := s.inner.stateObjects[addr] // Bingo: state object was self-destructed, call relevant hooks. - // If ether was sent to account post-selfdestruct, record as burnt. - if s.hooks.OnBalanceChange != nil { + if burnsBalance && s.hooks.OnBalanceChange != nil { if bal := obj.Balance(); bal.Sign() != 0 { s.hooks.OnBalanceChange(addr, bal.ToBig(), new(big.Int), tracing.BalanceDecreaseSelfdestructBurn) } } // Nonce is set to reset on self-destruct. + // + // TODO(rjl) shall we emit the nonce change if the pre-tx nonce was zero? if s.hooks.OnNonceChangeV2 != nil { s.hooks.OnNonceChangeV2(addr, obj.Nonce(), 0, tracing.NonceChangeSelfdestruct) } else if s.hooks.OnNonceChange != nil { diff --git a/core/state_transition.go b/core/state_transition.go index 3b1642e0b9..2923e110c7 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -790,12 +790,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } } - // EIP-7708: Emit the ETH-burn logs - if rules.IsAmsterdam { - for _, log := range st.evm.StateDB.LogsForBurnAccounts() { - st.evm.StateDB.AddLog(log) - } - } return &ExecutionResult{ UsedGas: gasUsed, MaxUsedGas: peakUsed, diff --git a/core/types/log.go b/core/types/log.go index 487ca57b5a..3ea1e0db7c 100644 --- a/core/types/log.go +++ b/core/types/log.go @@ -79,17 +79,3 @@ func EthTransferLog(from, to common.Address, amount *uint256.Int) *Log { Data: amount32[:], } } - -// EthBurnLog creates an ETH burn log according to EIP-7708. -// Specification: https://eips.ethereum.org/EIPS/eip-7708 -func EthBurnLog(from common.Address, amount *uint256.Int) *Log { - amount32 := amount.Bytes32() - return &Log{ - Address: params.SystemAddress, - Topics: []common.Hash{ - params.EthBurnLogEvent, - common.BytesToHash(from.Bytes()), - }, - Data: amount32[:], - } -} diff --git a/core/vm/instructions.go b/core/vm/instructions.go index b20cce4c0d..328623848e 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -928,8 +928,15 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro if newContract { if this != beneficiary { // Skip no-op transfer when self-destructing to self. evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct) + evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) + } else if !evm.chainRules.IsAmsterdam { + // Self-destructing to self burns the balance prior to EIP-8246. + // EIP-8246 (Amsterdam) removes this burn: the balance is left + // untouched and the account is preserved as a balance-only account + // at transaction finalization (unless its balance is zero, in which + // case EIP-161 deletes it). + evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) } - evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) evm.StateDB.SelfDestruct(this) } @@ -938,12 +945,10 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct) } - if evm.chainRules.IsAmsterdam && !balance.IsZero() { - if this != beneficiary { - evm.StateDB.AddLog(types.EthTransferLog(this, beneficiary, balance)) - } else if newContract { - evm.StateDB.AddLog(types.EthBurnLog(this, balance)) - } + // EIP-7708: emit a transfer log for the moved balance. EIP-8246 removes the + // SELFDESTRUCT burn entirely, so there is no longer a burn to log. + if evm.chainRules.IsAmsterdam && !balance.IsZero() && this != beneficiary { + evm.StateDB.AddLog(types.EthTransferLog(this, beneficiary, balance)) } if tracer := evm.Config.Tracer; tracer != nil { diff --git a/core/vm/interface.go b/core/vm/interface.go index a9938c2a28..5bba39069c 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -90,7 +90,6 @@ type StateDB interface { Snapshot() int AddLog(*types.Log) - LogsForBurnAccounts() []*types.Log AddPreimage(common.Hash, []byte) Witness() *stateless.Witness From 04bf0453036dffc7710684354efabdbe3970ffe0 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Mon, 29 Jun 2026 22:03:12 -0300 Subject: [PATCH 07/44] core/state: fix account prefetching for absent accounts (#35256) --- core/state/statedb.go | 11 ++++++----- core/state/statedb_test.go | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index 176445a575..09a896a89a 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -614,16 +614,17 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { } s.AccountReads += time.Since(start) - // Short circuit if the account is not found - if acct == nil { - return nil - } - // Schedule the resolved account for prefetching if it's enabled. + // Schedule the account path for prefetching if it's enabled. Even if the + // account is absent, the trie path proves its non-existence for witnesses. if s.prefetcher != nil { if err = s.prefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, []common.Address{addr}, nil, true); err != nil { log.Error("Failed to prefetch account", "addr", addr, "err", err) } } + // Short circuit if the account is not found + if acct == nil { + return nil + } // Insert into the live set obj := newObject(s, addr, acct) s.setStateObject(obj) diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 0bf9b50e7b..6f4282054e 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -998,6 +999,45 @@ func TestDeleteCreateRevert(t *testing.T) { } } +func TestWitnessIncludesAbsentAccountReads(t *testing.T) { + db := NewDatabaseForTesting() + state, _ := New(types.EmptyRootHash, db) + for i := byte(0); i < 3; i++ { + addr := common.Address{i + 1} + state.SetBalance(addr, uint256.NewInt(uint64(i+1)), tracing.BalanceChangeUnspecified) + } + root, err := state.Commit(0, false, false) + if err != nil { + t.Fatalf("failed to commit initial state: %v", err) + } + state, err = New(root, db) + if err != nil { + t.Fatalf("failed to reopen state: %v", err) + } + + witness := &stateless.Witness{ + Codes: make(map[string]struct{}), + State: make(map[string]struct{}), + } + state.StartPrefetcher("test", witness) + missing := common.HexToAddress("0x017655eac00c837122cabbbc0dd604a196906648") + if balance := state.GetBalance(missing); balance.Sign() != 0 { + t.Fatalf("unexpected balance for absent account: %v", balance) + } + if err := state.Error(); err != nil { + t.Fatalf("unexpected state error after read: %v", err) + } + if got := state.IntermediateRoot(false); got != root { + t.Fatalf("unexpected root after read-only access: have %x want %x", got, root) + } + if err := state.Error(); err != nil { + t.Fatalf("unexpected state error after root calculation: %v", err) + } + if len(witness.State) == 0 { + t.Fatal("missing witness nodes for absent account read") + } +} + // TestMissingTrieNodes tests that if the StateDB fails to load parts of the trie, // the Commit operation fails with an error // If we are missing trie nodes, we should not continue writing to the trie From 32986bf084f70162442544af4f6e6ca9c8298d42 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 30 Jun 2026 21:12:06 +0800 Subject: [PATCH 08/44] triedb/pathdb: prealloc node map in nodeSet.write (#35232) --- triedb/pathdb/nodes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/triedb/pathdb/nodes.go b/triedb/pathdb/nodes.go index 62c72c1953..590f7b499d 100644 --- a/triedb/pathdb/nodes.go +++ b/triedb/pathdb/nodes.go @@ -279,7 +279,7 @@ func (s *nodeSet) decode(r *rlp.Stream) error { // write flushes nodes into the provided database batch as a whole. func (s *nodeSet) write(batch ethdb.Batch, clean *fastcache.Cache) int { - nodes := make(map[common.Hash]map[string]*trienode.Node) + nodes := make(map[common.Hash]map[string]*trienode.Node, len(s.storageNodes)+1) if len(s.accountNodes) > 0 { nodes[common.Hash{}] = s.accountNodes } From 409aaefac158c3bd6ca3d9afb90232ba17102551 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 30 Jun 2026 21:12:47 +0800 Subject: [PATCH 09/44] log: clone terminal handler attrs (#35141) Clone the existing terminal handler attrs before appending new attrs. This avoids a potential attr memory overwrite when append reuses the backing array shared with the parent handler. --- log/handler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/log/handler.go b/log/handler.go index 0b51120e51..30a0aff49c 100644 --- a/log/handler.go +++ b/log/handler.go @@ -7,6 +7,7 @@ import ( "log/slog" "math/big" "reflect" + "slices" "sync" "time" @@ -97,7 +98,7 @@ func (h *TerminalHandler) WithAttrs(attrs []slog.Attr) slog.Handler { wr: h.wr, lvl: h.lvl, useColor: h.useColor, - attrs: append(h.attrs, attrs...), + attrs: append(slices.Clone(h.attrs), attrs...), fieldPadding: make(map[string]int), } } From 5c253e557e567e7702a46e8b23be0782394e32bf Mon Sep 17 00:00:00 2001 From: Bosul Mun Date: Wed, 1 Jul 2026 03:43:11 +0200 Subject: [PATCH 10/44] core/rawdb: prevent truncateHead from returning an error for empty tables (#35258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is related to the recent bug reported in #35210. While trying to reproduce the error, I found that when the head state is missing (e.g. unclean shutdown), we attempt to truncate the head to the most recent block with state across all chain freezer tables. However, for newly added tables such as the bal table, both the head and tail are initialized to the minimum head of the existing chain freezer tables. As a result, the `truncateHead` fails with the “truncate below tail” error. This PR fixes the issue by resetting newly added empty tables with `items` as the tail when `truncateHead(items)` is called on them. --- core/rawdb/freezer_table.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index c770e89989..d18b75c2f7 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -611,9 +611,18 @@ func (t *freezerTable) truncateHead(items uint64) error { if existing <= items { return nil } - if items < t.itemHidden.Load() { + + hidden := t.itemHidden.Load() + + if items < hidden { + if existing == hidden { + // Empty table means that it is newly added. Its tail would be + // at the head, so we have to align the table down to the new head. + return t.resetTo(items) + } return errors.New("truncation below tail") } + // We need to truncate, save the old size for metrics tracking oldSize, err := t.sizeNolock() if err != nil { From b6e2f13a9a381b1700722f0432dddb785340c726 Mon Sep 17 00:00:00 2001 From: cui Date: Wed, 1 Jul 2026 10:33:45 +0800 Subject: [PATCH 11/44] accounts/keystore: fix negative timezone sign in toISO8601 (#35245) --- accounts/keystore/key.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 9b2ac14712..34712cb842 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -230,7 +230,7 @@ func toISO8601(t time.Time) string { if name == "UTC" { tz = "Z" } else { - tz = fmt.Sprintf("%03d00", offset/3600) + tz = fmt.Sprintf("%+03d00", offset/3600) } return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) From dd672c6867b4a5f38a90537dbbe237a06d31d048 Mon Sep 17 00:00:00 2001 From: cui Date: Wed, 1 Jul 2026 10:34:08 +0800 Subject: [PATCH 12/44] eth: prealloc seen map in handleTransactions (#35237) --- eth/handler_eth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eth/handler_eth.go b/eth/handler_eth.go index 8704a86af4..7922687e0c 100644 --- a/eth/handler_eth.go +++ b/eth/handler_eth.go @@ -89,7 +89,7 @@ func (h *ethHandler) Handle(peer *eth.Peer, packet eth.Packet) error { // handleTransactions marks all given transactions as known to the peer // and performs basic validations. func handleTransactions(peer *eth.Peer, list []*types.Transaction, directBroadcast bool) error { - seen := make(map[common.Hash]struct{}) + seen := make(map[common.Hash]struct{}, len(list)) for _, tx := range list { if tx.Type() == types.BlobTxType { if directBroadcast { From 769c8c52c56b4faea77bc2aeb34a6abfa24cd9f0 Mon Sep 17 00:00:00 2001 From: cui Date: Wed, 1 Jul 2026 15:40:01 +0800 Subject: [PATCH 13/44] internal/ethapi: skip transfer capture for callcode in log tracer (#35248) --- internal/ethapi/logtracer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ethapi/logtracer.go b/internal/ethapi/logtracer.go index 54d2d653ea..0b7c27079a 100644 --- a/internal/ethapi/logtracer.go +++ b/internal/ethapi/logtracer.go @@ -80,7 +80,7 @@ func (t *tracer) Hooks() *tracing.Hooks { func (t *tracer) onEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { t.logs = append(t.logs, make([]*types.Log, 0)) - if vm.OpCode(typ) != vm.DELEGATECALL && value != nil && value.Cmp(common.Big0) > 0 { + if vm.OpCode(typ) != vm.DELEGATECALL && vm.OpCode(typ) != vm.CALLCODE && value != nil && value.Cmp(common.Big0) > 0 { t.captureTransfer(from, to, value) } } From 00a773dad7ad7a6be566872d852500e2e51df2e4 Mon Sep 17 00:00:00 2001 From: m1lestones <91078895+m1lestones@users.noreply.github.com> Date: Wed, 1 Jul 2026 03:40:46 -0400 Subject: [PATCH 14/44] accounts/abi/abigen: add GetABI() method to abigen v2 generated bindings (#35221) --- accounts/abi/abigen/source2.go.tpl | 5 +++ .../abigen/testdata/v2/callbackparam.go.txt | 5 +++ .../abi/abigen/testdata/v2/crowdsale.go.txt | 5 +++ accounts/abi/abigen/testdata/v2/dao.go.txt | 5 +++ .../testdata/v2/deeplynestedarray.go.txt | 5 +++ accounts/abi/abigen/testdata/v2/empty.go.txt | 5 +++ .../abigen/testdata/v2/eventchecker.go.txt | 5 +++ accounts/abi/abigen/testdata/v2/getter.go.txt | 5 +++ .../testdata/v2/identifiercollision.go.txt | 5 +++ .../abigen/testdata/v2/inputchecker.go.txt | 5 +++ .../abi/abigen/testdata/v2/interactor.go.txt | 5 +++ .../abigen/testdata/v2/nameconflict.go.txt | 5 +++ .../testdata/v2/numericmethodname.go.txt | 5 +++ .../abigen/testdata/v2/outputchecker.go.txt | 5 +++ .../abi/abigen/testdata/v2/overload.go.txt | 5 +++ .../abigen/testdata/v2/rangekeyword.go.txt | 5 +++ accounts/abi/abigen/testdata/v2/slicer.go.txt | 5 +++ .../abi/abigen/testdata/v2/structs.go.txt | 5 +++ accounts/abi/abigen/testdata/v2/token.go.txt | 5 +++ accounts/abi/abigen/testdata/v2/tuple.go.txt | 5 +++ accounts/abi/abigen/testdata/v2/tupler.go.txt | 5 +++ .../abi/abigen/testdata/v2/underscorer.go.txt | 5 +++ .../bind/v2/internal/contracts/db/bindings.go | 5 +++ .../v2/internal/contracts/events/bindings.go | 5 +++ .../contracts/nested_libraries/bindings.go | 40 +++++++++++++++++++ .../contracts/solc_errors/bindings.go | 10 +++++ .../contracts/uint256arrayreturn/bindings.go | 5 +++ 27 files changed, 175 insertions(+) diff --git a/accounts/abi/abigen/source2.go.tpl b/accounts/abi/abigen/source2.go.tpl index c517caf6f4..9285497511 100644 --- a/accounts/abi/abigen/source2.go.tpl +++ b/accounts/abi/abigen/source2.go.tpl @@ -59,6 +59,11 @@ var ( abi abi.ABI } + // GetABI returns the ABI associated with this contract binding. + func (c *{{.Type}}) GetABI() abi.ABI { + return c.abi + } + // New{{.Type}} creates a new instance of {{.Type}}. func New{{.Type}}() *{{.Type}} { parsed, err := {{.Type}}MetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/callbackparam.go.txt b/accounts/abi/abigen/testdata/v2/callbackparam.go.txt index d22b978486..a01b125acf 100644 --- a/accounts/abi/abigen/testdata/v2/callbackparam.go.txt +++ b/accounts/abi/abigen/testdata/v2/callbackparam.go.txt @@ -36,6 +36,11 @@ type CallbackParam struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *CallbackParam) GetABI() abi.ABI { + return c.abi +} + // NewCallbackParam creates a new instance of CallbackParam. func NewCallbackParam() *CallbackParam { parsed, err := CallbackParamMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/crowdsale.go.txt b/accounts/abi/abigen/testdata/v2/crowdsale.go.txt index f0bba246ab..66582aa5c6 100644 --- a/accounts/abi/abigen/testdata/v2/crowdsale.go.txt +++ b/accounts/abi/abigen/testdata/v2/crowdsale.go.txt @@ -36,6 +36,11 @@ type Crowdsale struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Crowdsale) GetABI() abi.ABI { + return c.abi +} + // NewCrowdsale creates a new instance of Crowdsale. func NewCrowdsale() *Crowdsale { parsed, err := CrowdsaleMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/dao.go.txt b/accounts/abi/abigen/testdata/v2/dao.go.txt index 0e9adba31e..a810c65bd9 100644 --- a/accounts/abi/abigen/testdata/v2/dao.go.txt +++ b/accounts/abi/abigen/testdata/v2/dao.go.txt @@ -36,6 +36,11 @@ type DAO struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *DAO) GetABI() abi.ABI { + return c.abi +} + // NewDAO creates a new instance of DAO. func NewDAO() *DAO { parsed, err := DAOMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/deeplynestedarray.go.txt b/accounts/abi/abigen/testdata/v2/deeplynestedarray.go.txt index 302f1d736f..559ea83422 100644 --- a/accounts/abi/abigen/testdata/v2/deeplynestedarray.go.txt +++ b/accounts/abi/abigen/testdata/v2/deeplynestedarray.go.txt @@ -36,6 +36,11 @@ type DeeplyNestedArray struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *DeeplyNestedArray) GetABI() abi.ABI { + return c.abi +} + // NewDeeplyNestedArray creates a new instance of DeeplyNestedArray. func NewDeeplyNestedArray() *DeeplyNestedArray { parsed, err := DeeplyNestedArrayMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/empty.go.txt b/accounts/abi/abigen/testdata/v2/empty.go.txt index 7082e20799..7deea15570 100644 --- a/accounts/abi/abigen/testdata/v2/empty.go.txt +++ b/accounts/abi/abigen/testdata/v2/empty.go.txt @@ -36,6 +36,11 @@ type Empty struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Empty) GetABI() abi.ABI { + return c.abi +} + // NewEmpty creates a new instance of Empty. func NewEmpty() *Empty { parsed, err := EmptyMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/eventchecker.go.txt b/accounts/abi/abigen/testdata/v2/eventchecker.go.txt index d0600d7c3e..213ab609f5 100644 --- a/accounts/abi/abigen/testdata/v2/eventchecker.go.txt +++ b/accounts/abi/abigen/testdata/v2/eventchecker.go.txt @@ -35,6 +35,11 @@ type EventChecker struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *EventChecker) GetABI() abi.ABI { + return c.abi +} + // NewEventChecker creates a new instance of EventChecker. func NewEventChecker() *EventChecker { parsed, err := EventCheckerMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/getter.go.txt b/accounts/abi/abigen/testdata/v2/getter.go.txt index 69a4fb54c8..69267767c2 100644 --- a/accounts/abi/abigen/testdata/v2/getter.go.txt +++ b/accounts/abi/abigen/testdata/v2/getter.go.txt @@ -36,6 +36,11 @@ type Getter struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Getter) GetABI() abi.ABI { + return c.abi +} + // NewGetter creates a new instance of Getter. func NewGetter() *Getter { parsed, err := GetterMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/identifiercollision.go.txt b/accounts/abi/abigen/testdata/v2/identifiercollision.go.txt index e7301521f4..1d14a7109d 100644 --- a/accounts/abi/abigen/testdata/v2/identifiercollision.go.txt +++ b/accounts/abi/abigen/testdata/v2/identifiercollision.go.txt @@ -36,6 +36,11 @@ type IdentifierCollision struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *IdentifierCollision) GetABI() abi.ABI { + return c.abi +} + // NewIdentifierCollision creates a new instance of IdentifierCollision. func NewIdentifierCollision() *IdentifierCollision { parsed, err := IdentifierCollisionMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/inputchecker.go.txt b/accounts/abi/abigen/testdata/v2/inputchecker.go.txt index 1743f336db..4522aea52a 100644 --- a/accounts/abi/abigen/testdata/v2/inputchecker.go.txt +++ b/accounts/abi/abigen/testdata/v2/inputchecker.go.txt @@ -35,6 +35,11 @@ type InputChecker struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *InputChecker) GetABI() abi.ABI { + return c.abi +} + // NewInputChecker creates a new instance of InputChecker. func NewInputChecker() *InputChecker { parsed, err := InputCheckerMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/interactor.go.txt b/accounts/abi/abigen/testdata/v2/interactor.go.txt index f33c95b639..3e88b4a521 100644 --- a/accounts/abi/abigen/testdata/v2/interactor.go.txt +++ b/accounts/abi/abigen/testdata/v2/interactor.go.txt @@ -36,6 +36,11 @@ type Interactor struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Interactor) GetABI() abi.ABI { + return c.abi +} + // NewInteractor creates a new instance of Interactor. func NewInteractor() *Interactor { parsed, err := InteractorMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/nameconflict.go.txt b/accounts/abi/abigen/testdata/v2/nameconflict.go.txt index 5e4a9ecaf0..13f96527ee 100644 --- a/accounts/abi/abigen/testdata/v2/nameconflict.go.txt +++ b/accounts/abi/abigen/testdata/v2/nameconflict.go.txt @@ -42,6 +42,11 @@ type NameConflict struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *NameConflict) GetABI() abi.ABI { + return c.abi +} + // NewNameConflict creates a new instance of NameConflict. func NewNameConflict() *NameConflict { parsed, err := NameConflictMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt b/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt index 0af31a1cfb..4f5392f19d 100644 --- a/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt +++ b/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt @@ -36,6 +36,11 @@ type NumericMethodName struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *NumericMethodName) GetABI() abi.ABI { + return c.abi +} + // NewNumericMethodName creates a new instance of NumericMethodName. func NewNumericMethodName() *NumericMethodName { parsed, err := NumericMethodNameMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/outputchecker.go.txt b/accounts/abi/abigen/testdata/v2/outputchecker.go.txt index f1c98f536e..8d3e475e98 100644 --- a/accounts/abi/abigen/testdata/v2/outputchecker.go.txt +++ b/accounts/abi/abigen/testdata/v2/outputchecker.go.txt @@ -35,6 +35,11 @@ type OutputChecker struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *OutputChecker) GetABI() abi.ABI { + return c.abi +} + // NewOutputChecker creates a new instance of OutputChecker. func NewOutputChecker() *OutputChecker { parsed, err := OutputCheckerMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/overload.go.txt b/accounts/abi/abigen/testdata/v2/overload.go.txt index 563edf7842..006a2c7c3c 100644 --- a/accounts/abi/abigen/testdata/v2/overload.go.txt +++ b/accounts/abi/abigen/testdata/v2/overload.go.txt @@ -36,6 +36,11 @@ type Overload struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Overload) GetABI() abi.ABI { + return c.abi +} + // NewOverload creates a new instance of Overload. func NewOverload() *Overload { parsed, err := OverloadMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/rangekeyword.go.txt b/accounts/abi/abigen/testdata/v2/rangekeyword.go.txt index 296de1fccc..76c5d72d29 100644 --- a/accounts/abi/abigen/testdata/v2/rangekeyword.go.txt +++ b/accounts/abi/abigen/testdata/v2/rangekeyword.go.txt @@ -36,6 +36,11 @@ type RangeKeyword struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *RangeKeyword) GetABI() abi.ABI { + return c.abi +} + // NewRangeKeyword creates a new instance of RangeKeyword. func NewRangeKeyword() *RangeKeyword { parsed, err := RangeKeywordMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/slicer.go.txt b/accounts/abi/abigen/testdata/v2/slicer.go.txt index 379f136453..416a629cb7 100644 --- a/accounts/abi/abigen/testdata/v2/slicer.go.txt +++ b/accounts/abi/abigen/testdata/v2/slicer.go.txt @@ -36,6 +36,11 @@ type Slicer struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Slicer) GetABI() abi.ABI { + return c.abi +} + // NewSlicer creates a new instance of Slicer. func NewSlicer() *Slicer { parsed, err := SlicerMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/structs.go.txt b/accounts/abi/abigen/testdata/v2/structs.go.txt index 4d8864cf6e..ee55ec5620 100644 --- a/accounts/abi/abigen/testdata/v2/structs.go.txt +++ b/accounts/abi/abigen/testdata/v2/structs.go.txt @@ -41,6 +41,11 @@ type Structs struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Structs) GetABI() abi.ABI { + return c.abi +} + // NewStructs creates a new instance of Structs. func NewStructs() *Structs { parsed, err := StructsMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/token.go.txt b/accounts/abi/abigen/testdata/v2/token.go.txt index 3bd60a6cdd..1bd364a918 100644 --- a/accounts/abi/abigen/testdata/v2/token.go.txt +++ b/accounts/abi/abigen/testdata/v2/token.go.txt @@ -36,6 +36,11 @@ type Token struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Token) GetABI() abi.ABI { + return c.abi +} + // NewToken creates a new instance of Token. func NewToken() *Token { parsed, err := TokenMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/tuple.go.txt b/accounts/abi/abigen/testdata/v2/tuple.go.txt index 10b634f3db..6aabdeb0fe 100644 --- a/accounts/abi/abigen/testdata/v2/tuple.go.txt +++ b/accounts/abi/abigen/testdata/v2/tuple.go.txt @@ -61,6 +61,11 @@ type Tuple struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Tuple) GetABI() abi.ABI { + return c.abi +} + // NewTuple creates a new instance of Tuple. func NewTuple() *Tuple { parsed, err := TupleMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/tupler.go.txt b/accounts/abi/abigen/testdata/v2/tupler.go.txt index 8643487042..5e6eafb07e 100644 --- a/accounts/abi/abigen/testdata/v2/tupler.go.txt +++ b/accounts/abi/abigen/testdata/v2/tupler.go.txt @@ -36,6 +36,11 @@ type Tupler struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Tupler) GetABI() abi.ABI { + return c.abi +} + // NewTupler creates a new instance of Tupler. func NewTupler() *Tupler { parsed, err := TuplerMetaData.ParseABI() diff --git a/accounts/abi/abigen/testdata/v2/underscorer.go.txt b/accounts/abi/abigen/testdata/v2/underscorer.go.txt index 13ec968508..abe3bdbacb 100644 --- a/accounts/abi/abigen/testdata/v2/underscorer.go.txt +++ b/accounts/abi/abigen/testdata/v2/underscorer.go.txt @@ -36,6 +36,11 @@ type Underscorer struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *Underscorer) GetABI() abi.ABI { + return c.abi +} + // NewUnderscorer creates a new instance of Underscorer. func NewUnderscorer() *Underscorer { parsed, err := UnderscorerMetaData.ParseABI() diff --git a/accounts/abi/bind/v2/internal/contracts/db/bindings.go b/accounts/abi/bind/v2/internal/contracts/db/bindings.go index 2fc57fba6d..b9a7e3164a 100644 --- a/accounts/abi/bind/v2/internal/contracts/db/bindings.go +++ b/accounts/abi/bind/v2/internal/contracts/db/bindings.go @@ -43,6 +43,11 @@ type DB struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *DB) GetABI() abi.ABI { + return c.abi +} + // NewDB creates a new instance of DB. func NewDB() *DB { parsed, err := DBMetaData.ParseABI() diff --git a/accounts/abi/bind/v2/internal/contracts/events/bindings.go b/accounts/abi/bind/v2/internal/contracts/events/bindings.go index 2eb5751f23..7acd312b65 100644 --- a/accounts/abi/bind/v2/internal/contracts/events/bindings.go +++ b/accounts/abi/bind/v2/internal/contracts/events/bindings.go @@ -36,6 +36,11 @@ type C struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *C) GetABI() abi.ABI { + return c.abi +} + // NewC creates a new instance of C. func NewC() *C { parsed, err := CMetaData.ParseABI() diff --git a/accounts/abi/bind/v2/internal/contracts/nested_libraries/bindings.go b/accounts/abi/bind/v2/internal/contracts/nested_libraries/bindings.go index d1cb08116b..406d71192f 100644 --- a/accounts/abi/bind/v2/internal/contracts/nested_libraries/bindings.go +++ b/accounts/abi/bind/v2/internal/contracts/nested_libraries/bindings.go @@ -40,6 +40,11 @@ type C1 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *C1) GetABI() abi.ABI { + return c.abi +} + // NewC1 creates a new instance of C1. func NewC1() *C1 { parsed, err := C1MetaData.ParseABI() @@ -118,6 +123,11 @@ type C2 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *C2) GetABI() abi.ABI { + return c.abi +} + // NewC2 creates a new instance of C2. func NewC2() *C2 { parsed, err := C2MetaData.ParseABI() @@ -192,6 +202,11 @@ type L1 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L1) GetABI() abi.ABI { + return c.abi +} + // NewL1 creates a new instance of L1. func NewL1() *L1 { parsed, err := L1MetaData.ParseABI() @@ -257,6 +272,11 @@ type L2 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L2) GetABI() abi.ABI { + return c.abi +} + // NewL2 creates a new instance of L2. func NewL2() *L2 { parsed, err := L2MetaData.ParseABI() @@ -322,6 +342,11 @@ type L2b struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L2b) GetABI() abi.ABI { + return c.abi +} + // NewL2b creates a new instance of L2b. func NewL2b() *L2b { parsed, err := L2bMetaData.ParseABI() @@ -384,6 +409,11 @@ type L3 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L3) GetABI() abi.ABI { + return c.abi +} + // NewL3 creates a new instance of L3. func NewL3() *L3 { parsed, err := L3MetaData.ParseABI() @@ -450,6 +480,11 @@ type L4 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L4) GetABI() abi.ABI { + return c.abi +} + // NewL4 creates a new instance of L4. func NewL4() *L4 { parsed, err := L4MetaData.ParseABI() @@ -515,6 +550,11 @@ type L4b struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *L4b) GetABI() abi.ABI { + return c.abi +} + // NewL4b creates a new instance of L4b. func NewL4b() *L4b { parsed, err := L4bMetaData.ParseABI() diff --git a/accounts/abi/bind/v2/internal/contracts/solc_errors/bindings.go b/accounts/abi/bind/v2/internal/contracts/solc_errors/bindings.go index 627b86f1b9..12d8b7f3e9 100644 --- a/accounts/abi/bind/v2/internal/contracts/solc_errors/bindings.go +++ b/accounts/abi/bind/v2/internal/contracts/solc_errors/bindings.go @@ -36,6 +36,11 @@ type C struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *C) GetABI() abi.ABI { + return c.abi +} + // NewC creates a new instance of C. func NewC() *C { parsed, err := CMetaData.ParseABI() @@ -173,6 +178,11 @@ type C2 struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *C2) GetABI() abi.ABI { + return c.abi +} + // NewC2 creates a new instance of C2. func NewC2() *C2 { parsed, err := C2MetaData.ParseABI() diff --git a/accounts/abi/bind/v2/internal/contracts/uint256arrayreturn/bindings.go b/accounts/abi/bind/v2/internal/contracts/uint256arrayreturn/bindings.go index 19d09bdd6a..f06788633c 100644 --- a/accounts/abi/bind/v2/internal/contracts/uint256arrayreturn/bindings.go +++ b/accounts/abi/bind/v2/internal/contracts/uint256arrayreturn/bindings.go @@ -36,6 +36,11 @@ type MyContract struct { abi abi.ABI } +// GetABI returns the ABI associated with this contract binding. +func (c *MyContract) GetABI() abi.ABI { + return c.abi +} + // NewMyContract creates a new instance of MyContract. func NewMyContract() *MyContract { parsed, err := MyContractMetaData.ParseABI() From ea242145cff75f9beee6f16701dbac494fbbff94 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 1 Jul 2026 16:48:13 +0800 Subject: [PATCH 15/44] core: implement EIP-7928 spec change (#35260) https://github.com/ethereum/EIPs/pull/11838/changes --- core/bal_test.go | 67 +++++++++++++++++++++++++++++++++++++++ core/vm/operations_acl.go | 16 ++++++++++ 2 files changed, 83 insertions(+) diff --git a/core/bal_test.go b/core/bal_test.go index 84579ca564..9409ff32d1 100644 --- a/core/bal_test.go +++ b/core/bal_test.go @@ -518,6 +518,73 @@ func TestBALStaticCallTargetIncluded(t *testing.T) { assertEmpty(t, assertPresent(t, b, target)) } +// makeValueCaller emits a single value-transferring CALL-family op (CALL 0xf1 +// or CALLCODE 0xf2) against `target` with value=1, then STOPs. Used together +// with a zero-balance caller to make the value transfer fail CanTransfer. +func makeValueCaller(op byte, target common.Address) []byte { + code := []byte{ + 0x60, 0x00, // retSize + 0x60, 0x00, // retOff + 0x60, 0x00, // argsSize + 0x60, 0x00, // argsOff + 0x60, 0x01, // value = 1 + 0x73, // PUSH20 target + } + code = append(code, target.Bytes()...) + return append(code, 0x5a, op, 0x50, 0x00) // GAS, op, POP, STOP +} + +// TestBALCallToDelegatedTargetBalanceFail asserts the EIP-7928 rule revised in +// ethereum/EIPs#11838: when a CALL targets an EIP-7702 delegated account and the +// delegated address passes its access_cost gas check, the delegated +// (implementation) address MUST appear in the BAL even when the call then fails +// its sender-balance check, because the delegation is resolved before that +// check. CALL routes through the EIP-8037 gas path. +func TestBALCallToDelegatedTargetBalanceFail(t *testing.T) { + delegated := common.HexToAddress("0xde1e9a7ed") // EOA carrying a 7702 designator + impl := common.HexToAddress("0x111111") // delegation target (implementation) + caller := common.HexToAddress("0xca11") // zero-balance contract issuing the CALL + + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeValueCaller(0xf1 /* CALL */, delegated), Balance: common.Big0}, + delegated: {Code: types.AddressToDelegation(impl), Balance: common.Big0}, + impl: {Code: []byte{0x00}, Balance: common.Big0}, // STOP + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertPresent(t, b, delegated) + // The call failed its sender-balance check, so the implementation never + // executed: it is recorded with an empty change set, but it MUST be present. + assertEmpty(t, assertPresent(t, b, impl)) +} + +// TestBALCallCodeToDelegatedTargetBalanceFail is the CALLCODE analogue of +// TestBALCallToDelegatedTargetBalanceFail, exercising the EIP-7702 gas path +// (CALLCODE/STATICCALL/DELEGATECALL) rather than the EIP-8037 one. +func TestBALCallCodeToDelegatedTargetBalanceFail(t *testing.T) { + delegated := common.HexToAddress("0xde1e9a7ed") + impl := common.HexToAddress("0x111111") + caller := common.HexToAddress("0xca11") + + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeValueCaller(0xf2 /* CALLCODE */, delegated), Balance: common.Big0}, + delegated: {Code: types.AddressToDelegation(impl), Balance: common.Big0}, + impl: {Code: []byte{0x00}, Balance: common.Big0}, // STOP + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertPresent(t, b, delegated) + assertEmpty(t, assertPresent(t, b, impl)) +} + // ============================== Revert behaviour ============================== // TestBALRevertedTxStillIncluded: a tx whose top-level call REVERTs still diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 8b2a627fef..e07eba451f 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -257,6 +257,14 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { return gasFunc } +// recordDelegationAccess records the EIP-7702 delegated target in the block +// access list (EIP-7928). +func recordDelegationAccess(evm *EVM, target common.Address) { + if evm.chainRules.IsAmsterdam { + evm.StateDB.GetCode(target) + } +} + var ( innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic) gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic) @@ -336,6 +344,10 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } + // The delegated address has passed its gas check; record it in the + // block access list now, before the call's sender-balance and + // call-stack-depth checks. + recordDelegationAccess(evm, target) } // Calculate the gas budget for the nested call. The costs defined by // EIP-2929 and EIP-7702 have already been applied. @@ -416,6 +428,10 @@ func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stat if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } + // The delegated address has passed its gas check; record it in the + // block access list now, before the call's sender-balance and + // call-stack-depth checks. + recordDelegationAccess(evm, target) } // Compute and charge state gas (new account creation) AFTER regular gas. From 0fbad29b943a90423289fb74fee236b6ea6d4388 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Wed, 1 Jul 2026 10:51:05 +0200 Subject: [PATCH 16/44] params: remove named hardforks from bpo schedule (#35029) --- consensus/misc/eip4844/eip4844.go | 2 - consensus/misc/eip4844/eip4844_test.go | 3 +- core/bal_test.go | 3 - core/bintrie_witness_test.go | 3 +- core/eth_transfer_logs_test.go | 4 -- core/genesis_test.go | 2 - core/txpool/blobpool/blobpool_test.go | 5 -- core/txpool/blobpool/cache_test.go | 2 +- eth/catalyst/api_benchmark_test.go | 1 - miner/stress/main.go | 5 +- params/config.go | 85 ++++++++++++-------------- tests/init.go | 25 ++------ 12 files changed, 52 insertions(+), 88 deletions(-) diff --git a/consensus/misc/eip4844/eip4844.go b/consensus/misc/eip4844/eip4844.go index 2ebf4f7155..add85092c2 100644 --- a/consensus/misc/eip4844/eip4844.go +++ b/consensus/misc/eip4844/eip4844.go @@ -73,8 +73,6 @@ func latestBlobConfig(cfg *params.ChainConfig, time uint64) (BlobConfig, error) bc = s.BPO2 case cfg.IsBPO1(london, time) && s.BPO1 != nil: bc = s.BPO1 - case cfg.IsOsaka(london, time) && s.Osaka != nil: - bc = s.Osaka case cfg.IsPrague(london, time) && s.Prague != nil: bc = s.Prague case cfg.IsCancun(london, time) && s.Cancun != nil: diff --git a/consensus/misc/eip4844/eip4844_test.go b/consensus/misc/eip4844/eip4844_test.go index 35934370af..4aa49c9cb1 100644 --- a/consensus/misc/eip4844/eip4844_test.go +++ b/consensus/misc/eip4844/eip4844_test.go @@ -120,7 +120,6 @@ func TestCalcBlobFeePostOsaka(t *testing.T) { BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: ¶ms.BlobConfig{ Target: 9, Max: 14, @@ -191,7 +190,7 @@ func TestFakeExponential(t *testing.T) { func TestCalcExcessBlobGasEIP7918(t *testing.T) { var ( cfg = params.MergedTestChainConfig - targetBlobs = cfg.BlobScheduleConfig.Osaka.Target + targetBlobs = cfg.BlobScheduleConfig.Prague.Target blobGasTarget = uint64(targetBlobs) * params.BlobTxBlobGasPerBlob ) diff --git a/core/bal_test.go b/core/bal_test.go index 9409ff32d1..64c2714b16 100644 --- a/core/bal_test.go +++ b/core/bal_test.go @@ -42,9 +42,6 @@ import ( func balChainConfig() *params.ChainConfig { cfg := *params.MergedTestChainConfig cfg.AmsterdamTime = new(uint64) - blob := *cfg.BlobScheduleConfig - blob.Amsterdam = blob.Osaka - cfg.BlobScheduleConfig = &blob return &cfg } diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go index 2cf7972cf8..9cbb489c4e 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -55,8 +55,9 @@ var ( UBTTime: u64(0), TerminalTotalDifficulty: common.Big0, EnableUBTAtGenesis: true, + // UBT inherits its blob schedule; nothing to declare here. BlobScheduleConfig: ¶ms.BlobScheduleConfig{ - UBT: params.DefaultPragueBlobConfig, + Prague: params.DefaultPragueBlobConfig, }, } ) diff --git a/core/eth_transfer_logs_test.go b/core/eth_transfer_logs_test.go index 815b56b588..a5a5f1894a 100644 --- a/core/eth_transfer_logs_test.go +++ b/core/eth_transfer_logs_test.go @@ -78,11 +78,7 @@ func testEthTransferLogs(t *testing.T, value uint64) { engine = beacon.New(ethash.NewFaker()) ) - //TODO remove this hacky config initialization when final Amsterdam config is available config.AmsterdamTime = new(uint64) - blobConfig := *config.BlobScheduleConfig - blobConfig.Amsterdam = blobConfig.Osaka - config.BlobScheduleConfig = &blobConfig gspec := &Genesis{ Config: &config, diff --git a/core/genesis_test.go b/core/genesis_test.go index 67b88fc415..80ee53493e 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -293,8 +293,6 @@ func TestBinaryGenesisCommit(t *testing.T) { BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, - UBT: params.DefaultPragueBlobConfig, }, } diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index 0e182947f0..a2ec06bed4 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -1215,11 +1215,6 @@ func TestBillyMigration(t *testing.T) { Max: maxBlobs, UpdateFraction: params.DefaultCancunBlobConfig.UpdateFraction, }, - Osaka: ¶ms.BlobConfig{ - Target: maxBlobs / 2, - Max: maxBlobs, - UpdateFraction: params.DefaultCancunBlobConfig.UpdateFraction, - }, }, } chain := &testBlockChain{ diff --git a/core/txpool/blobpool/cache_test.go b/core/txpool/blobpool/cache_test.go index da13cda4b2..3a0436ab85 100644 --- a/core/txpool/blobpool/cache_test.go +++ b/core/txpool/blobpool/cache_test.go @@ -94,7 +94,7 @@ func newTestCache(t *testing.T, txConfig []txSpec) *testCache { CancunTime: &cancunTime, OsakaTime: &cancunTime, BlobScheduleConfig: ¶ms.BlobScheduleConfig{ - Osaka: ¶ms.BlobConfig{ + Cancun: ¶ms.BlobConfig{ Target: 1, Max: 1, UpdateFraction: params.DefaultCancunBlobConfig.UpdateFraction, diff --git a/eth/catalyst/api_benchmark_test.go b/eth/catalyst/api_benchmark_test.go index ee0a0a4888..6d6ad59f7f 100644 --- a/eth/catalyst/api_benchmark_test.go +++ b/eth/catalyst/api_benchmark_test.go @@ -197,7 +197,6 @@ func newBenchmarkBlobEnv(b *testing.B, blobCount int, version byte, fork benchFo config.BlobScheduleConfig = ¶ms.BlobScheduleConfig{ Cancun: ¶ms.BlobConfig{Target: 6, Max: 128, UpdateFraction: 3338477}, Prague: ¶ms.BlobConfig{Target: 6, Max: 128, UpdateFraction: 5007716}, - Osaka: ¶ms.BlobConfig{Target: 6, Max: 128, UpdateFraction: 5007716}, } // Configure fork times based on requested fork switch fork { diff --git a/miner/stress/main.go b/miner/stress/main.go index aaf0993c37..7a8436b01e 100644 --- a/miner/stress/main.go +++ b/miner/stress/main.go @@ -138,7 +138,10 @@ func makeGenesis(faucets []*ecdsa.PrivateKey) *core.Genesis { blockZero := uint64(0) config.AmsterdamTime = &blockZero - config.BlobScheduleConfig.Amsterdam = ¶ms.BlobConfig{ + // Amsterdam inherits its blob schedule from the most recent BPO; activate BPO1 + // at the same time with the target/max we want exercised by the stress harness. + config.BPO1Time = &blockZero + config.BlobScheduleConfig.BPO1 = ¶ms.BlobConfig{ Target: 14, Max: 21, UpdateFraction: 13739630, diff --git a/params/config.go b/params/config.go index 1d69335f97..b277fb303c 100644 --- a/params/config.go +++ b/params/config.go @@ -69,7 +69,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, BPO1: DefaultBPO1BlobConfig, BPO2: DefaultBPO2BlobConfig, }, @@ -105,7 +104,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, BPO1: DefaultBPO1BlobConfig, BPO2: DefaultBPO2BlobConfig, }, @@ -141,7 +139,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, BPO1: DefaultBPO1BlobConfig, BPO2: DefaultBPO2BlobConfig, }, @@ -177,7 +174,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, BPO1: DefaultBPO1BlobConfig, BPO2: DefaultBPO2BlobConfig, }, @@ -235,7 +231,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, }, } @@ -330,7 +325,6 @@ var ( BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, }, } @@ -379,12 +373,6 @@ var ( Max: 9, UpdateFraction: 5007716, } - // DefaultOsakaBlobConfig is the default blob configuration for the Osaka fork. - DefaultOsakaBlobConfig = &BlobConfig{ - Target: 6, - Max: 9, - UpdateFraction: 5007716, - } // DefaultBPO1BlobConfig is the default blob configuration for the BPO1 fork. DefaultBPO1BlobConfig = &BlobConfig{ Target: 10, @@ -413,7 +401,6 @@ var ( DefaultBlobSchedule = &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, Prague: DefaultPragueBlobConfig, - Osaka: DefaultOsakaBlobConfig, } ) @@ -670,7 +657,7 @@ func (c *ChainConfig) Description() string { banner += fmt.Sprintf(" - Prague: @%-10v blob: (%s)\n", *c.PragueTime, c.BlobScheduleConfig.Prague) } if c.OsakaTime != nil { - banner += fmt.Sprintf(" - Osaka: @%-10v blob: (%s)\n", *c.OsakaTime, c.BlobScheduleConfig.Osaka) + banner += fmt.Sprintf(" - Osaka: @%-10v\n", *c.OsakaTime) } if c.BPO1Time != nil { banner += fmt.Sprintf(" - BPO1: @%-10v blob: (%s)\n", *c.BPO1Time, c.BlobScheduleConfig.BPO1) @@ -688,10 +675,10 @@ func (c *ChainConfig) Description() string { banner += fmt.Sprintf(" - BPO5: @%-10v blob: (%s)\n", *c.BPO5Time, c.BlobScheduleConfig.BPO5) } if c.AmsterdamTime != nil { - banner += fmt.Sprintf(" - Amsterdam: @%-10v blob: (%s)\n", *c.AmsterdamTime, c.BlobScheduleConfig.Amsterdam) + banner += fmt.Sprintf(" - Amsterdam: @%-10v\n", *c.AmsterdamTime) } if c.UBTTime != nil { - banner += fmt.Sprintf(" - UBT: @%-10v blob: (%s)\n", *c.UBTTime, c.BlobScheduleConfig.UBT) + banner += fmt.Sprintf(" - UBT: @%-10v\n", *c.UBTTime) } banner += fmt.Sprintf("\nAll fork specifications can be found at https://ethereum.github.io/execution-specs/src/ethereum/forks/\n") return banner @@ -713,17 +700,18 @@ func (bc *BlobConfig) String() string { } // BlobScheduleConfig determines target and max number of blobs allow per fork. +// +// From Prague onward, the blob schedule is updated only at BPO (Blob Parameter-Only) +// forks. Named forks such as Osaka or Amsterdam inherit the most recently configured +// BPO entry and must not declare their own BlobConfig. type BlobScheduleConfig struct { - Cancun *BlobConfig `json:"cancun,omitempty"` - Prague *BlobConfig `json:"prague,omitempty"` - Osaka *BlobConfig `json:"osaka,omitempty"` - BPO1 *BlobConfig `json:"bpo1,omitempty"` - BPO2 *BlobConfig `json:"bpo2,omitempty"` - BPO3 *BlobConfig `json:"bpo3,omitempty"` - BPO4 *BlobConfig `json:"bpo4,omitempty"` - BPO5 *BlobConfig `json:"bpo5,omitempty"` - Amsterdam *BlobConfig `json:"amsterdam,omitempty"` - UBT *BlobConfig `json:"ubt,omitempty"` + Cancun *BlobConfig `json:"cancun,omitempty"` + Prague *BlobConfig `json:"prague,omitempty"` + BPO1 *BlobConfig `json:"bpo1,omitempty"` + BPO2 *BlobConfig `json:"bpo2,omitempty"` + BPO3 *BlobConfig `json:"bpo3,omitempty"` + BPO4 *BlobConfig `json:"bpo4,omitempty"` + BPO5 *BlobConfig `json:"bpo5,omitempty"` } // IsHomestead returns whether num is either equal to the homestead block or greater. @@ -1000,13 +988,11 @@ func (c *ChainConfig) CheckConfigForkOrder() error { }{ {name: "cancun", timestamp: c.CancunTime, config: bsc.Cancun}, {name: "prague", timestamp: c.PragueTime, config: bsc.Prague}, - {name: "osaka", timestamp: c.OsakaTime, config: bsc.Osaka}, {name: "bpo1", timestamp: c.BPO1Time, config: bsc.BPO1}, {name: "bpo2", timestamp: c.BPO2Time, config: bsc.BPO2}, {name: "bpo3", timestamp: c.BPO3Time, config: bsc.BPO3}, {name: "bpo4", timestamp: c.BPO4Time, config: bsc.BPO4}, {name: "bpo5", timestamp: c.BPO5Time, config: bsc.BPO5}, - {name: "amsterdam", timestamp: c.AmsterdamTime, config: bsc.Amsterdam}, } { if cur.config != nil { if err := cur.config.validate(); err != nil { @@ -1169,28 +1155,33 @@ func (c *ChainConfig) LatestFork(time uint64) forks.Fork { } } -// BlobConfig returns the blob config associated with the provided fork. +// BlobConfig returns the blob config active at the provided fork. Since named +// forks (Osaka, Amsterdam, ...) no longer carry their own blob schedule, the +// lookup walks down from fork through the BPO chain to Prague/Cancun and returns +// the first non-nil entry. func (c *ChainConfig) BlobConfig(fork forks.Fork) *BlobConfig { - switch fork { - case forks.BPO5: - return c.BlobScheduleConfig.BPO5 - case forks.BPO4: - return c.BlobScheduleConfig.BPO4 - case forks.BPO3: - return c.BlobScheduleConfig.BPO3 - case forks.BPO2: - return c.BlobScheduleConfig.BPO2 - case forks.BPO1: - return c.BlobScheduleConfig.BPO1 - case forks.Osaka: - return c.BlobScheduleConfig.Osaka - case forks.Prague: - return c.BlobScheduleConfig.Prague - case forks.Cancun: - return c.BlobScheduleConfig.Cancun - default: + if c.BlobScheduleConfig == nil { return nil } + bsc := c.BlobScheduleConfig + chain := []struct { + at forks.Fork + cfg *BlobConfig + }{ + {forks.BPO5, bsc.BPO5}, + {forks.BPO4, bsc.BPO4}, + {forks.BPO3, bsc.BPO3}, + {forks.BPO2, bsc.BPO2}, + {forks.BPO1, bsc.BPO1}, + {forks.Prague, bsc.Prague}, + {forks.Cancun, bsc.Cancun}, + } + for _, e := range chain { + if e.at <= fork && e.cfg != nil { + return e.cfg + } + } + return nil } // ActiveSystemContracts returns the currently active system contracts at the diff --git a/tests/init.go b/tests/init.go index 2550eb1231..67f199203f 100644 --- a/tests/init.go +++ b/tests/init.go @@ -434,7 +434,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, }, }, "PragueToOsakaAtTime15k": { @@ -461,7 +460,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, }, }, "BPO1": { @@ -489,7 +487,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, }, }, @@ -518,7 +515,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, }, }, @@ -548,7 +544,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, }, @@ -579,7 +574,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, }, @@ -611,7 +605,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, @@ -644,7 +637,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, @@ -678,7 +670,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, @@ -713,7 +704,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, BPO1: bpo1BlobConfig, BPO2: bpo2BlobConfig, BPO3: params.DefaultBPO3BlobConfig, @@ -747,14 +737,12 @@ var Forks = map[string]*params.ChainConfig{ AmsterdamTime: u64(0), DepositContractAddress: params.MainnetChainConfig.DepositContractAddress, BlobScheduleConfig: ¶ms.BlobScheduleConfig{ - Cancun: params.DefaultCancunBlobConfig, - Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, - BPO1: bpo1BlobConfig, - BPO2: bpo2BlobConfig, - BPO3: params.DefaultBPO3BlobConfig, - BPO4: params.DefaultBPO4BlobConfig, - Amsterdam: params.DefaultBPO4BlobConfig, // TODO update when defined + Cancun: params.DefaultCancunBlobConfig, + Prague: params.DefaultPragueBlobConfig, + BPO1: bpo1BlobConfig, + BPO2: bpo2BlobConfig, + BPO3: params.DefaultBPO3BlobConfig, + BPO4: params.DefaultBPO4BlobConfig, }, }, "Verkle": { @@ -801,7 +789,6 @@ var Forks = map[string]*params.ChainConfig{ BlobScheduleConfig: ¶ms.BlobScheduleConfig{ Cancun: params.DefaultCancunBlobConfig, Prague: params.DefaultPragueBlobConfig, - Osaka: params.DefaultOsakaBlobConfig, }, }, } From 21652ef9ef00a823d140b2c94593354105b5b702 Mon Sep 17 00:00:00 2001 From: Bosul Mun Date: Wed, 1 Jul 2026 11:12:46 +0200 Subject: [PATCH 17/44] core/txpool: add blobTxForPool migration in limbo (#35209) This PR adds blobTxForPool migration support in limbo. Previously, there was no conversion path from limbo entries containing types.Transaction. Now that we have the new blobTxForPool type, this PR adds migration logic between both types. New test code (limbo_test.go) to test this conversion is added. --------- Co-authored-by: Felix Lange --- core/txpool/blobpool/limbo.go | 59 +++++++++++++++++++++++- core/txpool/blobpool/limbo_test.go | 72 ++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 core/txpool/blobpool/limbo_test.go diff --git a/core/txpool/blobpool/limbo.go b/core/txpool/blobpool/limbo.go index e696a6fcc0..f941f06ec4 100644 --- a/core/txpool/blobpool/limbo.go +++ b/core/txpool/blobpool/limbo.go @@ -65,11 +65,18 @@ func newLimbo(config *params.ChainConfig, datadir string) (*limbo, error) { } // Index all limboed blobs on disk and delete anything unprocessable - var fails []uint64 + var ( + fails []uint64 + convert []uint64 + ) index := func(id uint64, size uint32, data []byte) { - if l.parseBlob(id, data) != nil { + err := l.parseBlob(id, data) + if err != nil { fails = append(fails, id) } + if errors.Is(err, errLegacyTx) { + convert = append(convert, id) + } } store, err := billy.Open(billy.Options{Path: datadir, Repair: true}, slotter, index) if err != nil { @@ -77,6 +84,39 @@ func newLimbo(config *params.ChainConfig, datadir string) (*limbo, error) { } l.store = store + // Migrate legacy limbo entries to blobTxForPool. Note all ids in `converted` are also + // in `fails`, and the converted entries will be deleted by the loop that handles + // `fails`. + for _, id := range convert { + data, err := l.store.Get(id) + if err != nil { + continue + } + var legacy struct { + TxHash common.Hash + Block uint64 + Tx *types.Transaction + } + if err := rlp.DecodeBytes(data, &legacy); err != nil { + continue + } + blob, err := rlp.EncodeToBytes(&limboBlob{ + TxHash: legacy.TxHash, + Block: legacy.Block, + Ptx: newBlobTxForPool(legacy.Tx), + }) + if err != nil { + continue + } + newID, err := l.store.Put(blob) + if err != nil { + continue + } + if err := l.parseBlob(newID, blob); err != nil { + fails = append(fails, newID) + } + } + if len(fails) > 0 { log.Warn("Dropping invalidated limboed blobs", "ids", fails) for _, id := range fails { @@ -99,6 +139,10 @@ func (l *limbo) Close() error { func (l *limbo) parseBlob(id uint64, data []byte) error { item := new(limboBlob) if err := rlp.DecodeBytes(data, item); err != nil { + // This entry may have been stored with the legacy limboBlob type + if isLegacy(data) { + return errLegacyTx + } // This path is impossible unless the disk data representation changes // across restarts. For that ever improbable case, recover gracefully // by ignoring this data entry. @@ -122,6 +166,17 @@ func (l *limbo) parseBlob(id uint64, data []byte) error { return nil } +// isLegacy reports whether data is a limbo entry with the legacy limboBlob +// {TxHash, Block, Tx *types.Transaction} type. +func isLegacy(data []byte) bool { + elems, err := rlp.SplitListValues(data) + if err != nil || len(elems) < 3 { + return false + } + kind, content, _, err := rlp.Split(elems[2]) + return err == nil && kind == rlp.String && len(content) > 1 && content[0] == types.BlobTxType +} + // finalize evicts all blobs belonging to a recently finalized block or older. func (l *limbo) finalize(final *types.Header) { // Just in case there's no final block yet (network not yet merged, weird diff --git a/core/txpool/blobpool/limbo_test.go b/core/txpool/blobpool/limbo_test.go new file mode 100644 index 0000000000..722587b5c1 --- /dev/null +++ b/core/txpool/blobpool/limbo_test.go @@ -0,0 +1,72 @@ +// 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 blobpool + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/billy" +) + +// TestLimboLegacyMigration checks whether limbo entries in the legacy limboBlob type +// are migrated to the blobTxForPool layout on startup instead of being dropped. +func TestLimboLegacyMigration(t *testing.T) { + key, _ := crypto.GenerateKey() + tx := makeMultiBlobTx(0, 10, 100, 100, 2, 0, key) + + dir := t.TempDir() + + // Write a single entry using the legacy on-disk layout. + store, err := billy.Open(billy.Options{Path: dir}, newSlotterEIP7594(params.BlobTxMaxBlobs), nil) + if err != nil { + t.Fatalf("failed to open store: %v", err) + } + legacy := struct { + TxHash common.Hash + Block uint64 + Tx *types.Transaction + }{tx.Hash(), 42, tx} + data, err := rlp.EncodeToBytes(&legacy) + if err != nil { + t.Fatalf("failed to encode legacy entry: %v", err) + } + if _, err := store.Put(data); err != nil { + t.Fatalf("failed to store legacy entry: %v", err) + } + store.Close() + + // Open the limbo, which should migrate the legacy entry. + l, err := newLimbo(new(params.ChainConfig), dir) + if err != nil { + t.Fatalf("failed to open limbo: %v", err) + } + defer l.Close() + + // The migrated transaction must be tracked and reconstruct the original. + ptx, err := l.pull(tx.Hash()) + if err != nil { + t.Fatalf("failed to pull migrated tx: %v", err) + } + if got := ptx.ToTx().Hash(); got != tx.Hash() { + t.Fatalf("migrated tx hash mismatch: got %x, want %x", got, tx.Hash()) + } +} From 7e625dd54822499d7fece76de9850377d65957c4 Mon Sep 17 00:00:00 2001 From: Rafael Matias Date: Wed, 1 Jul 2026 11:55:12 +0200 Subject: [PATCH 18/44] cmd: add amsterdam override flag (#35213) Co-authored-by: MariusVanDerWijden --- cmd/geth/chaincmd.go | 5 +++++ cmd/geth/config.go | 4 ++++ cmd/geth/main.go | 1 + cmd/utils/flags.go | 5 +++++ core/eip8246_test.go | 3 --- core/genesis.go | 12 ++++++++---- core/vm/eip8037_test.go | 3 --- eth/backend.go | 3 +++ eth/ethconfig/config.go | 3 +++ eth/ethconfig/gen_config.go | 6 ++++++ 10 files changed, 35 insertions(+), 10 deletions(-) diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 891507d2ed..2c312f4e41 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -62,6 +62,7 @@ var ( Flags: slices.Concat([]cli.Flag{ utils.CachePreimagesFlag, utils.OverrideOsaka, + utils.OverrideAmsterdam, utils.OverrideBPO1, utils.OverrideBPO2, utils.OverrideUBT, @@ -287,6 +288,10 @@ func initGenesis(ctx *cli.Context) error { v := ctx.Uint64(utils.OverrideOsaka.Name) overrides.OverrideOsaka = &v } + if ctx.IsSet(utils.OverrideAmsterdam.Name) { + v := ctx.Uint64(utils.OverrideAmsterdam.Name) + overrides.OverrideAmsterdam = &v + } if ctx.IsSet(utils.OverrideBPO1.Name) { v := ctx.Uint64(utils.OverrideBPO1.Name) overrides.OverrideBPO1 = &v diff --git a/cmd/geth/config.go b/cmd/geth/config.go index ab9a336349..8ac9e7f6bd 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -228,6 +228,10 @@ func makeFullNode(ctx *cli.Context) *node.Node { v := ctx.Uint64(utils.OverrideOsaka.Name) cfg.Eth.OverrideOsaka = &v } + if ctx.IsSet(utils.OverrideAmsterdam.Name) { + v := ctx.Uint64(utils.OverrideAmsterdam.Name) + cfg.Eth.OverrideAmsterdam = &v + } if ctx.IsSet(utils.OverrideBPO1.Name) { v := ctx.Uint64(utils.OverrideBPO1.Name) cfg.Eth.OverrideBPO1 = &v diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 5e90164aaa..d3934a8cb6 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -57,6 +57,7 @@ var ( utils.USBFlag, utils.SmartCardDaemonPathFlag, utils.OverrideOsaka, + utils.OverrideAmsterdam, utils.OverrideBPO1, utils.OverrideBPO2, utils.OverrideUBT, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index a248d4fa8a..9d06e54f6c 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -254,6 +254,11 @@ var ( Usage: "Manually specify the Osaka fork timestamp, overriding the bundled setting", Category: flags.EthCategory, } + OverrideAmsterdam = &cli.Uint64Flag{ + Name: "override.amsterdam", + Usage: "Manually specify the Amsterdam fork timestamp, overriding the bundled setting", + Category: flags.EthCategory, + } OverrideBPO1 = &cli.Uint64Flag{ Name: "override.bpo1", Usage: "Manually specify the bpo1 fork timestamp, overriding the bundled setting", diff --git a/core/eip8246_test.go b/core/eip8246_test.go index 2fd39e3d44..18049d2279 100644 --- a/core/eip8246_test.go +++ b/core/eip8246_test.go @@ -49,9 +49,6 @@ func TestEIP8246SelfdestructNoBurn(t *testing.T) { // TODO: drop this hacky Amsterdam config initialization once the final // Amsterdam config is available (mirrors TestEthTransferLogs). config.AmsterdamTime = new(uint64) - blobConfig := *config.BlobScheduleConfig - blobConfig.Amsterdam = blobConfig.Osaka - config.BlobScheduleConfig = &blobConfig gspec := &Genesis{ Config: &config, diff --git a/core/genesis.go b/core/genesis.go index ec3e70a975..910a3fb0f5 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -277,10 +277,11 @@ func (e *GenesisMismatchError) Error() string { // ChainOverrides contains the changes to chain config. type ChainOverrides struct { - OverrideOsaka *uint64 - OverrideBPO1 *uint64 - OverrideBPO2 *uint64 - OverrideUBT *uint64 + OverrideOsaka *uint64 + OverrideAmsterdam *uint64 + OverrideBPO1 *uint64 + OverrideBPO2 *uint64 + OverrideUBT *uint64 } // apply applies the chain overrides on the supplied chain config. @@ -291,6 +292,9 @@ func (o *ChainOverrides) apply(cfg *params.ChainConfig) error { if o.OverrideOsaka != nil { cfg.OsakaTime = o.OverrideOsaka } + if o.OverrideAmsterdam != nil { + cfg.AmsterdamTime = o.OverrideAmsterdam + } if o.OverrideBPO1 != nil { cfg.BPO1Time = o.OverrideBPO1 } diff --git a/core/vm/eip8037_test.go b/core/vm/eip8037_test.go index 658096d228..07f0099207 100644 --- a/core/vm/eip8037_test.go +++ b/core/vm/eip8037_test.go @@ -45,9 +45,6 @@ var ( func amsterdam8037Config() *params.ChainConfig { cfg := *params.MergedTestChainConfig cfg.AmsterdamTime = new(uint64) - blob := *cfg.BlobScheduleConfig - blob.Amsterdam = blob.Osaka - cfg.BlobScheduleConfig = &blob return &cfg } diff --git a/eth/backend.go b/eth/backend.go index 7438486696..366f911785 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -280,6 +280,9 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if config.OverrideOsaka != nil { overrides.OverrideOsaka = config.OverrideOsaka } + if config.OverrideAmsterdam != nil { + overrides.OverrideAmsterdam = config.OverrideAmsterdam + } if config.OverrideBPO1 != nil { overrides.OverrideBPO1 = config.OverrideBPO1 } diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 99a0fa89f7..378bb64a1e 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -206,6 +206,9 @@ type Config struct { // OverrideOsaka (TODO: remove after the fork) OverrideOsaka *uint64 `toml:",omitempty"` + // OverrideAmsterdam (TODO: remove after the fork) + OverrideAmsterdam *uint64 `toml:",omitempty"` + // OverrideBPO1 (TODO: remove after the fork) OverrideBPO1 *uint64 `toml:",omitempty"` diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index 61d6ccb168..b24cde8a6a 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -64,6 +64,7 @@ func (c Config) MarshalTOML() (interface{}, error) { RPCEVMTimeout time.Duration RPCTxFeeCap float64 OverrideOsaka *uint64 `toml:",omitempty"` + OverrideAmsterdam *uint64 `toml:",omitempty"` OverrideBPO1 *uint64 `toml:",omitempty"` OverrideBPO2 *uint64 `toml:",omitempty"` OverrideUBT *uint64 `toml:",omitempty"` @@ -119,6 +120,7 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.RPCEVMTimeout = c.RPCEVMTimeout enc.RPCTxFeeCap = c.RPCTxFeeCap enc.OverrideOsaka = c.OverrideOsaka + enc.OverrideAmsterdam = c.OverrideAmsterdam enc.OverrideBPO1 = c.OverrideBPO1 enc.OverrideBPO2 = c.OverrideBPO2 enc.OverrideUBT = c.OverrideUBT @@ -178,6 +180,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { RPCEVMTimeout *time.Duration RPCTxFeeCap *float64 OverrideOsaka *uint64 `toml:",omitempty"` + OverrideAmsterdam *uint64 `toml:",omitempty"` OverrideBPO1 *uint64 `toml:",omitempty"` OverrideBPO2 *uint64 `toml:",omitempty"` OverrideUBT *uint64 `toml:",omitempty"` @@ -330,6 +333,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.OverrideOsaka != nil { c.OverrideOsaka = dec.OverrideOsaka } + if dec.OverrideAmsterdam != nil { + c.OverrideAmsterdam = dec.OverrideAmsterdam + } if dec.OverrideBPO1 != nil { c.OverrideBPO1 = dec.OverrideBPO1 } From 59e89e81e57814a96c429c5cdcaa6ca2e0d6b143 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 2 Jul 2026 04:06:16 +0800 Subject: [PATCH 19/44] cmd/devp2p/internal/ethtest: fix swapped args in readAccounts error (#35247) --- cmd/devp2p/internal/ethtest/chain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/devp2p/internal/ethtest/chain.go b/cmd/devp2p/internal/ethtest/chain.go index b44e6aa36a..36c395cdef 100644 --- a/cmd/devp2p/internal/ethtest/chain.go +++ b/cmd/devp2p/internal/ethtest/chain.go @@ -335,7 +335,7 @@ func readAccounts(file string) (map[common.Address]*senderInfo, error) { for addr, acc := range keys { pk, err := crypto.HexToECDSA(common.Bytes2Hex(acc.Key)) if err != nil { - return nil, fmt.Errorf("unable to read private key for %s: %v", err, addr) + return nil, fmt.Errorf("unable to read private key for %s: %v", addr, err) } accounts[addr] = &senderInfo{Key: pk, Nonce: 0} } From dd8dd1520f6c4569d1f2a9287fed49de2190f11b Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Thu, 2 Jul 2026 09:05:51 +0800 Subject: [PATCH 20/44] core: implement EIP-8038 (#35216) Implements https://eips.ethereum.org/EIPS/eip-8038 --- core/eip8038_test.go | 110 ++++++++++++++++ core/state_transition.go | 30 +++-- core/state_transition_test.go | 13 +- core/vm/eip8037_test.go | 17 ++- core/vm/eip8038_test.go | 237 ++++++++++++++++++++++++++++++++++ core/vm/eips.go | 34 +++-- core/vm/gas_table.go | 101 ++++++++------- core/vm/jump_table.go | 6 +- core/vm/operations_acl.go | 103 ++++++++++++--- params/protocol_params.go | 21 ++- 10 files changed, 573 insertions(+), 99 deletions(-) create mode 100644 core/eip8038_test.go create mode 100644 core/vm/eip8038_test.go diff --git a/core/eip8038_test.go b/core/eip8038_test.go new file mode 100644 index 0000000000..d8977c6dd2 --- /dev/null +++ b/core/eip8038_test.go @@ -0,0 +1,110 @@ +// 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 . + +// EIP-8038 authorization accounting tests. The per-authorization intrinsic gas +// pre-charges ACCOUNT_WRITE (regular) on top of REGULAR_PER_AUTH_BASE_COST. +// applyAuthorization refunds that ACCOUNT_WRITE to the refund counter in exactly +// the cases where no new account leaf is written: an invalid authorization, or +// an authority whose account already exists. These white-box tests invoke +// applyAuthorization directly and read the raw refund counter, so they observe +// the refund before the EIP-3529 cap is applied. + +package core + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// newAuthTestTransition builds a minimal stateTransition with a state reservoir, +// suitable for calling applyAuthorization directly. +func newAuthTestTransition(sdb *state.StateDB) *stateTransition { + st := newStateTransition(amsterdamCoreEVM(sdb), &Message{}, NewGasPool(30_000_000)) + st.gasRemaining = vm.NewGasBudget(0, 1_000_000) // reservoir for state-gas refills + return st +} + +// A net-new delegation on a fresh authority writes a new account leaf, so the +// intrinsic ACCOUNT_WRITE stands (no refund). +func TestAuthAccountWriteNetNewNoRefund(t *testing.T) { + auth, _ := signAuth(t, authKeyA, delegate8037, 0) + st := newAuthTestTransition(mkState(senderAlloc(nil))) + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil { + t.Fatal(err) + } + if got := st.state.GetRefund(); got != 0 { + t.Fatalf("refund = %d, want 0 (net-new account write)", got) + } +} + +// A pre-existing authority writes no new account leaf, so the intrinsic +// ACCOUNT_WRITE is refunded. +func TestAuthAccountWriteExistsRefund(t *testing.T) { + auth, authority := signAuth(t, authKeyA, delegate8037, 0) + st := newAuthTestTransition(mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}))) + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil { + t.Fatal(err) + } + if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { + t.Fatalf("refund = %d, want %d (account already exists)", got, params.AccountWriteAmsterdam) + } +} + +// An invalid authorization is skipped without writing any account leaf, so its +// intrinsic ACCOUNT_WRITE is refunded. +func TestAuthAccountWriteInvalidRefund(t *testing.T) { + k, _ := crypto.HexToECDSA(authKeyA) + bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{ + ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id + }) + st := newAuthTestTransition(mkState(senderAlloc(nil))) + if err := st.applyAuthorization(rules8037, &bad, map[common.Address]bool{}); err == nil { + t.Fatal("expected invalid-authorization error") + } + if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { + t.Fatalf("refund = %d, want %d (invalid authorization)", got, params.AccountWriteAmsterdam) + } +} + +// The same authority across two authorizations writes its account leaf only +// once: the first auth pays ACCOUNT_WRITE, the second (which now sees the +// account as existing) is refunded. +func TestAuthAccountWriteDuplicateOnce(t *testing.T) { + a0, _ := signAuth(t, authKeyA, delegate8037, 0) + a1, _ := signAuth(t, authKeyA, delegate8037, 1) + st := newAuthTestTransition(mkState(senderAlloc(nil))) + delegates := map[common.Address]bool{} + if err := st.applyAuthorization(rules8037, &a0, delegates); err != nil { + t.Fatal(err) + } + if got := st.state.GetRefund(); got != 0 { + t.Fatalf("refund after first auth = %d, want 0", got) + } + if err := st.applyAuthorization(rules8037, &a1, delegates); err != nil { + t.Fatal(err) + } + if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam { + t.Fatalf("refund after duplicate auth = %d, want %d", got, params.AccountWriteAmsterdam) + } +} diff --git a/core/state_transition.go b/core/state_transition.go index 2923e110c7..877ed0638f 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -87,7 +87,7 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set // Add gas for authorizations if authList != nil { if rules.IsAmsterdam { - gas.RegularGas += uint64(len(authList)) * params.TxAuthTupleRegularGas + gas.RegularGas += uint64(len(authList)) * (params.AccountWriteAmsterdam + params.RegularPerAuthBaseCost) gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte } else { gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas @@ -126,14 +126,22 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set if accessList != nil { addresses := uint64(len(accessList)) storageKeys := uint64(accessList.StorageKeys()) - if (math.MaxUint64-gas.RegularGas)/params.TxAccessListAddressGas < addresses { + + // Amsterdam re-prices the per-entry access-list cost + addressCost := params.TxAccessListAddressGas + storageKeyCost := params.TxAccessListStorageKeyGas + if rules.IsAmsterdam { + addressCost = params.TxAccessListAddressGasAmsterdam + storageKeyCost = params.TxAccessListStorageKeyGasAmsterdam + } + if (math.MaxUint64-gas.RegularGas)/addressCost < addresses { return vm.GasCosts{}, ErrGasUintOverflow } - gas.RegularGas += addresses * params.TxAccessListAddressGas - if (math.MaxUint64-gas.RegularGas)/params.TxAccessListStorageKeyGas < storageKeys { + gas.RegularGas += addresses * addressCost + if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys { return vm.GasCosts{}, ErrGasUintOverflow } - gas.RegularGas += storageKeys * params.TxAccessListStorageKeyGas + gas.RegularGas += storageKeys * storageKeyCost // EIP-7981: access list data is charged in addition to the base charge. if rules.IsAmsterdam { @@ -955,20 +963,13 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio } // applyAuthorization applies an EIP-7702 code delegation to the state and, -// under EIP-8037, reconciles the per-authorization intrinsic state-gas pre- -// charge so that, per authority: -// -// - the account portion (AccountCreationSize × CPSB) is charged at most -// once, and only when the account did not exist before the tx -// -// - the delegation-indicator portion (AuthorizationCreationSize × CPSB) is -// charged at most once, and only when the authority ends the tx delegated -// having started it undelegated. +// adjust the pre-charged intrinsic cost accordingly. func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, delegates map[common.Address]bool) error { authority, err := st.validateAuthorization(auth) if err != nil { if rules.IsAmsterdam { st.gasRemaining.RefundState((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) + st.state.AddRefund(params.AccountWriteAmsterdam) } return err } @@ -981,6 +982,7 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se } else { if st.state.Exist(authority) { st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + st.state.AddRefund(params.AccountWriteAmsterdam) } authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte diff --git a/core/state_transition_test.go b/core/state_transition_test.go index ace43aa13a..60edad52e5 100644 --- a/core/state_transition_test.go +++ b/core/state_transition_test.go @@ -243,7 +243,7 @@ func TestIntrinsicGas(t *testing.T) { // 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*params.TxAccessListAddressGasAmsterdam + 3*params.TxAccessListStorageKeyGasAmsterdam + 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost}, }, { @@ -298,7 +298,7 @@ func TestIntrinsicGas(t *testing.T) { want: vm.GasCosts{ RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + 32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas + - 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + + 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost, StateGas: params.AccountCreationSize * params.CostPerStateByte, }, @@ -314,15 +314,16 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, isAmsterdam: true, - // EIP-8037 splits the auth-tuple charge into regular + state gas: - // regular: TxAuthTupleRegularGas (7500) per auth + // EIP-8037 splits the auth-tuple charge into regular + state gas, with + // the values finalized by EIP-8038: + // regular: ACCOUNT_WRITE (8,000) + REGULAR_PER_AUTH_BASE_COST (7,500) per auth // state: (AuthorizationCreationSize + AccountCreationSize) * CostPerStateByte per auth want: vm.GasCosts{ RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + 100*params.TxDataNonZeroGasEIP2028 + - 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas + + 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + - 1*params.TxAuthTupleRegularGas, + 1*(params.AccountWriteAmsterdam+params.RegularPerAuthBaseCost), StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte, }, }, diff --git a/core/vm/eip8037_test.go b/core/vm/eip8037_test.go index 07f0099207..0eafc3e4af 100644 --- a/core/vm/eip8037_test.go +++ b/core/vm/eip8037_test.go @@ -190,16 +190,25 @@ func TestSStoreChargedAtOpcodeEnd(t *testing.T) { } // The SSTORE reentrancy sentry checks gas_left only; the reservoir is excluded. -// Uses a noop write (1->1->1) so the sentry is the sole gate. +// Uses a noop write (1->1->1): the two PUSH1s cost 6, leaving gas_left at the +// sentry (2300) for a 2306 budget. Under EIP-8038 the cold-slot access that +// follows a cleared sentry costs COLD_STORAGE_ACCESS (3000). func TestSStoreStipendExcludesReservoir(t *testing.T) { - // regular at the sentry, huge reservoir: must still fail. + // regular at the sentry, huge reservoir: must still fail, proving the + // reservoir does not count toward the sentry. if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(2306, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err == nil { t.Fatal("expected sentry failure with regular gas at the limit") } - // one more regular gas clears the sentry. - if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(2307, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err != nil { + // Enough regular gas to clear the sentry and pay the cold-slot access + // (6 for the PUSH1s + COLD_STORAGE_ACCESS) succeeds with a huge reservoir. + regular := 6 + params.ColdStorageAccessAmsterdam + if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(regular, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err != nil { t.Fatalf("unexpected failure above sentry: %v", err) } + // One gas short of the cold-slot access still fails (now on OOG, not sentry). + if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(regular-1, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err == nil { + t.Fatal("expected OOG when regular gas cannot cover cold-slot access") + } } // ---- CALL / CREATE bytecode helpers ---- diff --git a/core/vm/eip8038_test.go b/core/vm/eip8038_test.go new file mode 100644 index 0000000000..c08c54fb2c --- /dev/null +++ b/core/vm/eip8038_test.go @@ -0,0 +1,237 @@ +// 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 . + +// Opcode-level tests for EIP-8038 (state-access gas cost update). They reuse the +// Amsterdam harness from eip8037_test.go and assert the re-priced regular-gas, +// state-gas and refund-counter accounting. + +package vm + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// run8038 executes code at a contract address under the Amsterdam ruleset and +// returns the resulting budget together with the transaction's refund counter. +func run8038(t *testing.T, code []byte, gas GasBudget, value *uint256.Int, setup func(*state.StateDB, common.Address)) (GasBudget, uint64, error) { + t.Helper() + self := common.BytesToAddress([]byte("self")) + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + statedb.CreateAccount(self) + statedb.SetCode(self, code, tracing.CodeChangeUnspecified) + if setup != nil { + setup(statedb, self) + } + statedb.Finalise(true) + _, result, err := amsterdam8037EVM(statedb).Call(common.Address{}, self, nil, gas, value) + return result, statedb.GetRefund(), err +} + +// TestEIP8038SStore exercises SSTORE under Amsterdam (EIP-8037 + EIP-8038), +// asserting the two-dimensional charge (regular + state gas) and the net refund +// counter. It covers single stores in isolation (the EIP-8038 cases-table rows, +// cold access), the warm-access variants, the dirty-slot refund reversals and +// multi-store round trips. +// +// Each sstore() is "PUSH1 val; PUSH1 slot; SSTORE", so the non-SSTORE overhead is +// 6 gas (two PUSH1) per store. The first store to a slot is cold and the rest are +// warm, so the access component is COLD_STORAGE_ACCESS + (n-1) * WARM_ACCESS for n +// stores. STORAGE_WRITE is charged once per "first change" (current == original). +// GAS_STORAGE_SET is state gas, charged when a slot is created from zero and +// refilled to the reservoir when that creation is undone within the same tx. +func TestEIP8038SStore(t *testing.T) { + const ( + push = uint64(6) // two PUSH1 per SSTORE + cold = params.ColdStorageAccessAmsterdam + warm = params.WarmStorageReadCostEIP2929 + write = params.StorageWriteAmsterdam + clear = params.StorageClearRefundAmsterdam + ) + set := uint64(params.StorageCreationSize * params.CostPerStateByte) // GAS_STORAGE_SET + + // access(n) is the access-only regular cost for n stores: cold first, warm rest. + access := func(n uint64) uint64 { return cold + (n-1)*warm } + + cases := []struct { + name string + orig byte // committed (pre-tx) value; 0 means a fresh slot + vals []byte // values written to slot 0, in order + wantReg uint64 + wantState int64 + wantRfnd uint64 + }{ + // Single store, cold access (EIP-8038 cases table, Cold rows + noop). + {"noop (1->1)", 1, []byte{1}, push + cold, 0, 0}, + {"create (0->1)", 0, []byte{1}, push + cold + write, int64(set), 0}, + {"first change (1->2)", 1, []byte{2}, push + cold + write, 0, 0}, + {"clear (1->0)", 1, []byte{0}, push + cold + write, 0, clear}, + // Two stores, warm access on the second (Warm rows of the cases table). + {"create warm (0->0->1)", 0, []byte{0, 1}, 2*push + access(2) + write, int64(set), 0}, + {"first change warm (1->1->2)", 1, []byte{1, 2}, 2*push + access(2) + write, 0, 0}, + {"clear warm (1->1->0)", 1, []byte{1, 0}, 2*push + access(2) + write, 0, clear}, + {"dirty modified again (1->2->3)", 1, []byte{2, 3}, 2*push + access(2) + write, 0, 0}, + // Two stores, refund reversals when a slot returns toward its original. + {"reset to zero (0->1->0)", 0, []byte{1, 0}, 2*push + access(2) + write, 0, write}, + {"reset to original (1->2->1)", 1, []byte{2, 1}, 2*push + access(2) + write, 0, write}, + {"cleared then restored (1->0->1)", 1, []byte{0, 1}, 2*push + access(2) + write, 0, write}, + {"cleared then new value (1->0->2)", 1, []byte{0, 2}, 2*push + access(2) + write, 0, 0}, + // Three stores, round trips (note the state-gas refill on the 0-> path). + {"0->1->0->1", 0, []byte{1, 0, 1}, 3*push + access(3) + 2*write, int64(set), write}, + {"1->0->1->0", 1, []byte{0, 1, 0}, 3*push + access(3) + 2*write, 0, clear + write}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var code []byte + for _, v := range tc.vals { + code = append(code, sstore(0, v)...) + } + var setup func(*state.StateDB, common.Address) + if tc.orig != 0 { + setup = setSlot(0, tc.orig) + } + res, refund, err := run8038(t, code, hugeBudget(), new(uint256.Int), setup) + if err != nil { + t.Fatal(err) + } + if res.UsedRegularGas != tc.wantReg { + t.Errorf("regular gas = %d, want %d", res.UsedRegularGas, tc.wantReg) + } + if res.UsedStateGas != tc.wantState { + t.Errorf("state gas = %d, want %d", res.UsedStateGas, tc.wantState) + } + if refund != tc.wantRfnd { + t.Errorf("refund = %d, want %d", refund, tc.wantRfnd) + } + }) + } +} + +// TestEIP8038SLoad checks the re-priced SLOAD access costs (cold 3000, warm 100). +func TestEIP8038SLoad(t *testing.T) { + push := uint64(3) // PUSH1 slot + // PUSH1 0x00; SLOAD + cold := []byte{0x60, 0x00, 0x54} + res, _, err := run8038(t, cold, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + if want := push + params.ColdStorageAccessAmsterdam; res.UsedRegularGas != want { + t.Fatalf("cold SLOAD = %d, want %d", res.UsedRegularGas, want) + } + // PUSH1 0x00; SLOAD; PUSH1 0x00; SLOAD -> second access is warm. + warm := []byte{0x60, 0x00, 0x54, 0x60, 0x00, 0x54} + res, _, err = run8038(t, warm, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + want := 2*push + params.ColdStorageAccessAmsterdam + params.WarmStorageReadCostEIP2929 + if res.UsedRegularGas != want { + t.Fatalf("cold+warm SLOAD = %d, want %d", res.UsedRegularGas, want) + } +} + +// TestEIP8038AccountAccess checks the re-priced cold-account access for the +// account-reading opcodes and the extra WARM_ACCESS surcharge for EXTCODESIZE +// and EXTCODECOPY (their second database read). +func TestEIP8038AccountAccess(t *testing.T) { + push20 := uint64(3) + addr := common.BytesToAddress([]byte("some-cold-account")) + + // pushAddr emits PUSH20 . + pushAddr := func() []byte { return append([]byte{0x73}, addr.Bytes()...) } + + cold := params.ColdAccountAccessAmsterdam + warm := params.WarmStorageReadCostEIP2929 + + t.Run("BALANCE", func(t *testing.T) { + code := append(pushAddr(), 0x31) // BALANCE + res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + if want := push20 + cold; res.UsedRegularGas != want { + t.Fatalf("cold BALANCE = %d, want %d", res.UsedRegularGas, want) + } + }) + t.Run("EXTCODEHASH", func(t *testing.T) { + code := append(pushAddr(), 0x3f) // EXTCODEHASH + res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + if want := push20 + cold; res.UsedRegularGas != want { + t.Fatalf("cold EXTCODEHASH = %d, want %d", res.UsedRegularGas, want) + } + }) + t.Run("EXTCODESIZE adds WARM_ACCESS", func(t *testing.T) { + code := append(pushAddr(), 0x3b) // EXTCODESIZE + res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + if want := push20 + cold + warm; res.UsedRegularGas != want { + t.Fatalf("cold EXTCODESIZE = %d, want %d", res.UsedRegularGas, want) + } + }) + t.Run("EXTCODECOPY adds WARM_ACCESS", func(t *testing.T) { + // PUSH1 0 (length); PUSH1 0 (codeOffset); PUSH1 0 (destOffset); PUSH20 addr; EXTCODECOPY + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00} + code = append(code, pushAddr()...) + code = append(code, 0x3c) // EXTCODECOPY + res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil) + if err != nil { + t.Fatal(err) + } + // three PUSH1 + one PUSH20 = 12 gas, zero-length copy => no memory/copy gas. + if want := uint64(12) + cold + warm; res.UsedRegularGas != want { + t.Fatalf("cold EXTCODECOPY = %d, want %d", res.UsedRegularGas, want) + } + }) +} + +// TestEIP8038SelfdestructAccountWrite checks that SELFDESTRUCT sending a positive +// balance to an empty account is charged the cold access, an additional +// ACCOUNT_WRITE (regular) and GAS_NEW_ACCOUNT (state). +func TestEIP8038SelfdestructAccountWrite(t *testing.T) { + beneficiary := common.BytesToAddress([]byte("fresh-beneficiary")) + // PUSH20 beneficiary; SELFDESTRUCT + code := append([]byte{0x73}, beneficiary.Bytes()...) + code = append(code, 0xff) + + // Fund the contract so it sends a positive balance on self-destruct. + fundSelf := func(db *state.StateDB, self common.Address) { + db.AddBalance(self, uint256.NewInt(1), tracing.BalanceChangeUnspecified) + } + res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), fundSelf) + if err != nil { + t.Fatal(err) + } + const push20 = uint64(3) + wantReg := push20 + params.SelfdestructGasEIP150 + params.ColdAccountAccessAmsterdam + params.AccountWriteAmsterdam + if res.UsedRegularGas != wantReg { + t.Fatalf("regular gas = %d, want %d", res.UsedRegularGas, wantReg) + } + if want := int64(params.AccountCreationSize * params.CostPerStateByte); res.UsedStateGas != want { + t.Fatalf("state gas = %d, want %d", res.UsedStateGas, want) + } +} diff --git a/core/vm/eips.go b/core/vm/eips.go index 814e4be788..8a09856029 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -43,7 +43,8 @@ var activators = map[int]func(*JumpTable){ 7939: enable7939, 8024: enable8024, 7843: enable7843, - 8037: enable8037, + 8037: enable8037And8038, + 8038: enable8037And8038, } // EnableEIP enables the given EIP on the config. @@ -584,13 +585,30 @@ func enable7843(jt *JumpTable) { } } -// enable8037 enables the multidimensional-metering as specified in EIP-8037. -func enable8037(jt *JumpTable) { - jt[CREATE].constantGas = params.CreateGasAmsterdam +// enable8037And8038 enables EIP-8037 (multidimensional state-gas metering) +// together with EIP-8038 (state-access gas cost update). +func enable8037And8038(jt *JumpTable) { + jt[CREATE].constantGas = params.CreateAccessAmsterdam jt[CREATE].dynamicGas = gasCreateEip8037 - jt[CREATE2].constantGas = params.CreateGasAmsterdam + jt[CREATE2].constantGas = params.CreateAccessAmsterdam jt[CREATE2].dynamicGas = gasCreate2Eip8037 - jt[CALL].dynamicGas = gasCallEIP8037 - jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037 - jt[SSTORE].dynamicGas = gasSStore8037 + + // Storage-access opcodes + jt[SLOAD].dynamicGas = gasSLoad8038 + jt[SSTORE].dynamicGas = gasSStore8037And8038 + + // Account-access opcodes + jt[BALANCE].dynamicGas = gasEip8038AccountCheck + jt[EXTCODEHASH].dynamicGas = gasEip8038AccountCheck + jt[EXTCODESIZE].dynamicGas = gasExtCodeSize8038 + jt[EXTCODECOPY].dynamicGas = gasExtCodeCopy8038 + + // Call family + jt[CALL].dynamicGas = gasCall8038 + jt[CALLCODE].dynamicGas = gasCallCode8038 + jt[STATICCALL].dynamicGas = gasStaticCall8038 + jt[DELEGATECALL].dynamicGas = gasDelegateCall8038 + + // SELFDESTRUCT + jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037And8038 } diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 4bd971e711..8cc63cb236 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -478,6 +478,28 @@ func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memor return gas, nil } +// gasCallCodeIntrinsic8038 mirrors gasCallCodeIntrinsic but charges the +// re-priced CALL_VALUE (ACCOUNT_WRITE + CALL_STIPEND) on value transfers per +// EIP-8038. CALLCODE executes in the caller's context, so it never creates a +// new account and has no state-gas component. +func gasCallCodeIntrinsic8038(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 + ) + if stack.back(2).Sign() != 0 && !evm.chainRules.IsEIP4762 { + gas += params.CallValueTransferAmsterdam + } + if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { + return 0, ErrGasUintOverflow + } + return gas, nil +} + func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { gas, err := memoryGasCost(mem, memorySize) if err != nil { @@ -576,10 +598,11 @@ func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, }, nil } -// 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 regularGasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { +// regularGasCall8038 is the intrinsic regular-gas calculator for CALL in +// Amsterdam. It computes memory expansion plus the re-priced CALL_VALUE +// (ACCOUNT_WRITE + CALL_STIPEND) on value transfers, but excludes new account +// creation, which is handled as state gas by stateGasCall8037. +func regularGasCall8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { var ( gas uint64 transfersValue = !stack.back(2).IsZero() @@ -593,7 +616,7 @@ func regularGasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, } var transferGas uint64 if transfersValue && !evm.chainRules.IsEIP4762 { - transferGas = params.CallValueTransferGas + transferGas = params.CallValueTransferAmsterdam } var overflow bool if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow { @@ -630,7 +653,9 @@ func stateGasCall8037(evm *EVM, contract *Contract, stack *Stack) (uint64, error return gas, nil } -func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +// gasSelfdestruct8037And8038 implements the SELFDESTRUCT gas charging under +// the EIP8037 and EIP-8038. +func gasSelfdestruct8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { if evm.readOnly { return GasCosts{}, ErrWriteProtection } @@ -639,11 +664,11 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory address = common.Address(stack.peek().Bytes20()) ) if !evm.StateDB.AddressInAccessList(address) { - // If the caller cannot afford the cost, this change will be rolled back + // If the caller cannot afford the cost, this change will be rolled back. evm.StateDB.AddAddressToAccessList(address) - gas.RegularGas = params.ColdAccountAccessCostEIP2929 + gas.RegularGas = params.ColdAccountAccessAmsterdam } - // Check we have enough regular gas before we add the address to the BAL + // Check we have enough regular gas before we add the address to the BAL. if contract.Gas.RegularGas < gas.RegularGas { return gas, ErrOutOfGas } @@ -654,79 +679,63 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory // // Funding such an account makes it permanent state growth and must be charged. if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { + gas.RegularGas += params.AccountWriteAmsterdam gas.StateGas += params.AccountCreationSize * evm.Context.CostPerStateByte } return gas, nil } -func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +// gasSStore8037And8038 implements the SSTORE gas charging under EIP-8037 and +// EIP-8038. +func gasSStore8037And8038(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 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 + value = common.Hash(y.Bytes32()) ) // Check slot presence in the access list + access := params.WarmStorageReadCostEIP2929 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 + access = params.ColdStorageAccessAmsterdam evm.StateDB.AddSlotToAccessList(contract.Address(), slot) } - value := common.Hash(y.Bytes32()) + stateSet := params.StorageCreationSize * evm.Context.CostPerStateByte if current == value { // noop (1) - // EIP 2200 original clause: - // return params.SloadGasEIP2200, nil - return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // SLOAD_GAS + return GasCosts{RegularGas: access}, nil } - if original == current { + if original == current { // first change of the slot (2.1) if original == (common.Hash{}) { // create slot (2.1.1) return GasCosts{ - RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929, - StateGas: params.StorageCreationSize * evm.Context.CostPerStateByte, + RegularGas: access + params.StorageWriteAmsterdam, + StateGas: stateSet, }, nil } if value == (common.Hash{}) { // delete slot (2.1.2b) - evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + evm.StateDB.AddRefund(params.StorageClearRefundAmsterdam) } - // 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) + return GasCosts{RegularGas: access + params.StorageWriteAmsterdam}, 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) + evm.StateDB.SubRefund(params.StorageClearRefundAmsterdam) } else if value == (common.Hash{}) { // delete slot (2.2.1.2) - evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529) + evm.StateDB.AddRefund(params.StorageClearRefundAmsterdam) } } - if original == value { + if original == value { // reset to original value (2.2.2) 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) + contract.Gas.RefundState(stateSet) } + evm.StateDB.AddRefund(params.StorageWriteAmsterdam) } - // EIP-2200 original clause: - //return params.SloadGasEIP2200, nil // dirty update (2.2) - return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // dirty update (2.2) + return GasCosts{RegularGas: access}, nil // dirty update (2.2) } diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 5cc5e34ced..6b931083fe 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -99,9 +99,9 @@ func newVerkleInstructionSet() JumpTable { 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) + enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode) + enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE) + enable8037And8038(&instructionSet) // EIP-8037 (state-gas metering) + EIP-8038 (state-access repricing) return validate(instructionSet) } diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index e07eba451f..111cc6a00c 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -101,16 +101,25 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { loc := stack.peek() slot := common.Hash(loc.Bytes32()) - // Check slot presence in the access list if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { - // If the caller cannot afford the cost, this change will be rolled back - // If he does afford it, we can skip checking the same thing later on, during execution evm.StateDB.AddSlotToAccessList(contract.Address(), slot) return GasCosts{RegularGas: params.ColdSloadCostEIP2929}, nil } return GasCosts{RegularGas: params.WarmStorageReadCostEIP2929}, nil } +// gasSLoad8038 mirrors gasSLoadEIP2929 but uses the EIP-8038 COLD_STORAGE_ACCESS +// for a cold slot. +func gasSLoad8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + loc := stack.peek() + slot := common.Hash(loc.Bytes32()) + if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + evm.StateDB.AddSlotToAccessList(contract.Address(), slot) + return GasCosts{RegularGas: params.ColdStorageAccessAmsterdam}, nil + } + return GasCosts{RegularGas: params.WarmStorageReadCostEIP2929}, nil +} + // gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929 // EIP spec: // > If the target is not in accessed_addresses, @@ -137,6 +146,34 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo return GasCosts{RegularGas: gas}, nil } +// gasExtCodeCopy8038 mirrors gasExtCodeCopyEIP2929 but uses the EIP-8038 +// COLD_ACCOUNT_ACCESS and adds an extra WARM_ACCESS for the second +// database read EXTCODECOPY performs. +func gasExtCodeCopy8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + // memory expansion first (dynamic part of pre-2929 implementation) + gasCost, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize) + if err != nil { + return GasCosts{}, err + } + gas := gasCost.RegularGas + addr := common.Address(stack.peek().Bytes20()) + // Check slot presence in the access list + if !evm.StateDB.AddressInAccessList(addr) { + evm.StateDB.AddAddressToAccessList(addr) + var overflow bool + // We charge (cold-warm), since 'warm' is already charged as constantGas + if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessAmsterdam-params.WarmStorageReadCostEIP2929); overflow { + return GasCosts{}, ErrGasUintOverflow + } + } + // Additional WARM_ACCESS for the second database read (contract code). + var overflow bool + if gas, overflow = math.SafeAdd(gas, params.WarmStorageReadCostEIP2929); overflow { + return GasCosts{}, ErrGasUintOverflow + } + return GasCosts{RegularGas: gas}, nil +} + // gasEip2929AccountCheck checks whether the first stack item (as address) is present in the access list. // If it is, this method returns '0', otherwise 'cold-warm' gas, presuming that the opcode using it // is also using 'warm' as constant factor. @@ -156,6 +193,32 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem return GasCosts{}, nil } +// gasEip8038AccountCheck mirrors gasEip2929AccountCheck but uses the EIP-8038 +// COLD_ACCOUNT_ACCESS. Used by BALANCE and EXTCODEHASH. +func gasEip8038AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + addr := common.Address(stack.peek().Bytes20()) + // Check slot presence in the access list + if !evm.StateDB.AddressInAccessList(addr) { + // If the caller cannot afford the cost, this change will be rolled back + evm.StateDB.AddAddressToAccessList(addr) + // The warm storage read cost is already charged as constantGas + return GasCosts{RegularGas: params.ColdAccountAccessAmsterdam - params.WarmStorageReadCostEIP2929}, nil + } + return GasCosts{}, nil +} + +// gasExtCodeSize8038 prices EXTCODESIZE under EIP-8038: the gasEip8038AccountCheck +// surcharge plus an additional WARM_ACCESS for the second database read (code size). +func gasExtCodeSize8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { + cost, err := gasEip8038AccountCheck(evm, contract, stack, mem, memorySize) + if err != nil { + return GasCosts{}, err + } + // Additional WARM_ACCESS for the second database read (contract size). + cost.RegularGas += params.WarmStorageReadCostEIP2929 + return cost, nil +} + func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { addr := common.Address(stack.back(addressPosition).Bytes20()) @@ -266,11 +329,10 @@ func recordDelegationAccess(evm *EVM, target common.Address) { } var ( - innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic) - gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic) - gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic) - gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic) - innerGasCallEIP8037 = makeCallVariantGasCallEIP8037(regularGasCall8037, stateGasCall8037) + innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic, params.ColdAccountAccessCostEIP2929) + gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic, params.ColdAccountAccessCostEIP2929) + gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic, params.ColdAccountAccessCostEIP2929) + gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic, params.ColdAccountAccessCostEIP2929) ) func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { @@ -285,15 +347,24 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem return innerGasCallEIP7702(evm, contract, stack, mem, memorySize) } -func gasCallEIP8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { +var ( + innerGasCall8038 = makeCallVariantGasCallEIP8037(regularGasCall8038, stateGasCall8037, params.ColdAccountAccessAmsterdam) + gasCallCode8038 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic8038, params.ColdAccountAccessAmsterdam) + gasDelegateCall8038 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic, params.ColdAccountAccessAmsterdam) + gasStaticCall8038 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic, params.ColdAccountAccessAmsterdam) +) + +// gasCall8038 prices CALL for Amsterdam, guarding against value transfers in a +// read-only context before delegating to the state-gas-aware wrapper. +func gasCall8038(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) + return innerGasCall8038(evm, contract, stack, mem, memorySize) } -func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { +func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc, coldCost uint64) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( eip2929Cost uint64 @@ -308,7 +379,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { // The WarmStorageReadCostEIP2929 (100) is already deducted in the form // of a constant cost, so the cost to charge for cold access, if any, // is Cold - Warm - eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + eip2929Cost = coldCost - params.WarmStorageReadCostEIP2929 // Charge the remaining difference here already, to correctly calculate // available gas for call @@ -339,7 +410,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { eip7702Cost = params.WarmStorageReadCostEIP2929 } else { evm.StateDB.AddAddressToAccessList(target) - eip7702Cost = params.ColdAccountAccessCostEIP2929 + eip7702Cost = coldCost } if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas @@ -388,7 +459,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(regularFunc regularGasFunc, stateGasFunc stateGasFunc) gasFunc { +func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stateGasFunc, coldCost uint64) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) { var ( eip2929Cost uint64 @@ -398,7 +469,7 @@ func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stat // EIP-2929 cold access check. if !evm.StateDB.AddressInAccessList(addr) { evm.StateDB.AddAddressToAccessList(addr) - eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 + eip2929Cost = coldCost - params.WarmStorageReadCostEIP2929 if !contract.chargeRegular(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } @@ -423,7 +494,7 @@ func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stat eip7702Cost = params.WarmStorageReadCostEIP2929 } else { evm.StateDB.AddAddressToAccessList(target) - eip7702Cost = params.ColdAccountAccessCostEIP2929 + eip7702Cost = coldCost } if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas diff --git a/params/protocol_params.go b/params/protocol_params.go index 7416eb8cff..a56e810074 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -88,7 +88,6 @@ 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. @@ -101,7 +100,14 @@ 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 + + // RegularPerAuthBaseCost is defined in EIP-8037 as the sum of: + // + // - Calldata cost: 1,616 (101 bytes × 16) + // - Recovering authority address (ecRecover) + // - Reading nonce and code of authority (cold access) + // - Storing values in already warm account: 2 x WARM_ACCESS + RegularPerAuthBaseCost uint64 = 7500 // EIP-2780: resource-based intrinsic transaction gas. TxBaseCost2780 uint64 = 12000 @@ -110,6 +116,17 @@ const ( TxValueCost2780 uint64 = 4244 TransferLogCost2780 uint64 = 1756 + // EIP-8038: state-access gas cost update (Amsterdam). + ColdAccountAccessAmsterdam uint64 = 3000 // COLD_ACCOUNT_ACCESS: cold touch of an account + AccountWriteAmsterdam uint64 = 8000 // ACCOUNT_WRITE: surcharge for first-time write to an account + CallValueTransferAmsterdam uint64 = 10300 // CALL_VALUE = ACCOUNT_WRITE + CallStipend (2300) + ColdStorageAccessAmsterdam uint64 = 3000 // COLD_STORAGE_ACCESS: cold touch of a storage slot + StorageWriteAmsterdam uint64 = 10000 // STORAGE_WRITE: surcharge for first-time write to a storage slot + StorageClearRefundAmsterdam uint64 = 12480 // STORAGE_CLEAR_REFUND: refund for clearing a storage slot + CreateAccessAmsterdam uint64 = 11000 // CREATE_ACCESS = ACCOUNT_WRITE + COLD_STORAGE_ACCESS + TxAccessListAddressGasAmsterdam uint64 = 3000 // ACCESS_LIST_ADDRESS_COST + TxAccessListStorageKeyGasAmsterdam uint64 = 3000 // ACCESS_LIST_STORAGE_KEY_COST + // 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) From 0a47739f11bdbb4e5668c95e692e922f4e632bf7 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 2 Jul 2026 16:40:28 +0800 Subject: [PATCH 21/44] cmd/devp2p: add already used flag crawlParallelismFlag (#35031) --- cmd/devp2p/discv5cmd.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/devp2p/discv5cmd.go b/cmd/devp2p/discv5cmd.go index dd253dd082..8b39829e60 100644 --- a/cmd/devp2p/discv5cmd.go +++ b/cmd/devp2p/discv5cmd.go @@ -58,6 +58,7 @@ var ( Action: discv5Crawl, Flags: slices.Concat(discoveryNodeFlags, []cli.Flag{ crawlTimeoutFlag, + crawlParallelismFlag, }), } discv5TestCommand = &cli.Command{ From b4f749af113ea0d5f4a514ae967f74181b7ece91 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 2 Jul 2026 16:43:42 +0800 Subject: [PATCH 22/44] core/types: reduce allocs in CreateBloom (#35234) --- core/types/bloom9.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/types/bloom9.go b/core/types/bloom9.go index 1d57e8e4bc..1b6c961c61 100644 --- a/core/types/bloom9.go +++ b/core/types/bloom9.go @@ -110,9 +110,9 @@ func CreateBloom(receipt *Receipt) Bloom { buf [6]byte ) for _, log := range receipt.Logs { - bin.AddWithBuffer(log.Address.Bytes(), &buf) - for _, b := range log.Topics { - bin.AddWithBuffer(b[:], &buf) + bin.AddWithBuffer(log.Address[:], &buf) + for k := range log.Topics { + bin.AddWithBuffer(log.Topics[k][:], &buf) } } return bin From df51c49770c7d3fd06916b46a75125e33afd3226 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 2 Jul 2026 16:44:04 +0800 Subject: [PATCH 23/44] core/rawdb: fix freezer close sync error message (#35135) Print the expected freezer close sync error before the actual value in TestFreezerCloseSync. The assertion already compared have and want correctly, but the failure message reported them in the wrong order. --- core/rawdb/freezer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/rawdb/freezer_test.go b/core/rawdb/freezer_test.go index 0fc4f90011..2197a29772 100644 --- a/core/rawdb/freezer_test.go +++ b/core/rawdb/freezer_test.go @@ -516,7 +516,7 @@ func TestFreezerCloseSync(t *testing.T) { if err := f.SyncAncient(); err == nil { t.Fatalf("want error, have nil") } else if have, want := err.Error(), "[closed closed]"; have != want { - t.Fatalf("want %v, have %v", have, want) + t.Fatalf("want %v, have %v", want, have) } } From 46b79ea078fc77d5a43fc155d91a5899743762d2 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 2 Jul 2026 18:34:06 +0800 Subject: [PATCH 24/44] cmd/devp2p: fix test error message formats (#35138) Include the actual and expected request IDs in the devp2p test response failure message. This also fixes the typo in the diagnostic so mismatched responses report a useful error. --------- Co-authored-by: Bosul Mun --- cmd/devp2p/internal/ethtest/suite.go | 6 +++--- cmd/devp2p/internal/v5test/discv5tests.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/devp2p/internal/ethtest/suite.go b/cmd/devp2p/internal/ethtest/suite.go index 6dc77941c1..fa883806c8 100644 --- a/cmd/devp2p/internal/ethtest/suite.go +++ b/cmd/devp2p/internal/ethtest/suite.go @@ -423,7 +423,7 @@ func (s *Suite) TestGetBlockBodies(t *utesting.T) { t.Fatalf("error reading block bodies msg: %v", err) } if got, want := resp.RequestId, req.RequestId; got != want { - t.Fatalf("unexpected request id in respond", got, want) + t.Fatalf("unexpected request id in response: got %d, want %d", got, want) } if resp.List.Len() != len(req.GetBlockBodiesRequest) { t.Fatalf("wrong bodies in response: expected %d bodies, got %d", len(req.GetBlockBodiesRequest), resp.List.Len()) @@ -467,7 +467,7 @@ func (s *Suite) TestGetReceipts(t *utesting.T) { t.Fatalf("error reading block receipts msg: %v", err) } if got, want := resp.RequestId, req.RequestId; got != want { - t.Fatalf("unexpected request id in respond", got, want) + t.Fatalf("unexpected request id in response: got %d, want %d", got, want) } if resp.List.Len() != len(req.GetReceiptsRequest) { t.Fatalf("wrong receipts in response: expected %d receipts, got %d", len(req.GetReceiptsRequest), resp.List.Len()) @@ -488,7 +488,7 @@ func (s *Suite) TestGetReceipts(t *utesting.T) { t.Fatalf("error reading block receipts msg: %v", err) } if got, want := resp.RequestId, req.RequestId; got != want { - t.Fatalf("unexpected request id in respond", got, want) + t.Fatalf("unexpected request id in response: got %d, want %d", got, want) } if resp.List.Len() != len(req.GetReceiptsRequest) { t.Fatalf("wrong receipts in response: expected %d receipts, got %d", len(req.GetReceiptsRequest), resp.List.Len()) diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index 4dc2507693..17d39d5ad0 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -186,7 +186,7 @@ func (s *Suite) TestHandshakeResend(t *utesting.T) { t.Fatalf("wrong nonce %x in WHOAREYOU (want %x)", resp.Nonce[:], challenge1.Nonce[:]) } if !bytes.Equal(resp.ChallengeData, challenge1.ChallengeData) { - t.Fatalf("wrong ChallengeData in resent WHOAREYOU (want %x)", resp.ChallengeData, challenge1.ChallengeData) + t.Fatalf("wrong ChallengeData in resent WHOAREYOU: got %x, want %x", resp.ChallengeData, challenge1.ChallengeData) } resp.Node = conn.remote default: From c8953d10c238519389c1c806951a35c9db3482f6 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Thu, 2 Jul 2026 20:56:18 +0800 Subject: [PATCH 25/44] core, consensus/misc, params: implement EIP-7997 (#35223) EIP: https://eips.ethereum.org/EIPS/eip-7997 --- consensus/misc/eip7997.go | 47 ++++++++++++++++ core/chain_makers.go | 5 ++ core/eip7997_test.go | 113 ++++++++++++++++++++++++++++++++++++++ core/genesis.go | 2 + core/state_processor.go | 18 ++++++ params/protocol_params.go | 4 ++ 6 files changed, 189 insertions(+) create mode 100644 consensus/misc/eip7997.go create mode 100644 core/eip7997_test.go diff --git a/consensus/misc/eip7997.go b/consensus/misc/eip7997.go new file mode 100644 index 0000000000..8564d5283a --- /dev/null +++ b/consensus/misc/eip7997.go @@ -0,0 +1,47 @@ +// 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 misc + +import ( + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" +) + +// ApplyEIP7997 inserts the deterministic deployment factory into the state as an +// irregular state transition, as specified by EIP-7997. The factory is a keyless +// CREATE2 factory that, once present at the canonical address on every EVM chain, +// allows contracts to be deployed at identical addresses across chains. +func ApplyEIP7997(statedb vm.StateDB) { + // The account must hold the canonical factory runtime code. If its code hash + // already matches, the chain satisfies EIP-7997 and nothing needs to change. + wantHash := crypto.Keccak256Hash(params.DeterministicFactoryCode) + if statedb.GetCodeHash(params.DeterministicFactoryAddress) == wantHash { + return + } + if !statedb.Exist(params.DeterministicFactoryAddress) { + statedb.CreateAccount(params.DeterministicFactoryAddress) + } + statedb.CreateContract(params.DeterministicFactoryAddress) + statedb.SetCode(params.DeterministicFactoryAddress, params.DeterministicFactoryCode, tracing.CodeChangeUnspecified) + + // Preserve a pre-existing nonce; only bump the default zero nonce to 1. + if statedb.GetNonce(params.DeterministicFactoryAddress) == 0 { + statedb.SetNonce(params.DeterministicFactoryAddress, 1, tracing.NonceChangeNewContract) + } +} diff --git a/core/chain_makers.go b/core/chain_makers.go index d93ce80dca..f02e0341bb 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -385,6 +385,11 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse if config.DAOForkSupport && config.DAOForkBlock != nil && config.DAOForkBlock.Cmp(b.header.Number) == 0 { misc.ApplyDAOHardFork(statedb) } + // EIP-7997: insert the deterministic deployment factory at the Amsterdam + // activation block via an irregular state transition. + if config.IsAmsterdam(b.header.Number, b.header.Time) && !config.IsAmsterdam(parent.Number(), parent.Time()) { + misc.ApplyEIP7997(statedb) + } if config.IsPrague(b.header.Number, b.header.Time) || config.IsUBT(b.header.Number, b.header.Time) { // EIP-2935 diff --git a/core/eip7997_test.go b/core/eip7997_test.go new file mode 100644 index 0000000000..4c4d26507b --- /dev/null +++ b/core/eip7997_test.go @@ -0,0 +1,113 @@ +// 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 . + +// Tests for EIP-7997: the deterministic deployment factory inserted as an +// irregular state transition at the Amsterdam activation block. + +package core + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// TestApplyEIP7997 verifies the irregular state transition seeds the factory +// account with the canonical code and nonce. +func TestApplyEIP7997(t *testing.T) { + sdb := mkState(nil) + misc.ApplyEIP7997(sdb) + + if got := sdb.GetCode(params.DeterministicFactoryAddress); !bytes.Equal(got, params.DeterministicFactoryCode) { + t.Fatalf("factory code mismatch:\n got %x\nwant %x", got, params.DeterministicFactoryCode) + } + if got := sdb.GetNonce(params.DeterministicFactoryAddress); got != 1 { + t.Fatalf("factory nonce = %d, want %d", got, 1) + } +} + +// TestApplyEIP7997Existing checks that a chain which already hosts the factory +// (for example via its keyless creation transaction) is left untouched, so the +// transition never rewrites an existing nonce. +func TestApplyEIP7997Existing(t *testing.T) { + sdb := mkState(types.GenesisAlloc{ + params.DeterministicFactoryAddress: {Code: params.DeterministicFactoryCode, Nonce: 5}, + }) + misc.ApplyEIP7997(sdb) + + if got := sdb.GetNonce(params.DeterministicFactoryAddress); got != 5 { + t.Fatalf("existing factory nonce overwritten: got %d, want 5", got) + } +} + +// TestApplyEIP7997WrongCode checks that an account occupying the factory address +// with the wrong code is force-overwritten with the canonical runtime code, while +// a pre-existing non-zero nonce is preserved. +func TestApplyEIP7997WrongCode(t *testing.T) { + sdb := mkState(types.GenesisAlloc{ + params.DeterministicFactoryAddress: {Code: []byte{0x60, 0x00}, Nonce: 7}, + }) + misc.ApplyEIP7997(sdb) + + if got := sdb.GetCode(params.DeterministicFactoryAddress); !bytes.Equal(got, params.DeterministicFactoryCode) { + t.Fatalf("factory code not overwritten:\n got %x\nwant %x", got, params.DeterministicFactoryCode) + } + if got := sdb.GetNonce(params.DeterministicFactoryAddress); got != 7 { + t.Fatalf("factory nonce = %d, want %d (existing nonce must be preserved)", got, 7) + } +} + +// TestEIP7997FactoryDeploys exercises the inserted factory bytecode: calling it +// with a salt followed by init code must CREATE2-deploy the contract at the +// canonical deterministic address and return that address (20 bytes, unpadded). +func TestEIP7997FactoryDeploys(t *testing.T) { + sdb := mkState(nil) + misc.ApplyEIP7997(sdb) + + var ( + caller = common.Address{0xca} + salt [32]byte + // initcode returning the single-byte runtime 0xfe: + // PUSH1 0xfe PUSH1 0x00 MSTORE8 PUSH1 0x01 PUSH1 0x00 RETURN + initcode = common.FromHex("60fe60005360016000f3") + ) + salt[31] = 0x42 + + input := append(append([]byte{}, salt[:]...), initcode...) + + ret, _, err := amsterdamCoreEVM(sdb).Call(caller, params.DeterministicFactoryAddress, input, vm.NewGasBudget(10_000_000, 0), new(uint256.Int)) + if err != nil { + t.Fatalf("factory call failed: %v", err) + } + + want := crypto.CreateAddress2(params.DeterministicFactoryAddress, salt, crypto.Keccak256(initcode)) + if len(ret) != 20 { + t.Fatalf("factory returned %d bytes, want 20", len(ret)) + } + if got := common.BytesToAddress(ret); got != want { + t.Fatalf("factory returned address %x, want %x", got, want) + } + if code := sdb.GetCode(want); !bytes.Equal(code, []byte{0xfe}) { + t.Fatalf("deployed runtime code = %x, want fe", code) + } +} diff --git a/core/genesis.go b/core/genesis.go index 910a3fb0f5..7fad915ce2 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -725,6 +725,8 @@ func DeveloperGenesisBlock(gasLimit uint64, faucet *common.Address) *Genesis { // EIP-8282 - Builder Execution Requests params.BuilderDepositAddress: {Nonce: 1, Code: params.BuilderDepositCode, Balance: common.Big0}, params.BuilderExitAddress: {Nonce: 1, Code: params.BuilderExitCode, Balance: common.Big0}, + // EIP-7997 - Deterministic deployment factory + params.DeterministicFactoryAddress: {Nonce: 1, Code: params.DeterministicFactoryCode, Balance: common.Big0}, }, } if faucet != nil { diff --git a/core/state_processor.go b/core/state_processor.go index 5b81abef6f..bc565db6f9 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -81,6 +81,11 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated if config.DAOForkSupport && config.DAOForkBlock != nil && config.DAOForkBlock.Cmp(block.Number()) == 0 { misc.ApplyDAOHardFork(tracingStateDB) } + // EIP-7997: insert the deterministic deployment factory at the Amsterdam + // activation block via an irregular state transition. + if isEIP7997Transition(config, p.chain, header) { + misc.ApplyEIP7997(tracingStateDB) + } var ( context = NewEVMBlockContext(header, p.chain, nil) signer = types.MakeSigner(config, header.Number, header.Time) @@ -137,6 +142,19 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated }, nil } +// isEIP7997Transition reports whether the given header belongs to the first block +// on which the Amsterdam fork is active. +func isEIP7997Transition(config *params.ChainConfig, chain ChainContext, header *types.Header) bool { + if header.Number.Sign() == 0 || !config.IsAmsterdam(header.Number, header.Time) { + return false + } + parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1) + if parent == nil { + return false + } + return !config.IsAmsterdam(parent.Number, parent.Time) +} + // PreExecution processes pre-execution system calls. func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Hash, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) *bal.ConstructionBlockAccessList { _, _, spanEnd := telemetry.StartSpan(ctx, "core.preExecution") diff --git a/params/protocol_params.go b/params/protocol_params.go index a56e810074..cace632931 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -270,6 +270,10 @@ var ( BuilderDepositCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe146101065760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461023457600182026001905f5b5f82111560695781019083028483029004916001019190604e565b90939004925050503660b814608957366102345734610234575f5260205ff35b8034106102345760383567ffffffffffffffff1680633b9aca001161023457633b9aca00029034031061023457600154600101600155600354806006026004015f358155600101602035815560010160403581556001016060358155600101608035815560010160a035905560b85f5f3760b85fa0600101600355005b600354600254808203806101001161011d57506101005b5f5b8181146101c3578281016006026004018160b8028154815260200181600101548152602001816002015480825260401c67ffffffffffffffff16816010018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360200181600301548152602001816004015481526020019060050154905260010161011f565b91018092146101d557906002556101e0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561020d57505f5b6001546020828201116102225750505f610228565b01602090035b5f555f60015560b8025ff35b5f5ffd") BuilderExitAddress = common.HexToAddress("0x000014574A74c805590AFF9499fc7A690f008282") BuilderExitCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461018857600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603014608857366101885734610188575f5260205ff35b341061018857600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101175782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160e1565b91018092146101295790600255610134565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561016157505f5b6001546002828201116101765750505f61017c565b01600290035b5f555f6001556044025ff35b5f5ffd") + + // EIP-7997 - Deterministic deployment factory (keyless CREATE2 factory) + DeterministicFactoryAddress = common.HexToAddress("0x4e59b44847b379578588920cA78FbF26c0B4956C") + DeterministicFactoryCode = common.FromHex("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3") ) // System log events. From 448515a4482506047ae361b757e354c452a3d4a8 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 3 Jul 2026 00:12:58 +0800 Subject: [PATCH 26/44] accounts/external: forward blob fee cap to external signer (#35167) SignTx populated BlobHashes and the sidecar fields (Blobs/Commitments/ Proofs) for a blob transaction but never set args.BlobFeeCap. As a result the external (clef) signer received maxFeePerBlobGas:null and signed a transaction inconsistent with the one passed in, silently dropping the blob fee cap. Set args.BlobFeeCap from tx.BlobGasFeeCap() so the signing request faithfully reflects the input transaction. This mirrors the existing handling of the other blob-tx fields. --- accounts/external/backend.go | 1 + 1 file changed, 1 insertion(+) diff --git a/accounts/external/backend.go b/accounts/external/backend.go index 42eaf661cc..d73fb06ad0 100644 --- a/accounts/external/backend.go +++ b/accounts/external/backend.go @@ -237,6 +237,7 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio } if tx.Type() == types.BlobTxType { args.BlobHashes = tx.BlobHashes() + args.BlobFeeCap = (*hexutil.Big)(tx.BlobGasFeeCap()) sidecar := tx.BlobTxSidecar() if sidecar == nil { return nil, errors.New("blobs must be present for signing") From f9417bb279956e2a62a5077a631383d4a83daaf5 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 3 Jul 2026 00:18:58 +0800 Subject: [PATCH 27/44] node: match auth-schema case insensitively (#35022) --- node/jwt_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/jwt_handler.go b/node/jwt_handler.go index 637ae19686..4f85c12367 100644 --- a/node/jwt_handler.go +++ b/node/jwt_handler.go @@ -47,7 +47,7 @@ func (handler *jwtHandler) ServeHTTP(out http.ResponseWriter, r *http.Request) { strToken string claims jwt.RegisteredClaims ) - if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") { + if auth := r.Header.Get("Authorization"); len(auth) >= 7 && strings.EqualFold(auth[:7], "bearer ") { strToken = strings.TrimPrefix(auth, "Bearer ") } if len(strToken) == 0 { From 7aa7806c0958ed943965e8180fdeeecea8171516 Mon Sep 17 00:00:00 2001 From: ozpool <151670776+ozpool@users.noreply.github.com> Date: Thu, 2 Jul 2026 21:50:25 +0530 Subject: [PATCH 28/44] cmd/devp2p: close the iterator in discv4 LookupRandom (#35206) The LookupRandom RPC handler obtains a node iterator from RandomNodes() but never closes it. RandomNodes() returns a lookupIterator backed by a cancelable context derived from the listener's lifetime context (newLookupIterator -> context.WithCancel). The iterator's Close() is what calls the cancel func, so each LookupRandom call that never closes leaks the cancel func (and any in-flight lookup goroutine) until the discv4 listener shuts down. When the listener serves the RPC API (discv4 --rpc) it is long-lived, so repeated LookupRandom calls accumulate the leak. Close the iterator when the handler returns. This matches how the crawler already releases the same RandomNodes() iterators (crawl.go closes every iterator it consumes). --- cmd/devp2p/discv4cmd.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/devp2p/discv4cmd.go b/cmd/devp2p/discv4cmd.go index 1089210c57..98b04311b4 100644 --- a/cmd/devp2p/discv4cmd.go +++ b/cmd/devp2p/discv4cmd.go @@ -430,6 +430,7 @@ type discv4API struct { func (api *discv4API) LookupRandom(n int) (ns []*enode.Node) { it := api.host.RandomNodes() + defer it.Close() for len(ns) < n && it.Next() { ns = append(ns, it.Node()) } From 3d50e3eb0ec3ef626900a6dce9fd04e052b61054 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Fri, 3 Jul 2026 14:30:56 +0800 Subject: [PATCH 29/44] core/types/bal: enhance the bal validation (#35281) EIP-7928 requires that each address appears exactly once in the block access list. In the `BlockAccessList.Validate`, the strict `isStrictlySortedFunc` is required to detect the duplicated accounts. --- core/types/bal/bal_encoding.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 522192cee8..db891b92d0 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -80,7 +80,7 @@ func (e *BlockAccessList) DecodeRLP(s *rlp.Stream) error { // according to the spec or any code changes are contained which exceed protocol // max code size. func (e *BlockAccessList) Validate(blockGasLimit uint64, blockTxCount int) error { - if !slices.IsSortedFunc(*e, func(a, b AccountAccess) int { + if !isStrictlySortedFunc(*e, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) { return errors.New("block access list accounts not in lexicographic order") From d617f4fcdb11d02b4482765d9b6e8e7e1d44e046 Mon Sep 17 00:00:00 2001 From: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:31:53 +0300 Subject: [PATCH 30/44] rpc: reject block parameter with neither number nor hash (#35271) --- rpc/types.go | 3 +++ rpc/types_test.go | 2 ++ 2 files changed, 5 insertions(+) diff --git a/rpc/types.go b/rpc/types.go index 578d3f86dd..7720a8d9c6 100644 --- a/rpc/types.go +++ b/rpc/types.go @@ -158,6 +158,9 @@ func (bnh *BlockNumberOrHash) UnmarshalJSON(data []byte) error { if e.BlockNumber != nil && e.BlockHash != nil { return errors.New("cannot specify both BlockHash and BlockNumber, choose one or the other") } + if e.BlockNumber == nil && e.BlockHash == nil { + return errors.New("must specify either BlockHash or BlockNumber") + } bnh.BlockNumber = e.BlockNumber bnh.BlockHash = e.BlockHash bnh.RequireCanonical = e.RequireCanonical diff --git a/rpc/types_test.go b/rpc/types_test.go index 9dd6fa6508..35294c3dba 100644 --- a/rpc/types_test.go +++ b/rpc/types_test.go @@ -109,6 +109,8 @@ func TestBlockNumberOrHash_UnmarshalJSON(t *testing.T) { 27: {`{"blockNumber":"safe"}`, false, BlockNumberOrHashWithNumber(SafeBlockNumber)}, 28: {`{"blockNumber":"finalized"}`, false, BlockNumberOrHashWithNumber(FinalizedBlockNumber)}, 29: {`{"blockNumber":"0x1", "blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}`, true, BlockNumberOrHash{}}, + 30: {`{}`, true, BlockNumberOrHash{}}, + 31: {`{"requireCanonical":true}`, true, BlockNumberOrHash{}}, } for i, test := range tests { From aa3d286f543c3940a00169334693579c0f364558 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 3 Jul 2026 14:59:28 +0800 Subject: [PATCH 31/44] cmd/utils: keep metrics tag values containing '=' (#35168) --- cmd/utils/flags.go | 2 +- cmd/utils/flags_test.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 9d06e54f6c..a6f7576ea2 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -2284,7 +2284,7 @@ func SplitTagsFlag(tagsFlag string) map[string]string { for _, t := range tags { if t != "" { - kv := strings.Split(t, "=") + kv := strings.SplitN(t, "=", 2) if len(kv) == 2 { tagsMap[kv[0]] = kv[1] diff --git a/cmd/utils/flags_test.go b/cmd/utils/flags_test.go index 0be3370d4a..382bcd9eec 100644 --- a/cmd/utils/flags_test.go +++ b/cmd/utils/flags_test.go @@ -52,7 +52,9 @@ func Test_SplitTagsFlag(t *testing.T) { { "garbage", "smth=smthelse=123", - map[string]string{}, + map[string]string{ + "smth": "smthelse=123", + }, }, } for _, tt := range tests { From 3006c4411bec41d619135c51b2e724c852a070cc Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Fri, 3 Jul 2026 17:15:07 +0800 Subject: [PATCH 32/44] eth/protocols/eth: fix blockAccessList empty marker (#35286) This PR addresses an issue in the eth71 `BlockAccessListsMsg` handler, specifically: - if the requested bal is not accessible in the server side, 0x80 (EmptyString) will be returned as the marker - at the client side, old message definition `rlp.RawList[RawBlockAccessList]` assumes all the elements are List - the message with 0x80 (kind = string) won't be decoded correctly - the peer will be disconnected The message definition has been changed to `rlp.RawList[rlp.RawValue]`, which is aligned with the one in SNAP/2 protocol. --- eth/protocols/eth/handler_test.go | 38 ++++++++++++++++++++++++++++++- eth/protocols/eth/handlers.go | 11 ++++++--- eth/protocols/eth/peer.go | 2 +- eth/protocols/eth/protocol.go | 12 ++++------ 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go index 874b4ad5a5..117e1b0aba 100644 --- a/eth/protocols/eth/handler_test.go +++ b/eth/protocols/eth/handler_test.go @@ -742,7 +742,7 @@ func testGetBlockAccessLists(t *testing.T, protocol uint) { var ( hashes []common.Hash - expect rlp.RawList[RawBlockAccessList] + expect rlp.RawList[rlp.RawValue] ) for i := uint64(0); i <= backend.chain.CurrentBlock().Number.Uint64(); i++ { block := backend.chain.GetBlockByNumber(i) @@ -771,6 +771,42 @@ func testGetBlockAccessLists(t *testing.T, protocol uint) { } } +// TestBlockAccessListsUnavailableDecode checks that a BlockAccessLists response +// containing the EIP-8159 unavailability marker (RLP empty string). +func TestBlockAccessListsUnavailableDecode(t *testing.T) { + t.Parallel() + + balRaw := makeTestBAL(t, common.Address{0x11}) + + // Assemble a response the way the serving side does, with the middle + // entry signaled as unavailable. + var list rlp.RawList[rlp.RawValue] + list.AppendRaw(balRaw) + list.AppendRaw(rlp.EmptyString) + list.AppendRaw(balRaw) + + enc, err := rlp.EncodeToBytes(&BlockAccessListPacket{RequestId: 42, List: list}) + if err != nil { + t.Fatalf("failed to encode packet: %v", err) + } + var packet BlockAccessListPacket + if err := rlp.DecodeBytes(enc, &packet); err != nil { + t.Fatalf("failed to decode packet: %v", err) + } + bals, err := packet.List.Items() + if err != nil { + t.Fatalf("failed to decode BAL entries: %v", err) + } + if len(bals) != 3 { + t.Fatalf("wrong entry count: got %d, want 3", len(bals)) + } + for i, want := range [][]byte{balRaw, rlp.EmptyString, balRaw} { + if !bytes.Equal(bals[i], want) { + t.Errorf("entry %d mismatch: got %x, want %x", i, bals[i], want) + } + } +} + type decoder struct { msg []byte } diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go index 71942cc9ad..f8f5c8e8e7 100644 --- a/eth/protocols/eth/handlers.go +++ b/eth/protocols/eth/handlers.go @@ -679,10 +679,10 @@ func handleGetBlockAccessLists(backend Backend, msg Decoder, peer *Peer) error { // serviceGetBlockAccessListsQuery assembles the response to a BAL query. // Unavailable BALs are returned as empty list entries. -func serviceGetBlockAccessListsQuery(chain *core.BlockChain, query GetBlockAccessListsRequest) rlp.RawList[RawBlockAccessList] { +func serviceGetBlockAccessListsQuery(chain *core.BlockChain, query GetBlockAccessListsRequest) rlp.RawList[rlp.RawValue] { var ( bytes int - bals rlp.RawList[RawBlockAccessList] + bals rlp.RawList[rlp.RawValue] ) for _, hash := range query { if bytes >= softResponseLimit || bals.Len() >= maxBALsServe { @@ -720,7 +720,12 @@ func handleBlockAccessLists(backend Backend, msg Decoder, peer *Peer) error { metadata := func() interface{} { hashes := make([]common.Hash, len(bals)) for i := range bals { - hashes[i] = crypto.Keccak256Hash(bals[i].Bytes()) + // Unavailable BALs (signaled by the empty string) are marked + // with the zero hash + if bytes.Equal(bals[i], rlp.EmptyString) { + continue + } + hashes[i] = crypto.Keccak256Hash(bals[i]) } return hashes } diff --git a/eth/protocols/eth/peer.go b/eth/protocols/eth/peer.go index 63f93840c3..1e33fbe408 100644 --- a/eth/protocols/eth/peer.go +++ b/eth/protocols/eth/peer.go @@ -258,7 +258,7 @@ func (p *Peer) ReplyReceiptsRLP70(id uint64, receipts rlp.RawList[*ReceiptList], } // ReplyBlockAccessLists is the response to GetBlockAccessLists (EIP-8159). -func (p *Peer) ReplyBlockAccessLists(id uint64, list rlp.RawList[RawBlockAccessList]) error { +func (p *Peer) ReplyBlockAccessLists(id uint64, list rlp.RawList[rlp.RawValue]) error { return p2p.Send(p.rw, BlockAccessListsMsg, &BlockAccessListPacket{ RequestId: id, List: list, diff --git a/eth/protocols/eth/protocol.go b/eth/protocols/eth/protocol.go index a6c45f83ec..e7ffcf6d5e 100644 --- a/eth/protocols/eth/protocol.go +++ b/eth/protocols/eth/protocol.go @@ -24,7 +24,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/forkid" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/rlp" ) @@ -299,15 +298,14 @@ type GetBlockAccessListsPacket struct { GetBlockAccessListsRequest } -type RawBlockAccessList struct { - rlp.RawList[bal.AccountAccess] -} - -type BlockAccessListResponse []RawBlockAccessList +// BlockAccessListResponse holds one raw entry per requested hash. Entries are +// kept as raw values because, per EIP-8159, the RLP empty string signals an +// unavailable BAL (an empty list is itself a valid BAL). +type BlockAccessListResponse []rlp.RawValue type BlockAccessListPacket struct { RequestId uint64 - List rlp.RawList[RawBlockAccessList] + List rlp.RawList[rlp.RawValue] } func (*StatusPacket) Name() string { return "Status" } From 6eae868a95d082a522e86eb391263aa7539f3e96 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 3 Jul 2026 19:13:46 +0800 Subject: [PATCH 33/44] p2p/discover: always increment TALKREQ drop counter on timeout (#35243) Increment `dropCount` on every TALKREQ dropped due to overload timeout. Previously the counter was only updated when the throttled warning log was emitted. --- p2p/discover/v5_talk.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/discover/v5_talk.go b/p2p/discover/v5_talk.go index dca09870d8..648e091769 100644 --- a/p2p/discover/v5_talk.go +++ b/p2p/discover/v5_talk.go @@ -102,10 +102,10 @@ func (t *talkSystem) handleRequest(id enode.ID, addr netip.AddrPort, req *v5wire }() case <-timeout.C: // Couldn't get it in time, drop the request. + t.dropCount++ if time.Since(t.lastLog) > 5*time.Second { log.Warn("Dropping TALKREQ due to overload", "ndrop", t.dropCount) t.lastLog = time.Now() - t.dropCount++ } case <-t.transport.closeCtx.Done(): // Transport closed, drop the request. From 217506364da76b5ca35bc00d55597dcff5547803 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 3 Jul 2026 20:50:59 +0800 Subject: [PATCH 34/44] accounts: fix missing wallet drop (#35133) The drop helper uses sort.Search on the sorted wallet list, but the returned index can point at the next greater wallet when the target URL is missing. Fix this by rewriting the drop to use slices.DeleteFunc and a map. --------- Co-authored-by: Felix Lange --- accounts/manager.go | 16 +++++++------- accounts/manager_test.go | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 accounts/manager_test.go diff --git a/accounts/manager.go b/accounts/manager.go index a2218e54dd..ebf7bbd62c 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -18,6 +18,7 @@ package accounts import ( "reflect" + "slices" "sort" "sync" @@ -254,13 +255,12 @@ func merge(slice []Wallet, wallets ...Wallet) []Wallet { // drop is the counterpart of merge, which looks up wallets from within the sorted // cache and removes the ones specified. func drop(slice []Wallet, wallets ...Wallet) []Wallet { - for _, wallet := range wallets { - n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) - if n == len(slice) { - // Wallet not found, may happen during startup - continue - } - slice = append(slice[:n], slice[n+1:]...) + remove := make(map[URL]struct{}, len(wallets)) + for _, w := range wallets { + remove[w.URL()] = struct{}{} } - return slice + return slices.DeleteFunc(slice, func(w Wallet) bool { + _, ok := remove[w.URL()] + return ok + }) } diff --git a/accounts/manager_test.go b/accounts/manager_test.go new file mode 100644 index 0000000000..6727be156e --- /dev/null +++ b/accounts/manager_test.go @@ -0,0 +1,47 @@ +// 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 accounts + +import "testing" + +type managerTestWallet struct { + Wallet + url URL +} + +func (w managerTestWallet) URL() URL { + return w.url +} + +func TestDropMissingWallet(t *testing.T) { + t.Parallel() + + wallets := []Wallet{ + managerTestWallet{url: URL{Scheme: "test", Path: "a"}}, + managerTestWallet{url: URL{Scheme: "test", Path: "c"}}, + } + dropped := drop(wallets, managerTestWallet{url: URL{Scheme: "test", Path: "b"}}) + + if len(dropped) != len(wallets) { + t.Fatalf("drop removed wallet for missing URL: got %d wallets, want %d", len(dropped), len(wallets)) + } + for i := range dropped { + if got, want := dropped[i].URL(), wallets[i].URL(); got != want { + t.Fatalf("wallet %d mismatch: got %v, want %v", i, got, want) + } + } +} From e3b6d0c86f53772dce6780e84414771849fdd925 Mon Sep 17 00:00:00 2001 From: ozpool <151670776+ozpool@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:24:51 +0530 Subject: [PATCH 35/44] cmd/devp2p: fix swapped args in private-key error message (#35193) This fixes an error where a failed key decode prints its arguments in the wrong order. From 415fe376a3ab475a20e0ee08c8bcd47cfea7993c Mon Sep 17 00:00:00 2001 From: rayoo Date: Sun, 5 Jul 2026 11:21:24 -0700 Subject: [PATCH 36/44] p2p/enode: close sources added after FairMix shutdown (#35298) `FairMix.AddSource` takes ownership of the supplied iterator, but if the mixer has already been closed it currently returns without closing that iterator. Close the rejected source in this shutdown path so callers racing with `Close` do not leak iterator resources. --- p2p/enode/iter.go | 1 + 1 file changed, 1 insertion(+) diff --git a/p2p/enode/iter.go b/p2p/enode/iter.go index 54c2fc7258..d0ccf984d3 100644 --- a/p2p/enode/iter.go +++ b/p2p/enode/iter.go @@ -356,6 +356,7 @@ func (m *FairMix) AddSource(it Iterator) { defer m.mu.Unlock() if m.closed == nil { + it.Close() return } m.wg.Add(1) From 2919267b64e4aa9d4dc7ea9262821f7371d083ac Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Mon, 6 Jul 2026 03:39:09 +0200 Subject: [PATCH 37/44] core: remaining spec fixes for glamsterdam (#35279) This PR fixes five remaining differences between master and glamsterdam-devnet-6: - RegularCost for Auths has been increased, since 8037 increases the WARM_ACCESS cost - Floor is anchored to the transaction base cost, not the intrinsic cost - Auth destinations need to be recorded in the BAL before the call is executed - Change the place where intrinsic gas is verified in calls to delegated addresses - Refund state gas directly to reservoir in outer tx frame --------- Co-authored-by: Gary Rong --- core/state_transition.go | 22 ++++++++++++---------- core/vm/gascosts.go | 16 ++++++++++++++++ core/vm/operations_acl.go | 8 +++++--- params/protocol_params.go | 8 +------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/core/state_transition.go b/core/state_transition.go index 877ed0638f..a3a9efeaa4 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -242,12 +242,10 @@ func FloorDataGas(rules params.Rules, from common.Address, to *common.Address, v tokenCost = params.TxCostFloorPerToken } - // The floor is anchored to the transaction base cost. Under EIP-2780 that - // base is the per-resource decomposition (the same one used by the intrinsic - // gas), so the floor never undercuts the transaction's own base. + // The floor is anchored to the transaction base cost. floorBase := params.TxGas if rules.IsAmsterdam { - floorBase = intrinsicBaseGasEIP2780(from, to, value) + floorBase = params.TxBaseCost2780 } // Check for overflow if (math.MaxUint64-floorBase)/tokenCost < tokens { @@ -739,7 +737,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // If the contract creation failed, or the destination was pre-existing, // refund the account-creation state gas pre-charged in IntrinsicGas. if rules.IsAmsterdam && !creation { - st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + st.gasRemaining.RefundStateToReservoir(params.AccountCreationSize * st.evm.Context.CostPerStateByte) } } else { // Increment the nonce for the next transaction. @@ -755,6 +753,10 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // performing the resolution and warming. if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok { st.state.AddAddressToAccessList(addr) + // Record in BAL + if rules.IsAmsterdam { + st.state.GetCode(addr) + } } // EIP-2780: charge the transaction's top-level recipient costs. If the // budget cannot cover the charge, the top frame halts out of gas. @@ -968,7 +970,7 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se authority, err := st.validateAuthorization(auth) if err != nil { if rules.IsAmsterdam { - st.gasRemaining.RefundState((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) + st.gasRemaining.RefundStateToReservoir((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) st.state.AddRefund(params.AccountWriteAmsterdam) } return err @@ -981,7 +983,7 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se } } else { if st.state.Exist(authority) { - st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + st.gasRemaining.RefundStateToReservoir(params.AccountCreationSize * st.evm.Context.CostPerStateByte) st.state.AddRefund(params.AccountWriteAmsterdam) } authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte @@ -993,17 +995,17 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se } if auth.Address == (common.Address{}) { // Clearing writes no indicator, refill this auth's state charge. - st.gasRemaining.RefundState(authBase) + st.gasRemaining.RefundStateToReservoir(authBase) // The indicator was created by an earlier auth within the same // transaction, refill the state charge as it's no longer justified. if curDelegated && !preDelegated { - st.gasRemaining.RefundState(authBase) + st.gasRemaining.RefundStateToReservoir(authBase) } } else if curDelegated || preDelegated { // The 23-byte slot is already occupied, overwriting it writes no // new bytes, refill the state charge. - st.gasRemaining.RefundState(authBase) + st.gasRemaining.RefundStateToReservoir(authBase) } } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 10b8eeb033..76a2c7af60 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -196,6 +196,22 @@ func (g *GasBudget) RefundState(s uint64) { g.UsedStateGas -= int64(s) } +// RefundStateToReservoir credits a state-gas refund directly to the +// reservoir, without repaying spilled regular gas first. +// +// Per the spec's set_delegation, authorization refunds (and the post-create +// new-account refund) are added to message.state_gas_reservoir directly, in +// contrast to the LIFO inline refunds handled by RefundState. The usage +// counter is decremented by the full amount, matching the spec's +// tx_state_gas = intrinsic_state + state_gas_used - state_refund and +// preserving the per-frame invariant: +// +// StateGas + UsedStateGas == initialStateGas + Spilled +func (g *GasBudget) RefundStateToReservoir(s uint64) { + g.StateGas += s + g.UsedStateGas -= int64(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 diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 111cc6a00c..3669626d5a 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -400,7 +400,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc, coldCost uint // 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 { + if !contract.chargeRegular(intrinsicCost, evm.Config.Tracer, tracing.GasChangeIgnored) { return GasCosts{}, ErrOutOfGas } @@ -422,7 +422,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc, coldCost uint } // 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, stack.back(0)) + evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, 0, stack.back(0)) if err != nil { return GasCosts{}, err } @@ -430,11 +430,13 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc, coldCost uint // adding it to the return, it will be charged outside of this function, as // part of the dynamic gas. This will ensure it is correctly reported to // tracers. - contract.Gas.RegularGas += eip2929Cost + eip7702Cost + contract.Gas.RegularGas += eip2929Cost + eip7702Cost + intrinsicCost + // 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 + contract.Gas.UsedRegularGas -= intrinsicCost // Aggregate the gas costs from all components, including EIP-2929, EIP-7702, // the CALL opcode itself, and the cost incurred by nested calls. diff --git a/params/protocol_params.go b/params/protocol_params.go index cace632931..c4a74b794a 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -101,13 +101,7 @@ const ( TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702 - // RegularPerAuthBaseCost is defined in EIP-8037 as the sum of: - // - // - Calldata cost: 1,616 (101 bytes × 16) - // - Recovering authority address (ecRecover) - // - Reading nonce and code of authority (cold access) - // - Storing values in already warm account: 2 x WARM_ACCESS - RegularPerAuthBaseCost uint64 = 7500 + RegularPerAuthBaseCost uint64 = 7816 // As defined by EIP-8037 and EIP-8038 // EIP-2780: resource-based intrinsic transaction gas. TxBaseCost2780 uint64 = 12000 From 3620a947ee5fd487ebd3653e3d5250156361bac3 Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 6 Jul 2026 17:27:34 +0800 Subject: [PATCH 38/44] params: return AmsterdamTime from ChainConfig.Timestamp (#35293) ## Summary - Add the missing `forks.Amsterdam` case to `ChainConfig.Timestamp`. - Callers can now look up the Amsterdam fork activation time. ## Test plan - [x] `go test -short ./params/` --- params/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/params/config.go b/params/config.go index b277fb303c..7b4493609f 100644 --- a/params/config.go +++ b/params/config.go @@ -1231,6 +1231,8 @@ func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 { return c.CancunTime case fork == forks.Shanghai: return c.ShanghaiTime + case fork == forks.Amsterdam: + return c.AmsterdamTime default: return nil } From 7c1959b306fd2094afe353a38577fd929b3624ea Mon Sep 17 00:00:00 2001 From: Rafael Matias Date: Mon, 6 Jul 2026 11:30:32 +0200 Subject: [PATCH 39/44] rpc: add --rpc.http-body-limit flag for HTTP request body size (#35224) The HTTP JSON-RPC server caps request bodies at a hardcoded 5 MB (`defaultBodyLimit`), with no way to raise it. EEST bloatnet depth benchmarks post worst-case batch requests larger than that during fill-stateful, which the server rejects with `413 Request Entity Too Large`. This adds `node.Config.HTTPBodyLimit` (default 5 MB), wired through to the HTTP/WS server config, and exposes it via `--rpc.http-body-limit` (value in **megabytes**, default 5). Default behaviour is unchanged. --- cmd/geth/main.go | 1 + cmd/utils/flags.go | 10 ++++++++++ node/config.go | 3 +++ node/defaults.go | 1 + node/node.go | 1 + 5 files changed, 16 insertions(+) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index d3934a8cb6..31608e85fb 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -177,6 +177,7 @@ var ( utils.AllowUnprotectedTxs, utils.BatchRequestLimit, utils.BatchResponseMaxSize, + utils.HTTPBodyLimitFlag, utils.RPCTxSyncDefaultTimeoutFlag, utils.RPCTxSyncMaxTimeoutFlag, utils.RPCGlobalRangeLimitFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index a6f7576ea2..64c41b00b9 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -861,6 +861,12 @@ var ( Value: node.DefaultConfig.BatchResponseMaxSize, Category: flags.APICategory, } + HTTPBodyLimitFlag = &cli.IntFlag{ + Name: "rpc.http-body-limit", + Usage: "Maximum size (in megabytes) of an HTTP request body", + Value: node.DefaultConfig.HTTPBodyLimit / (1024 * 1024), + Category: flags.APICategory, + } // Network Settings MaxPeersFlag = &cli.IntFlag{ @@ -1361,6 +1367,10 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) { if ctx.IsSet(BatchResponseMaxSize.Name) { cfg.BatchResponseMaxSize = ctx.Int(BatchResponseMaxSize.Name) } + + if ctx.IsSet(HTTPBodyLimitFlag.Name) { + cfg.HTTPBodyLimit = ctx.Int(HTTPBodyLimitFlag.Name) * 1024 * 1024 + } } // setGraphQL creates the GraphQL listener interface string from the set diff --git a/node/config.go b/node/config.go index 77ca78a471..b0f8753a7d 100644 --- a/node/config.go +++ b/node/config.go @@ -198,6 +198,9 @@ type Config struct { // BatchResponseMaxSize is the maximum number of bytes returned from a batched rpc call. BatchResponseMaxSize int `toml:",omitempty"` + // HTTPBodyLimit is the maximum size (in bytes) of an HTTP request body. + HTTPBodyLimit int `toml:",omitempty"` + // JWTSecret is the path to the hex-encoded jwt secret. JWTSecret string `toml:",omitempty"` diff --git a/node/defaults.go b/node/defaults.go index 3410fa2ae5..145507ffe9 100644 --- a/node/defaults.go +++ b/node/defaults.go @@ -67,6 +67,7 @@ var DefaultConfig = Config{ WSModules: []string{"net", "web3"}, BatchRequestLimit: 1000, BatchResponseMaxSize: 25 * 1000 * 1000, + HTTPBodyLimit: 5 * 1024 * 1024, GraphQLVirtualHosts: []string{"localhost"}, P2P: p2p.Config{ ListenAddr: ":30303", diff --git a/node/node.go b/node/node.go index 7c0d69775c..3dd780f42b 100644 --- a/node/node.go +++ b/node/node.go @@ -392,6 +392,7 @@ func (n *Node) startRPC() error { rpcConfig := rpcEndpointConfig{ batchItemLimit: n.config.BatchRequestLimit, batchResponseSizeLimit: n.config.BatchResponseMaxSize, + httpBodyLimit: n.config.HTTPBodyLimit, } initHttp := func(server *httpServer, port int) error { From 69d88aee93d85ec86931b1ba93967d0d0fdacd12 Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 6 Jul 2026 17:31:08 +0800 Subject: [PATCH 40/44] eth/tracers: include log index in erc7562 tracer output (#35200) Populate the Index field on captured logs, matching callTracer behaviour. --- eth/tracers/native/erc7562.go | 1 + 1 file changed, 1 insertion(+) diff --git a/eth/tracers/native/erc7562.go b/eth/tracers/native/erc7562.go index 0bf80d77b5..d0a2813381 100644 --- a/eth/tracers/native/erc7562.go +++ b/eth/tracers/native/erc7562.go @@ -308,6 +308,7 @@ func (t *erc7562Tracer) OnLog(log1 *types.Log) { Address: log1.Address, Topics: log1.Topics, Data: log1.Data, + Index: hexutil.Uint(log1.Index), Position: hexutil.Uint(len(t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Calls)), } t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs = append(t.callstackWithOpcodes[len(t.callstackWithOpcodes)-1].Logs, l) From a8a7116395ed435fe223ee75160d3eabddb1a73a Mon Sep 17 00:00:00 2001 From: Richard Creighton Date: Mon, 6 Jul 2026 11:36:29 +0100 Subject: [PATCH 41/44] cmd/evm: respect --fuzz=false (#34975) Passing `--fuzz=false` currently still selects blocktest fuzz output mode because the command checks whether the flag was set. This switches the blocktest fuzz-mode checks to use the boolean flag value, so explicit false keeps the normal output path while `--fuzz` still enables fuzz-oriented reporting. --- cmd/evm/blockrunner.go | 6 ++--- cmd/geth/chaincmd.go | 6 ++--- cmd/geth/main.go | 6 ++--- cmd/utils/flags.go | 4 +-- cmd/utils/flags_test.go | 52 +++++++++++++++++++++++++++++++++++++++ cmd/workload/testsuite.go | 2 +- 6 files changed, 64 insertions(+), 12 deletions(-) diff --git a/cmd/evm/blockrunner.go b/cmd/evm/blockrunner.go index c6fac5396e..b175f05dc7 100644 --- a/cmd/evm/blockrunner.go +++ b/cmd/evm/blockrunner.go @@ -78,7 +78,7 @@ func blockTestCmd(ctx *cli.Context) error { return err } // During fuzzing, we report the result after every block - if !ctx.IsSet(FuzzFlag.Name) { + if !ctx.Bool(FuzzFlag.Name) { report(ctx, results) } } @@ -101,7 +101,7 @@ func runBlockTest(ctx *cli.Context, fname string) ([]testResult, error) { tracer := tracerFromFlags(ctx) // Suppress INFO logs during fuzzing - if ctx.IsSet(FuzzFlag.Name) { + if ctx.Bool(FuzzFlag.Name) { log.SetDefault(log.NewLogger(log.DiscardHandler())) } @@ -140,7 +140,7 @@ func runBlockTest(ctx *cli.Context, fname string) ([]testResult, error) { } // When fuzzing, write results after every block - if ctx.IsSet(FuzzFlag.Name) { + if ctx.Bool(FuzzFlag.Name) { report(ctx, []testResult{*result}) } results = append(results, *result) diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 2c312f4e41..95b0ec274e 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -797,8 +797,8 @@ func downloadEra(ctx *cli.Context) error { var network = "mainnet" if utils.IsNetworkPreset(ctx) { switch { - case ctx.IsSet(utils.MainnetFlag.Name): - case ctx.IsSet(utils.SepoliaFlag.Name): + case ctx.Bool(utils.MainnetFlag.Name): + case ctx.Bool(utils.SepoliaFlag.Name): network = "sepolia" default: return errors.New("unsupported network, no known era1 checksums") @@ -825,7 +825,7 @@ func downloadEra(ctx *cli.Context) error { return err } switch { - case ctx.IsSet(eraAllFlag.Name): + case ctx.Bool(eraAllFlag.Name): return l.DownloadAll(dir) case ctx.IsSet(eraBlockFlag.Name): diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 31608e85fb..14e3a549cb 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -291,13 +291,13 @@ func main() { func prepare(ctx *cli.Context) { // If we're running a known preset, log it for convenience. switch { - case ctx.IsSet(utils.SepoliaFlag.Name): + case ctx.Bool(utils.SepoliaFlag.Name): log.Info("Starting Geth on Sepolia testnet...") - case ctx.IsSet(utils.HoleskyFlag.Name): + case ctx.Bool(utils.HoleskyFlag.Name): log.Info("Starting Geth on Holesky testnet...") - case ctx.IsSet(utils.HoodiFlag.Name): + case ctx.Bool(utils.HoodiFlag.Name): log.Info("Starting Geth on Hoodi testnet...") case !ctx.IsSet(utils.NetworkIdFlag.Name): diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 64c41b00b9..fd9893fe81 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1915,7 +1915,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.IsSet(RPCGlobalTxFeeCapFlag.Name) { cfg.RPCTxFeeCap = ctx.Float64(RPCGlobalTxFeeCapFlag.Name) } - if ctx.IsSet(NoDiscoverFlag.Name) { + if ctx.Bool(NoDiscoverFlag.Name) { cfg.EthDiscoveryURLs, cfg.SnapDiscoveryURLs = []string{}, []string{} } else if ctx.IsSet(DNSDiscoveryFlag.Name) { urls := ctx.String(DNSDiscoveryFlag.Name) @@ -2353,7 +2353,7 @@ func tryMakeReadOnlyDatabase(ctx *cli.Context, stack *node.Node) ethdb.Database func IsNetworkPreset(ctx *cli.Context) bool { for _, flag := range NetworkFlags { bFlag, _ := flag.(*cli.BoolFlag) - if ctx.IsSet(bFlag.Name) { + if ctx.Bool(bFlag.Name) { return true } } diff --git a/cmd/utils/flags_test.go b/cmd/utils/flags_test.go index 382bcd9eec..81525ce3b2 100644 --- a/cmd/utils/flags_test.go +++ b/cmd/utils/flags_test.go @@ -18,8 +18,11 @@ package utils import ( + "flag" "reflect" "testing" + + "github.com/urfave/cli/v2" ) func Test_SplitTagsFlag(t *testing.T) { @@ -66,3 +69,52 @@ func Test_SplitTagsFlag(t *testing.T) { }) } } + +func TestIsNetworkPresetUsesFlagValue(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + args []string + want bool + }{ + { + name: "unset", + want: false, + }, + { + name: "enabled", + args: []string{"--sepolia"}, + want: true, + }, + { + name: "explicit false", + args: []string{"--sepolia=false"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := newTestContext(t, tt.args, NetworkFlags...) + if got := IsNetworkPreset(ctx); got != tt.want { + t.Fatalf("IsNetworkPreset() = %v, want %v", got, tt.want) + } + }) + } +} + +func newTestContext(t *testing.T, args []string, flags ...cli.Flag) *cli.Context { + t.Helper() + + set := flag.NewFlagSet("test", flag.ContinueOnError) + for _, f := range flags { + if err := f.Apply(set); err != nil { + t.Fatal(err) + } + } + if err := set.Parse(args); err != nil { + t.Fatal(err) + } + return cli.NewContext(nil, set, nil) +} diff --git a/cmd/workload/testsuite.go b/cmd/workload/testsuite.go index 4e33522f1b..998dbfe165 100644 --- a/cmd/workload/testsuite.go +++ b/cmd/workload/testsuite.go @@ -122,7 +122,7 @@ func validateHistoryPruneErr(err error, blockNum uint64, historyPruneBlock *uint func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) { flags.CheckExclusive(ctx, testMainnetFlag, testSepoliaFlag) - if (ctx.IsSet(testMainnetFlag.Name) || ctx.IsSet(testSepoliaFlag.Name)) && ctx.IsSet(filterQueryFileFlag.Name) { + if (ctx.Bool(testMainnetFlag.Name) || ctx.Bool(testSepoliaFlag.Name)) && ctx.IsSet(filterQueryFileFlag.Name) { exit(filterQueryFileFlag.Name + " cannot be used with " + testMainnetFlag.Name + " or " + testSepoliaFlag.Name) } From e5c5e1897d2740b42d7fd77e5730f898543a2649 Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 6 Jul 2026 22:10:25 +0800 Subject: [PATCH 42/44] core/txpool/locals: nil journal writer before checking close error (#35300) This PR clears `journal.writer` immediately after `Close()` in `rotate` and `setupWriter`, before checking the error. This prevents leaving a stale file handle that could be reused if close fails. --------- Co-authored-by: Bosul Mun --- core/txpool/locals/journal.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/txpool/locals/journal.go b/core/txpool/locals/journal.go index cd2be8a794..aac59bd91f 100644 --- a/core/txpool/locals/journal.go +++ b/core/txpool/locals/journal.go @@ -119,10 +119,11 @@ func (journal *journal) load(add func([]*types.Transaction) []error) error { func (journal *journal) setupWriter() error { if journal.writer != nil { - if err := journal.writer.Close(); err != nil { + err := journal.writer.Close() + journal.writer = nil + if err != nil { return err } - journal.writer = nil } // Re-open the journal file for appending @@ -152,10 +153,11 @@ func (journal *journal) insert(tx *types.Transaction) error { func (journal *journal) rotate(all map[common.Address]types.Transactions) error { // Close the current journal (if any is open) if journal.writer != nil { - if err := journal.writer.Close(); err != nil { + err := journal.writer.Close() + journal.writer = nil + if err != nil { return err } - journal.writer = nil } // Generate a new journal with the contents of the current pool replacement, err := os.OpenFile(journal.path+".new", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) From cf35a1b6f6f7aed8fdc12a782645cfa96711afe8 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 7 Jul 2026 20:38:08 +0800 Subject: [PATCH 43/44] core/vm: add access cost check (#35261) This is an edge case found by @weiihann. Under 8038, the cold storage access cost is increased to 3,000 gas, which exceeds the sentry check threshold. Therefore, the sentry check no longer guarantees that the remaining gas is sufficient to cover a cold slot access. Therefore, an additional access affordability check is added to eliminate the potential DoS vector. --- core/vm/eip8038_test.go | 17 +++++++++++++++++ core/vm/gas_table.go | 15 ++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/core/vm/eip8038_test.go b/core/vm/eip8038_test.go index c08c54fb2c..f20a9d8a0e 100644 --- a/core/vm/eip8038_test.go +++ b/core/vm/eip8038_test.go @@ -21,6 +21,7 @@ package vm import ( + "strings" "testing" "github.com/ethereum/go-ethereum/common" @@ -235,3 +236,19 @@ func TestEIP8038SelfdestructAccountWrite(t *testing.T) { t.Fatalf("state gas = %d, want %d", res.UsedStateGas, want) } } + +// TestEIP8038SStoreAccessGuard covers the affordability check that bails out +// before the slot is read once the gas left cannot cover the slot's access cost. +// The two PUSH1s cost 6, so a 2506 budget leaves 2500 at the SSTORE: above the +// reentrancy sentry (2300) yet below COLD_STORAGE_ACCESS (3000). The guard must +// fire, distinguishable from the sentry/charge OOG by its "slot access" message. +func TestEIP8038SStoreAccessGuard(t *testing.T) { + budget := NewGasBudget(6+params.SstoreSentryGasEIP2200+200, 0) + _, _, err := run8038(t, sstore(0, 1), budget, new(uint256.Int), nil) + if err == nil { + t.Fatal("expected failure: gas left cannot cover cold-slot access") + } + if !strings.Contains(err.Error(), "not enough gas for slot access") { + t.Fatalf("got %q, want the slot-access guard error", err) + } +} diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 8cc63cb236..d8fbf2b461 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -696,10 +696,10 @@ func gasSStore8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memor return GasCosts{}, errors.New("not enough gas for reentrancy sentry") } var ( - y, x = stack.back(1), stack.peek() - slot = common.Hash(x.Bytes32()) - current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) - value = common.Hash(y.Bytes32()) + y, x = stack.back(1), stack.peek() + slot = common.Hash(x.Bytes32()) + value = common.Hash(y.Bytes32()) + stateSet = params.StorageCreationSize * evm.Context.CostPerStateByte ) // Check slot presence in the access list access := params.WarmStorageReadCostEIP2929 @@ -707,7 +707,12 @@ func gasSStore8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memor access = params.ColdStorageAccessAmsterdam evm.StateDB.AddSlotToAccessList(contract.Address(), slot) } - stateSet := params.StorageCreationSize * evm.Context.CostPerStateByte + // Check access cost affordability before reading slot + if contract.Gas.RegularGas < access { + return GasCosts{}, errors.New("not enough gas for slot access") + } + // Read the slot value for gas cost measurement + current, original := evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) if current == value { // noop (1) return GasCosts{RegularGas: access}, nil From 4d2181aa413d05e1fb602209fc362aca229c8661 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 7 Jul 2026 20:40:25 +0800 Subject: [PATCH 44/44] cmd, core, eth, miner: apply 7997 in block building (#35285) This PR applies the 7997 irregular state transition in t8n, block building, simulation and tracing. --- cmd/evm/internal/t8ntool/execution.go | 7 ++++ core/state_processor.go | 34 +++++++---------- eth/state_accessor.go | 2 +- eth/tracers/api.go | 8 ++-- internal/ethapi/simulate.go | 2 +- miner/payload_building_test.go | 55 +++++++++++++++++++++++++++ miner/worker.go | 2 +- 7 files changed, 82 insertions(+), 28 deletions(-) diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index f35b28510c..d711ae56ac 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -242,6 +242,13 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, chainConfig.DAOForkBlock.Cmp(new(big.Int).SetUint64(pre.Env.Number)) == 0 { misc.ApplyDAOHardFork(statedb) } + // EIP-7997: insert the deterministic deployment factory at the Amsterdam + // activation block via an irregular state transition. + if pre.Env.Number > 0 && + chainConfig.IsAmsterdam(new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) && + !chainConfig.IsAmsterdam(new(big.Int).SetUint64(pre.Env.Number-1), pre.Env.ParentTimestamp) { + misc.ApplyEIP7997(statedb) + } evm := vm.NewEVM(vmContext, statedb, chainConfig, vmConfig) if beaconRoot := pre.Env.ParentBeaconBlockRoot; beaconRoot != nil { core.ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList) diff --git a/core/state_processor.go b/core/state_processor.go index bc565db6f9..27009cb49a 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -81,10 +81,9 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated if config.DAOForkSupport && config.DAOForkBlock != nil && config.DAOForkBlock.Cmp(block.Number()) == 0 { misc.ApplyDAOHardFork(tracingStateDB) } - // EIP-7997: insert the deterministic deployment factory at the Amsterdam - // activation block via an irregular state transition. - if isEIP7997Transition(config, p.chain, header) { - misc.ApplyEIP7997(tracingStateDB) + parent := p.chain.GetHeader(block.ParentHash(), block.NumberU64()-1) + if parent == nil { + return nil, fmt.Errorf("missing parent %#x", block.ParentHash()) } var ( context = NewEVMBlockContext(header, p.chain, nil) @@ -98,7 +97,7 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated evm.SetJumpDestCache(jumpDestCache) } // Run the pre-execution system calls - blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time())) + blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), parent, config, evm, block.Number(), block.Time())) // Iterate over and process the individual transactions for i, tx := range block.Transactions() { @@ -142,27 +141,20 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated }, nil } -// isEIP7997Transition reports whether the given header belongs to the first block -// on which the Amsterdam fork is active. -func isEIP7997Transition(config *params.ChainConfig, chain ChainContext, header *types.Header) bool { - if header.Number.Sign() == 0 || !config.IsAmsterdam(header.Number, header.Time) { - return false - } - parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1) - if parent == nil { - return false - } - return !config.IsAmsterdam(parent.Number, parent.Time) -} - -// PreExecution processes pre-execution system calls. -func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Hash, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) *bal.ConstructionBlockAccessList { +// PreExecution processes pre-execution state changes and system calls. +func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent *types.Header, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) *bal.ConstructionBlockAccessList { _, _, spanEnd := telemetry.StartSpan(ctx, "core.preExecution") defer spanEnd(nil) var blockAccessList *bal.ConstructionBlockAccessList if config.IsAmsterdam(number, time) { blockAccessList = bal.NewConstructionBlockAccessList() + + // EIP-7997: insert the deterministic deployment factory at the Amsterdam + // activation block via an irregular state transition. + if !config.IsAmsterdam(parent.Number, parent.Time) { + misc.ApplyEIP7997(evm.StateDB) + } } // EIP-4788 if beaconRoot != nil { @@ -170,7 +162,7 @@ func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Ha } // EIP-2935 if config.IsPrague(number, time) || config.IsUBT(number, time) { - ProcessParentBlockHash(parent, evm, blockAccessList) + ProcessParentBlockHash(parent.Hash(), evm, blockAccessList) } return blockAccessList } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 3c3539dbdb..7bcd83d581 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -250,7 +250,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, defer evm.Release() // Run pre-execution system calls - core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), eth.blockchain.Config(), evm, block.Number(), block.Time()) + core.PreExecution(ctx, block.BeaconRoot(), parent.Header(), eth.blockchain.Config(), evm, block.Number(), block.Time()) if txIndex == 0 && len(block.Transactions()) == 0 { return nil, context, statedb, release, nil diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 88132b4b63..d103b8ef35 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -373,7 +373,7 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil) evm := vm.NewEVM(context, statedb, api.backend.ChainConfig(), vm.Config{}) - core.PreExecution(ctx, next.BeaconRoot(), next.ParentHash(), api.backend.ChainConfig(), evm, next.Number(), next.Time()) + core.PreExecution(ctx, next.BeaconRoot(), block.Header(), api.backend.ChainConfig(), evm, next.Number(), next.Time()) evm.Release() // Clean out any pending release functions of trace state. Note this // step must be done after constructing tracing state, because the @@ -523,7 +523,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config ) defer evm.Release() // Run pre-execution system calls - core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), chainConfig, evm, block.Number(), block.Time()) + core.PreExecution(ctx, block.BeaconRoot(), parent.Header(), chainConfig, evm, block.Number(), block.Time()) for i, tx := range block.Transactions() { if err := ctx.Err(); err != nil { @@ -582,7 +582,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac defer evm.Release() // Run pre-execution system calls - core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), api.backend.ChainConfig(), evm, block.Number(), block.Time()) + core.PreExecution(ctx, block.BeaconRoot(), parent.Header(), api.backend.ChainConfig(), evm, block.Number(), block.Time()) // JS tracers have high overhead. In this case run a parallel // process that generates states in one thread and traces txes @@ -754,7 +754,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block defer evm.Release() // Run pre-execution system calls - core.PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), chainConfig, evm, block.Number(), block.Time()) + core.PreExecution(ctx, block.BeaconRoot(), parent.Header(), chainConfig, evm, block.Number(), block.Time()) for i, tx := range block.Transactions() { // Prepare the transaction for un-traced execution diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 8462194b1d..0da2672756 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -322,7 +322,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, evm.SetPrecompiles(precompiles) } // Run pre-execution system calls - blockAccessList.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, sim.chainConfig, evm, header.Number, header.Time)) + blockAccessList.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, parent, sim.chainConfig, evm, header.Number, header.Time)) var allLogs []*types.Log for i, call := range block.Calls { diff --git a/miner/payload_building_test.go b/miner/payload_building_test.go index f8e495cc99..734a5605ba 100644 --- a/miner/payload_building_test.go +++ b/miner/payload_building_test.go @@ -17,6 +17,7 @@ package miner import ( + "bytes" "context" "math/big" "reflect" @@ -26,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/clique" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" @@ -115,6 +117,7 @@ func newTestWorkerBackend(t *testing.T, chainConfig *params.ChainConfig, engine copy(gspec.ExtraData[32:32+common.AddressLength], testBankAddress.Bytes()) e.Authorize(testBankAddress) case *ethash.Ethash: + case *beacon.Beacon: default: t.Fatalf("unexpected consensus engine type: %T", engine) } @@ -194,6 +197,58 @@ func TestBuildPayload(t *testing.T) { } } +// TestBuildPayloadAmsterdamTransition verifies that a locally built payload for +// the first Amsterdam block contains the EIP-7997 deterministic deployment +// factory, i.e. the block-building path applies the same irregular state +// transition as block processing and the resulting block is importable. +func TestBuildPayloadAmsterdamTransition(t *testing.T) { + var ( + db = rawdb.NewMemoryDatabase() + recipient = common.HexToAddress("0xdeadbeef") + ) + config := new(params.ChainConfig) + *config = *params.MergedTestChainConfig + config.AmsterdamTime = new(uint64) + *config.AmsterdamTime = 1 // genesis (t=0) is pre-Amsterdam, the first block crosses the fork + + w, b := newTestWorker(t, config, beacon.New(ethash.NewFaker()), db, 0) + + var ( + beaconRoot = common.Hash{0x01} + slotNum = uint64(1) + ) + payload, err := w.buildPayload(context.Background(), &BuildPayloadArgs{ + Parent: b.chain.CurrentBlock().Hash(), + Timestamp: 1, + FeeRecipient: recipient, + Withdrawals: types.Withdrawals{}, + BeaconRoot: &beaconRoot, + SlotNum: &slotNum, + }, false) + if err != nil { + t.Fatalf("Failed to build payload %v", err) + } + block := payload.empty + if !config.IsAmsterdam(block.Number(), block.Time()) { + t.Fatal("transition block is not an Amsterdam block") + } + // The block must be importable: Process applies EIP-7997 independently, so + // a payload built without the factory would fail the state root check here. + if _, err := b.chain.InsertChain(types.Blocks{block}); err != nil { + t.Fatalf("failed to insert transition block: %v", err) + } + statedb, err := b.chain.StateAt(block.Header()) + if err != nil { + t.Fatalf("failed to open state at transition block: %v", err) + } + if code := statedb.GetCode(params.DeterministicFactoryAddress); !bytes.Equal(code, params.DeterministicFactoryCode) { + t.Fatalf("factory code missing from built payload state:\n got %x\nwant %x", code, params.DeterministicFactoryCode) + } + if nonce := statedb.GetNonce(params.DeterministicFactoryAddress); nonce != 1 { + t.Fatalf("factory nonce = %d, want 1", nonce) + } +} + func TestPayloadId(t *testing.T) { t.Parallel() ids := make(map[string]int) diff --git a/miner/worker.go b/miner/worker.go index 01a14b8a02..7f0e11f30a 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -326,7 +326,7 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams, return nil, err } // Run pre-execution system calls - env.bal.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time)) + env.bal.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, parent, miner.chainConfig, env.evm, header.Number, header.Time)) return env, nil }