diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 2700aed505..aaf0e280b7 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -20,6 +20,7 @@ import ( "crypto/ecdsa" "math" "math/big" + "strings" "testing" "github.com/ethereum/go-ethereum/common" @@ -432,3 +433,105 @@ func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Tr } return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)) } + +// TestEIP8037MaxRegularGasValidation tests that transactions with floor data gas +// exceeding MaxTxGas are rejected in Amsterdam. +func TestEIP8037MaxRegularGasValidation(t *testing.T) { + var ( + // Create an Amsterdam-enabled chain config + config = ¶ms.ChainConfig{ + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + GrayGlacierBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), + ShanghaiTime: u64(0), + CancunTime: u64(0), + PragueTime: u64(0), + OsakaTime: u64(0), + AmsterdamTime: u64(0), + TerminalTotalDifficulty: big.NewInt(0), + Ethash: new(params.EthashConfig), + BlobScheduleConfig: ¶ms.BlobScheduleConfig{ + Cancun: params.DefaultCancunBlobConfig, + Prague: params.DefaultPragueBlobConfig, + Osaka: params.DefaultOsakaBlobConfig, + Amsterdam: params.DefaultOsakaBlobConfig, + }, + } + signer = types.LatestSigner(config) + key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + ) + + // Calculate how much data is needed to exceed MaxTxGas with floor data gas. + // FloorDataGas = TxGas + tokens * TxCostFloorPerToken + // For non-zero bytes: tokens = 4 per byte, so cost = 40 gas per byte + // MaxTxGas = 16,777,216 + // To exceed: tokens * 10 > MaxTxGas - TxGas = 16,756,216 + // For non-zero bytes: bytes * 4 * 10 > 16,756,216 → bytes > 418,905 + dataSize := 420000 // This should exceed MaxTxGas + largeData := make([]byte, dataSize) + for i := range largeData { + largeData[i] = 0xFF // Non-zero bytes have higher token cost + } + + // Verify that floor data gas exceeds MaxTxGas + floorGas, err := FloorDataGas(largeData) + if err != nil { + t.Fatalf("Failed to calculate floor data gas: %v", err) + } + if floorGas <= params.MaxTxGas { + t.Fatalf("Test setup error: floor data gas %d should exceed MaxTxGas %d", floorGas, params.MaxTxGas) + } + t.Logf("Floor data gas: %d, MaxTxGas: %d", floorGas, params.MaxTxGas) + + // Create a transaction with large calldata. The gas limit is set high enough + // to cover the floor data gas, but since floor data gas > MaxTxGas, + // this should be rejected in Amsterdam. + tx, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ + Nonce: 0, + GasTipCap: big.NewInt(params.InitialBaseFee), + GasFeeCap: big.NewInt(params.InitialBaseFee), + Gas: floorGas + 1000000, // Enough gas to cover floor + state gas + To: &common.Address{}, + Value: big.NewInt(0), + Data: largeData, + }), signer, key1) + + var ( + db = rawdb.NewMemoryDatabase() + gspec = &Genesis{ + Config: config, + GasLimit: 100_000_000, // High block gas limit to not interfere with the test + Alloc: types.GenesisAlloc{ + common.HexToAddress("0x71562b71999873DB5b286dF957af199Ec94617F7"): types.Account{ + Balance: new(big.Int).Mul(big.NewInt(1000000), big.NewInt(params.Ether)), // Lots of ether + Nonce: 0, + }, + }, + } + blockchain, _ = NewBlockChain(db, gspec, beacon.New(ethash.NewFaker()), nil) + ) + defer blockchain.Stop() + + block := GenerateBadBlock(gspec.ToBlock(), beacon.New(ethash.NewFaker()), types.Transactions{tx}, config, false) + _, err = blockchain.InsertChain(types.Blocks{block}) + if err == nil { + t.Fatal("block with floor data gas > MaxTxGas should have been rejected") + } + // The error should indicate that max regular gas exceeds the limit + if !strings.Contains(err.Error(), "max regular gas") || !strings.Contains(err.Error(), "exceeds limit") { + t.Errorf("unexpected error message: %v", err) + } + t.Logf("Got expected error: %v", err) +} diff --git a/core/state_transition.go b/core/state_transition.go index 4b2cd7989f..1ac580f3ef 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -470,11 +470,29 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if err != nil { return nil, err } + + // Compute the floor data cost (EIP-7623), needed for both Prague and Amsterdam validation. + if rules.IsPrague { + floorDataGas, err = FloorDataGas(msg.Data) + if err != nil { + return nil, err + } + if msg.GasLimit < floorDataGas { + return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas) + } + } + if rules.IsAmsterdam { // EIP-8037: total intrinsic must fit within the transaction gas limit. if msg.GasLimit < gas.Sum() { return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, msg.GasLimit, gas.Sum()) } + // EIP-8037: the regular gas consumption (intrinsic or floor) must fit within MaxTxGas. + // The transaction gas limit is no longer statically capped, but regular gas usage is. + maxRegularGas := max(gas.RegularGas, floorDataGas) + if maxRegularGas > params.MaxTxGas { + return nil, fmt.Errorf("%w: max regular gas %d exceeds limit %d", ErrIntrinsicGas, maxRegularGas, params.MaxTxGas) + } // Split remaining execution gas into regular and state reservoir. executionGas := msg.GasLimit - gas.Sum() regularGas := min(params.MaxTxGas-gas.RegularGas, executionGas) @@ -485,17 +503,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } st.gasRemaining.Sub(gas) } - - // Gas limit suffices for the floor data cost (EIP-7623) - if rules.IsPrague { - floorDataGas, err = FloorDataGas(msg.Data) - if err != nil { - return nil, err - } - if msg.GasLimit < floorDataGas { - return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas) - } - } if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { if rules.IsAmsterdam { t.OnGasChange(msg.GasLimit, st.gasRemaining.RegularGas+st.gasRemaining.StateGas, tracing.GasChangeTxIntrinsicGas)