diff --git a/core/eip8037_test.go b/core/eip8037_test.go index 0f001e80a3..dc6ba53110 100644 --- a/core/eip8037_test.go +++ b/core/eip8037_test.go @@ -665,9 +665,11 @@ func TestAuthClearRefillBase(t *testing.T) { } } -// 0->a->0 in one tx: the indicator created by an earlier auth and cleared by a -// later one writes zero net bytes; the earlier indicator charge is refilled. -func TestAuthClearSameTxDoubleRefill(t *testing.T) { +// 0->a->0 in one tx: the AUTH_BASE state charge for the indicator set by an +// earlier auth is levied once and is never credited back, even when a later +// auth clears the indicator in the same transaction. The account leaf charge +// plus the kept AUTH_BASE charge remain. +func TestAuthClearSameTxKeepsCharge(t *testing.T) { set, authority := signAuth(t, authKeyA, delegate8037, 0) clr, _ := signAuth(t, authKeyA, common.Address{}, 1) sdb := mkState(senderAlloc(nil)) @@ -676,8 +678,8 @@ func TestAuthClearSameTxDoubleRefill(t *testing.T) { t.Fatal(err) } _ = authority - if want := newAccountState; gp.cumulativeState != want { - t.Fatalf("state gas = %d, want %d (net-zero delegation)", gp.cumulativeState, want) + if want := authWorstState; gp.cumulativeState != want { + t.Fatalf("state gas = %d, want %d (account leaf + kept AUTH_BASE)", gp.cumulativeState, want) } } diff --git a/core/eip8038_test.go b/core/eip8038_test.go index df7d472c22..82a95babfb 100644 --- a/core/eip8038_test.go +++ b/core/eip8038_test.go @@ -44,7 +44,7 @@ func newAuthTestTransition(sdb *state.StateDB) *stateTransition { 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 { + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]*authDelegationState{}); err != nil { t.Fatal(err) } if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want { @@ -62,7 +62,7 @@ func TestAuthRuntimeChargeNetNew(t *testing.T) { 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 { + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]*authDelegationState{}); err != nil { t.Fatal(err) } if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want { @@ -81,7 +81,7 @@ 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 { + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]*authDelegationState{}); err != nil { t.Fatal(err) } if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want { @@ -99,7 +99,7 @@ func TestAuthRuntimeInvalidNoCharge(t *testing.T) { ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id }) st := newAuthTestTransition(mkState(senderAlloc(nil))) - if err := st.applyAuthorization(rules8037, &bad, map[common.Address]bool{}); err == nil { + if err := st.applyAuthorization(rules8037, &bad, map[common.Address]*authDelegationState{}); err == nil { t.Fatal("expected invalid-authorization error") } if st.gasRemaining.UsedRegularGas != 0 || st.gasRemaining.UsedStateGas != 0 { @@ -115,7 +115,7 @@ func TestAuthRuntimeDuplicateAuthorityOnce(t *testing.T) { a0, _ := signAuth(t, authKeyA, delegate8037, 0) a1, _ := signAuth(t, authKeyA, delegate8037, 1) st := newAuthTestTransition(mkState(senderAlloc(nil))) - delegates := map[common.Address]bool{} + delegates := map[common.Address]*authDelegationState{} if err := st.applyAuthorization(rules8037, &a0, delegates); err != nil { t.Fatal(err) } @@ -136,7 +136,7 @@ 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 { + if err := st.applyAuthorization(rules8037, &auth, map[common.Address]*authDelegationState{}); err != ErrOutOfGasRuntime { t.Fatalf("err = %v, want ErrOutOfGasRuntime", err) } if st.state.GetNonce(authority) != 0 || len(st.state.GetCode(authority)) != 0 { diff --git a/core/state_transition.go b/core/state_transition.go index 1cce3a38cd..f8dae22696 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -991,6 +991,7 @@ func (st *stateTransition) settleGas(rules params.Rules, floorDataGas uint64) (g // Settle down the final gas consumption in the block-level pool if rules.IsAmsterdam { + txRegularGas = max(txRegularGas, floorDataGas) if err = st.gp.ChargeGasAmsterdam(txRegularGas, txStateGas, gasUsed); err != nil { return 0, 0, err } @@ -1043,8 +1044,17 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio return authority, nil } +// authDelegationState tracks, per authority within a single transaction, the +// delegation state observed at transaction start and whether a (non-null) +// delegation has already been set for it earlier in the transaction. It mirrors +// the spec's `delegated_before_tx` flag and `delegation_set_for` set. +type authDelegationState struct { + preDelegated bool // authority carried a delegation at transaction start + setInTx bool // a non-null delegation was set for it earlier in this tx +} + // 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 { +func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, delegates map[common.Address]*authDelegationState) error { authority, err := st.validateAuthorization(auth) if err != nil { return err @@ -1062,11 +1072,12 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se var cost vm.GasCosts authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte - preDelegated, seen := delegates[authority] + track, seen := delegates[authority] if !seen { - preDelegated = curDelegated - delegates[authority] = preDelegated + track = &authDelegationState{preDelegated: curDelegated} + delegates[authority] = track } + preDelegated := track.preDelegated // 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 @@ -1091,25 +1102,23 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se if st.state.Empty(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 { + // Writing the 23-byte delegation indicator adds net-new state bytes when + // the authority held no delegation at the start of the transaction and + // none has been set for it earlier in the transaction. This AUTH_BASE + // state charge is levied at most once per authority and is never + // credited back: a delegation set and then cleared within the same + // transaction keeps its charge. + if auth.Address != (common.Address{}) && !preDelegated && !track.setInTx { 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 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 } + // Record that a non-null delegation has been set for this authority in + // this transaction, so a later set does not charge AUTH_BASE again. + if auth.Address != (common.Address{}) { + track.setInTx = true + } } // Update nonce and account code. st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization) @@ -1132,9 +1141,9 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se // 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) + delegates := make(map[common.Address]*authDelegationState) for _, auth := range auths { - if err := st.applyAuthorization(rules, &auth, preDelegated); err == ErrOutOfGasRuntime { + if err := st.applyAuthorization(rules, &auth, delegates); err == ErrOutOfGasRuntime { return false } }