Skip to content

Commit 99778e1

Browse files
committed
core: re-implement eip-2780
1 parent 4d2181a commit 99778e1

13 files changed

Lines changed: 863 additions & 267 deletions

core/eip2780_test.go

Lines changed: 439 additions & 13 deletions
Large diffs are not rendered by default.

core/eip8037_test.go

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -189,19 +189,24 @@ var (
189189

190190
// ===================== Top-level create transaction ======================
191191

192-
// A creation tx's intrinsic gas pre-charges one account creation as state gas.
193-
func TestCreateTxIntrinsicChargesAccountUnconditionally(t *testing.T) {
192+
// A creation tx's intrinsic gas is state-independent: the new-account state
193+
// charge depends on whether the deployment target exists and is charged at
194+
// runtime (EIP-2780), not intrinsically.
195+
func TestCreateTxIntrinsicNoStateGas(t *testing.T) {
194196
cost, err := IntrinsicGas(nil, nil, nil, common.Address{}, nil, nil, rules8037, params.CostPerStateByte)
195197
if err != nil {
196198
t.Fatal(err)
197199
}
198-
if cost.StateGas != newAccountState {
199-
t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, newAccountState)
200+
if cost.StateGas != 0 {
201+
t.Fatalf("intrinsic state gas = %d, want 0", cost.StateGas)
202+
}
203+
if want := params.TxBaseCost2780 + params.CreateAccessAmsterdam; cost.RegularGas != want {
204+
t.Fatalf("intrinsic regular gas = %d, want %d", cost.RegularGas, want)
200205
}
201206
}
202207

203-
// Creating onto a pre-existing (balance-only) address refills the account
204-
// portion; only the code deposit is charged as state gas.
208+
// Creating onto a pre-existing (balance-only) address incurs no new-account
209+
// runtime charge; only the code deposit is charged as state gas.
205210
func TestCreateTxPreexistingDestRefill(t *testing.T) {
206211
derived := crypto.CreateAddress(senderAddr, 0)
207212
sdb := mkState(senderAlloc(types.GenesisAlloc{derived: {Balance: big.NewInt(1)}}))
@@ -214,7 +219,8 @@ func TestCreateTxPreexistingDestRefill(t *testing.T) {
214219
}
215220
}
216221

217-
// A creation tx that reverts refills the account-creation charge.
222+
// A creation tx that reverts refills the account-creation charge applied at
223+
// runtime.
218224
func TestCreateTxRevertRefill(t *testing.T) {
219225
sdb := mkState(senderAlloc(nil))
220226
res, gp, err := applyMsg(t, sdb, createTx(0, 1_000_000, revertI))
@@ -229,7 +235,8 @@ func TestCreateTxRevertRefill(t *testing.T) {
229235
}
230236
}
231237

232-
// An address collision burns gas_left while refilling the account charge.
238+
// An address collision burns gas_left. The colliding target exists, so no
239+
// new-account state gas is charged at runtime in the first place.
233240
func TestCreateTxCollisionConsumesGasLeft(t *testing.T) {
234241
const gas = 1_000_000
235242
derived := crypto.CreateAddress(senderAddr, 0)
@@ -242,12 +249,11 @@ func TestCreateTxCollisionConsumesGasLeft(t *testing.T) {
242249
t.Fatal("expected collision failure")
243250
}
244251
if gp.cumulativeState != 0 {
245-
t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState)
252+
t.Fatalf("state gas = %d, want 0 (never charged)", gp.cumulativeState)
246253
}
247-
// All forwarded gas_left is burned; only the refilled account charge (which
248-
// had spilled into regular) returns to gas_left. So regular gas consumed is
249-
// exactly tx.gas - newAccountState, with no other refund.
250-
if want := uint64(gas) - newAccountState; gp.cumulativeRegular != want {
254+
// All forwarded gas_left is burned: the whole gas limit is consumed as
255+
// regular gas.
256+
if want := uint64(gas); gp.cumulativeRegular != want {
251257
t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, want)
252258
}
253259
}
@@ -472,18 +478,26 @@ const authKeyA = "02020202020202020202020202020202020202020202020202020020202020
472478

473479
var delegate8037 = common.HexToAddress("0xde1e8a7e")
474480

475-
// Intrinsic gas pre-charges the worst-case (account + indicator) per auth.
476-
func TestAuthIntrinsicWorstCase(t *testing.T) {
481+
// Intrinsic gas charges only the state-independent per-authorization base;
482+
// the state-dependent charges are applied at runtime (EIP-2780).
483+
func TestAuthIntrinsicBaseOnly(t *testing.T) {
477484
cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, common.Address{}, &delegate8037, nil, rules8037, params.CostPerStateByte)
478485
if err != nil {
479486
t.Fatal(err)
480487
}
481-
if cost.StateGas != authWorstState {
482-
t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, authWorstState)
488+
if cost.StateGas != 0 {
489+
t.Fatalf("intrinsic state gas = %d, want 0", cost.StateGas)
490+
}
491+
// The recipient touch and the per-authorization authority access (priced
492+
// into RegularPerAuthBaseCost) are both charged at the cold rate
493+
// unconditionally at the intrinsic phase (EIP-2780).
494+
want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam + params.RegularPerAuthBaseCost
495+
if cost.RegularGas != want {
496+
t.Fatalf("intrinsic regular gas = %d, want %d", cost.RegularGas, want)
483497
}
484498
}
485499

486-
// An invalid authorization refills its entire intrinsic state-gas charge.
500+
// An invalid authorization incurs no runtime state-gas charge.
487501
func TestAuthInvalidRefillFull(t *testing.T) {
488502
k, _ := crypto.HexToECDSA(authKeyA)
489503
bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{
@@ -499,7 +513,8 @@ func TestAuthInvalidRefillFull(t *testing.T) {
499513
}
500514
}
501515

502-
// A pre-existing authority refills the account portion (indicator stands).
516+
// A pre-existing authority is not charged for an account leaf; only the
517+
// net-new indicator bytes are charged at runtime.
503518
func TestAuthAccountExistsRefill(t *testing.T) {
504519
auth, authority := signAuth(t, authKeyA, delegate8037, 0)
505520
sdb := mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}))
@@ -508,12 +523,12 @@ func TestAuthAccountExistsRefill(t *testing.T) {
508523
t.Fatal(err)
509524
}
510525
if gp.cumulativeState != authBaseState {
511-
t.Fatalf("state gas = %d, want %d (account refilled)", gp.cumulativeState, authBaseState)
526+
t.Fatalf("state gas = %d, want %d (indicator only)", gp.cumulativeState, authBaseState)
512527
}
513528
}
514529

515-
// Setting a delegation on an already-delegated authority refills the indicator
516-
// portion (and the account portion, since the authority already exists).
530+
// Setting a delegation on an already-delegated authority writes no net-new
531+
// bytes (and no account leaf, since the authority exists): no state charge.
517532
func TestAuthSetOnDelegatedRefillBase(t *testing.T) {
518533
auth, authority := signAuth(t, authKeyA, delegate8037, 0)
519534
pre := types.AddressToDelegation(common.HexToAddress("0xabcd"))
@@ -523,11 +538,12 @@ func TestAuthSetOnDelegatedRefillBase(t *testing.T) {
523538
t.Fatal(err)
524539
}
525540
if gp.cumulativeState != 0 {
526-
t.Fatalf("state gas = %d, want 0 (account+indicator refilled)", gp.cumulativeState)
541+
t.Fatalf("state gas = %d, want 0 (nothing net-new)", gp.cumulativeState)
527542
}
528543
}
529544

530-
// A net-new delegation on a fresh authority keeps the full worst-case charge.
545+
// A net-new delegation on a fresh authority is charged the account leaf plus
546+
// the indicator bytes at runtime.
531547
func TestAuthSetNetNewNoRefill(t *testing.T) {
532548
auth, _ := signAuth(t, authKeyA, delegate8037, 0)
533549
sdb := mkState(senderAlloc(nil))
@@ -536,11 +552,12 @@ func TestAuthSetNetNewNoRefill(t *testing.T) {
536552
t.Fatal(err)
537553
}
538554
if gp.cumulativeState != authWorstState {
539-
t.Fatalf("state gas = %d, want %d (no refill)", gp.cumulativeState, authWorstState)
555+
t.Fatalf("state gas = %d, want %d (leaf + indicator)", gp.cumulativeState, authWorstState)
540556
}
541557
}
542558

543-
// Clearing a delegation writes no indicator, so the indicator portion refills.
559+
// Clearing a delegation writes no indicator, so only the (new) account leaf is
560+
// charged at runtime.
544561
func TestAuthClearRefillBase(t *testing.T) {
545562
auth, _ := signAuth(t, authKeyA, common.Address{}, 0) // clear (address ZERO)
546563
sdb := mkState(senderAlloc(nil))
@@ -549,12 +566,12 @@ func TestAuthClearRefillBase(t *testing.T) {
549566
t.Fatal(err)
550567
}
551568
if want := newAccountState; gp.cumulativeState != want {
552-
t.Fatalf("state gas = %d, want %d (indicator refilled)", gp.cumulativeState, want)
569+
t.Fatalf("state gas = %d, want %d (account leaf only)", gp.cumulativeState, want)
553570
}
554571
}
555572

556573
// 0->a->0 in one tx: the indicator created by an earlier auth and cleared by a
557-
// later one writes zero net bytes, so both indicator charges refill.
574+
// later one writes zero net bytes; the earlier indicator charge is refilled.
558575
func TestAuthClearSameTxDoubleRefill(t *testing.T) {
559576
set, authority := signAuth(t, authKeyA, delegate8037, 0)
560577
clr, _ := signAuth(t, authKeyA, common.Address{}, 1)

core/eip8038_test.go

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,6 @@
1414
// You should have received a copy of the GNU Lesser General Public License
1515
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
1616

17-
// EIP-8038 authorization accounting tests. The per-authorization intrinsic gas
18-
// pre-charges ACCOUNT_WRITE (regular) on top of REGULAR_PER_AUTH_BASE_COST.
19-
// applyAuthorization refunds that ACCOUNT_WRITE to the refund counter in exactly
20-
// the cases where no new account leaf is written: an invalid authorization, or
21-
// an authority whose account already exists. These white-box tests invoke
22-
// applyAuthorization directly and read the raw refund counter, so they observe
23-
// the refund before the EIP-3529 cap is applied.
24-
2517
package core
2618

2719
import (
@@ -37,43 +29,71 @@ import (
3729
"github.com/holiman/uint256"
3830
)
3931

40-
// newAuthTestTransition builds a minimal stateTransition with a state reservoir,
41-
// suitable for calling applyAuthorization directly.
32+
// newAuthTestTransition builds a minimal stateTransition with a runtime gas
33+
// budget, suitable for calling applyAuthorization directly.
4234
func newAuthTestTransition(sdb *state.StateDB) *stateTransition {
4335
st := newStateTransition(amsterdamCoreEVM(sdb), &Message{}, NewGasPool(30_000_000))
44-
st.gasRemaining = vm.NewGasBudget(0, 1_000_000) // reservoir for state-gas refills
36+
st.gasRemaining = vm.NewGasBudget(1_000_000, 1_000_000)
4537
return st
4638
}
4739

48-
// A net-new delegation on a fresh authority writes a new account leaf, so the
49-
// intrinsic ACCOUNT_WRITE stands (no refund).
50-
func TestAuthAccountWriteNetNewNoRefund(t *testing.T) {
40+
// A net-new delegation on a fresh, cold authority is charged ACCOUNT_WRITE in
41+
// regular gas (the authority's cold access is paid unconditionally at the
42+
// intrinsic phase, not here), plus the account leaf and the indicator bytes in
43+
// state gas.
44+
func TestAuthRuntimeChargeNetNew(t *testing.T) {
5145
auth, _ := signAuth(t, authKeyA, delegate8037, 0)
5246
st := newAuthTestTransition(mkState(senderAlloc(nil)))
5347
if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil {
5448
t.Fatal(err)
5549
}
56-
if got := st.state.GetRefund(); got != 0 {
57-
t.Fatalf("refund = %d, want 0 (net-new account write)", got)
50+
if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want {
51+
t.Fatalf("regular charged = %d, want %d", st.gasRemaining.UsedRegularGas, want)
52+
}
53+
if want := int64(authWorstState); st.gasRemaining.UsedStateGas != want {
54+
t.Fatalf("state charged = %d, want %d", st.gasRemaining.UsedStateGas, want)
55+
}
56+
}
57+
58+
// A pre-existing authority writes no new account leaf, but its first write in
59+
// the transaction still carries ACCOUNT_WRITE; the authority's cold access is
60+
// paid at the intrinsic phase, so only the net-new indicator bytes are charged
61+
// as state gas here.
62+
func TestAuthRuntimeChargeExistingAccount(t *testing.T) {
63+
auth, authority := signAuth(t, authKeyA, delegate8037, 0)
64+
st := newAuthTestTransition(mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}})))
65+
if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil {
66+
t.Fatal(err)
67+
}
68+
if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want {
69+
t.Fatalf("regular charged = %d, want %d", st.gasRemaining.UsedRegularGas, want)
70+
}
71+
if want := int64(authBaseState); st.gasRemaining.UsedStateGas != want {
72+
t.Fatalf("state charged = %d, want %d", st.gasRemaining.UsedStateGas, want)
5873
}
5974
}
6075

61-
// A pre-existing authority writes no new account leaf, so the intrinsic
62-
// ACCOUNT_WRITE is refunded.
63-
func TestAuthAccountWriteExistsRefund(t *testing.T) {
76+
// No cold surcharge is ever charged at runtime — the authority access is priced
77+
// at the intrinsic phase — so an authority already warmed by the access list or
78+
// an earlier authorization pays only the first-write surcharge, as it would
79+
// whether warm or cold.
80+
func TestAuthRuntimeChargeWarmAuthority(t *testing.T) {
6481
auth, authority := signAuth(t, authKeyA, delegate8037, 0)
6582
st := newAuthTestTransition(mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}})))
83+
st.state.AddAddressToAccessList(authority)
6684
if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil {
6785
t.Fatal(err)
6886
}
69-
if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam {
70-
t.Fatalf("refund = %d, want %d (account already exists)", got, params.AccountWriteAmsterdam)
87+
if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want {
88+
t.Fatalf("regular charged = %d, want %d (warm authority)", st.gasRemaining.UsedRegularGas, want)
89+
}
90+
if want := int64(authBaseState); st.gasRemaining.UsedStateGas != want {
91+
t.Fatalf("state charged = %d, want %d", st.gasRemaining.UsedStateGas, want)
7192
}
7293
}
7394

74-
// An invalid authorization is skipped without writing any account leaf, so its
75-
// intrinsic ACCOUNT_WRITE is refunded.
76-
func TestAuthAccountWriteInvalidRefund(t *testing.T) {
95+
// An invalid authorization is skipped without any runtime charge.
96+
func TestAuthRuntimeInvalidNoCharge(t *testing.T) {
7797
k, _ := crypto.HexToECDSA(authKeyA)
7898
bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{
7999
ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id
@@ -82,29 +102,44 @@ func TestAuthAccountWriteInvalidRefund(t *testing.T) {
82102
if err := st.applyAuthorization(rules8037, &bad, map[common.Address]bool{}); err == nil {
83103
t.Fatal("expected invalid-authorization error")
84104
}
85-
if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam {
86-
t.Fatalf("refund = %d, want %d (invalid authorization)", got, params.AccountWriteAmsterdam)
105+
if st.gasRemaining.UsedRegularGas != 0 || st.gasRemaining.UsedStateGas != 0 {
106+
t.Fatalf("charged = <%d,%d>, want <0,0> (invalid authorization)",
107+
st.gasRemaining.UsedRegularGas, st.gasRemaining.UsedStateGas)
87108
}
88109
}
89110

90-
// The same authority across two authorizations writes its account leaf only
91-
// once: the first auth pays ACCOUNT_WRITE, the second (which now sees the
92-
// account as existing) is refunded.
93-
func TestAuthAccountWriteDuplicateOnce(t *testing.T) {
111+
// The same authority across two authorizations is charged once: the first auth
112+
// warms the authority, materializes the account and installs the indicator, so
113+
// the second incurs no further charge.
114+
func TestAuthRuntimeDuplicateAuthorityOnce(t *testing.T) {
94115
a0, _ := signAuth(t, authKeyA, delegate8037, 0)
95116
a1, _ := signAuth(t, authKeyA, delegate8037, 1)
96117
st := newAuthTestTransition(mkState(senderAlloc(nil)))
97118
delegates := map[common.Address]bool{}
98119
if err := st.applyAuthorization(rules8037, &a0, delegates); err != nil {
99120
t.Fatal(err)
100121
}
101-
if got := st.state.GetRefund(); got != 0 {
102-
t.Fatalf("refund after first auth = %d, want 0", got)
103-
}
104122
if err := st.applyAuthorization(rules8037, &a1, delegates); err != nil {
105123
t.Fatal(err)
106124
}
107-
if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam {
108-
t.Fatalf("refund after duplicate auth = %d, want %d", got, params.AccountWriteAmsterdam)
125+
if want := params.AccountWriteAmsterdam; st.gasRemaining.UsedRegularGas != want {
126+
t.Fatalf("regular charged = %d, want %d (once)", st.gasRemaining.UsedRegularGas, want)
127+
}
128+
if want := int64(authWorstState); st.gasRemaining.UsedStateGas != want {
129+
t.Fatalf("state charged = %d, want %d (once)", st.gasRemaining.UsedStateGas, want)
130+
}
131+
}
132+
133+
// A budget that cannot cover the runtime charge aborts authorization
134+
// processing with ErrOutOfGasRuntime, without mutating the authority.
135+
func TestAuthRuntimeOutOfGas(t *testing.T) {
136+
auth, authority := signAuth(t, authKeyA, delegate8037, 0)
137+
st := newAuthTestTransition(mkState(senderAlloc(nil)))
138+
st.gasRemaining = vm.NewGasBudget(10_000, 0) // covers neither leaf nor indicator
139+
if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != ErrOutOfGasRuntime {
140+
t.Fatalf("err = %v, want ErrOutOfGasRuntime", err)
141+
}
142+
if st.state.GetNonce(authority) != 0 || len(st.state.GetCode(authority)) != 0 {
143+
t.Fatal("authority mutated despite out-of-gas runtime charge")
109144
}
110145
}

core/error.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,9 @@ var (
137137
ErrAuthorizationInvalidSignature = errors.New("EIP-7702 authorization has invalid signature")
138138
ErrAuthorizationDestinationHasCode = errors.New("EIP-7702 authorization destination is a contract")
139139
ErrAuthorizationNonceMismatch = errors.New("EIP-7702 authorization nonce does not match current account nonce")
140+
141+
// ErrOutOfGasRuntime is returned when the transaction's gas budget cannot
142+
// cover an EIP-2780 runtime charge. The transaction remains valid: the top
143+
// frame halts out of gas and its state changes are reverted.
144+
ErrOutOfGasRuntime = errors.New("out of gas covering EIP-2780 runtime charge")
140145
)

0 commit comments

Comments
 (0)