From e8d192ad6b5fa9a4c5600099258507a2a2423814 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Fri, 3 Jul 2026 15:40:05 +0800 Subject: [PATCH] core: re-implement eip-2780 --- core/eip2780_test.go | 452 +++++++++++++++++- core/eip8037_test.go | 73 +-- core/eip8038_test.go | 105 ++-- core/error.go | 5 + core/state_transition.go | 380 +++++++++------ core/state_transition_test.go | 40 +- .../tracing/gen_gas_change_reason_stringer.go | 7 +- core/tracing/hooks.go | 4 + core/vm/common.go | 1 - core/vm/gas_table.go | 16 +- core/vm/gascosts.go | 18 +- core/vm/interface.go | 2 + params/protocol_params.go | 29 +- 13 files changed, 864 insertions(+), 268 deletions(-) diff --git a/core/eip2780_test.go b/core/eip2780_test.go index d9545923ff..09205cc31e 100644 --- a/core/eip2780_test.go +++ b/core/eip2780_test.go @@ -37,6 +37,7 @@ func TestEIP2780Intrinsic(t *testing.T) { name string to *common.Address value *uint256.Int + auths []types.SetCodeAuthorization want vm.GasCosts }{ { @@ -55,41 +56,52 @@ func TestEIP2780Intrinsic(t *testing.T) { name: "zero-value call", to: &to, value: uint256.NewInt(0), - // TxBaseCost + ColdAccountAccess = 15,000 - want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780}, + // TxBaseCost + ColdAccountAccess = 15,000; the recipient touch is + // charged at the cold rate unconditionally at the intrinsic phase. + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam}, }, { 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 + + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + 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. + // TxBaseCost + CreateAccess = 23,000 regular. The new-account state + // charge depends on whether the deployment target exists and is + // charged at runtime, not intrinsically. want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780, - StateGas: params.AccountCreationSize * params.CostPerStateByte, + RegularGas: params.TxBaseCost2780 + params.CreateAccessAmsterdam, }, }, { name: "contract creation, value > 0", to: nil, value: uint256.NewInt(1), - // TxBaseCost + CreateAccess + TransferLogCost = 24,756 regular, plus account creation. + // TxBaseCost + CreateAccess + TransferLogCost = 24,756 regular. want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + params.TransferLogCost2780, - StateGas: params.AccountCreationSize * params.CostPerStateByte, + RegularGas: params.TxBaseCost2780 + params.CreateAccessAmsterdam + params.TransferLogCost2780, }, }, + { + name: "value transfer with authorizations", + to: &to, + value: uint256.NewInt(1), + auths: make([]types.SetCodeAuthorization, 3), + // Each authorization adds the state-independent per-auth base + // (cold authority access included). + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + + params.TxValueCost2780 + params.TransferLogCost2780 + 3*params.RegularPerAuthBaseCost}, + }, } 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) + got, err := IntrinsicGas(nil, nil, tc.auths, from, tc.to, tc.value, rules8037, params.CostPerStateByte) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -105,7 +117,7 @@ func TestEIP2780Intrinsic(t *testing.T) { // (intrinsic + top-level + execution) recorded in the block gas pool. func TestEIP2780Gas(t *testing.T) { const ( - cold = params.ColdAccountAccess2780 + cold = params.ColdAccountAccessAmsterdam base = params.TxBaseCost2780 valueCst = params.TxValueCost2780 + params.TransferLogCost2780 ) @@ -154,9 +166,9 @@ func TestEIP2780Gas(t *testing.T) { // 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}, + {"create/zero-value", createTx(0, 300_000, nil), base + params.CreateAccessAmsterdam, newAccountState}, // case 10: contract-creation transaction, value > 0. - {"create/value", valueCreateTx(1), base + params.CreateAccess2780 + params.TransferLogCost2780, newAccountState}, + {"create/value", valueCreateTx(1), base + params.CreateAccessAmsterdam + params.TransferLogCost2780, newAccountState}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -190,6 +202,180 @@ func TestEIP2780NewAccountFunded(t *testing.T) { } } +// callTxAL builds a signed dynamic-fee call carrying an access list. +func callTxAL(nonce uint64, to common.Address, value int64, gas uint64, al types.AccessList) *types.Transaction { + return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{ + ChainID: cfg8037.ChainID, Nonce: nonce, To: &to, Value: big.NewInt(value), + Gas: gas, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), AccessList: al, + }) +} + +// accessListEntryCost is the total intrinsic cost of one address-only access +// list entry: the EIP-8038 per-address charge plus the EIP-7981 data charge. +const accessListEntryCost = params.TxAccessListAddressGasAmsterdam + + common.AddressLength*params.TxCostFloorPerToken7976*params.TxTokenPerNonZeroByte + +// TestEIP2780WarmRecipientStillChargedCold verifies that a recipient warmed by +// the transaction's access list is still charged the recipient touch at the +// cold rate: per EIP-2780 that touch is priced unconditionally at the intrinsic +// phase, so an access-list entry does not discount it. The total is the +// intrinsic cold recipient charge plus the access-list entry itself, with no +// separate runtime charge. +func TestEIP2780WarmRecipientStillChargedCold(t *testing.T) { + to := common.HexToAddress("0xe0a0000000000000000000000000000000000009") + sdb := mkState(senderAlloc(types.GenesisAlloc{to: {Balance: big.NewInt(1)}})) + al := types.AccessList{{Address: to}} + res, gp, err := applyMsg(t, sdb, callTxAL(0, to, 0, 100_000, al)) + if err != nil { + t.Fatal(err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + accessListEntryCost + if gp.cumulativeRegular != want { + t.Errorf("regular gas = %d, want %d (cold recipient, no access-list discount)", gp.cumulativeRegular, want) + } +} + +// TestEIP2780DelegatedWarmTarget verifies that resolving the recipient's +// delegation is charged at the warm rate when the target was warmed by the +// access list, rather than the flat cold rate. +func TestEIP2780DelegatedWarmTarget(t *testing.T) { + var ( + target = common.HexToAddress("0x7a76000000000000000000000000000000000002") // codeless + delegated = common.HexToAddress("0xde1e000000000000000000000000000000000002") + ) + sdb := mkState(senderAlloc(types.GenesisAlloc{ + delegated: {Code: types.AddressToDelegation(target)}, + })) + al := types.AccessList{{Address: target}} + res, gp, err := applyMsg(t, sdb, callTxAL(0, delegated, 0, 100_000, al)) + if err != nil { + t.Fatal(err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + accessListEntryCost + // recipient cold access (intrinsic) + params.WarmAccountAccessAmsterdam // warm delegation-target access (runtime) + if gp.cumulativeRegular != want { + t.Errorf("regular gas = %d, want %d (warm delegation target)", gp.cumulativeRegular, want) + } +} + +// TestEIP2780RecipientColdInIntrinsic exercises the validity boundary created +// by charging the recipient touch at the cold rate unconditionally in the +// intrinsic phase: a zero-value call funded one gas below the cold-inclusive +// intrinsic (TX_BASE_COST + COLD_ACCOUNT_ACCESS) is rejected as intrinsic-gas +// too low, while funding exactly that amount is valid and included with no +// further runtime charge. +func TestEIP2780RecipientColdInIntrinsic(t *testing.T) { + to := common.HexToAddress("0xe0a000000000000000000000000000000000000a") + intrinsic := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam // 15,000 + + // One gas short of the intrinsic cost: the transaction is invalid. + sdb := mkState(senderAlloc(types.GenesisAlloc{to: {Balance: big.NewInt(1)}})) + if _, _, err := applyMsg(t, sdb, callTx(0, to, 0, intrinsic-1, nil)); err == nil { + t.Fatal("expected intrinsic-gas-too-low error, got nil") + } + + // Funded for exactly the intrinsic cost: valid, included, and fully + // consumed with no runtime cold surcharge to halt on. + sdb = mkState(senderAlloc(types.GenesisAlloc{to: {Balance: big.NewInt(1)}})) + res, _, err := applyMsg(t, sdb, callTx(0, to, 0, intrinsic, nil)) + if err != nil { + t.Fatalf("transaction should be valid: %v", err) + } + if res.Err != nil { + t.Fatalf("unexpected execution error: %v", res.Err) + } + if res.UsedGas != intrinsic { + t.Fatalf("used gas = %d, want %d", res.UsedGas, intrinsic) + } + if sdb.GetNonce(senderAddr) != 1 { + t.Fatal("sender nonce not consumed") + } +} + +// TestEIP2780RuntimeOOGRevertsDelegations verifies that running out of gas on +// a runtime authorization charge halts the (still valid) transaction and +// reverts all state changes, including the already applied EIP-7702 +// delegations — while the sender's nonce increment persists. +func TestEIP2780RuntimeOOGRevertsDelegations(t *testing.T) { + auth, authority := signAuth(t, authKeyA, delegate8037, 0) + sdb := mkState(senderAlloc(nil)) + // Gas covers the intrinsic cost (TX_BASE_COST + the cold-inclusive + // per-authorization base for a self-call) but not the runtime authorization + // charges (ACCOUNT_WRITE + account + indicator bytes). + tx := types.MustSignNewTx(senderKey, signer8037, &types.SetCodeTx{ + ChainID: uint256.MustFromBig(cfg8037.ChainID), Nonce: 0, To: senderAddr, + Value: new(uint256.Int), Gas: 30_000, GasFeeCap: new(uint256.Int), + GasTipCap: new(uint256.Int), AuthList: []types.SetCodeAuthorization{auth}, + }) + res, _, err := applyMsg(t, sdb, tx) + 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 != 30_000 { + t.Fatalf("used gas = %d, want all 30000 burnt", res.UsedGas) + } + if code := sdb.GetCode(authority); len(code) != 0 { + t.Fatalf("delegation persisted despite runtime OOG: %x", code) + } + if sdb.GetNonce(authority) != 0 { + t.Fatal("authority nonce persisted despite runtime OOG") + } + if sdb.GetNonce(senderAddr) != 1 { + t.Fatal("sender nonce not consumed") + } +} + +// TestEIP2780SelfTransferDelegated verifies that a self-transfer incurs no +// recipient touch or value charges (the account is warm and existent as the +// sender), while resolving the sender's own delegation is still paid for. +func TestEIP2780SelfTransferDelegated(t *testing.T) { + target := common.HexToAddress("0x7a76000000000000000000000000000000000003") // codeless + sdb := mkState(types.GenesisAlloc{ + senderAddr: {Balance: big.NewInt(1e18), Code: types.AddressToDelegation(target)}, + }) + res, gp, err := applyMsg(t, sdb, callTx(0, senderAddr, 1, 100_000, nil)) + if err != nil { + t.Fatal(err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam // base + cold delegation target + if gp.cumulativeRegular != want { + t.Errorf("regular gas = %d, want %d (base + delegation resolution)", gp.cumulativeRegular, want) + } +} + +// TestEIP2780CreateInsufficientStateGas verifies that a contract-creation +// transaction funded for its intrinsic gas but not the runtime new-account +// state charge is included, halts out of gas and consumes the nonce. +func TestEIP2780CreateInsufficientStateGas(t *testing.T) { + sdb := mkState(senderAlloc(nil)) + intrinsic := params.TxBaseCost2780 + params.CreateAccessAmsterdam // 23,000 + res, _, err := applyMsg(t, sdb, createTx(0, intrinsic, 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 != intrinsic { + t.Fatalf("used gas = %d, want %d", res.UsedGas, intrinsic) + } + if sdb.GetNonce(senderAddr) != 1 { + t.Fatal("sender nonce not consumed") + } +} + // 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, @@ -212,3 +398,243 @@ func TestEIP2780InsufficientGasForCallCharge(t *testing.T) { t.Fatal("recipient should not be created when the call charge cannot be paid") } } + +// TestEIP2780HaltKeepsAuthStateGas verifies that when the top-most frame halts +// exceptionally, the EIP-7702 delegations applied before the frame was persist. +func TestEIP2780HaltKeepsAuthStateGas(t *testing.T) { + halting := common.HexToAddress("0xbad0000000000000000000000000000000000001") + sdb := mkState(senderAlloc(types.GenesisAlloc{ + halting: {Code: []byte{0xfe}}, // INVALID + })) + auth, authority := signAuth(t, authKeyA, delegate8037, 0) + + // A gas limit above MaxTxGas so the state reservoir covers the runtime + // authorization state charges (218,790) without spilling into regular gas. + gasLimit := params.MaxTxGas + 300_000 + tx := types.MustSignNewTx(senderKey, signer8037, &types.SetCodeTx{ + ChainID: uint256.MustFromBig(cfg8037.ChainID), Nonce: 0, To: halting, + Value: new(uint256.Int), Gas: gasLimit, GasFeeCap: new(uint256.Int), + GasTipCap: new(uint256.Int), AuthList: []types.SetCodeAuthorization{auth}, + }) + res, gp, err := applyMsg(t, sdb, tx) + if err != nil { + t.Fatalf("transaction should remain valid: %v", err) + } + if res.Err == nil { + t.Fatal("expected the frame to halt") + } + if code := sdb.GetCode(authority); len(code) == 0 { + t.Fatal("delegation should persist through an in-frame halt") + } + // The regular dimension is burned in full by the halt; the state dimension + // keeps the delegation's durable growth: a new account leaf plus the + // 23-byte indicator. + if gp.cumulativeRegular != params.MaxTxGas { + t.Errorf("regular gas = %d, want %d", gp.cumulativeRegular, params.MaxTxGas) + } + if gp.cumulativeState != authWorstState { + t.Errorf("state gas = %d, want %d (delegation state growth persisted)", gp.cumulativeState, authWorstState) + } + if want := params.MaxTxGas + authWorstState; res.UsedGas != want { + t.Errorf("used gas = %d, want %d", res.UsedGas, want) + } +} + +// TestEIP2780AuthorityAccountWrite pins the first-write ACCOUNT_WRITE rule for +// authorities: the surcharge applies to the first paid write to the account +// within the transaction, regardless of whether the account exists, and is +// skipped when the write is already paid for: by TX_BASE_COST for the sender, +// by TX_VALUE_COST for the recipient of a value-bearing transaction, or by a +// preceding valid authorization. +func TestEIP2780AuthorityAccountWrite(t *testing.T) { + const ( + base = params.TxBaseCost2780 + cold = params.ColdAccountAccessAmsterdam + aw = params.AccountWriteAmsterdam + perAuth = params.RegularPerAuthBaseCost + valueCst = params.TxValueCost2780 + params.TransferLogCost2780 + ) + existingEOA := common.HexToAddress("0xe0a0000000000000000000000000000000000002") + + auth0, authority := signAuth(t, authKeyA, delegate8037, 0) + auth1, _ := signAuth(t, authKeyA, delegate8037, 1) + authBadNonce, _ := signAuth(t, authKeyA, delegate8037, 5) + + // Self-sponsored authorization: the sender's nonce is bumped before the + // authorization list is processed, hence nonce 1. + senderAuth, err := types.SignSetCode(senderKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate8037, Nonce: 1, + }) + if err != nil { + t.Fatal(err) + } + // tx builds a SetCode transaction with an explicit value. + tx := func(to common.Address, value uint64, auths ...types.SetCodeAuthorization) *types.Transaction { + return types.MustSignNewTx(senderKey, signer8037, &types.SetCodeTx{ + ChainID: uint256.MustFromBig(cfg8037.ChainID), Nonce: 0, To: to, + Value: uint256.NewInt(value), Gas: 1_000_000, + GasFeeCap: new(uint256.Int), GasTipCap: new(uint256.Int), AuthList: auths, + }) + } + fundedAuthority := types.GenesisAlloc{authority: {Balance: big.NewInt(1)}} + + cases := []struct { + name string + alloc types.GenesisAlloc + tx *types.Transaction + wantRegular, wantState uint64 + }{ + { + // Materializing a fresh authority pays the first-write surcharge + // alongside the new-account state gas and the indicator bytes. + name: "fresh authority", + tx: tx(existingEOA, 0, auth0), + wantRegular: base + cold + perAuth + aw, + wantState: authWorstState, + }, + { + // An existing authority still pays the surcharge: the nonce and + // indicator stores are the first write to the account within the + // transaction. + name: "existing authority", + alloc: fundedAuthority, + tx: tx(existingEOA, 0, auth0), + wantRegular: base + cold + perAuth + aw, + wantState: authBaseState, + }, + { + // Self-sponsored: the sender's account write is prepaid by + // TX_BASE_COST, no surcharge. + name: "authority is sender", + tx: tx(existingEOA, 0, senderAuth), + wantRegular: base + cold + perAuth, + wantState: authBaseState, + }, + { + // authority == tx.to with zero value: no TX_VALUE_COST was paid, + // so the authorization write is the first paid write and the + // surcharge applies. The recipient becomes delegated, adding a + // cold delegation-target access at runtime. + name: "authority is recipient, zero value", + alloc: fundedAuthority, + tx: tx(authority, 0, auth0), + wantRegular: base + cold + perAuth + aw + cold, + wantState: authBaseState, + }, + { + // authority == tx.to with value: TX_VALUE_COST prepaid the + // recipient write, so no surcharge is due. + name: "authority is recipient, value", + alloc: fundedAuthority, + tx: tx(authority, 1, auth0), + wantRegular: base + cold + valueCst + perAuth + cold, + wantState: authBaseState, + }, + { + // Fresh authority == tx.to with value: the authorization pays the + // new-account state gas, and the recipient charge then sees an + // existing account, so the leaf is not paid for twice. + name: "authority is fresh recipient, value", + tx: tx(authority, 1, auth0), + wantRegular: base + cold + valueCst + perAuth + cold, + wantState: authWorstState, + }, + { + // The same authority twice: only the first valid authorization + // carries the surcharge, the account creation and the indicator. + name: "same authority twice", + tx: tx(existingEOA, 0, auth0, auth1), + wantRegular: base + cold + 2*perAuth + aw, + wantState: authWorstState, + }, + { + // An invalid authorization performs no write and does not count + // as the first write; the following valid one pays in full. The + // per-auth intrinsic base is still paid for the invalid tuple. + name: "invalid then valid", + tx: tx(existingEOA, 0, authBadNonce, auth0), + wantRegular: base + cold + 2*perAuth + aw, + wantState: authWorstState, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + alloc := types.GenesisAlloc{existingEOA: {Balance: big.NewInt(1)}} + for addr, acc := range tc.alloc { + alloc[addr] = acc + } + res, gp, err := applyMsg(t, mkState(senderAlloc(alloc)), 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) + } + }) + } +} + +// TestEIP2780DelegationTargetPrewarmed pins the warm rate for delegation +// targets that are already in accessed_addresses when the recipient is +// loaded. +func TestEIP2780DelegationTargetPrewarmed(t *testing.T) { + const ( + base = params.TxBaseCost2780 + cold = params.ColdAccountAccessAmsterdam + warm = params.WarmAccountAccessAmsterdam + aw = params.AccountWriteAmsterdam + perAuth = params.RegularPerAuthBaseCost + ) + delegatedAcct := common.HexToAddress("0xde1e000000000000000000000000000000000002") + + t.Run("target is sender", func(t *testing.T) { + sdb := mkState(senderAlloc(types.GenesisAlloc{ + delegatedAcct: {Code: types.AddressToDelegation(senderAddr)}, + })) + res, gp, err := applyMsg(t, sdb, callTx(0, delegatedAcct, 0, 100_000, nil)) + if err != nil { + t.Fatalf("consensus error: %v", err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + if want := base + cold + warm; gp.cumulativeRegular != want { + t.Errorf("regular gas = %d, want %d (warm delegation target)", gp.cumulativeRegular, want) + } + if gp.cumulativeState != 0 { + t.Errorf("state gas = %d, want 0", gp.cumulativeState) + } + }) + + t.Run("target warmed by authorization", func(t *testing.T) { + // A clearing authorization from a fresh authority: it creates the + // authority account (nonce bump) and warms it, without installing an + // indicator. + // + // The recipient's pre-existing delegation then resolves to + // the freshly warmed, codeless authority at the warm rate. + authClear, authority := signAuth(t, authKeyA, common.Address{}, 0) + sdb := mkState(senderAlloc(types.GenesisAlloc{ + delegatedAcct: {Code: types.AddressToDelegation(authority)}, + })) + res, gp, err := applyMsg(t, sdb, setCodeTx(0, delegatedAcct, []types.SetCodeAuthorization{authClear})) + if err != nil { + t.Fatalf("consensus error: %v", err) + } + if res.Err != nil { + t.Fatalf("execution failed: %v", res.Err) + } + if want := base + cold + perAuth + aw + warm; gp.cumulativeRegular != want { + t.Errorf("regular gas = %d, want %d (auth-warmed delegation target)", gp.cumulativeRegular, want) + } + if gp.cumulativeState != newAccountState { + t.Errorf("state gas = %d, want %d (authority account created)", gp.cumulativeState, newAccountState) + } + }) +} diff --git a/core/eip8037_test.go b/core/eip8037_test.go index 6d506cba96..d4956ed138 100644 --- a/core/eip8037_test.go +++ b/core/eip8037_test.go @@ -189,19 +189,24 @@ var ( // ===================== Top-level create transaction ====================== -// A creation tx's intrinsic gas pre-charges one account creation as state gas. -func TestCreateTxIntrinsicChargesAccountUnconditionally(t *testing.T) { +// A creation tx's intrinsic gas is state-independent: the new-account state +// charge depends on whether the deployment target exists and is charged at +// runtime (EIP-2780), not intrinsically. +func TestCreateTxIntrinsicNoStateGas(t *testing.T) { cost, err := IntrinsicGas(nil, nil, nil, common.Address{}, nil, nil, rules8037, params.CostPerStateByte) if err != nil { t.Fatal(err) } - if cost.StateGas != newAccountState { - t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, newAccountState) + if cost.StateGas != 0 { + t.Fatalf("intrinsic state gas = %d, want 0", cost.StateGas) + } + if want := params.TxBaseCost2780 + params.CreateAccessAmsterdam; cost.RegularGas != want { + t.Fatalf("intrinsic regular gas = %d, want %d", cost.RegularGas, want) } } -// Creating onto a pre-existing (balance-only) address refills the account -// portion; only the code deposit is charged as state gas. +// Creating onto a pre-existing (balance-only) address incurs no new-account +// runtime charge; only the code deposit is charged as state gas. func TestCreateTxPreexistingDestRefill(t *testing.T) { derived := crypto.CreateAddress(senderAddr, 0) sdb := mkState(senderAlloc(types.GenesisAlloc{derived: {Balance: big.NewInt(1)}})) @@ -214,7 +219,8 @@ func TestCreateTxPreexistingDestRefill(t *testing.T) { } } -// A creation tx that reverts refills the account-creation charge. +// A creation tx that reverts refills the account-creation charge applied at +// runtime. func TestCreateTxRevertRefill(t *testing.T) { sdb := mkState(senderAlloc(nil)) res, gp, err := applyMsg(t, sdb, createTx(0, 1_000_000, revertI)) @@ -229,7 +235,8 @@ func TestCreateTxRevertRefill(t *testing.T) { } } -// An address collision burns gas_left while refilling the account charge. +// An address collision burns gas_left. The colliding target exists, so no +// new-account state gas is charged at runtime in the first place. func TestCreateTxCollisionConsumesGasLeft(t *testing.T) { const gas = 1_000_000 derived := crypto.CreateAddress(senderAddr, 0) @@ -242,12 +249,11 @@ func TestCreateTxCollisionConsumesGasLeft(t *testing.T) { t.Fatal("expected collision failure") } if gp.cumulativeState != 0 { - t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState) + t.Fatalf("state gas = %d, want 0 (never charged)", gp.cumulativeState) } - // All forwarded gas_left is burned; only the refilled account charge (which - // had spilled into regular) returns to gas_left. So regular gas consumed is - // exactly tx.gas - newAccountState, with no other refund. - if want := uint64(gas) - newAccountState; gp.cumulativeRegular != want { + // All forwarded gas_left is burned: the whole gas limit is consumed as + // regular gas. + if want := uint64(gas); gp.cumulativeRegular != want { t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, want) } } @@ -472,18 +478,26 @@ const authKeyA = "02020202020202020202020202020202020202020202020202020020202020 var delegate8037 = common.HexToAddress("0xde1e8a7e") -// Intrinsic gas pre-charges the worst-case (account + indicator) per auth. -func TestAuthIntrinsicWorstCase(t *testing.T) { +// Intrinsic gas charges only the state-independent per-authorization base; +// the state-dependent charges are applied at runtime (EIP-2780). +func TestAuthIntrinsicBaseOnly(t *testing.T) { cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, common.Address{}, &delegate8037, nil, rules8037, params.CostPerStateByte) if err != nil { t.Fatal(err) } - if cost.StateGas != authWorstState { - t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, authWorstState) + if cost.StateGas != 0 { + t.Fatalf("intrinsic state gas = %d, want 0", cost.StateGas) + } + // The recipient touch and the per-authorization authority access (priced + // into RegularPerAuthBaseCost) are both charged at the cold rate + // unconditionally at the intrinsic phase (EIP-2780). + want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + params.RegularPerAuthBaseCost + if cost.RegularGas != want { + t.Fatalf("intrinsic regular gas = %d, want %d", cost.RegularGas, want) } } -// An invalid authorization refills its entire intrinsic state-gas charge. +// An invalid authorization incurs no runtime state-gas charge. func TestAuthInvalidRefillFull(t *testing.T) { k, _ := crypto.HexToECDSA(authKeyA) bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{ @@ -499,7 +513,8 @@ func TestAuthInvalidRefillFull(t *testing.T) { } } -// A pre-existing authority refills the account portion (indicator stands). +// A pre-existing authority is not charged for an account leaf; only the +// net-new indicator bytes are charged at runtime. func TestAuthAccountExistsRefill(t *testing.T) { auth, authority := signAuth(t, authKeyA, delegate8037, 0) sdb := mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}})) @@ -508,12 +523,12 @@ func TestAuthAccountExistsRefill(t *testing.T) { t.Fatal(err) } if gp.cumulativeState != authBaseState { - t.Fatalf("state gas = %d, want %d (account refilled)", gp.cumulativeState, authBaseState) + t.Fatalf("state gas = %d, want %d (indicator only)", gp.cumulativeState, authBaseState) } } -// Setting a delegation on an already-delegated authority refills the indicator -// portion (and the account portion, since the authority already exists). +// Setting a delegation on an already-delegated authority writes no net-new +// bytes (and no account leaf, since the authority exists): no state charge. func TestAuthSetOnDelegatedRefillBase(t *testing.T) { auth, authority := signAuth(t, authKeyA, delegate8037, 0) pre := types.AddressToDelegation(common.HexToAddress("0xabcd")) @@ -523,11 +538,12 @@ func TestAuthSetOnDelegatedRefillBase(t *testing.T) { t.Fatal(err) } if gp.cumulativeState != 0 { - t.Fatalf("state gas = %d, want 0 (account+indicator refilled)", gp.cumulativeState) + t.Fatalf("state gas = %d, want 0 (nothing net-new)", gp.cumulativeState) } } -// A net-new delegation on a fresh authority keeps the full worst-case charge. +// A net-new delegation on a fresh authority is charged the account leaf plus +// the indicator bytes at runtime. func TestAuthSetNetNewNoRefill(t *testing.T) { auth, _ := signAuth(t, authKeyA, delegate8037, 0) sdb := mkState(senderAlloc(nil)) @@ -536,11 +552,12 @@ func TestAuthSetNetNewNoRefill(t *testing.T) { t.Fatal(err) } if gp.cumulativeState != authWorstState { - t.Fatalf("state gas = %d, want %d (no refill)", gp.cumulativeState, authWorstState) + t.Fatalf("state gas = %d, want %d (leaf + indicator)", gp.cumulativeState, authWorstState) } } -// Clearing a delegation writes no indicator, so the indicator portion refills. +// Clearing a delegation writes no indicator, so only the (new) account leaf is +// charged at runtime. func TestAuthClearRefillBase(t *testing.T) { auth, _ := signAuth(t, authKeyA, common.Address{}, 0) // clear (address ZERO) sdb := mkState(senderAlloc(nil)) @@ -549,12 +566,12 @@ func TestAuthClearRefillBase(t *testing.T) { t.Fatal(err) } if want := newAccountState; gp.cumulativeState != want { - t.Fatalf("state gas = %d, want %d (indicator refilled)", gp.cumulativeState, want) + t.Fatalf("state gas = %d, want %d (account leaf only)", gp.cumulativeState, want) } } // 0->a->0 in one tx: the indicator created by an earlier auth and cleared by a -// later one writes zero net bytes, so both indicator charges refill. +// later one writes zero net bytes; the earlier indicator charge is refilled. func TestAuthClearSameTxDoubleRefill(t *testing.T) { set, authority := signAuth(t, authKeyA, delegate8037, 0) clr, _ := signAuth(t, authKeyA, common.Address{}, 1) diff --git a/core/eip8038_test.go b/core/eip8038_test.go index d8977c6dd2..df7d472c22 100644 --- a/core/eip8038_test.go +++ b/core/eip8038_test.go @@ -14,14 +14,6 @@ // 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 ( @@ -37,43 +29,71 @@ import ( "github.com/holiman/uint256" ) -// newAuthTestTransition builds a minimal stateTransition with a state reservoir, -// suitable for calling applyAuthorization directly. +// newAuthTestTransition builds a minimal stateTransition with a runtime gas +// budget, 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 + st.gasRemaining = vm.NewGasBudget(1_000_000, 1_000_000) 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) { +// A net-new delegation on a fresh, cold authority is charged ACCOUNT_WRITE in +// regular gas (the authority's cold access is paid unconditionally at the +// intrinsic phase, not here), plus the account leaf and the indicator bytes in +// state gas. +func TestAuthRuntimeChargeNetNew(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) + if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want { + t.Fatalf("regular charged = %d, want %d", st.gasRemaining.UsedRegularGas, want) + } + if want := int64(authWorstState); st.gasRemaining.UsedStateGas != want { + t.Fatalf("state charged = %d, want %d", st.gasRemaining.UsedStateGas, want) } } -// A pre-existing authority writes no new account leaf, so the intrinsic -// ACCOUNT_WRITE is refunded. -func TestAuthAccountWriteExistsRefund(t *testing.T) { +// A pre-existing authority writes no new account leaf, but its first write in +// the transaction still carries ACCOUNT_WRITE; the authority's cold access is +// paid at the intrinsic phase, so only the net-new indicator bytes are charged +// as state gas here. +func TestAuthRuntimeChargeExistingAccount(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) + if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want { + t.Fatalf("regular charged = %d, want %d", st.gasRemaining.UsedRegularGas, want) + } + if want := int64(authBaseState); st.gasRemaining.UsedStateGas != want { + t.Fatalf("state charged = %d, want %d", st.gasRemaining.UsedStateGas, want) } } -// An invalid authorization is skipped without writing any account leaf, so its -// intrinsic ACCOUNT_WRITE is refunded. -func TestAuthAccountWriteInvalidRefund(t *testing.T) { +// No cold surcharge is ever charged at runtime — the authority access is priced +// at the intrinsic phase — so an authority already warmed by the access list or +// an earlier authorization pays only the first-write surcharge, as it would +// whether warm or cold. +func TestAuthRuntimeChargeWarmAuthority(t *testing.T) { + auth, authority := signAuth(t, authKeyA, delegate8037, 0) + st := newAuthTestTransition(mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}))) + st.state.AddAddressToAccessList(authority) + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil { + t.Fatal(err) + } + if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want { + t.Fatalf("regular charged = %d, want %d (warm authority)", st.gasRemaining.UsedRegularGas, want) + } + if want := int64(authBaseState); st.gasRemaining.UsedStateGas != want { + t.Fatalf("state charged = %d, want %d", st.gasRemaining.UsedStateGas, want) + } +} + +// An invalid authorization is skipped without any runtime charge. +func TestAuthRuntimeInvalidNoCharge(t *testing.T) { k, _ := crypto.HexToECDSA(authKeyA) bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{ ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id @@ -82,15 +102,16 @@ func TestAuthAccountWriteInvalidRefund(t *testing.T) { 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) + if st.gasRemaining.UsedRegularGas != 0 || st.gasRemaining.UsedStateGas != 0 { + t.Fatalf("charged = <%d,%d>, want <0,0> (invalid authorization)", + st.gasRemaining.UsedRegularGas, st.gasRemaining.UsedStateGas) } } -// 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) { +// The same authority across two authorizations is charged once: the first auth +// warms the authority, materializes the account and installs the indicator, so +// the second incurs no further charge. +func TestAuthRuntimeDuplicateAuthorityOnce(t *testing.T) { a0, _ := signAuth(t, authKeyA, delegate8037, 0) a1, _ := signAuth(t, authKeyA, delegate8037, 1) st := newAuthTestTransition(mkState(senderAlloc(nil))) @@ -98,13 +119,27 @@ func TestAuthAccountWriteDuplicateOnce(t *testing.T) { 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) + if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want { + t.Fatalf("regular charged = %d, want %d (once)", st.gasRemaining.UsedRegularGas, want) + } + if want := int64(authWorstState); st.gasRemaining.UsedStateGas != want { + t.Fatalf("state charged = %d, want %d (once)", st.gasRemaining.UsedStateGas, want) + } +} + +// A budget that cannot cover the runtime charge aborts authorization +// processing with ErrOutOfGasRuntime, without mutating the authority. +func TestAuthRuntimeOutOfGas(t *testing.T) { + auth, authority := signAuth(t, authKeyA, delegate8037, 0) + st := newAuthTestTransition(mkState(senderAlloc(nil))) + st.gasRemaining = vm.NewGasBudget(10_000, 0) // covers neither leaf nor indicator + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != ErrOutOfGasRuntime { + t.Fatalf("err = %v, want ErrOutOfGasRuntime", err) + } + if st.state.GetNonce(authority) != 0 || len(st.state.GetCode(authority)) != 0 { + t.Fatal("authority mutated despite out-of-gas runtime charge") } } diff --git a/core/error.go b/core/error.go index 26b007f9d9..446ae4757a 100644 --- a/core/error.go +++ b/core/error.go @@ -137,4 +137,9 @@ var ( ErrAuthorizationInvalidSignature = errors.New("EIP-7702 authorization has invalid signature") ErrAuthorizationDestinationHasCode = errors.New("EIP-7702 authorization destination is a contract") ErrAuthorizationNonceMismatch = errors.New("EIP-7702 authorization nonce does not match current account nonce") + + // ErrOutOfGasRuntime is returned when the transaction's gas budget cannot + // cover an EIP-2780 runtime charge. The transaction remains valid: the top + // frame halts out of gas and its state changes are reverted. + ErrOutOfGasRuntime = errors.New("out of gas covering EIP-2780 runtime charge") ) diff --git a/core/state_transition.go b/core/state_transition.go index 4967675164..884d33e6b1 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/core/tracing" "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/crypto/kzg4844" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -75,10 +76,6 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set var gas vm.GasCosts 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 if isContractCreation && rules.IsHomestead { gas.RegularGas = params.TxGasContractCreation } else { @@ -87,14 +84,13 @@ 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.AccountWriteAmsterdam + params.RegularPerAuthBaseCost) - gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte + gas.RegularGas += uint64(len(authList)) * params.RegularPerAuthBaseCost } else { gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas } } - dataLen := uint64(len(data)) // Bump the required gas by the amount of transactional data + dataLen := uint64(len(data)) if dataLen > 0 { // Zero and non-zero bytes are priced differently z := uint64(bytes.Count(data, []byte{0})) @@ -123,6 +119,7 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set gas.RegularGas += lenWords * params.InitCodeWordGas } } + // Add the gas for accessList if accessList != nil { addresses := uint64(len(accessList)) storageKeys := uint64(accessList.StorageKeys()) @@ -170,17 +167,21 @@ func intrinsicBaseGasEIP2780(from common.Address, to *common.Address, value *uin isSelfTransfer = to != nil && *to == from hasValue = value != nil && !value.IsZero() ) - // tx.sender: signature recovery plus the sender account access and write. + // tx.sender: signature recovery, the sender account's access and write, + // and the inclusion of the transaction in the block (which is transient + // and expires with history). gas := params.TxBaseCost2780 - // tx.to charge. + // tx.to charge. Per EIP-2780 the recipient touch is charged at the cold + // rate unconditionally at the intrinsic phase, independent of the account's + // warm/cold state. switch { case isSelfTransfer: // The recipient account is already accessed and written as the sender. case isContractCreation: - gas += params.CreateAccess2780 + gas += params.CreateAccessAmsterdam default: - gas += params.ColdAccountAccess2780 + gas += params.ColdAccountAccessAmsterdam } // tx.value charge. @@ -242,10 +243,12 @@ func FloorDataGas(rules params.Rules, from common.Address, to *common.Address, v tokenCost = params.TxCostFloorPerToken } - // The floor is anchored to the transaction base cost. + // 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 = params.TxBaseCost2780 + floorBase = intrinsicBaseGasEIP2780(from, to, value) } // Check for overflow if (math.MaxUint64-floorBase)/tokenCost < tokens { @@ -260,7 +263,6 @@ func toWordSize(size uint64) uint64 { if size > math.MaxUint64-31 { return math.MaxUint64/32 + 1 } - return (size + 31) / 32 } @@ -537,10 +539,12 @@ func (st *stateTransition) buyGas() error { // - Blob fee-cap not below the current blob base fee (Cancun+). // - EIP-7702 set-code-tx shape: non-nil `To` and non-empty // authorization list. +// - EIP-3860 init code size cap on create transactions (Shanghai+, +// with the raised Amsterdam cap). // // The SkipNonceChecks / SkipTransactionChecks / NoBaseFee flags bypass // subsets of these checks for simulation paths (eth_call, eth_estimateGas). -func (st *stateTransition) preCheck() error { +func (st *stateTransition) preCheck(rules params.Rules) error { // Only check transactions that are not fake msg := st.msg if !msg.SkipNonceChecks { @@ -557,13 +561,9 @@ func (st *stateTransition) preCheck() error { msg.From.Hex(), stNonce) } } - var ( - isOsaka = st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time) - isAmsterdam = st.evm.ChainConfig().IsAmsterdam(st.evm.Context.BlockNumber, st.evm.Context.Time) - ) if !msg.SkipTransactionChecks { // Verify tx gas limit does not exceed EIP-7825 cap. - if !isAmsterdam && isOsaka && msg.GasLimit > params.MaxTxGas { + if !rules.IsAmsterdam && rules.IsOsaka && msg.GasLimit > params.MaxTxGas { return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit) } // Make sure the sender is an EOA @@ -574,7 +574,7 @@ func (st *stateTransition) preCheck() error { } } // Make sure that transaction gasFeeCap is greater than the baseFee (post london) - if st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) { + if rules.IsLondon { // Skip the checks if gas fields are zero and baseFee was explicitly disabled (eth_call) skipCheck := st.evm.Config.NoBaseFee && msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0 if !skipCheck { @@ -601,7 +601,7 @@ func (st *stateTransition) preCheck() error { if len(msg.BlobHashes) == 0 { return ErrMissingBlobHashes } - if isOsaka && len(msg.BlobHashes) > params.BlobTxMaxBlobs { + if rules.IsOsaka && len(msg.BlobHashes) > params.BlobTxMaxBlobs { return ErrTooManyBlobs } for i, hash := range msg.BlobHashes { @@ -611,7 +611,7 @@ func (st *stateTransition) preCheck() error { } } // Check that the user is paying at least the current blob fee - if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) { + if rules.IsCancun { if st.blobGasUsed() > 0 { // Skip the checks if gas fields are zero and blobBaseFee was explicitly disabled (eth_call) skipCheck := st.evm.Config.NoBaseFee && msg.BlobGasFeeCap.BitLen() == 0 @@ -634,6 +634,12 @@ func (st *stateTransition) preCheck() error { return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From) } } + // Check whether the init code size has been exceeded (EIP-3860). + if msg.To == nil { + if err := vm.CheckMaxInitCodeSize(&rules, uint64(len(msg.Data))); err != nil { + return err + } + } return st.buyGas() } @@ -649,20 +655,20 @@ func (st *stateTransition) preCheck() error { // If a consensus error is encountered, it is returned directly with a // nil EVM execution result. func (st *stateTransition) execute() (*ExecutionResult, error) { - // Validate the message and pre-pay gas. - if err := st.preCheck(); err != nil { - return nil, err - } - - // Charge intrinsic gas (with overflow detection inside IntrinsicGas). - // Under Amsterdam the cost is two-dimensional and Charge debits both - // regular and state in one step. var ( msg = st.msg rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time) contractCreation = msg.To == nil floorDataGas uint64 ) + // Validate the message and pre-pay gas. + if err := st.preCheck(rules); err != nil { + return nil, err + } + + // Charge intrinsic gas (with overflow detection inside IntrinsicGas). + // Under Amsterdam the cost is two-dimensional and Charge debits both + // regular and state in one step. cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules, st.evm.Context.CostPerStateByte) if err != nil { return nil, err @@ -720,54 +726,13 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // Execute the top-most frame var ( - ret []byte - vmerr error // vm errors do not effect consensus and are therefore not assigned to err - result vm.GasBudget + ret []byte + vmerr error // vm errors do not effect consensus ) if contractCreation { - // Check whether the init code size has been exceeded. - if err := vm.CheckMaxInitCodeSize(&rules, uint64(len(msg.Data))); err != nil { - return nil, err - } - // Execute the transaction's creation. - var creation bool - ret, _, result, creation, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value) - st.gasRemaining.Absorb(result) - - // 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.RefundStateToReservoir(params.AccountCreationSize * st.evm.Context.CostPerStateByte) - } + ret, vmerr = st.executeCreate(rules, value) } else { - // Increment the nonce for the next transaction. - st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) - - // Apply EIP-7702 authorizations. - st.applyAuthorizations(rules, msg.SetCodeAuthorizations) - - // Perform convenience warming of sender's delegation target. Although the - // sender is already warmed in Prepare(..), it's possible a delegation to - // the account was deployed during this transaction. To handle correctly, - // simply wait until the final state of delegations is determined before - // 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. - 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) - } + ret, vmerr = st.executeCall(rules, value) } // Settle down the gas usage and refund the ETH back if any remaining @@ -808,43 +773,155 @@ 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 +// executeCreate runs the top-level frame of a contract-creation transaction +// and returns the EVM return data and the frame-level execution error. +func (st *stateTransition) executeCreate(rules params.Rules, value *uint256.Int) ([]byte, error) { + msg := st.msg + + var chargedCreation bool + if rules.IsAmsterdam { + addr := crypto.CreateAddress(msg.From, st.state.GetNonce(msg.From)) + if !st.state.Exist(addr) { + if !st.chargeRuntimeGas(vm.GasCosts{StateGas: params.AccountCreationSize * st.evm.Context.CostPerStateByte}) { + // The nonce increment normally performed inside evm.Create + // must still happen for the included transaction. + st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeContractCreator) + st.gasRemaining = st.gasRemaining.ExitHalt() + return nil, vm.ErrOutOfGas + } + chargedCreation = true + } } - 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 + // The first frame is entered with the gas remaining after the runtime + // charges. + ret, _, result, creation, vmerr := st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value) + st.gasRemaining.Absorb(result) + + // If the contract creation failed (e.g. the initcode reverted), + // refill the account-creation state gas charged at runtime. + if rules.IsAmsterdam && chargedCreation && !creation { + st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) } - if cost == (vm.GasCosts{}) { - return true + // If the top-most frame halted, drain the leftover regular gas rather + // than returning it to the sender. The frame exit itself already burned + // its gas left, but the refill above repays the regular gas the charge + // originally borrowed, and on a halt that repayment must be burned as + // well. The state dimension is left untouched. + if rules.IsAmsterdam && vmerr != nil && vmerr != vm.ErrExecutionReverted { + st.gasRemaining.DrainRegular() } + return ret, vmerr +} + +// executeCall runs the top-level frame of a message-call transaction and +// returns the EVM return data and the frame-level execution error. +func (st *stateTransition) executeCall(rules params.Rules, value *uint256.Int) ([]byte, error) { + msg := st.msg + + // Increment the nonce for the next transaction. + st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) + + if rules.IsAmsterdam { + snapshot := st.state.Snapshot() + if !st.applyAuthorizations(rules, st.msg.SetCodeAuthorizations) { + st.state.RevertToSnapshot(snapshot) + st.gasRemaining = st.gasRemaining.ExitHalt() + return nil, vm.ErrOutOfGas + } + if !st.chargeCallRecipientEIP2780(value) { + st.state.RevertToSnapshot(snapshot) + st.gasRemaining = st.gasRemaining.ExitHalt() + return nil, vm.ErrOutOfGas + } + } else { + // Apply EIP-7702 authorizations. + st.applyAuthorizations(rules, msg.SetCodeAuthorizations) + + // Perform convenience warming of sender's delegation target. Although the + // sender is already warmed in Prepare(..), it's possible a delegation to + // the account was deployed during this transaction. To handle correctly, + // simply wait until the final state of delegations is determined before + // performing the resolution and warming. + if addr, ok := types.ParseDelegation(st.state.GetCode(*msg.To)); ok { + st.state.AddAddressToAccessList(addr) + } + } + ret, result, vmerr := st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) + st.gasRemaining.Absorb(result) + + // If the call frame reverts or halts exceptionally, the charged state-gas + // is refilled back to the state reservoir in Amsterdam. + if rules.IsAmsterdam && vmerr != nil && !value.IsZero() && st.evm.StateDB.Empty(st.to()) { + st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte) + } + // If the top-most frame halted, drain the leftover regular gas rather + // than returning it to the sender. The frame exit itself already burned + // its gas left, but the refill above repays the regular gas the charge + // originally borrowed, and on a halt that repayment must be burned as + // well. + if rules.IsAmsterdam && vmerr != nil && vmerr != vm.ErrExecutionReverted { + st.gasRemaining.DrainRegular() + } + return ret, vmerr +} + +// chargeRuntimeGas deducts an EIP-2780 runtime charge from the transaction's +// gas budget and reports whether the budget covered it. +func (st *stateTransition) chargeRuntimeGas(cost vm.GasCosts) bool { 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) + st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxRuntimeGas) + } + return true +} + +// chargeCallRecipientEIP2780 applies the EIP-2780 runtime charges for the +// top-level recipient of a message-call transaction, as the first frame is +// entered: +// +// - the recipient touch was already charged at the cold rate unconditionally +// at the intrinsic phase (EIP-2780) and the account is warm from +// statedb.Prepare (EIP-2929), so no access charge or warming is due here; +// +// - if the recipient is EIP-161 non-existent and the transaction carries +// value, the durable state growth of the new account; +// +// - if the recipient is an EIP-7702 delegated account, resolving the +// delegation loads the target's code: a cold account access, or a warm +// access if the target is already warm. +// +// Each charge is deducted before the state access it prices is performed: +// under EIP-7928 every account load is recorded in the block access list, so +// an access the budget cannot cover must not happen at all. +func (st *stateTransition) chargeCallRecipientEIP2780(value *uint256.Int) bool { + 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) { + if !st.chargeRuntimeGas(vm.GasCosts{StateGas: params.AccountCreationSize * st.evm.Context.CostPerStateByte}) { + return false + } + } + if target, delegated := types.ParseDelegation(st.state.GetCode(to)); delegated { + // Pay the delegation-target access before the target is warmed and + // its code resolved (loaded) on frame entry. + cost := vm.GasCosts{RegularGas: params.ColdAccountAccessAmsterdam} + if st.state.AddressInAccessList(target) { + cost.RegularGas = params.WarmAccountAccessAmsterdam + } + if !st.chargeRuntimeGas(cost) { + return false + } + st.state.AddAddressToAccessList(target) + + // Record the delegation in the block level accessList explicitly + st.state.GetCode(target) } return true } @@ -964,48 +1041,76 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio return authority, nil } -// applyAuthorization applies an EIP-7702 code delegation to the state and, -// adjust the pre-charged intrinsic cost accordingly. +// applyAuthorization applies an EIP-7702 code delegation to the state. 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.RefundStateToReservoir((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) - st.state.AddRefund(params.AccountWriteAmsterdam) - } return err } - prevDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority)) + oldDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority)) if !rules.IsAmsterdam { if st.state.Exist(authority) { st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas) } } else { - if st.state.Exist(authority) { - st.gasRemaining.RefundStateToReservoir(params.AccountCreationSize * st.evm.Context.CostPerStateByte) - st.state.AddRefund(params.AccountWriteAmsterdam) - } + // EIP-2780: charge the state-dependent authorization costs at runtime. + // The authority's cold access was already charged unconditionally at the + // intrinsic phase, so only state-dependent costs remain here. + var cost vm.GasCosts authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte - preDelegated, ok := delegates[authority] - if !ok { + preDelegated, seen := delegates[authority] + if !seen { preDelegated = curDelegated delegates[authority] = preDelegated } - if auth.Address == (common.Address{}) { - // Clearing writes no indicator, refill this auth's state charge. - 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.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.RefundStateToReservoir(authBase) + // Every valid authorization writes the authority account: the + // nonce bump, and possibly the delegation indicator. The first + // write to an account within the transaction carries the + // first-write surcharge. At this point the accounts whose write + // has already been paid for are: + // + // - the sender: TX_BASE_COST prices its account write, and the + // gas prepayment and nonce bump have already happened; + // + // - authorities written by preceding valid authorizations in + // this list, which carried the surcharge themselves; + // + // - tx.to, but only when the transaction carries value: + // TX_VALUE_COST prepaid the recipient write at the intrinsic + // phase. A zero-value transaction pays no TX_VALUE_COST, so a + // write to tx.to here is still the first paid write. + hasValue := st.msg.Value != nil && !st.msg.Value.IsZero() + if !seen && authority != st.msg.From && (authority != st.to() || !hasValue) { + cost.RegularGas += params.AccountWriteAmsterdam + } + // Durable state growth of the new account + if !st.state.Exist(authority) { + cost.StateGas += params.AccountCreationSize * st.evm.Context.CostPerStateByte + } + // Writing the 23-byte delegation indicator into a previously empty + // slot adds net-new state bytes. Overwriting an occupied slot, or one + // occupied at transaction start, writes no new bytes. + if auth.Address != (common.Address{}) && !curDelegated && !preDelegated { + cost.StateGas += authBase + } + // Clearing an indicator that was created by an earlier authorization + // within the same transaction writes zero net bytes; refill the + // earlier state charge as it is no longer justified. + // + // Note that the refund and the charges above can never apply to the + // same authorization. The refund requires the indicator to have been + // created by a preceding authorization in this transaction, in which + // case the authority already exists, has already been written, and + // its indicator slot was empty at transaction start, so none of the + // charges is due. The ordering of the refund and the charge is + // therefore irrelevant. + if auth.Address == (common.Address{}) && curDelegated && !preDelegated { + st.gasRemaining.RefundState(authBase) + } + if !st.chargeRuntimeGas(cost) { + return ErrOutOfGasRuntime } } @@ -1020,18 +1125,23 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se return nil } // Install delegation to auth.Address if the delegation changed - if !curDelegated || auth.Address != prevDelegation { + if !curDelegated || auth.Address != oldDelegation { st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) } return nil } -// applyAuthorizations applies an EIP-7702 code delegation to the state. -func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) { +// applyAuthorizations applies the EIP-7702 code delegations to the state. +// It reports whether the transaction budget covered all runtime authorization +// charges. +func (st *stateTransition) applyAuthorizations(rules params.Rules, auths []types.SetCodeAuthorization) bool { preDelegated := make(map[common.Address]bool) for _, auth := range auths { - st.applyAuthorization(rules, &auth, preDelegated) + if err := st.applyAuthorization(rules, &auth, preDelegated); err == ErrOutOfGasRuntime { + return false + } } + return true } // calcRefund computes the EIP-3529 refund cap against tx_gas_used_before_refund. diff --git a/core/state_transition_test.go b/core/state_transition_test.go index 60edad52e5..9bec3c5bf7 100644 --- a/core/state_transition_test.go +++ b/core/state_transition_test.go @@ -241,8 +241,10 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028: true, isAmsterdam: true, // 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 + + // (15,000); the recipient touch is charged at the cold rate + // unconditionally at the intrinsic phase. Plus base access-list + // charge + EIP-7981 extra. + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + 2*params.TxAccessListAddressGasAmsterdam + 3*params.TxAccessListStorageKeyGasAmsterdam + 2*amsterdamAddressCost + 3*amsterdamStorageKeyCost}, }, @@ -263,11 +265,10 @@ func TestIntrinsicGas(t *testing.T) { isHomestead: true, isEIP2028: true, isAmsterdam: true, - // EIP-2780: creation regular gas is TxBaseCost + CreateAccess (23,000), - // and account-creation cost is charged as state gas. + // EIP-2780: creation regular gas is TxBaseCost + CreateAccess (23,000); + // the new-account state charge is applied at runtime. want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780, - StateGas: params.AccountCreationSize * params.CostPerStateByte, + RegularGas: params.TxBaseCost2780 + params.CreateAccessAmsterdam, }, }, { @@ -279,9 +280,8 @@ func TestIntrinsicGas(t *testing.T) { isEIP3860: true, // Shanghai gates init-code word gas isAmsterdam: true, want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + + RegularGas: params.TxBaseCost2780 + params.CreateAccessAmsterdam + 64*params.TxDataZeroGas + 2*params.InitCodeWordGas, - StateGas: params.AccountCreationSize * params.CostPerStateByte, }, }, { @@ -296,11 +296,10 @@ func TestIntrinsicGas(t *testing.T) { isEIP3860: true, isAmsterdam: true, want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + + RegularGas: params.TxBaseCost2780 + params.CreateAccessAmsterdam + 32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas + 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost, - StateGas: params.AccountCreationSize * params.CostPerStateByte, }, }, { @@ -314,17 +313,16 @@ func TestIntrinsicGas(t *testing.T) { }, isEIP2028: true, isAmsterdam: true, - // 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 + // EIP-2780: the recipient touch and the per-authorization authority + // access (priced into RegularPerAuthBaseCost) are both charged at the + // cold rate unconditionally at the intrinsic phase; the account leaf + // and indicator bytes are charged at runtime. want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + + RegularGas: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + 100*params.TxDataNonZeroGasEIP2028 + 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam + 1*amsterdamAddressCost + 1*amsterdamStorageKeyCost + - 1*(params.AccountWriteAmsterdam+params.RegularPerAuthBaseCost), - StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte, + 1*params.RegularPerAuthBaseCost, }, }, { @@ -333,7 +331,7 @@ func TestIntrinsicGas(t *testing.T) { isAmsterdam: true, value: uint256.NewInt(1), // EIP-2780: TxBaseCost + ColdAccountAccess + TransferLogCost + TxValueCost = 21,000. - want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 + + want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + params.TransferLogCost2780 + params.TxValueCost2780}, }, { @@ -343,10 +341,10 @@ func TestIntrinsicGas(t *testing.T) { isEIP2028: true, isAmsterdam: true, value: uint256.NewInt(1), - // EIP-2780: TxBaseCost + CreateAccess + TransferLogCost = 24,756, plus account-creation state gas. + // EIP-2780: TxBaseCost + CreateAccess + TransferLogCost = 24,756; + // the new-account state charge is applied at runtime. want: vm.GasCosts{ - RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 + params.TransferLogCost2780, - StateGas: params.AccountCreationSize * params.CostPerStateByte, + RegularGas: params.TxBaseCost2780 + params.CreateAccessAmsterdam + params.TransferLogCost2780, }, }, } diff --git a/core/tracing/gen_gas_change_reason_stringer.go b/core/tracing/gen_gas_change_reason_stringer.go index e40f490051..fd297b4de3 100644 --- a/core/tracing/gen_gas_change_reason_stringer.go +++ b/core/tracing/gen_gas_change_reason_stringer.go @@ -29,21 +29,22 @@ func _() { _ = x[GasChangeWitnessContractCollisionCheck-18] _ = x[GasChangeTxDataFloor-19] _ = x[GasChangeRefundAccountCreation-20] + _ = x[GasChangeTxRuntimeGas-21] _ = x[GasChangeIgnored-255] } const ( - _GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorRefundAccountCreation" + _GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorRefundAccountCreationTxRuntimeGas" _GasChangeReason_name_1 = "Ignored" ) var ( - _GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353, 374} + _GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353, 374, 386} ) func (i GasChangeReason) String() string { switch { - case i <= 20: + case i <= 21: return _GasChangeReason_name_0[_GasChangeReason_index_0[i]:_GasChangeReason_index_0[i+1]] case i == 255: return _GasChangeReason_name_1 diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index 667c6341b4..6b2b7288c0 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -476,6 +476,10 @@ const ( // pre-charged account-creation cost when no account is created. GasChangeRefundAccountCreation GasChangeReason = 20 + // GasChangeTxRuntimeGas is the amount of gas charged for the state-dependent + // costs of the transaction per EIP-2780. + GasChangeTxRuntimeGas GasChangeReason = 21 + // GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as // it will be "manually" tracked by a direct emit of the gas change event. GasChangeIgnored GasChangeReason = 0xFF diff --git a/core/vm/common.go b/core/vm/common.go index 5059b4af37..46be1a813a 100644 --- a/core/vm/common.go +++ b/core/vm/common.go @@ -36,7 +36,6 @@ func CheckMaxInitCodeSize(rules *params.Rules, size uint64) error { return fmt.Errorf("%w: code size %v limit %v", ErrMaxInitCodeSizeExceeded, size, params.MaxInitCodeSize) } } - return nil } diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index d8fbf2b461..2d07b462a6 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -698,22 +698,26 @@ func gasSStore8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memor var ( 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 - if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { + access := params.WarmStorageAccessAmsterdam + _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot) + if !slotPresent { access = params.ColdStorageAccessAmsterdam - evm.StateDB.AddSlotToAccessList(contract.Address(), slot) } // Check access cost affordability before reading slot if contract.Gas.RegularGas < access { return GasCosts{}, errors.New("not enough gas for slot access") } + if !slotPresent { + evm.StateDB.AddSlotToAccessList(contract.Address(), slot) + } // Read the slot value for gas cost measurement - current, original := evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) - + var ( + value = common.Hash(y.Bytes32()) + current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot) + ) if current == value { // noop (1) return GasCosts{RegularGas: access}, nil } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 76a2c7af60..24ab156afb 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -196,20 +196,10 @@ 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) +// DrainRegular burns the remaining regular-gas. +func (g *GasBudget) DrainRegular() { + g.UsedRegularGas += g.RegularGas + g.RegularGas = 0 } // Forward drains `regular` regular gas and the entire state reservoir from diff --git a/core/vm/interface.go b/core/vm/interface.go index 5bba39069c..4adee2451f 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -77,9 +77,11 @@ type StateDB interface { AddressInAccessList(addr common.Address) bool SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) + // AddAddressToAccessList adds the given address to the access list. This operation is safe to perform // even if the feature/fork is not active yet AddAddressToAccessList(addr common.Address) + // AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform // even if the feature/fork is not active yet AddSlotToAccessList(addr common.Address, slot common.Hash) diff --git a/params/protocol_params.go b/params/protocol_params.go index f6f71e0dc0..6d2e7e6ef1 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -31,8 +31,6 @@ const ( MaxTxGas uint64 = 1 << 24 // Maximum transaction gas limit after eip-7825 (16,777,216). MaximumExtraDataSize uint64 = 32 // Maximum size extra data may be after Genesis. - ExpByteGas uint64 = 10 // Times ceil(log256(exponent)) for the EXP instruction. - SloadGas uint64 = 50 // CallValueTransferGas uint64 = 9000 // Paid for CALL when the value transfer is non-zero. CallNewAccountGas uint64 = 25000 // Paid for CALL when the destination address didn't exist prior. TxGas uint64 = 21000 // Per transaction not creating a contract. NOTE: Not payable on data of calls between transactions. @@ -75,8 +73,7 @@ const ( // Which becomes: 5000 - 2100 + 1900 = 4800 SstoreClearsScheduleRefundEIP3529 uint64 = SstoreResetGasEIP2200 - ColdSloadCostEIP2929 + TxAccessListStorageKeyGas - JumpdestGas uint64 = 1 // Once per JUMPDEST operation. - EpochDuration uint64 = 30000 // Duration between proof-of-work epochs. + JumpdestGas uint64 = 1 // Once per JUMPDEST operation. CreateDataGas uint64 = 200 // CallCreateDepth uint64 = 1024 // Maximum depth of call/create stack. @@ -84,7 +81,6 @@ const ( LogGas uint64 = 375 // Per LOG* operation. CopyGas uint64 = 3 // Multiplied by the number of 32-byte words that are copied (round up) for any *COPY operation and added. StackLimit uint64 = 1024 // Maximum size of VM stack allowed. - TierStepGas uint64 = 0 // Once per operation, for a selection of them. 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 @@ -101,20 +97,29 @@ 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 uint64 = 7816 // As defined by EIP-8037 and EIP-8038 + // RegularPerAuthBaseCost is the state-independent per-authorization floor, + // defined in EIP-8037 as the sum of: + // + // - Calldata cost for the authorization tuple + // - ECDSA recovery of the authority address (per EIP-7904) + // - Cold authority access (COLD_ACCOUNT_ACCESS) + // - Warm writes to the authority account + RegularPerAuthBaseCost uint64 = 7816 - // EIP-2780: resource-based intrinsic transaction gas. - TxBaseCost2780 uint64 = 12000 - ColdAccountAccess2780 uint64 = 3000 - CreateAccess2780 uint64 = 11000 - TxValueCost2780 uint64 = 4244 - TransferLogCost2780 uint64 = 1756 + // EIP-2780: resource-based intrinsic transaction gas. The access primitives + // it references (COLD_ACCOUNT_ACCESS, WARM_ACCESS, CREATE_ACCESS) are the + // EIP-8038 parameters defined below. + TxBaseCost2780 uint64 = 12000 + 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 + WarmAccountAccessAmsterdam uint64 = 100 // WARM_ACCESS: warm 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 + WarmStorageAccessAmsterdam uint64 = 100 // WARM_STORAGE_ACCESS: warm 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