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)