@@ -19,6 +19,7 @@ package execmodule_test
1919import (
2020 "bytes"
2121 "context"
22+ "crypto/ecdsa"
2223 "crypto/rand"
2324 "encoding/binary"
2425 "encoding/hex"
@@ -524,7 +525,7 @@ func TestAssembleBlockWithContractCreation(t *testing.T) {
524525 require .NoError (t , err )
525526
526527 contractTx , err := types .SignTx (
527- types .NewContractCreation (1 , uint256 .NewInt (0 ), 200_000 , uint256 .NewInt (baseFee ), changerBytecode ),
528+ types .NewContractCreation (1 , uint256 .NewInt (0 ), 300_000 , uint256 .NewInt (baseFee ), changerBytecode ),
528529 * types .LatestSignerForChainID (m .ChainConfig .ChainID ), m .Key ,
529530 )
530531 require .NoError (t , err )
@@ -678,7 +679,7 @@ func TestAssembleBlockMixedTxTypes(t *testing.T) {
678679 changerBytecode , err := hex .DecodeString (contracts .ChangerBin [2 :])
679680 require .NoError (t , err )
680681 tx2 , err := types .SignTx (
681- types .NewContractCreation (2 , uint256 .NewInt (0 ), 200_000 , uint256 .NewInt (baseFee ), changerBytecode ),
682+ types .NewContractCreation (2 , uint256 .NewInt (0 ), 300_000 , uint256 .NewInt (baseFee ), changerBytecode ),
682683 * types .LatestSignerForChainID (m .ChainConfig .ChainID ), m .Key )
683684 require .NoError (t , err )
684685
@@ -1108,10 +1109,11 @@ func drainHeaders(t *testing.T, ch <-chan [][]byte, timeout time.Duration) {
11081109// TestAssembleBlockStateGasLimit verifies that the builder respects the EIP-8037
11091110// block validity invariant: gas_used = max(regular, state) <= gas_limit.
11101111//
1111- // Contract creations have high intrinsic state gas (~131K per create at
1112- // CostPerStateByte=1174) but low regular gas (~30K). With a 500K gas limit,
1113- // about 4 creates would push state gas past the limit even though regular gas
1114- // has room. Without the fix the builder would produce an invalid block.
1112+ // Contract creations have high intrinsic state gas (~184K per create at
1113+ // CostPerStateByte=1530, STATE_BYTES_PER_NEW_ACCOUNT=120) but low regular gas
1114+ // (~30K). With a 500K gas limit, about 3 creates would push state gas past
1115+ // the limit even though regular gas has room. Without the fix the builder
1116+ // would produce an invalid block.
11151117func TestAssembleBlockStateGasLimit (t * testing.T ) {
11161118 t .Parallel ()
11171119 ctx := t .Context ()
@@ -1150,7 +1152,7 @@ func TestAssembleBlockStateGasLimit(t *testing.T) {
11501152 rlpTxs := make ([][]byte , 10 )
11511153 for i := range rlpTxs {
11521154 tx , txErr := types .SignTx (
1153- types .NewContractCreation (uint64 (i ), uint256 .NewInt (0 ), 200_000 , uint256 .NewInt (baseFee ), deployCode ),
1155+ types .NewContractCreation (uint64 (i ), uint256 .NewInt (0 ), 300_000 , uint256 .NewInt (baseFee ), deployCode ),
11541156 * types .LatestSignerForChainID (m .ChainConfig .ChainID ), privKey ,
11551157 )
11561158 require .NoError (t , txErr )
@@ -1303,6 +1305,255 @@ func TestAssembleBlockStateGasLimitSSTORE(t *testing.T) {
13031305 require .NoError (t , err )
13041306}
13051307
1308+ // TestAssembleBlockGasPoolSnapshotRestoreBug exercises the per-tx gas pool
1309+ // snapshot/restore in the block assembler's commitTx path. Under EIP-8037 the
1310+ // pool tracks regular and state gas as separate dimensions, so a restore that
1311+ // only captures the regular dimension and seeds both on restore wrongly
1312+ // inflates the state pool, letting a follow-up tx exceed the block's
1313+ // state-gas limit.
1314+ //
1315+ // The scenario relies on a tx whose intrinsic state gas (the part the txpool
1316+ // can see when filtering) fits in the remaining pool but whose total state
1317+ // gas (intrinsic + on-success code-deposit) does not: such a tx passes the
1318+ // txpool's filter, reaches the assembler, and fails ConsumeState inside
1319+ // ApplyTransaction. The bug then surfaces if a successor tx in the same
1320+ // batch consumes the inflated state pool that the restore wrongly handed
1321+ // back.
1322+ //
1323+ // Fresh senders (each at nonce 0) are required so a failed tx doesn't
1324+ // nonce-block its successors within the batch.
1325+ func TestAssembleBlockGasPoolSnapshotRestoreBug (t * testing.T ) {
1326+ t .Parallel ()
1327+ ctx := t .Context ()
1328+
1329+ // Initcode that deploys a 100-byte runtime (100 zero bytes) via CODECOPY.
1330+ // Per byte deployed: 1530 state gas (CPSB) + 200 regular gas. So each
1331+ // CREATE consumes ~153K state gas on top of the 183.6K intrinsic
1332+ // NEW_ACCOUNT charge — a total of ~337K state per tx.
1333+ const runtimeLen = 100
1334+ initHeader := []byte {
1335+ 0x60 , byte (runtimeLen ), // PUSH1 length
1336+ 0x60 , 0x0c , // PUSH1 12 (runtime offset in initcode)
1337+ 0x60 , 0x00 , // PUSH1 0 (memory destination)
1338+ 0x39 , // CODECOPY
1339+ 0x60 , byte (runtimeLen ), // PUSH1 length
1340+ 0x60 , 0x00 , // PUSH1 0 (memory offset)
1341+ 0xf3 , // RETURN
1342+ }
1343+ deployCode := append (initHeader , make ([]byte , runtimeLen )... )
1344+
1345+ // With a 1_000_000 block gas limit, two txs (~337K state each) leave
1346+ // the pool at ~326K; a third has 184K intrinsic state (fits in pool by
1347+ // txpool's filter) but needs 337K total (fails ConsumeState during
1348+ // execution). A fourth tx then succeeds against the wrongly inflated
1349+ // pool, pushing the block's total state gas past gas_limit.
1350+ const numSenders = 4
1351+ keys := make ([]* ecdsa.PrivateKey , numSenders )
1352+ addrs := make ([]common.Address , numSenders )
1353+ for i := range keys {
1354+ k , err := crypto .GenerateKey ()
1355+ require .NoError (t , err )
1356+ keys [i ] = k
1357+ addrs [i ] = crypto .PubkeyToAddress (k .PublicKey )
1358+ }
1359+
1360+ alloc := types.GenesisAlloc {}
1361+ for _ , a := range addrs {
1362+ alloc [a ] = types.GenesisAccount {Balance : new (big.Int ).Exp (big .NewInt (10 ), big .NewInt (18 ), nil )}
1363+ }
1364+ genesis := & types.Genesis {
1365+ Config : chain .AllProtocolChanges ,
1366+ GasLimit : 1_000_000 ,
1367+ Alloc : alloc ,
1368+ }
1369+
1370+ m := execmoduletester .New (t ,
1371+ execmoduletester .WithGenesisSpec (genesis ),
1372+ execmoduletester .WithKey (keys [0 ]),
1373+ execmoduletester .WithTxPool (),
1374+ )
1375+ exec := m .ExecModule
1376+ txpool := m .TxPoolGrpcServer
1377+
1378+ chainPack , err := blockgen .GenerateChain (m .ChainConfig , m .Genesis , m .Engine , m .DB , 1 ,
1379+ func (i int , gen * blockgen.BlockGen ) {})
1380+ require .NoError (t , err )
1381+ require .NoError (t , m .InsertChain (chainPack ))
1382+
1383+ signer := * types .LatestSignerForChainID (m .ChainConfig .ChainID )
1384+ baseFee := chainPack .TopBlock .BaseFee ().Uint64 ()
1385+
1386+ rlpTxs := make ([][]byte , numSenders )
1387+ for i , k := range keys {
1388+ tx , err := types .SignTx (
1389+ types .NewContractCreation (0 , uint256 .NewInt (0 ), 400_000 , uint256 .NewInt (baseFee ), deployCode ),
1390+ signer , k ,
1391+ )
1392+ require .NoError (t , err )
1393+ var buf bytes.Buffer
1394+ require .NoError (t , tx .EncodeRLP (& buf ))
1395+ rlpTxs [i ] = buf .Bytes ()
1396+ }
1397+ r , err := txpool .Add (ctx , & txpoolproto.AddRequest {RlpTxs : rlpTxs })
1398+ require .NoError (t , err )
1399+ for _ , e := range r .Errors {
1400+ require .Equal (t , "success" , e )
1401+ }
1402+
1403+ slotNumber := uint64 (1 )
1404+ parentBeaconBlockRoot := randomHash ()
1405+ payloadId , err := assembleBlock (ctx , exec , & builder.Parameters {
1406+ ParentHash : chainPack .TopBlock .Hash (),
1407+ Timestamp : chainPack .TopBlock .Header ().Time + 1 ,
1408+ PrevRandao : randomHash (),
1409+ SuggestedFeeRecipient : common.Address {1 },
1410+ Withdrawals : make ([]* types.Withdrawal , 0 ),
1411+ ParentBeaconBlockRoot : & parentBeaconBlockRoot ,
1412+ SlotNumber : & slotNumber ,
1413+ })
1414+ require .NoError (t , err )
1415+ block , err := getAssembledBlock (ctx , exec , payloadId )
1416+ require .NoError (t , err )
1417+
1418+ require .Greater (t , len (block .Transactions ()), 0 , "block should contain at least one tx" )
1419+ require .LessOrEqual (t , block .GasUsed (), block .GasLimit (),
1420+ "gas_used (max of regular, state) must not exceed gas_limit" )
1421+
1422+ require .NoError (t , insertValidateAndUfc1By1 (ctx , exec , []* types.Block {block }))
1423+ }
1424+
1425+ // TestAssembleBlockGasPoolMultiBatchInitBug exercises the block assembler's
1426+ // per-batch gas-pool initialisation. The block builder calls AddTransactions
1427+ // repeatedly with batches of up to 50 txs from the txpool. Each call must
1428+ // build the pool with the *per-dimension* remaining budget; seeding both
1429+ // dimensions from the regular-only cumulative gas wrongly inflates the state
1430+ // pool when state gas has run ahead of regular gas after the previous batch,
1431+ // letting a tx in the next batch consume state past gas_limit.
1432+ //
1433+ // The scenario: 50 contract creations in batch 1 push cumulative state gas
1434+ // near the block gas limit while keeping cumulative regular gas low (CREATE
1435+ // has ~184K intrinsic state vs ~30K intrinsic regular per tx). The 51st tx
1436+ // has small intrinsic state (so the txpool's state-aware filter admits it
1437+ // into batch 2) but a large code-deposit state on execution. With the pool
1438+ // init seeded from regular gas only, batch 2 starts with a state pool that
1439+ // matches the regular dimension — i.e. far more than the real remaining
1440+ // state budget — and the 51st tx is wrongly accepted.
1441+ func TestAssembleBlockGasPoolMultiBatchInitBug (t * testing.T ) {
1442+ t .Parallel ()
1443+ ctx := t .Context ()
1444+
1445+ // 50 zero-deposit CREATE txs (initcode returns nothing) at higher gas
1446+ // price for batch 1, plus 1 large-deposit CREATE at lower gas price so
1447+ // the txpool orders it into batch 2.
1448+ const numBatch1 = 50
1449+ const numTotal = numBatch1 + 1
1450+ keys := make ([]* ecdsa.PrivateKey , numTotal )
1451+ addrs := make ([]common.Address , numTotal )
1452+ for i := range keys {
1453+ k , err := crypto .GenerateKey ()
1454+ require .NoError (t , err )
1455+ keys [i ] = k
1456+ addrs [i ] = crypto .PubkeyToAddress (k .PublicKey )
1457+ }
1458+
1459+ alloc := types.GenesisAlloc {}
1460+ for _ , a := range addrs {
1461+ alloc [a ] = types.GenesisAccount {Balance : new (big.Int ).Exp (big .NewInt (10 ), big .NewInt (18 ), nil )}
1462+ }
1463+ // 10M block limit fits ~54 CREATEs by intrinsic state (184K each ≈ 9.2M
1464+ // for 50). Leaves ~800K of state headroom for batch 2, which the trigger
1465+ // tx's ~1.2M total state exceeds.
1466+ genesis := & types.Genesis {
1467+ Config : chain .AllProtocolChanges ,
1468+ GasLimit : 10_000_000 ,
1469+ Alloc : alloc ,
1470+ }
1471+
1472+ m := execmoduletester .New (t ,
1473+ execmoduletester .WithGenesisSpec (genesis ),
1474+ execmoduletester .WithKey (keys [0 ]),
1475+ execmoduletester .WithTxPool (),
1476+ )
1477+ exec := m .ExecModule
1478+ txpool := m .TxPoolGrpcServer
1479+
1480+ chainPack , err := blockgen .GenerateChain (m .ChainConfig , m .Genesis , m .Engine , m .DB , 1 ,
1481+ func (i int , gen * blockgen.BlockGen ) {})
1482+ require .NoError (t , err )
1483+ require .NoError (t , m .InsertChain (chainPack ))
1484+
1485+ signer := * types .LatestSignerForChainID (m .ChainConfig .ChainID )
1486+ baseFee := chainPack .TopBlock .BaseFee ().Uint64 ()
1487+ highPrice := uint256 .NewInt (baseFee * 2 ) // higher tip → batch 1
1488+ lowPrice := uint256 .NewInt (baseFee ) // baseFee only → batch 2
1489+
1490+ // Batch 1 initcode: PUSH1 0, PUSH1 0, RETURN → zero-byte runtime, no
1491+ // code-deposit state. Per-tx state is just the 184K intrinsic.
1492+ batch1Init := []byte {0x60 , 0x00 , 0x60 , 0x00 , 0xf3 }
1493+
1494+ // Batch 2 initcode deploys a ~660-byte runtime via CODECOPY. Per-byte
1495+ // deposit cost: 1530 state + 200 regular. ~660 bytes → ~1M state on top
1496+ // of the 184K intrinsic.
1497+ const triggerRuntimeLen = 660
1498+ triggerInit := []byte {
1499+ 0x61 , byte (triggerRuntimeLen >> 8 ), byte (triggerRuntimeLen & 0xff ), // PUSH2 length
1500+ 0x60 , 0x0d , // PUSH1 13 (runtime offset)
1501+ 0x60 , 0x00 , // PUSH1 0 (memory dest)
1502+ 0x39 , // CODECOPY
1503+ 0x61 , byte (triggerRuntimeLen >> 8 ), byte (triggerRuntimeLen & 0xff ), // PUSH2 length
1504+ 0x60 , 0x00 , // PUSH1 0 (memory offset)
1505+ 0xf3 , // RETURN
1506+ }
1507+ triggerInit = append (triggerInit , make ([]byte , triggerRuntimeLen )... )
1508+
1509+ rlpTxs := make ([][]byte , numTotal )
1510+ for i := range numBatch1 {
1511+ tx , txErr := types .SignTx (
1512+ types .NewContractCreation (0 , uint256 .NewInt (0 ), 250_000 , highPrice , batch1Init ),
1513+ signer , keys [i ],
1514+ )
1515+ require .NoError (t , txErr )
1516+ var buf bytes.Buffer
1517+ require .NoError (t , tx .EncodeRLP (& buf ))
1518+ rlpTxs [i ] = buf .Bytes ()
1519+ }
1520+ triggerTx , err := types .SignTx (
1521+ types .NewContractCreation (0 , uint256 .NewInt (0 ), 2_000_000 , lowPrice , triggerInit ),
1522+ signer , keys [numBatch1 ],
1523+ )
1524+ require .NoError (t , err )
1525+ var triggerBuf bytes.Buffer
1526+ require .NoError (t , triggerTx .EncodeRLP (& triggerBuf ))
1527+ rlpTxs [numBatch1 ] = triggerBuf .Bytes ()
1528+
1529+ r , err := txpool .Add (ctx , & txpoolproto.AddRequest {RlpTxs : rlpTxs })
1530+ require .NoError (t , err )
1531+ for _ , e := range r .Errors {
1532+ require .Equal (t , "success" , e )
1533+ }
1534+
1535+ slotNumber := uint64 (1 )
1536+ parentBeaconBlockRoot := randomHash ()
1537+ payloadId , err := assembleBlock (ctx , exec , & builder.Parameters {
1538+ ParentHash : chainPack .TopBlock .Hash (),
1539+ Timestamp : chainPack .TopBlock .Header ().Time + 1 ,
1540+ PrevRandao : randomHash (),
1541+ SuggestedFeeRecipient : common.Address {1 },
1542+ Withdrawals : make ([]* types.Withdrawal , 0 ),
1543+ ParentBeaconBlockRoot : & parentBeaconBlockRoot ,
1544+ SlotNumber : & slotNumber ,
1545+ })
1546+ require .NoError (t , err )
1547+ block , err := getAssembledBlock (ctx , exec , payloadId )
1548+ require .NoError (t , err )
1549+
1550+ require .Greater (t , len (block .Transactions ()), 0 , "block should contain at least one tx" )
1551+ require .LessOrEqual (t , block .GasUsed (), block .GasLimit (),
1552+ "gas_used (max of regular, state) must not exceed gas_limit" )
1553+
1554+ require .NoError (t , insertValidateAndUfc1By1 (ctx , exec , []* types.Block {block }))
1555+ }
1556+
13061557func TestEIP7708BurnLogWhenCoinbaseSelfDestructs (t * testing.T ) {
13071558 // Regression test for https://github.com/erigontech/erigon/issues/19951
13081559 //
@@ -1346,7 +1597,7 @@ func TestEIP7708BurnLogWhenCoinbaseSelfDestructs(t *testing.T) {
13461597 gen .SetCoinbase (coinbaseAddr )
13471598
13481599 tx , txErr := types .SignTx (
1349- types .NewContractCreation (nonce , uint256 .NewInt (0 ), 200_000 , uint256 .NewInt (gasPrice ), initCode ),
1600+ types .NewContractCreation (nonce , uint256 .NewInt (0 ), 300_000 , uint256 .NewInt (gasPrice ), initCode ),
13501601 * signer ,
13511602 privKey ,
13521603 )
0 commit comments