diff --git a/core/eip2780_test.go b/core/eip2780_test.go index d2bb39bf50..cee5b3f685 100644 --- a/core/eip2780_test.go +++ b/core/eip2780_test.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -185,19 +186,6 @@ func TestEIP2780Gas(t *testing.T) { } } -// TestEIP2780NewAccountFunded verifies that a value transfer creating a new -// account both materializes and funds the recipient. -func TestEIP2780NewAccountFunded(t *testing.T) { - fresh := common.HexToAddress("0xbeef000000000000000000000000000000000002") - sdb := mkState(senderAlloc(nil)) - if _, _, err := applyMsg(t, sdb, callTx(0, fresh, 1, 300_000, nil)); err != nil { - t.Fatal(err) - } - if !sdb.Exist(fresh) || sdb.GetBalance(fresh).Cmp(uint256.NewInt(1)) != 0 { - t.Fatalf("recipient not funded: exist=%v balance=%v", sdb.Exist(fresh), sdb.GetBalance(fresh)) - } -} - // 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{ @@ -212,11 +200,7 @@ 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. +// the transaction's access list is still charged the recipient at the cold rate. func TestEIP2780WarmRecipientStillChargedCold(t *testing.T) { to := common.HexToAddress("0xe0a0000000000000000000000000000000000009") sdb := mkState(senderAlloc(types.GenesisAlloc{to: {Balance: big.NewInt(1)}})) @@ -260,79 +244,97 @@ func TestEIP2780DelegatedWarmTarget(t *testing.T) { } } -// 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. +// a runtime authorization charge halts the transaction and reverts all state +// changes, including the already applied EIP-7702 delegations — while the +// sender's nonce increment persists. +// +// The halt burns the regular dimension in full; the state dimension is +// refilled by the revert and the reservoir — if any — is preserved and +// returned to the sender rather than burnt. 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) + cases := []struct { + name string + gas uint64 + numAuths int + wantUsed uint64 // = gas − reservoir: all regular burnt, reservoir returned + }{ + // No state reservoir (gas below MaxTxGas). 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): everything is burnt. + {"no-reservoir", 30_000, 1, 30_000}, + + // A 100,000 state reservoir (gas above MaxTxGas). The 100 + // authorizations' state charges (~21.9M) overwhelm the reservoir and + // the regular budget they spill into. The reservoir is made whole by + // the halt-refill and returned to the sender. + {"with-reservoir", params.MaxTxGas + 100_000, 100, params.MaxTxGas}, } - 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") + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var ( + auths = make([]types.SetCodeAuthorization, tc.numAuths) + authorities = make([]common.Address, tc.numAuths) + ) + for i := range auths { + key, _ := crypto.GenerateKey() + auth, err := types.SignSetCode(key, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate8037, Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + auths[i], authorities[i] = auth, crypto.PubkeyToAddress(key.PublicKey) + } + sdb := mkState(senderAlloc(nil)) + tx := types.MustSignNewTx(senderKey, signer8037, + &types.SetCodeTx{ + ChainID: uint256.MustFromBig(cfg8037.ChainID), + Nonce: 0, + To: senderAddr, + Value: new(uint256.Int), + Gas: tc.gas, + GasFeeCap: new(uint256.Int), + GasTipCap: new(uint256.Int), + AuthList: auths, + }) + res, gp, 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 != tc.wantUsed { + t.Fatalf("used gas = %d, want %d", res.UsedGas, tc.wantUsed) + } + // The charged state gas was refilled on the halt: the receipt is + // all regular, burnt in full, and only the reservoir survives. + if gp.cumulativeState != 0 { + t.Fatalf("state gas = %d, want 0 (refilled on halt)", gp.cumulativeState) + } + if gp.cumulativeRegular != tc.wantUsed { + t.Fatalf("regular gas = %d, want %d (burnt in full)", gp.cumulativeRegular, tc.wantUsed) + } + for i, authority := range authorities { + if code := sdb.GetCode(authority); len(code) != 0 { + t.Fatalf("delegation %d persisted despite runtime OOG: %x", i, code) + } + if sdb.GetNonce(authority) != 0 { + t.Fatalf("authority %d nonce persisted despite runtime OOG", i) + } + } + 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. +// recipient touch or value charges, 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{ @@ -393,47 +395,207 @@ func TestEIP2780InsufficientGasForCallCharge(t *testing.T) { if sdb.Exist(fresh) { t.Fatal("recipient should not be created when the call charge cannot be paid") } + if sdb.GetNonce(senderAddr) != 1 { + t.Fatal("sender nonce not consumed") + } } -// 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) +// TestEIP2780FirstFrameHaltPreservesPreExecution verifies the gas and state +// semantics when the top-most frame — message call or creation — halts +// exceptionally after the pre-execution phase completed: +// +// - state changes applied before the frame was entered persist together +// with their state-gas charge (the EIP-7702 delegations of a call tx); +// - state gas pre-charged for the frame itself is refilled when the halt +// voids it (the account-creation charge of a creation tx); +// - after the refill the regular dimension is burnt in full, while any +// remaining state reservoir is preserved and returned to the sender. +func TestEIP2780FirstFrameHaltPreservesPreExecution(t *testing.T) { + halting := common.HexToAddress("0xbad0000000000000000000000000000000000002") + cases := []struct { + name string + create bool + gas uint64 + wantUsed uint64 // = gas − preserved reservoir + wantRegular uint64 + wantState uint64 + }{ + // Message call carrying one authorization: the delegation and its + // state charge (account + indicator) survive the halt. + // + // Without a reservoir the charge spills from regular gas and everything is + // burnt; + // + // With a reservoir, the reservoir remainder is preserved. + {"call/no-reservoir", false, 1_000_000, 1_000_000, 1_000_000 - authWorstState, authWorstState}, + {"call/with-reservoir", false, params.MaxTxGas + 300_000, params.MaxTxGas + authWorstState, params.MaxTxGas, authWorstState}, - // 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) + // Creation whose init code halts: no durable account is created, so + // the pre-charged account creation is refilled and no state gas + // remains. + // + // Without a reservoir the refill repays spilled regular gas, which the + // halt then burns along with the rest; + // + // With a reservoir, the refill makes the reservoir whole again and it + // is preserved. + {"create/no-reservoir", true, 1_000_000, 1_000_000, 1_000_000, 0}, + {"create/with-reservoir", true, params.MaxTxGas + 100_000, params.MaxTxGas, params.MaxTxGas, 0}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + sdb := mkState(senderAlloc(types.GenesisAlloc{ + halting: {Code: []byte{0xfe}}, // INVALID + })) + var ( + tx *types.Transaction + authority common.Address + ) + if tc.create { + tx = createTx(0, tc.gas, []byte{0xfe}) // init code: INVALID + } else { + var auth types.SetCodeAuthorization + auth, authority = signAuth(t, authKeyA, delegate8037, 0) + tx = types.MustSignNewTx(senderKey, signer8037, + &types.SetCodeTx{ + ChainID: uint256.MustFromBig(cfg8037.ChainID), + Nonce: 0, + To: halting, + Value: new(uint256.Int), + Gas: tc.gas, + 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 res.UsedGas != tc.wantUsed { + t.Fatalf("used gas = %d, want %d", res.UsedGas, tc.wantUsed) + } + if gp.cumulativeRegular != tc.wantRegular { + t.Fatalf("regular gas = %d, want %d (burnt in full)", gp.cumulativeRegular, tc.wantRegular) + } + if gp.cumulativeState != tc.wantState { + t.Fatalf("state gas = %d, want %d", gp.cumulativeState, tc.wantState) + } + if tc.create { + // The halted creation is fully reverted: no durable account. + derived := crypto.CreateAddress(senderAddr, 0) + if code := sdb.GetCode(derived); len(code) != 0 { + t.Fatalf("created code persisted despite halt: %x", code) + } + if sdb.GetNonce(derived) != 0 { + t.Fatal("created account nonce persisted despite halt") + } + } else { + // The delegation applied before the frame was entered persists. + if code := sdb.GetCode(authority); len(code) == 0 { + t.Fatal("delegation should persist through an in-frame halt") + } + } + if sdb.GetNonce(senderAddr) != 1 { + t.Fatal("sender nonce not consumed") + } + }) + } +} + +// TestEIP2780CreatePreExecutionOOGPreservesReservoir verifies that when a +// creation transaction cannot afford the pre-execution account-creation state +// charge (before the init-code frame is entered), the transaction halts with +// all regular gas burnt while the state reservoir — never touched, since the +// charge is atomic and was not applied — is preserved and returned to the +// sender. +func TestEIP2780CreatePreExecutionOOGPreservesReservoir(t *testing.T) { + // Regular gas left for the pre-execution charge; together with the + // reservoir it must not cover the account-creation cost. + const ( + regularLeft = 100_000 + reservoir = 50_000 + ) + // Plain creation intrinsic: TX_BASE_COST + CREATE_ACCESS. + plainIntrinsic, err := IntrinsicGas(nil, nil, nil, senderAddr, nil, new(uint256.Int), rules8037) if err != nil { - t.Fatalf("transaction should remain valid: %v", err) + t.Fatal(err) } - if res.Err == nil { - t.Fatal("expected the frame to halt") + // For the reservoir case the gas limit must exceed MaxTxGas, which leaves + // a huge regular budget by default. A big access list drives the intrinsic + // cost close to MaxTxGas, shrinking the regular budget back down to + // roughly regularLeft. Storage keys work because their intrinsic charge + // exceeds their EIP-7623/7976 floor contribution. + al := types.AccessList{{Address: common.HexToAddress("0xa1")}} + baseIntrinsic, err := IntrinsicGas(nil, al, nil, senderAddr, nil, new(uint256.Int), rules8037) + if err != nil { + t.Fatal(err) } - if code := sdb.GetCode(authority); len(code) == 0 { - t.Fatal("delegation should persist through an in-frame halt") + perKey := params.TxAccessListStorageKeyGasAmsterdam + uint64(common.HashLength)*params.TxCostFloorPerToken7976*params.TxTokenPerNonZeroByte + + // Fill the transaction with accessList, drain the gas and make it + // insufficient for account-creation cost. + al[0].StorageKeys = make([]common.Hash, (params.MaxTxGas-regularLeft-baseIntrinsic)/perKey) + alIntrinsic, err := IntrinsicGas(nil, al, nil, senderAddr, nil, new(uint256.Int), rules8037) + if err != nil { + t.Fatal(err) } - // The regular dimension is burned in full by the halt: the intrinsic cost - // plus the entire regular execution budget, which together equal the - // MaxTxGas cap. 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 left := params.MaxTxGas - alIntrinsic; left+reservoir >= newAccountState { + t.Fatalf("setup: regular %d + reservoir %d must not cover the creation charge %d", left, reservoir, newAccountState) } - if gp.cumulativeState != authWorstState { - t.Errorf("state gas = %d, want %d (delegation state growth persisted)", gp.cumulativeState, authWorstState) + alCreateTx := types.MustSignNewTx(senderKey, signer8037, + &types.DynamicFeeTx{ + ChainID: cfg8037.ChainID, + Nonce: 0, + To: nil, + Value: big.NewInt(0), + Gas: params.MaxTxGas + reservoir, + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + AccessList: al, + }) + + cases := []struct { + name string + tx *types.Transaction + wantUsed uint64 // = gas − preserved reservoir + }{ + // Gas below MaxTxGas: no reservoir, the whole limit is burnt. + {"no-reservoir", createTx(0, plainIntrinsic+regularLeft, nil), plainIntrinsic + regularLeft}, + + // Gas above MaxTxGas: the reservoir survives the halt untouched and + // is returned to the sender. + {"with-reservoir", alCreateTx, params.MaxTxGas}, } - if want := params.MaxTxGas + authWorstState; res.UsedGas != want { - t.Errorf("used gas = %d, want %d", res.UsedGas, want) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + sdb := mkState(senderAlloc(nil)) + res, gp, err := applyMsg(t, sdb, tc.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 != tc.wantUsed { + t.Fatalf("used gas = %d, want %d", res.UsedGas, tc.wantUsed) + } + if gp.cumulativeRegular != tc.wantUsed { + t.Fatalf("regular gas = %d, want %d (burnt in full)", gp.cumulativeRegular, tc.wantUsed) + } + if gp.cumulativeState != 0 { + t.Fatalf("state gas = %d, want 0 (charge never applied)", gp.cumulativeState) + } + if derived := crypto.CreateAddress(senderAddr, 0); sdb.Exist(derived) { + t.Fatal("target account should not be created when the charge cannot be paid") + } + if sdb.GetNonce(senderAddr) != 1 { + t.Fatal("sender nonce not consumed") + } + }) } } diff --git a/core/eip8037_test.go b/core/eip8037_test.go index ad01a90cce..0f001e80a3 100644 --- a/core/eip8037_test.go +++ b/core/eip8037_test.go @@ -21,6 +21,7 @@ package core import ( + "errors" "math/big" "testing" @@ -99,16 +100,13 @@ func applyMsg(t *testing.T, sdb *state.StateDB, tx *types.Transaction) (*Executi t.Fatalf("to message: %v", err) } gp := NewGasPool(evm.Context.GasLimit) - // Drive the stateTransition directly (as ApplyMessage does) so the test can - // inspect the final tx-level GasBudget vector via st.gasRemaining. + evm.SetTxContext(NewEVMTxContext(msg)) st := newStateTransition(evm, msg, gp) res, err := st.execute() if err == nil && res != nil { assertPoolSane(t, res, gp) - // The budget is seeded with the post-intrinsic remainder: the intrinsic - // cost counts towards the MaxTxGas regular cap and the execution gas - // exceeding the regular budget forms the reservoir. + intrinsic, ierr := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules8037) if ierr != nil { t.Fatalf("intrinsic gas: %v", ierr) @@ -263,6 +261,66 @@ func TestCreateTxCollisionConsumesGasLeft(t *testing.T) { } } +// An account can exist yet be EIP-161-empty in the middle of a transaction, +// e.g. after being touched as the zero-balance beneficiary of a SELFDESTRUCT. +// Deploying onto such an account should charge account-creation cost. +func TestCreate2TransientEmptyDestNoRefill(t *testing.T) { + var ( + orchestrator = common.HexToAddress("0xc0de000000000000000000000000000000000002") + destructor = common.HexToAddress("0xc0de000000000000000000000000000000000003") + target = crypto.CreateAddress2(orchestrator, [32]byte{}, crypto.Keccak256(deploy3)) + ) + // destructor: SELFDESTRUCT with zero balance to the future CREATE2 target, + // leaving it existing but EIP-161-empty for the rest of the transaction. + destructorCode := append(append([]byte{0x73}, target.Bytes()...), 0xff) // PUSH20 target, SELFDESTRUCT + + // orchestrator: CALL destructor (persist the success flag in slot 0), + // then CREATE2 deploy3 with salt 0, targeting the touched address. + code := []byte{ + 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, // ret/arg sizes and offsets, value = 0 + 0x73, // PUSH20 destructor + } + code = append(code, destructor.Bytes()...) + code = append(code, + 0x62, 0x03, 0x0d, 0x40, // PUSH3 200,000 call gas + 0xf1, // CALL + 0x60, 0x00, 0x55, // SSTORE the call result at slot 0 + 0x64, 0x60, 0x03, 0x60, 0x00, 0xf3, // PUSH5 deploy3 init code + 0x60, 0x00, 0x52, // MSTORE at word 0 (right-aligned, code at offset 27) + 0x60, 0x00, // salt = 0 + 0x60, 0x05, // size = 5 + 0x60, 0x1b, // offset = 27 + 0x60, 0x00, // endowment = 0 + 0xf5, 0x50, // CREATE2, POP + 0x00, // STOP + ) + sdb := mkState(senderAlloc(types.GenesisAlloc{ + orchestrator: {Code: code}, + destructor: {Code: destructorCode}, + })) + res, gp, err := applyMsg(t, sdb, callTx(0, orchestrator, 0, 2_000_000, nil)) + if err != nil { + t.Fatal(err) + } + if res.Failed() { + t.Fatalf("execution failed: %v", res.Err) + } + // The inner call must have succeeded, so the target was touched into an + // existing-but-empty account before the CREATE2 executed. + if flag := sdb.GetState(orchestrator, common.Hash{}); flag != common.BigToHash(big.NewInt(1)) { + t.Fatalf("destructor call flag = %v, want 1", flag) + } + if code := sdb.GetCode(target); len(code) != 3 { + t.Fatalf("deployed code length = %d, want 3", len(code)) + } + // State gas: the orchestrator's flag slot, the created contract account + // (charged, not refilled) and the 3-byte code deposit. + want := newSlotState + newAccountState + uint64(3*params.CostPerStateByte) + if gp.cumulativeState != want { + t.Fatalf("state gas = %d, want %d (account creation must not be refilled)", gp.cumulativeState, want) + } +} + // ======================== Transaction validation ========================= // The regular dimension must have room for min(tx.gas, MaxTxGas). @@ -311,15 +369,50 @@ func TestValidationIntrinsicRegularCap(t *testing.T) { for i := range al { al[i].Address = common.BigToAddress(big.NewInt(int64(i + 1))) } - tx := types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{ - ChainID: cfg8037.ChainID, Nonce: 0, To: &senderAddr, Value: big.NewInt(0), - Gas: 25_000_000, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), AccessList: al, - }) + tx := types.MustSignNewTx(senderKey, signer8037, + &types.DynamicFeeTx{ + ChainID: cfg8037.ChainID, + Nonce: 0, + To: &senderAddr, + Value: big.NewInt(0), + Gas: 25_000_000, + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + AccessList: al, + }) if _, _, err := applyMsg(t, mkState(senderAlloc(nil)), tx); err == nil { t.Fatal("expected rejection for intrinsic regular over MaxTxGas") } } +// The EIP-7623/7976 calldata floor is capped by MaxTxGas even when the gas +// limit covers it: a transaction whose floor cost exceeds the cap is rejected +// regardless of its (much smaller) intrinsic gas. +func TestValidationFloorCostCap(t *testing.T) { + // All-zero calldata: the floor charges 64/byte while the intrinsic + // charges only 4/byte, so the floor crosses the cap long before the + // intrinsic does. + data := make([]byte, 300_000) // floor ~19.2M > 16.77M cap, intrinsic ~1.2M + floor, err := FloorDataGas(rules8037, senderAddr, &senderAddr, new(uint256.Int), data, nil) + if err != nil { + t.Fatal(err) + } + intrinsic, err := IntrinsicGas(data, nil, nil, senderAddr, &senderAddr, new(uint256.Int), rules8037) + if err != nil { + t.Fatal(err) + } + if floor <= params.MaxTxGas || intrinsic > params.MaxTxGas { + t.Fatalf("setup: floor %d must exceed cap %d while intrinsic %d stays below", + floor, params.MaxTxGas, intrinsic) + } + // The gas limit covers the floor, so the rejection can only come from + // the MaxTxGas cap on the floor cost. + tx := callTx(0, senderAddr, 0, floor+1_000_000, data) + if _, _, err := applyMsg(t, mkState(senderAlloc(nil)), tx); !errors.Is(err, ErrFloorDataGas) { + t.Fatalf("expected ErrFloorDataGas, got %v", err) + } +} + // ========================= Refund and gas used =========================== // clearSlots deploys a contract that zeroes slots 1..n, each preset to 1.