diff --git a/cl/beacon/handler/block_production.go b/cl/beacon/handler/block_production.go index 73ecd76710a..4b0bb5540d0 100644 --- a/cl/beacon/handler/block_production.go +++ b/cl/beacon/handler/block_production.go @@ -817,6 +817,8 @@ func (a *ApiHandler) produceBeaconBody( if stateVersion.AfterOrEqual(clparams.GloasVersion) { sn := hexutil.Uint64(targetSlot) attrs.SlotNumber = &sn + tgl := hexutil.Uint64(a.beaconChainCfg.DefaultBuilderGasLimit) + attrs.TargetGasLimit = &tgl } idBytes, err := a.engine.ForkChoiceUpdate( ctx, diff --git a/cl/phase1/stages/forkchoice.go b/cl/phase1/stages/forkchoice.go index d4f0d0168dd..5276232f18d 100644 --- a/cl/phase1/stages/forkchoice.go +++ b/cl/phase1/stages/forkchoice.go @@ -274,6 +274,8 @@ func emitNextPaylodAttributesEvent(cfg *Cfg, headSlot uint64, headRoot common.Ha if cfg.beaconCfg.GetCurrentStateVersion(epoch).AfterOrEqual(clparams.GloasVersion) { sn := hexutil.Uint64(nextSlot) payloadAttributes.SlotNumber = &sn + tgl := hexutil.Uint64(cfg.beaconCfg.DefaultBuilderGasLimit) + payloadAttributes.TargetGasLimit = &tgl } e := &beaconevents.PayloadAttributesData{ Version: cfg.beaconCfg.GetCurrentStateVersion(epoch).String(), diff --git a/execution/builder/create_block.go b/execution/builder/create_block.go index 1d7587f2449..b4f2d0e071b 100644 --- a/execution/builder/create_block.go +++ b/execution/builder/create_block.go @@ -164,7 +164,13 @@ func createBlock(ctx context.Context, sd *execctx.SharedDomains, tx kv.TemporalT uncles: mapset.NewSet[common.Hash](), } - header := MakeEmptyHeader(parent, cfg.chainConfig, timestamp, cfg.builder.BuilderConfig.GasLimit) + targetGasLimit := cfg.builder.BuilderConfig.GasLimit + if cfg.blockBuilderParameters != nil && cfg.blockBuilderParameters.TargetGasLimit != nil { + // PayloadAttributesV4: CL-supplied target gas limit takes precedence over the + // static --miner.gaslimit so the EL follows engine_forkchoiceUpdatedV4. + targetGasLimit = cfg.blockBuilderParameters.TargetGasLimit + } + header := MakeEmptyHeader(parent, cfg.chainConfig, timestamp, targetGasLimit) if err := misc.VerifyGaslimit(parent.GasLimit, header.GasLimit); err != nil { logger.Warn("Failed to verify gas limit given by the validator, defaulting to parent gas limit", "err", err) header.GasLimit = parent.GasLimit diff --git a/execution/builder/parameters.go b/execution/builder/parameters.go index c06344a42a8..a9c5025737b 100644 --- a/execution/builder/parameters.go +++ b/execution/builder/parameters.go @@ -33,6 +33,7 @@ type Parameters struct { Withdrawals []*types.Withdrawal // added in Shapella (EIP-4895) ParentBeaconBlockRoot *common.Hash // added in Dencun (EIP-4788) SlotNumber *uint64 // added in Amsterdam (EIP-7843) + TargetGasLimit *uint64 // added in Amsterdam PayloadAttributesV4 // CustomTxnProvider overrides the block's transaction source when non-nil. // nil → use the injected TxnProvider (normal mempool path) CustomTxnProvider txnprovider.TxnProvider diff --git a/execution/engineapi/engine_api_builder_test.go b/execution/engineapi/engine_api_builder_test.go index b2ea4375ad2..21b7ae62c44 100644 --- a/execution/engineapi/engine_api_builder_test.go +++ b/execution/engineapi/engine_api_builder_test.go @@ -38,6 +38,7 @@ import ( "github.com/erigontech/erigon/execution/protocol/params" "github.com/erigontech/erigon/execution/state/contracts" "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/node/ethconfig" "github.com/erigontech/erigon/rpc" ) @@ -264,6 +265,57 @@ func TestEngineApiBlockGasOverflowSpillsToNextBlock(t *testing.T) { }) } +// TestEngineApiV4TargetGasLimitOverridesMinerGasLimit checks that a CL-supplied +// targetGasLimit in PayloadAttributesV4 (engine_forkchoiceUpdatedV4) overrides +// the EL's static --miner.gaslimit when building a block — and that the +// resulting block respects the CL target as a cap. +// +// Setup picks numbers so the two values produce distinguishable block contents: +// - parent gas limit = 42_000 (room for two 21K-gas transfers) +// - static --miner.gaslimit = 21_000 (would cap the block at one transfer) +// - CL targetGasLimit = 42_000 (room for two transfers) +// +// Three transfers are submitted; only two must fit. If the static target won, +// the block would gas-limit at ~41_960 and contain a single transfer. +// See https://github.com/ethereum/execution-apis/pull/796. +func TestEngineApiV4TargetGasLimitOverridesMinerGasLimit(t *testing.T) { + ctx := t.Context() + logger := testlog.Logger(t, log.LvlDebug) + const targetGasLimit uint64 = 42_000 + const minerGasLimit uint64 = 21_000 + genesis, coinbaseKey, err := engineapitester.DefaultEngineApiTesterGenesis() + require.NoError(t, err) + genesis.GasLimit = targetGasLimit + eat, err := engineapitester.InitialiseEngineApiTester(ctx, engineapitester.EngineApiTesterInitArgs{ + Logger: logger, + DataDir: t.TempDir(), + Genesis: genesis, + CoinbaseKey: coinbaseKey, + EthConfigTweaker: func(config *ethconfig.Config) { + gl := minerGasLimit + config.Builder.GasLimit = &gl + }, + }) + require.NoError(t, err) + t.Cleanup(func() { + err := eat.Close() + require.NoError(t, err) + }) + eat.Run(t, func(ctx context.Context, t *testing.T, eat engineapitester.EngineApiTester) { + receiver := common.HexToAddress("0x42") + // Submit 3 transfers; only 2 should fit under the CL-supplied 42K cap. + for i := 0; i < 3; i++ { + _, err := eat.Transactor.SubmitSimpleTransfer(eat.CoinbaseKey, receiver, big.NewInt(int64(i+1))) + require.NoError(t, err) + } + payload, err := eat.MockCl.BuildCanonicalBlock(ctx) + require.NoError(t, err) + // Block gas limit follows the CL target — not the EL's --miner.gaslimit. + require.Equal(t, hexutil.Uint64(targetGasLimit), payload.ExecutionPayload.GasLimit) + require.Len(t, payload.ExecutionPayload.Transactions, 2) + }) +} + func TestEngineApiSequentialNonceAdvancement(t *testing.T) { ctx := t.Context() logger := testlog.Logger(t, log.LvlDebug) diff --git a/execution/engineapi/engine_server.go b/execution/engineapi/engine_server.go index d61dfa12f44..8a70f987f5b 100644 --- a/execution/engineapi/engine_server.go +++ b/execution/engineapi/engine_server.go @@ -231,6 +231,10 @@ func (s *EngineServer) validatePayloadAttributesPostFCU(version clparams.StateVe if version >= clparams.GloasVersion && payloadAttributes.SlotNumber == nil { return &engine_helpers.InvalidPayloadAttributesErr // SlotNumber required for Glamsterdam (EIP-7843) } + // TODO: enable once tests catch up with glamsterdam-devnet-4 spec + // if version >= clparams.GloasVersion && payloadAttributes.TargetGasLimit == nil { + // return &engine_helpers.InvalidPayloadAttributesErr // TargetGasLimit required for V4 attrs + // } return nil } @@ -795,6 +799,7 @@ func (s *EngineServer) forkchoiceUpdated(ctx context.Context, forkchoiceState *e PrevRandao: payloadAttributes.PrevRandao, SuggestedFeeRecipient: payloadAttributes.SuggestedFeeRecipient, SlotNumber: (*uint64)(payloadAttributes.SlotNumber), + TargetGasLimit: (*uint64)(payloadAttributes.TargetGasLimit), } if version >= clparams.CapellaVersion { diff --git a/execution/engineapi/engine_types/jsonrpc.go b/execution/engineapi/engine_types/jsonrpc.go index 44b44389e73..0e011bc576c 100644 --- a/execution/engineapi/engine_types/jsonrpc.go +++ b/execution/engineapi/engine_types/jsonrpc.go @@ -70,6 +70,7 @@ type PayloadAttributes struct { Withdrawals []*types.Withdrawal `json:"withdrawals"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot"` SlotNumber *hexutil.Uint64 `json:"slotNumber"` + TargetGasLimit *hexutil.Uint64 `json:"targetGasLimit"` SSZVersion clparams.StateVersion `json:"-"` } diff --git a/execution/engineapi/engine_types/ssz.go b/execution/engineapi/engine_types/ssz.go index 9c3935a54a1..dc1d179602f 100644 --- a/execution/engineapi/engine_types/ssz.go +++ b/execution/engineapi/engine_types/ssz.go @@ -302,6 +302,10 @@ func (a *PayloadAttributes) EncodeSSZ(dst []byte) ([]byte, error) { if a.SlotNumber != nil { slot = uint64(*a.SlotNumber) } + var targetGasLimit uint64 + if a.TargetGasLimit != nil { + targetGasLimit = uint64(*a.TargetGasLimit) + } switch version { case clparams.BellatrixVersion: return ssz2.MarshalSSZ(dst, uint64(a.Timestamp), a.PrevRandao[:], a.SuggestedFeeRecipient[:]) @@ -310,7 +314,7 @@ func (a *PayloadAttributes) EncodeSSZ(dst []byte) ([]byte, error) { case clparams.DenebVersion: return ssz2.MarshalSSZ(dst, uint64(a.Timestamp), a.PrevRandao[:], a.SuggestedFeeRecipient[:], withdrawals, root[:]) default: - return ssz2.MarshalSSZ(dst, uint64(a.Timestamp), a.PrevRandao[:], a.SuggestedFeeRecipient[:], withdrawals, root[:], slot) + return ssz2.MarshalSSZ(dst, uint64(a.Timestamp), a.PrevRandao[:], a.SuggestedFeeRecipient[:], withdrawals, root[:], slot, targetGasLimit) } } @@ -320,6 +324,7 @@ func (a *PayloadAttributes) DecodeSSZ(buf []byte, version int) error { var timestamp uint64 var root common.Hash var slot uint64 + var targetGasLimit uint64 switch a.SSZVersion { case clparams.BellatrixVersion: if err := ssz2.UnmarshalSSZ(buf, version, ×tamp, a.PrevRandao[:], a.SuggestedFeeRecipient[:]); err != nil { @@ -337,13 +342,15 @@ func (a *PayloadAttributes) DecodeSSZ(buf []byte, version int) error { a.Withdrawals = withdrawalsFromList(withdrawals) a.ParentBeaconBlockRoot = &root default: - if err := ssz2.UnmarshalSSZ(buf, version, ×tamp, a.PrevRandao[:], a.SuggestedFeeRecipient[:], withdrawals, root[:], &slot); err != nil { + if err := ssz2.UnmarshalSSZ(buf, version, ×tamp, a.PrevRandao[:], a.SuggestedFeeRecipient[:], withdrawals, root[:], &slot, &targetGasLimit); err != nil { return err } a.Withdrawals = withdrawalsFromList(withdrawals) a.ParentBeaconBlockRoot = &root slotNumber := hexutil.Uint64(slot) a.SlotNumber = &slotNumber + tgl := hexutil.Uint64(targetGasLimit) + a.TargetGasLimit = &tgl } a.Timestamp = hexutil.Uint64(timestamp) return nil diff --git a/execution/engineapi/engineapitester/mock_cl.go b/execution/engineapi/engineapitester/mock_cl.go index 68ff01112f9..18394d673e9 100644 --- a/execution/engineapi/engineapitester/mock_cl.go +++ b/execution/engineapi/engineapitester/mock_cl.go @@ -55,6 +55,7 @@ type MockCl struct { engineApiClient *engineapi.JsonRpcClient suggestedFeeRecipient common.Address genesis common.Hash + genesisGasLimit uint64 state *MockClState blockListener *shutter.BlockListener chainConfig *chain.Config @@ -71,6 +72,7 @@ func NewMockCl(ctx context.Context, logger log.Logger, elClient *engineapi.JsonR blockListener: shutter.NewBlockListener(logger, stateChangesClient), suggestedFeeRecipient: genesis.Coinbase(), genesis: genesis.Hash(), + genesisGasLimit: genesis.GasLimit(), chainConfig: chainConfig, state: &MockClState{ ParentElBlock: genesis.Hash(), @@ -152,6 +154,8 @@ func (cl *MockCl) BuildNewPayload(ctx context.Context, opts ...BlockBuildingOpti } if cl.chainConfig.AmsterdamTime != nil { payloadAttributes.SlotNumber = (*hexutil.Uint64)(&slotNumber) + targetGasLimit := hexutil.Uint64(cl.genesisGasLimit) + payloadAttributes.TargetGasLimit = &targetGasLimit } cl.logger.Debug("[mock-cl] building block", "timestamp", timestamp) // start the block building process diff --git a/execution/engineapi/sszrest_test.go b/execution/engineapi/sszrest_test.go index ee2635975d6..54e7360e4de 100644 --- a/execution/engineapi/sszrest_test.go +++ b/execution/engineapi/sszrest_test.go @@ -247,11 +247,13 @@ func TestSSZRESTNewPayloadV5UsesGloasPayloadSchema(t *testing.T) { func TestSSZRESTForkchoiceV4UsesGloasPayloadAttributesSchema(t *testing.T) { slotNumber := hexutil.Uint64(456) + targetGasLimit := hexutil.Uint64(36000000) attrs := &engine_types.PayloadAttributes{ Timestamp: 1, SuggestedFeeRecipient: common.HexToAddress("0x1234"), Withdrawals: nil, SlotNumber: &slotNumber, + TargetGasLimit: &targetGasLimit, SSZVersion: clparams.GloasVersion, } state := engine_types.ForkChoiceState{} @@ -266,6 +268,8 @@ func TestSSZRESTForkchoiceV4UsesGloasPayloadAttributesSchema(t *testing.T) { require.NotNil(t, engineAttrs) require.NotNil(t, engineAttrs.SlotNumber) require.Equal(t, hexutil.Uint64(456), *engineAttrs.SlotNumber) + require.NotNil(t, engineAttrs.TargetGasLimit) + require.Equal(t, hexutil.Uint64(36000000), *engineAttrs.TargetGasLimit) } func TestExchangeCapabilitiesAdvertisesJSONRPCAndSSZREST(t *testing.T) { diff --git a/execution/engineapi/testing_api.go b/execution/engineapi/testing_api.go index 9db875e3603..906d9ed0b9b 100644 --- a/execution/engineapi/testing_api.go +++ b/execution/engineapi/testing_api.go @@ -206,6 +206,7 @@ func (t *testingImpl) BuildBlockV1( PrevRandao: payloadAttributes.PrevRandao, SuggestedFeeRecipient: payloadAttributes.SuggestedFeeRecipient, SlotNumber: (*uint64)(payloadAttributes.SlotNumber), + TargetGasLimit: (*uint64)(payloadAttributes.TargetGasLimit), CustomTxnProvider: customProvider, } if version >= clparams.CapellaVersion { diff --git a/execution/execmodule/chainreader/chain_reader.go b/execution/execmodule/chainreader/chain_reader.go index 64ac83b3771..9068ebd0dd7 100644 --- a/execution/execmodule/chainreader/chain_reader.go +++ b/execution/execmodule/chainreader/chain_reader.go @@ -336,6 +336,7 @@ func (c ChainReaderWriterEth1) AssembleBlock(baseHash common.Hash, attributes *e SuggestedFeeRecipient: attributes.SuggestedFeeRecipient, Withdrawals: attributes.Withdrawals, SlotNumber: (*uint64)(attributes.SlotNumber), + TargetGasLimit: (*uint64)(attributes.TargetGasLimit), ParentBeaconBlockRoot: attributes.ParentBeaconBlockRoot, } result, err := c.executionModule.AssembleBlock(context.Background(), params)