Skip to content

Commit e98f94d

Browse files
authored
execution: implement EIP-8037 changes and simplifications for bal-devnet-7 (#21207)
for https://notes.ethereum.org/@ethpandaops/bal-devnet-7
1 parent be461c2 commit e98f94d

45 files changed

Lines changed: 902 additions & 548 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

execution/exec/block_assembler.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,15 +182,20 @@ func (ba *BlockAssembler) AddTransactions(
182182
// based on position in the block's transaction list).
183183
txnIdx := len(ba.Txns)
184184
header := ba.AssembledBlock.Header
185-
// EIP-8037: initialize the pool from cumulative regular gas, not the
186-
// bottleneck (max of regular, state) stored in header.GasUsed. This
187-
// gives compute-heavy transactions access to the full regular gas
188-
// budget even when state gas dominates the bottleneck. State gas is
189-
// enforced in applyTransaction before FinalizeTx.
190-
gasPool := new(protocol.GasPool).AddGas(header.GasLimit - ba.gasUsed.BlockRegular)
185+
// EIP-8037: regular and state gas pools deplete independently. The
186+
// builder calls AddTransactions repeatedly with batches from the txpool,
187+
// so each call must initialise both dimensions from their own cumulative
188+
// usage. Seeding both from BlockRegular only would over-inflate the
189+
// state pool whenever state gas has run ahead of regular gas.
190+
blobBudget := uint64(0)
191191
if header.BlobGasUsed != nil {
192-
gasPool.AddBlobGas(ba.cfg.ChainConfig.GetMaxBlobGasPerBlock(header.Time) - *header.BlobGasUsed)
192+
blobBudget = ba.cfg.ChainConfig.GetMaxBlobGasPerBlock(header.Time) - *header.BlobGasUsed
193193
}
194+
gasPool := protocol.NewBlockGasPool(
195+
header.GasLimit-ba.gasUsed.BlockRegular,
196+
header.GasLimit-ba.gasUsed.BlockState,
197+
blobBudget,
198+
)
194199
signer := types.MakeSigner(ba.cfg.ChainConfig, header.Number.Uint64(), header.Time)
195200

196201
var coalescedLogs types.Logs
@@ -217,7 +222,13 @@ func (ba *BlockAssembler) AddTransactions(
217222

218223
var commitTx = func(txn types.Transaction, coinbase accounts.Address, vmConfig *vm.Config, chainConfig *chain.Config, ibs *state.IntraBlockState, current *AssembledBlock) ([]*types.Log, error) {
219224
ibs.SetTxContext(current.Header.Number.Uint64(), txnIdx)
220-
gasSnap := gasPool.Gas()
225+
// EIP-8037: regular and state gas pool dimensions can deplete
226+
// independently — execution-time state-gas (e.g. CREATE code deposit)
227+
// is not visible to the txpool's intrinsic-state-gas filter and may
228+
// drain the state pool faster than the regular pool. Snapshot both
229+
// dimensions so a failed-inclusion restore puts each one back.
230+
regularGasSnap := gasPool.RegularGasAvailable()
231+
stateGasSnap := gasPool.StateGasAvailable()
221232
blobGasSnap := gasPool.BlobGas()
222233
snap := ibs.PushSnapshot()
223234
defer ibs.PopSnapshot(snap)
@@ -229,14 +240,14 @@ func (ba *BlockAssembler) AddTransactions(
229240
paymasterContext, validationGasUsed, err := aa.ValidateAATransaction(aaTxn, ibs, gasPool, header, evm, chainConfig)
230241
if err != nil {
231242
ibs.RevertToSnapshot(snap, err)
232-
gasPool = new(protocol.GasPool).AddGas(gasSnap).AddBlobGas(blobGasSnap)
243+
gasPool = protocol.NewBlockGasPool(regularGasSnap, stateGasSnap, blobGasSnap)
233244
return nil, err
234245
}
235246

236247
status, aaGasUsed, err := aa.ExecuteAATransaction(aaTxn, paymasterContext, validationGasUsed, gasPool, evm, header, ibs)
237248
if err != nil {
238249
ibs.RevertToSnapshot(snap, err)
239-
gasPool = new(protocol.GasPool).AddGas(gasSnap).AddBlobGas(blobGasSnap)
250+
gasPool = protocol.NewBlockGasPool(regularGasSnap, stateGasSnap, blobGasSnap)
240251
return nil, err
241252
}
242253

@@ -263,7 +274,7 @@ func (ba *BlockAssembler) AddTransactions(
263274
// Restore cumulative gas to pre-tx values.
264275
*gasUsed = gasSnapshot
265276
ibs.RevertToSnapshot(snap, err)
266-
gasPool = new(protocol.GasPool).AddGas(gasSnap).AddBlobGas(blobGasSnap)
277+
gasPool = protocol.NewBlockGasPool(regularGasSnap, stateGasSnap, blobGasSnap)
267278
return nil, err
268279
}
269280
protocol.SetGasUsed(header, gasUsed)

execution/execmodule/exec_module_test.go

Lines changed: 259 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package execmodule_test
1919
import (
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.
11151117
func 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+
13061557
func 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

Comments
 (0)