core: re-implement eip-2780

This commit is contained in:
Gary Rong 2026-07-03 15:40:05 +08:00
parent 111e7b8b48
commit 0246297108
13 changed files with 864 additions and 268 deletions

View file

@ -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)
}
})
}

View file

@ -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)

View file

@ -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 <http://www.gnu.org/licenses/>.
// 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")
}
}

View file

@ -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")
)

View file

@ -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.

View file

@ -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,
},
},
}

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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