mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-19 19:30:44 +00:00
beacon/engine, eth/catalyst, miner: honor targetGasLimit from payload attributes
PayloadAttributesV4 carries a required targetGasLimit field (execution-apis#796) through which the consensus layer supplies the desired gas limit with each payload request. The field was previously dropped during JSON decoding, so locally built payloads always steered the gas limit towards the miner's configured gas ceiling (default 60M) regardless of the CL-registered value. On glamsterdam-devnet-7 this made every geth proposal step the block gas limit down by the maximum parent/1024 step while the other ELs honored the 300M target registered on the CL side. - decode targetGasLimit in PayloadAttributes - reject forkchoiceUpdatedV4 attributes without it, like slotNumber - thread it through BuildPayloadArgs (and the payload id) into the worker, where it takes precedence over miner.config.GasCeil; internal callers that leave it unset (simulated beacon, testing payloads) keep the gas-ceiling behavior Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0880a88587
commit
8f289d8f62
8 changed files with 260 additions and 48 deletions
|
|
@ -22,6 +22,7 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) {
|
|||
Withdrawals []*types.Withdrawal `json:"withdrawals"`
|
||||
BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"`
|
||||
SlotNumber *hexutil.Uint64 `json:"slotNumber"`
|
||||
TargetGasLimit *hexutil.Uint64 `json:"targetGasLimit"`
|
||||
}
|
||||
var enc PayloadAttributes
|
||||
enc.Timestamp = hexutil.Uint64(p.Timestamp)
|
||||
|
|
@ -30,6 +31,7 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) {
|
|||
enc.Withdrawals = p.Withdrawals
|
||||
enc.BeaconRoot = p.BeaconRoot
|
||||
enc.SlotNumber = (*hexutil.Uint64)(p.SlotNumber)
|
||||
enc.TargetGasLimit = (*hexutil.Uint64)(p.TargetGasLimit)
|
||||
return json.Marshal(&enc)
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +44,7 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error {
|
|||
Withdrawals []*types.Withdrawal `json:"withdrawals"`
|
||||
BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"`
|
||||
SlotNumber *hexutil.Uint64 `json:"slotNumber"`
|
||||
TargetGasLimit *hexutil.Uint64 `json:"targetGasLimit"`
|
||||
}
|
||||
var dec PayloadAttributes
|
||||
if err := json.Unmarshal(input, &dec); err != nil {
|
||||
|
|
@ -68,5 +71,8 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error {
|
|||
if dec.SlotNumber != nil {
|
||||
p.SlotNumber = (*uint64)(dec.SlotNumber)
|
||||
}
|
||||
if dec.TargetGasLimit != nil {
|
||||
p.TargetGasLimit = (*uint64)(dec.TargetGasLimit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,12 +74,14 @@ type PayloadAttributes struct {
|
|||
Withdrawals []*types.Withdrawal `json:"withdrawals"`
|
||||
BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"`
|
||||
SlotNumber *uint64 `json:"slotNumber"`
|
||||
TargetGasLimit *uint64 `json:"targetGasLimit"`
|
||||
}
|
||||
|
||||
// JSON type overrides for PayloadAttributes.
|
||||
type payloadAttributesMarshaling struct {
|
||||
Timestamp hexutil.Uint64
|
||||
SlotNumber *hexutil.Uint64
|
||||
TargetGasLimit *hexutil.Uint64
|
||||
}
|
||||
|
||||
//go:generate go run github.com/fjl/gencodec -type ExecutableData -field-override executableDataMarshaling -out ed_codec.go
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
|
@ -46,3 +48,42 @@ func TestBlobs(t *testing.T) {
|
|||
t.Fatalf("Expect 128 proofs in blobs bundle, got %v", len(env.BlobsBundle.Proofs))
|
||||
}
|
||||
}
|
||||
|
||||
// TestPayloadAttributesJSON verifies the JSON encoding of PayloadAttributes,
|
||||
// in particular that the amsterdam targetGasLimit field survives a round trip
|
||||
// and that attributes as sent by a consensus client on forkchoiceUpdatedV4
|
||||
// decode correctly.
|
||||
func TestPayloadAttributesJSON(t *testing.T) {
|
||||
// PayloadAttributesV4 as sent by a Gloas consensus client.
|
||||
input := `{
|
||||
"timestamp": "0x64",
|
||||
"prevRandao": "0x0202020202020202020202020202020202020202020202020202020202020202",
|
||||
"suggestedFeeRecipient": "0x0101010101010101010101010101010101010101",
|
||||
"withdrawals": [],
|
||||
"parentBeaconBlockRoot": "0x0303030303030303030303030303030303030303030303030303030303030303",
|
||||
"slotNumber": "0x10",
|
||||
"targetGasLimit": "0x11e1a300"
|
||||
}`
|
||||
var attr PayloadAttributes
|
||||
if err := json.Unmarshal([]byte(input), &attr); err != nil {
|
||||
t.Fatalf("failed to unmarshal payload attributes: %v", err)
|
||||
}
|
||||
if attr.SlotNumber == nil || *attr.SlotNumber != 16 {
|
||||
t.Fatalf("wrong slotNumber: %v", attr.SlotNumber)
|
||||
}
|
||||
if attr.TargetGasLimit == nil || *attr.TargetGasLimit != 300_000_000 {
|
||||
t.Fatalf("wrong targetGasLimit: %v", attr.TargetGasLimit)
|
||||
}
|
||||
// Round trip.
|
||||
encoded, err := json.Marshal(&attr)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal payload attributes: %v", err)
|
||||
}
|
||||
var decoded PayloadAttributes
|
||||
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal re-encoded payload attributes: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(attr, decoded) {
|
||||
t.Fatalf("payload attributes changed in round trip:\nbefore: %+v\nafter: %+v", attr, decoded)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,7 +216,8 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV3(ctx context.Context, update engine.
|
|||
}
|
||||
|
||||
// ForkchoiceUpdatedV4 is equivalent to V3 with the addition of slot number
|
||||
// in the payload attributes. It supports only PayloadAttributesV4.
|
||||
// and target gas limit in the payload attributes. It supports only
|
||||
// PayloadAttributesV4.
|
||||
func (api *ConsensusAPI) ForkchoiceUpdatedV4(ctx context.Context, update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) {
|
||||
if params != nil {
|
||||
switch {
|
||||
|
|
@ -226,6 +227,8 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV4(ctx context.Context, update engine.
|
|||
return engine.STATUS_INVALID, attributesErr("missing beacon root")
|
||||
case params.SlotNumber == nil:
|
||||
return engine.STATUS_INVALID, attributesErr("missing slot number")
|
||||
case params.TargetGasLimit == nil:
|
||||
return engine.STATUS_INVALID, attributesErr("missing target gas limit")
|
||||
case !api.checkFork(params.Timestamp, forks.Amsterdam):
|
||||
return engine.STATUS_INVALID, unsupportedForkErr("fcuV4 must only be called for amsterdam payloads")
|
||||
}
|
||||
|
|
@ -386,6 +389,7 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo
|
|||
Withdrawals: payloadAttributes.Withdrawals,
|
||||
BeaconRoot: payloadAttributes.BeaconRoot,
|
||||
SlotNum: payloadAttributes.SlotNumber,
|
||||
TargetGasLimit: payloadAttributes.TargetGasLimit,
|
||||
Version: payloadVersion,
|
||||
}
|
||||
id := args.Id()
|
||||
|
|
|
|||
|
|
@ -2014,3 +2014,45 @@ func runGetBlobs(t testing.TB, getBlobs getBlobsFn, start, limit int, fillRandom
|
|||
t.Fatalf("Unexpected result for case %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestForkchoiceUpdatedV4TargetGasLimit checks that forkchoiceUpdatedV4
|
||||
// rejects payload attributes without the targetGasLimit field, which is
|
||||
// required as of PayloadAttributesV4 (execution-apis#796).
|
||||
func TestForkchoiceUpdatedV4TargetGasLimit(t *testing.T) {
|
||||
genesis, blocks := generateMergeChain(10, true)
|
||||
n, ethservice := startEthService(t, genesis, blocks)
|
||||
defer n.Close()
|
||||
|
||||
var (
|
||||
api = newConsensusAPIWithoutHeartbeat(ethservice)
|
||||
parent = ethservice.BlockChain().CurrentHeader()
|
||||
fcState = engine.ForkchoiceStateV1{HeadBlockHash: parent.Hash()}
|
||||
beaconRoot = common.Hash{}
|
||||
slotNum = uint64(1)
|
||||
target = uint64(300_000_000)
|
||||
)
|
||||
// Missing targetGasLimit must be rejected as invalid payload attributes.
|
||||
attrs := &engine.PayloadAttributes{
|
||||
Timestamp: parent.Time + 1,
|
||||
Withdrawals: []*types.Withdrawal{},
|
||||
BeaconRoot: &beaconRoot,
|
||||
SlotNumber: &slotNum,
|
||||
}
|
||||
_, err := api.ForkchoiceUpdatedV4(context.Background(), fcState, attrs)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing targetGasLimit, got none")
|
||||
}
|
||||
if apiErr, ok := err.(*engine.EngineAPIError); !ok || apiErr.ErrorCode() != engine.InvalidPayloadAttributes.ErrorCode() {
|
||||
t.Fatalf("expected invalid payload attributes error for missing targetGasLimit, got %v", err)
|
||||
}
|
||||
// With targetGasLimit present, validation must get past the attribute
|
||||
// checks; on this pre-amsterdam chain it then fails the fork check.
|
||||
attrs.TargetGasLimit = &target
|
||||
_, err = api.ForkchoiceUpdatedV4(context.Background(), fcState, attrs)
|
||||
if err == nil {
|
||||
t.Fatal("expected unsupported fork error, got none")
|
||||
}
|
||||
if apiErr, ok := err.(*engine.EngineAPIError); !ok || apiErr.ErrorCode() != engine.UnsupportedFork.ErrorCode() {
|
||||
t.Fatalf("expected unsupported fork error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ type BuildPayloadArgs struct {
|
|||
Random common.Hash // The provided randomness value
|
||||
Withdrawals types.Withdrawals // The provided withdrawals
|
||||
BeaconRoot *common.Hash // The provided beaconRoot (Cancun)
|
||||
SlotNum *uint64 // The provided slotNumber
|
||||
SlotNum *uint64 // The provided slotNumber (Amsterdam)
|
||||
TargetGasLimit *uint64 // The provided targetGasLimit (Amsterdam)
|
||||
Version engine.PayloadVersion // Versioning byte for payload id calculation.
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +65,12 @@ func (args *BuildPayloadArgs) Id() engine.PayloadID {
|
|||
if args.SlotNum != nil {
|
||||
binary.Write(hasher, binary.BigEndian, args.SlotNum)
|
||||
}
|
||||
if args.TargetGasLimit != nil {
|
||||
// The leading tag byte keeps the preimage distinct from an args set
|
||||
// where only slotNum is present, as both encode as 8 big-endian bytes.
|
||||
hasher.Write([]byte{0x01})
|
||||
binary.Write(hasher, binary.BigEndian, args.TargetGasLimit)
|
||||
}
|
||||
var out engine.PayloadID
|
||||
copy(out[:], hasher.Sum(nil)[:8])
|
||||
out[0] = byte(args.Version)
|
||||
|
|
@ -254,6 +261,7 @@ func (miner *Miner) buildPayload(ctx context.Context, args *BuildPayloadArgs, wi
|
|||
withdrawals: args.Withdrawals,
|
||||
beaconRoot: args.BeaconRoot,
|
||||
slotNum: args.SlotNum,
|
||||
targetGasLimit: args.TargetGasLimit,
|
||||
noTxs: true,
|
||||
}
|
||||
empty := miner.generateWork(ctx, emptyParams, witness)
|
||||
|
|
@ -294,6 +302,7 @@ func (miner *Miner) buildPayload(ctx context.Context, args *BuildPayloadArgs, wi
|
|||
withdrawals: args.Withdrawals,
|
||||
beaconRoot: args.BeaconRoot,
|
||||
slotNum: args.SlotNum,
|
||||
targetGasLimit: args.TargetGasLimit,
|
||||
noTxs: false,
|
||||
}
|
||||
for {
|
||||
|
|
@ -351,6 +360,7 @@ func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*
|
|||
withdrawals: args.Withdrawals,
|
||||
beaconRoot: args.BeaconRoot,
|
||||
slotNum: args.SlotNum,
|
||||
targetGasLimit: args.TargetGasLimit,
|
||||
noTxs: empty,
|
||||
forceOverrides: true,
|
||||
overrideExtraData: extraData,
|
||||
|
|
|
|||
|
|
@ -249,8 +249,72 @@ func TestBuildPayloadAmsterdamTransition(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestBuildPayloadTargetGasLimit verifies that the CL-provided target gas
|
||||
// limit (engine API PayloadAttributesV4 targetGasLimit) steers the gas limit
|
||||
// of locally built payloads, and that the locally configured gas ceiling
|
||||
// still applies when no target is provided.
|
||||
func TestBuildPayloadTargetGasLimit(t *testing.T) {
|
||||
config := new(params.ChainConfig)
|
||||
*config = *params.MergedTestChainConfig
|
||||
config.AmsterdamTime = new(uint64)
|
||||
*config.AmsterdamTime = 1
|
||||
|
||||
newUint64 := func(v uint64) *uint64 { return &v }
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
target *uint64
|
||||
want func(parentGasLimit uint64) uint64
|
||||
}{
|
||||
{
|
||||
// Target above the parent gas limit: move up towards it.
|
||||
name: "target-above-parent",
|
||||
target: newUint64(2 * params.GenesisGasLimit),
|
||||
want: func(parent uint64) uint64 { return core.CalcGasLimit(parent, 2*params.GenesisGasLimit) },
|
||||
},
|
||||
{
|
||||
// Target below the parent gas limit: move down towards it.
|
||||
name: "target-below-parent",
|
||||
target: newUint64(params.MinGasLimit),
|
||||
want: func(parent uint64) uint64 { return core.CalcGasLimit(parent, params.MinGasLimit) },
|
||||
},
|
||||
{
|
||||
// No target provided: fall back to the configured gas ceiling.
|
||||
name: "no-target-falls-back-to-gas-ceil",
|
||||
target: nil,
|
||||
want: func(parent uint64) uint64 { return core.CalcGasLimit(parent, testConfig.GasCeil) },
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var (
|
||||
db = rawdb.NewMemoryDatabase()
|
||||
beaconRoot = common.Hash{0x01}
|
||||
slotNum = uint64(1)
|
||||
)
|
||||
w, b := newTestWorker(t, config, beacon.New(ethash.NewFaker()), db, 0)
|
||||
parent := b.chain.CurrentBlock()
|
||||
|
||||
payload, err := w.buildPayload(context.Background(), &BuildPayloadArgs{
|
||||
Parent: parent.Hash(),
|
||||
Timestamp: 1,
|
||||
FeeRecipient: common.HexToAddress("0xdeadbeef"),
|
||||
Withdrawals: types.Withdrawals{},
|
||||
BeaconRoot: &beaconRoot,
|
||||
SlotNum: &slotNum,
|
||||
TargetGasLimit: tt.target,
|
||||
}, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build payload: %v", err)
|
||||
}
|
||||
if got, want := payload.empty.GasLimit(), tt.want(parent.GasLimit); got != want {
|
||||
t.Fatalf("gas limit mismatch: got %d, want %d (parent %d)", got, want, parent.GasLimit)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayloadId(t *testing.T) {
|
||||
t.Parallel()
|
||||
newUint64 := func(v uint64) *uint64 { return &v }
|
||||
ids := make(map[string]int)
|
||||
for i, tt := range []*BuildPayloadArgs{
|
||||
{
|
||||
|
|
@ -317,6 +381,41 @@ func TestPayloadId(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
// With slot number
|
||||
{
|
||||
Parent: common.Hash{2},
|
||||
Timestamp: 2,
|
||||
Random: common.Hash{0x2},
|
||||
FeeRecipient: common.Address{0x2},
|
||||
SlotNum: newUint64(5),
|
||||
},
|
||||
// With target gas limit only, numerically equal to the slot number of
|
||||
// the previous case; the id must still be distinct.
|
||||
{
|
||||
Parent: common.Hash{2},
|
||||
Timestamp: 2,
|
||||
Random: common.Hash{0x2},
|
||||
FeeRecipient: common.Address{0x2},
|
||||
TargetGasLimit: newUint64(5),
|
||||
},
|
||||
// With both slot number and target gas limit
|
||||
{
|
||||
Parent: common.Hash{2},
|
||||
Timestamp: 2,
|
||||
Random: common.Hash{0x2},
|
||||
FeeRecipient: common.Address{0x2},
|
||||
SlotNum: newUint64(5),
|
||||
TargetGasLimit: newUint64(300000000),
|
||||
},
|
||||
// Different target gas limit
|
||||
{
|
||||
Parent: common.Hash{2},
|
||||
Timestamp: 2,
|
||||
Random: common.Hash{0x2},
|
||||
FeeRecipient: common.Address{0x2},
|
||||
SlotNum: newUint64(5),
|
||||
TargetGasLimit: newUint64(150000000),
|
||||
},
|
||||
} {
|
||||
id := tt.Id().String()
|
||||
if prev, exists := ids[id]; exists {
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ type generateParams struct {
|
|||
withdrawals types.Withdrawals // List of withdrawals to include in block (shanghai field)
|
||||
beaconRoot *common.Hash // The beacon root (cancun field).
|
||||
slotNum *uint64 // The slot number (amsterdam field).
|
||||
targetGasLimit *uint64 // The gas limit to aim for, provided by the CL (amsterdam field).
|
||||
noTxs bool // Flag whether an empty block without any transaction is expected
|
||||
|
||||
forceOverrides bool // Flag whether we should overwrite extraData and transactions
|
||||
|
|
@ -267,11 +268,18 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams,
|
|||
}
|
||||
timestamp = parent.Time + 1
|
||||
}
|
||||
// Determine the gas limit to aim for. Post-amsterdam the consensus layer
|
||||
// supplies it with each payload request (engine API targetGasLimit); if it
|
||||
// is absent, fall back to the locally configured gas ceiling.
|
||||
gasCeil := miner.config.GasCeil
|
||||
if genParams.targetGasLimit != nil {
|
||||
gasCeil = *genParams.targetGasLimit
|
||||
}
|
||||
// Construct the sealing block header.
|
||||
header := &types.Header{
|
||||
ParentHash: parent.Hash(),
|
||||
Number: new(big.Int).Add(parent.Number, common.Big1),
|
||||
GasLimit: core.CalcGasLimit(parent.GasLimit, miner.config.GasCeil),
|
||||
GasLimit: core.CalcGasLimit(parent.GasLimit, gasCeil),
|
||||
Time: timestamp,
|
||||
Coinbase: genParams.coinbase,
|
||||
}
|
||||
|
|
@ -291,7 +299,7 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams,
|
|||
header.BaseFee = eip1559.CalcBaseFee(miner.chainConfig, parent)
|
||||
if !miner.chainConfig.IsLondon(parent.Number) {
|
||||
parentGasLimit := parent.GasLimit * miner.chainConfig.ElasticityMultiplier()
|
||||
header.GasLimit = core.CalcGasLimit(parentGasLimit, miner.config.GasCeil)
|
||||
header.GasLimit = core.CalcGasLimit(parentGasLimit, gasCeil)
|
||||
}
|
||||
}
|
||||
// Run the consensus preparation with the default or customized consensus engine.
|
||||
|
|
|
|||
Loading…
Reference in a new issue