From 02bfdb5a85cacf1b93448fdbdb033de5e7de8a81 Mon Sep 17 00:00:00 2001 From: Oleksandr Lystopad Date: Fri, 14 Aug 2026 11:50:39 +0200 Subject: [PATCH 1/5] cl/beacon: resolve payload withdrawals in one place Split out of #23105 so it can be reviewed on its own. No behaviour change. produceBeaconBody chose between three withdrawal sources inline, each with its own hand-written conversion loop, and then assembled the attributes around the result. The choice is now a method that names what it selects between, and the attributes come from a single constructor. That leaves the fork-specific part of production as the two fields only Gloas sends. The three conversion loops go through the shared converter, which is what the doc comment on that converter has been describing. --- cl/beacon/handler/block_production.go | 128 ++++++++++----------- cl/beacon/handler/block_production_test.go | 43 +++++++ 2 files changed, 106 insertions(+), 65 deletions(-) diff --git a/cl/beacon/handler/block_production.go b/cl/beacon/handler/block_production.go index 89adbad8e3e..454f80ad73e 100644 --- a/cl/beacon/handler/block_production.go +++ b/cl/beacon/handler/block_production.go @@ -227,6 +227,56 @@ func computeBlockBuilderWindow(now, slotStart time.Time, cfg *clparams.BeaconCha } } +// payloadAttributes builds the attributes every fork sends. Withdrawals and the parent beacon block +// root go out regardless of the consensus fork because the execution layer decides what to do with +// them from the payload timestamp. +func payloadAttributes( + timestamp hexutil.Uint64, + prevRandao common.Hash, + feeRecipient common.Address, + withdrawals []*types.Withdrawal, + parentRoot *common.Hash, +) *engine_types.PayloadAttributes { + return &engine_types.PayloadAttributes{ + Timestamp: timestamp, + PrevRandao: prevRandao, + SuggestedFeeRecipient: feeRecipient, + Withdrawals: withdrawals, + ParentBeaconBlockRoot: parentRoot, + } +} + +// expectedWithdrawals resolves the withdrawals for the payload being built. Under Gloas the source +// depends on whether the head's payload was revealed: a FULL head is read from the state copy with +// that payload applied, an EMPTY one from the expectation the state already cached. +func (a *ApiHandler) expectedWithdrawals( + baseState, withParentPayload *state.CachingBeaconState, + stateVersion clparams.StateVersion, + targetSlot uint64, +) ([]*types.Withdrawal, error) { + epoch := targetSlot / a.beaconChainCfg.SlotsPerEpoch + if stateVersion.Before(clparams.GloasVersion) || withParentPayload != nil { + source := baseState + if withParentPayload != nil { + source = withParentPayload + } + clWithdrawals, err := state.GetExpectedWithdrawals(source, epoch) + if err != nil { + return nil, err + } + return cltypes.ConvertConsensusWithdrawalsToExecutionWithdrawals(clWithdrawals.Withdrawals), nil + } + cached := baseState.GetPayloadExpectedWithdrawals() + if cached == nil { + return nil, nil + } + consensusWithdrawals := make([]*cltypes.Withdrawal, cached.Len()) + for i := range consensusWithdrawals { + consensusWithdrawals[i] = cached.Get(i) + } + return cltypes.ConvertConsensusWithdrawalsToExecutionWithdrawals(consensusWithdrawals), nil +} + func shouldRetryGetPayload(now, deadline time.Time) bool { return now.Before(deadline) } @@ -976,73 +1026,21 @@ func (a *ApiHandler) produceBeaconBody( }() retryTime := 10 * time.Millisecond feeRecipient, _ := a.validatorParams.GetFeeRecipient(proposerIndex) - var withdrawals []*types.Withdrawal - switch { - case gloasWithdrawalsState != nil: - // GLOAS FULL: compute withdrawals from the state copy with parent payload applied - clWithdrawals, err := state.GetExpectedWithdrawals( - gloasWithdrawalsState, - targetSlot/a.beaconChainCfg.SlotsPerEpoch, - ) - if err != nil { - log.Error("BlockProduction: GetExpectedWithdrawals (FULL) failed", "err", err) - return - } - withdrawals = make([]*types.Withdrawal, 0, len(clWithdrawals.Withdrawals)) - for _, w := range clWithdrawals.Withdrawals { - withdrawals = append(withdrawals, &types.Withdrawal{ - Index: w.Index, - Amount: w.Amount, - Validator: w.Validator, - Address: w.Address, - }) - } - case stateVersion >= clparams.GloasVersion && gloasWithdrawalsState == nil: - // GLOAS EMPTY: use cached payload_expected_withdrawals from state - cachedWithdrawals := baseState.GetPayloadExpectedWithdrawals() - if cachedWithdrawals != nil { - withdrawals = make([]*types.Withdrawal, 0, cachedWithdrawals.Len()) - for i := 0; i < cachedWithdrawals.Len(); i++ { - w := cachedWithdrawals.Get(i) - withdrawals = append(withdrawals, &types.Withdrawal{ - Index: w.Index, - Amount: w.Amount, - Validator: w.Validator, - Address: w.Address, - }) - } - } - default: - // Pre-GLOAS: compute withdrawals normally - clWithdrawals, err := state.GetExpectedWithdrawals( - baseState, - targetSlot/a.beaconChainCfg.SlotsPerEpoch, - ) - if err != nil { - log.Error("BlockProduction: GetExpectedWithdrawals failed", "err", err) - return - } - withdrawals = make([]*types.Withdrawal, 0, len(clWithdrawals.Withdrawals)) - for _, w := range clWithdrawals.Withdrawals { - withdrawals = append(withdrawals, &types.Withdrawal{ - Index: w.Index, - Amount: w.Amount, - Validator: w.Validator, - Address: w.Address, - }) - } - } - - attrs := &engine_types.PayloadAttributes{ - Timestamp: hexutil.Uint64(state.ComputeTimestampAtSlot(baseState, targetSlot)), - PrevRandao: random, - SuggestedFeeRecipient: feeRecipient, - Withdrawals: withdrawals, - ParentBeaconBlockRoot: (*common.Hash)(&blockRoot), + withdrawals, err := a.expectedWithdrawals(baseState, gloasWithdrawalsState, stateVersion, targetSlot) + if err != nil { + log.Error("BlockProduction: GetExpectedWithdrawals failed", "err", err) + return } + attrs := payloadAttributes( + hexutil.Uint64(state.ComputeTimestampAtSlot(baseState, targetSlot)), + random, + feeRecipient, + withdrawals, + (*common.Hash)(&blockRoot), + ) if stateVersion.AfterOrEqual(clparams.GloasVersion) { - sn := hexutil.Uint64(targetSlot) - attrs.SlotNumber = &sn + slotNumber := hexutil.Uint64(targetSlot) + attrs.SlotNumber = &slotNumber attrs.TargetGasLimit = targetGasLimit } builderStartedAt := time.Now() diff --git a/cl/beacon/handler/block_production_test.go b/cl/beacon/handler/block_production_test.go index d52bb8594c8..7f7bbf12693 100644 --- a/cl/beacon/handler/block_production_test.go +++ b/cl/beacon/handler/block_production_test.go @@ -36,6 +36,7 @@ import ( "github.com/erigontech/erigon/cl/clparams" "github.com/erigontech/erigon/cl/cltypes" "github.com/erigontech/erigon/cl/cltypes/solid" + "github.com/erigontech/erigon/cl/phase1/core/state" "github.com/erigontech/erigon/cl/phase1/execution_client" "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/hexutil" @@ -732,3 +733,45 @@ func TestCaplinBlockProductionGlamsterdamSlotNumber(t *testing.T) { require.Equal(t, hexutil.Uint64(targetSlot), *spy.lastAttributes.SlotNumber, "SlotNumber should equal the target slot") } + +func TestExpectedWithdrawalsReadsTheRightSourcePerFork(t *testing.T) { + cfg := clparams.MainnetBeaconConfig + cfg.AltairForkEpoch, cfg.BellatrixForkEpoch, cfg.CapellaForkEpoch = 0, 0, 0 + a := &ApiHandler{beaconChainCfg: &cfg} + + capellaState := state.New(&cfg) + capellaState.SetVersion(clparams.CapellaVersion) + + // Before Gloas the expectation is computed from the head state itself, and the list is present + // even when empty: the execution layer rejects a nil one after Shanghai. + withdrawals, err := a.expectedWithdrawals(capellaState, nil, clparams.CapellaVersion, 0) + require.NoError(t, err) + require.NotNil(t, withdrawals) + require.Empty(t, withdrawals) + + gloasState := state.New(&cfg) + gloasState.SetVersion(clparams.GloasVersion) + + // A Gloas head whose payload was revealed is read from the state copy carrying that payload, + // not from the head state. + withParentPayload := state.New(&cfg) + withParentPayload.SetVersion(clparams.GloasVersion) + withdrawals, err = a.expectedWithdrawals(gloasState, withParentPayload, clparams.GloasVersion, 0) + require.NoError(t, err) + require.NotNil(t, withdrawals) + + // An EMPTY Gloas head uses the expectation the state already cached rather than computing a + // fresh one, so what it returns is whatever was cached. + withdrawals, err = a.expectedWithdrawals(gloasState, nil, clparams.GloasVersion, 0) + require.NoError(t, err) + require.Empty(t, withdrawals) + + cached := solid.NewDynamicListSSZ[*cltypes.Withdrawal](int(cfg.MaxWithdrawalsPerPayload)) + cached.Append(&cltypes.Withdrawal{Index: 7, Validator: 8, Address: common.Address{0xaa}, Amount: 9}) + gloasState.SetPayloadExpectedWithdrawals(cached) + withdrawals, err = a.expectedWithdrawals(gloasState, nil, clparams.GloasVersion, 0) + require.NoError(t, err) + require.Equal(t, []*types.Withdrawal{ + {Index: 7, Validator: 8, Address: common.Address{0xaa}, Amount: 9}, + }, withdrawals) +} From 32d6462e969795dff81c97f99173e06d0380f42a Mon Sep 17 00:00:00 2001 From: Oleksandr Lystopad Date: Fri, 14 Aug 2026 14:37:05 +0200 Subject: [PATCH 2/5] cl/beacon: leave the parent beacon block root off the versions that reject it The forkchoice call is versioned, and V1 and V2 refuse a parent beacon block root outright, so a field the chosen version does not carry has to be left out rather than sent and ignored. Carrying it below Deneb fails the request instead. --- cl/beacon/handler/block_production.go | 12 ++++++++-- cl/beacon/handler/block_production_test.go | 28 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/cl/beacon/handler/block_production.go b/cl/beacon/handler/block_production.go index 454f80ad73e..e8d8f5320a1 100644 --- a/cl/beacon/handler/block_production.go +++ b/cl/beacon/handler/block_production.go @@ -230,20 +230,27 @@ func computeBlockBuilderWindow(now, slotStart time.Time, cfg *clparams.BeaconCha // payloadAttributes builds the attributes every fork sends. Withdrawals and the parent beacon block // root go out regardless of the consensus fork because the execution layer decides what to do with // them from the payload timestamp. +// payloadAttributes builds the attributes for a version of the forkchoice call. The wire format is +// versioned, so a field the chosen version does not carry has to be left out rather than sent and +// ignored: V1 and V2 reject a parent beacon block root outright. func payloadAttributes( + version clparams.StateVersion, timestamp hexutil.Uint64, prevRandao common.Hash, feeRecipient common.Address, withdrawals []*types.Withdrawal, parentRoot *common.Hash, ) *engine_types.PayloadAttributes { - return &engine_types.PayloadAttributes{ + attrs := &engine_types.PayloadAttributes{ Timestamp: timestamp, PrevRandao: prevRandao, SuggestedFeeRecipient: feeRecipient, Withdrawals: withdrawals, - ParentBeaconBlockRoot: parentRoot, } + if version.AfterOrEqual(clparams.DenebVersion) { + attrs.ParentBeaconBlockRoot = parentRoot + } + return attrs } // expectedWithdrawals resolves the withdrawals for the payload being built. Under Gloas the source @@ -1032,6 +1039,7 @@ func (a *ApiHandler) produceBeaconBody( return } attrs := payloadAttributes( + stateVersion, hexutil.Uint64(state.ComputeTimestampAtSlot(baseState, targetSlot)), random, feeRecipient, diff --git a/cl/beacon/handler/block_production_test.go b/cl/beacon/handler/block_production_test.go index 7f7bbf12693..0c6ceab7580 100644 --- a/cl/beacon/handler/block_production_test.go +++ b/cl/beacon/handler/block_production_test.go @@ -775,3 +775,31 @@ func TestExpectedWithdrawalsReadsTheRightSourcePerFork(t *testing.T) { {Index: 7, Validator: 8, Address: common.Address{0xaa}, Amount: 9}, }, withdrawals) } + +func TestPayloadAttributesOmitTheParentRootBelowDeneb(t *testing.T) { + root := common.Hash{0xaa} + withdrawals := []*types.Withdrawal{{Index: 1}} + + for _, tc := range []struct { + version clparams.StateVersion + wantParentRoot bool + }{ + {clparams.BellatrixVersion, false}, + {clparams.CapellaVersion, false}, + {clparams.DenebVersion, true}, + {clparams.FuluVersion, true}, + {clparams.GloasVersion, true}, + } { + t.Run(tc.version.String(), func(t *testing.T) { + attrs := payloadAttributes(tc.version, 1, common.Hash{0xbb}, common.Address{0xcc}, withdrawals, &root) + + // The forkchoice call is versioned: V1 and V2 reject a parent beacon block root, so + // sending one below Deneb fails the request rather than being ignored. + require.Equal(t, tc.wantParentRoot, attrs.ParentBeaconBlockRoot != nil) + + require.Equal(t, withdrawals, attrs.Withdrawals) + require.Nil(t, attrs.SlotNumber, "only a Gloas proposal knows its slot number") + require.Nil(t, attrs.TargetGasLimit) + }) + } +} From a08789659bb0f145655679799878a803416d92b8 Mon Sep 17 00:00:00 2001 From: Oleksandr Lystopad Date: Fri, 14 Aug 2026 16:02:06 +0200 Subject: [PATCH 3/5] cl/beacon: address review Remove a stale doc block left stacked above the current one, which described the behaviour before the parent beacon block root was version-gated. Give the two Gloas states different withdrawal expectations, so the test proves the revealed path reads the state copy carrying the parent payload instead of merely agreeing with the head state, which two fresh states did whichever was read. --- cl/beacon/handler/block_production.go | 3 --- cl/beacon/handler/block_production_test.go | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cl/beacon/handler/block_production.go b/cl/beacon/handler/block_production.go index e8d8f5320a1..328cec38f81 100644 --- a/cl/beacon/handler/block_production.go +++ b/cl/beacon/handler/block_production.go @@ -227,9 +227,6 @@ func computeBlockBuilderWindow(now, slotStart time.Time, cfg *clparams.BeaconCha } } -// payloadAttributes builds the attributes every fork sends. Withdrawals and the parent beacon block -// root go out regardless of the consensus fork because the execution layer decides what to do with -// them from the payload timestamp. // payloadAttributes builds the attributes for a version of the forkchoice call. The wire format is // versioned, so a field the chosen version does not carry has to be left out rather than sent and // ignored: V1 and V2 reject a parent beacon block root outright. diff --git a/cl/beacon/handler/block_production_test.go b/cl/beacon/handler/block_production_test.go index 0c6ceab7580..f306c1696ba 100644 --- a/cl/beacon/handler/block_production_test.go +++ b/cl/beacon/handler/block_production_test.go @@ -753,12 +753,22 @@ func TestExpectedWithdrawalsReadsTheRightSourcePerFork(t *testing.T) { gloasState.SetVersion(clparams.GloasVersion) // A Gloas head whose payload was revealed is read from the state copy carrying that payload, - // not from the head state. + // not from the head state. Only that copy carries a pending builder withdrawal, so reading the + // wrong one comes back empty rather than merely equal. withParentPayload := state.New(&cfg) withParentPayload.SetVersion(clparams.GloasVersion) + pending := solid.NewDynamicListSSZ[*cltypes.BuilderPendingWithdrawal](int(cfg.MaxWithdrawalsPerPayload)) + pending.Append(&cltypes.BuilderPendingWithdrawal{FeeRecipient: common.Address{0xbb}, Amount: 12, BuilderIndex: 3}) + withParentPayload.SetBuilderPendingWithdrawals(pending) + withdrawals, err = a.expectedWithdrawals(gloasState, withParentPayload, clparams.GloasVersion, 0) require.NoError(t, err) - require.NotNil(t, withdrawals) + require.Equal(t, []*types.Withdrawal{{ + Index: 0, + Validator: state.ConvertBuilderIndexToValidatorIndex(3), + Address: common.Address{0xbb}, + Amount: 12, + }}, withdrawals) // An EMPTY Gloas head uses the expectation the state already cached rather than computing a // fresh one, so what it returns is whatever was cached. From acd5992e200e79183103f8623de851bba47e8fc6 Mon Sep 17 00:00:00 2001 From: Oleksandr Lystopad Date: Fri, 14 Aug 2026 16:22:03 +0200 Subject: [PATCH 4/5] cl/beacon: leave withdrawals off the version that cannot carry them Bellatrix dispatches to the forkchoice call's first version, which has no withdrawals field at all, and a strict execution client rejects a request that includes one rather than ignoring it. The expectation is a non-nil empty slice there, so it was being sent. --- cl/beacon/handler/block_production.go | 7 +++++-- cl/beacon/handler/block_production_test.go | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cl/beacon/handler/block_production.go b/cl/beacon/handler/block_production.go index 328cec38f81..149877e44a6 100644 --- a/cl/beacon/handler/block_production.go +++ b/cl/beacon/handler/block_production.go @@ -229,7 +229,8 @@ func computeBlockBuilderWindow(now, slotStart time.Time, cfg *clparams.BeaconCha // payloadAttributes builds the attributes for a version of the forkchoice call. The wire format is // versioned, so a field the chosen version does not carry has to be left out rather than sent and -// ignored: V1 and V2 reject a parent beacon block root outright. +// ignored: V1 carries no withdrawals and V1 and V2 no parent beacon block root, and a strict +// execution client rejects a request that includes them. func payloadAttributes( version clparams.StateVersion, timestamp hexutil.Uint64, @@ -242,7 +243,9 @@ func payloadAttributes( Timestamp: timestamp, PrevRandao: prevRandao, SuggestedFeeRecipient: feeRecipient, - Withdrawals: withdrawals, + } + if version.AfterOrEqual(clparams.CapellaVersion) { + attrs.Withdrawals = withdrawals } if version.AfterOrEqual(clparams.DenebVersion) { attrs.ParentBeaconBlockRoot = parentRoot diff --git a/cl/beacon/handler/block_production_test.go b/cl/beacon/handler/block_production_test.go index f306c1696ba..3900d8d9c23 100644 --- a/cl/beacon/handler/block_production_test.go +++ b/cl/beacon/handler/block_production_test.go @@ -786,19 +786,20 @@ func TestExpectedWithdrawalsReadsTheRightSourcePerFork(t *testing.T) { }, withdrawals) } -func TestPayloadAttributesOmitTheParentRootBelowDeneb(t *testing.T) { +func TestPayloadAttributesOmitFieldsTheChosenVersionCannotCarry(t *testing.T) { root := common.Hash{0xaa} withdrawals := []*types.Withdrawal{{Index: 1}} for _, tc := range []struct { - version clparams.StateVersion - wantParentRoot bool + version clparams.StateVersion + wantWithdrawals bool + wantParentRoot bool }{ - {clparams.BellatrixVersion, false}, - {clparams.CapellaVersion, false}, - {clparams.DenebVersion, true}, - {clparams.FuluVersion, true}, - {clparams.GloasVersion, true}, + {clparams.BellatrixVersion, false, false}, + {clparams.CapellaVersion, true, false}, + {clparams.DenebVersion, true, true}, + {clparams.FuluVersion, true, true}, + {clparams.GloasVersion, true, true}, } { t.Run(tc.version.String(), func(t *testing.T) { attrs := payloadAttributes(tc.version, 1, common.Hash{0xbb}, common.Address{0xcc}, withdrawals, &root) @@ -806,8 +807,7 @@ func TestPayloadAttributesOmitTheParentRootBelowDeneb(t *testing.T) { // The forkchoice call is versioned: V1 and V2 reject a parent beacon block root, so // sending one below Deneb fails the request rather than being ignored. require.Equal(t, tc.wantParentRoot, attrs.ParentBeaconBlockRoot != nil) - - require.Equal(t, withdrawals, attrs.Withdrawals) + require.Equal(t, tc.wantWithdrawals, attrs.Withdrawals != nil) require.Nil(t, attrs.SlotNumber, "only a Gloas proposal knows its slot number") require.Nil(t, attrs.TargetGasLimit) }) From 28073af64603017f19aacc0aad2cbcb0a55ffe3a Mon Sep 17 00:00:00 2001 From: Oleksandr Lystopad Date: Sun, 16 Aug 2026 10:11:37 +0200 Subject: [PATCH 5/5] cl/beacon: let one function own which fields each version carries The slot number and target gas limit were set by the caller after the constructor, so the constructor did not in fact own the versioned schema and the table test could not see them: deleting either assignment left the whole suite green while every Gloas proposal would have been rejected. Both move inside, and the test asserts the values arrive rather than only that the fields are non-nil. An unpopulated field still appears on the wire as null, so say unpopulated rather than omitted. --- cl/beacon/handler/block_production.go | 21 +++++++----- cl/beacon/handler/block_production_test.go | 38 +++++++++++++++------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/cl/beacon/handler/block_production.go b/cl/beacon/handler/block_production.go index 149877e44a6..0d1a155e49d 100644 --- a/cl/beacon/handler/block_production.go +++ b/cl/beacon/handler/block_production.go @@ -227,10 +227,10 @@ func computeBlockBuilderWindow(now, slotStart time.Time, cfg *clparams.BeaconCha } } -// payloadAttributes builds the attributes for a version of the forkchoice call. The wire format is -// versioned, so a field the chosen version does not carry has to be left out rather than sent and -// ignored: V1 carries no withdrawals and V1 and V2 no parent beacon block root, and a strict -// execution client rejects a request that includes them. +// payloadAttributes builds the attributes for a version of the forkchoice call, which is the one +// place that decides which fields each version carries. A field the chosen version does not define +// is left unpopulated rather than filled in and ignored: V1 has no withdrawals and V1 and V2 no +// parent beacon block root, and an execution client rejects a request that supplies them. func payloadAttributes( version clparams.StateVersion, timestamp hexutil.Uint64, @@ -238,6 +238,7 @@ func payloadAttributes( feeRecipient common.Address, withdrawals []*types.Withdrawal, parentRoot *common.Hash, + slotNumber, targetGasLimit *hexutil.Uint64, ) *engine_types.PayloadAttributes { attrs := &engine_types.PayloadAttributes{ Timestamp: timestamp, @@ -250,6 +251,10 @@ func payloadAttributes( if version.AfterOrEqual(clparams.DenebVersion) { attrs.ParentBeaconBlockRoot = parentRoot } + if version.AfterOrEqual(clparams.GloasVersion) { + attrs.SlotNumber = slotNumber + attrs.TargetGasLimit = targetGasLimit + } return attrs } @@ -1038,6 +1043,7 @@ func (a *ApiHandler) produceBeaconBody( log.Error("BlockProduction: GetExpectedWithdrawals failed", "err", err) return } + slotNumber := hexutil.Uint64(targetSlot) attrs := payloadAttributes( stateVersion, hexutil.Uint64(state.ComputeTimestampAtSlot(baseState, targetSlot)), @@ -1045,12 +1051,9 @@ func (a *ApiHandler) produceBeaconBody( feeRecipient, withdrawals, (*common.Hash)(&blockRoot), + &slotNumber, + targetGasLimit, ) - if stateVersion.AfterOrEqual(clparams.GloasVersion) { - slotNumber := hexutil.Uint64(targetSlot) - attrs.SlotNumber = &slotNumber - attrs.TargetGasLimit = targetGasLimit - } builderStartedAt := time.Now() idBytes, err := a.engine.ForkChoiceUpdate( ctx, diff --git a/cl/beacon/handler/block_production_test.go b/cl/beacon/handler/block_production_test.go index 3900d8d9c23..ddafb3b2085 100644 --- a/cl/beacon/handler/block_production_test.go +++ b/cl/beacon/handler/block_production_test.go @@ -789,27 +789,43 @@ func TestExpectedWithdrawalsReadsTheRightSourcePerFork(t *testing.T) { func TestPayloadAttributesOmitFieldsTheChosenVersionCannotCarry(t *testing.T) { root := common.Hash{0xaa} withdrawals := []*types.Withdrawal{{Index: 1}} + slotNumber := hexutil.Uint64(64) + targetGasLimit := hexutil.Uint64(36_000_000) for _, tc := range []struct { version clparams.StateVersion wantWithdrawals bool wantParentRoot bool + wantGloasFields bool }{ - {clparams.BellatrixVersion, false, false}, - {clparams.CapellaVersion, true, false}, - {clparams.DenebVersion, true, true}, - {clparams.FuluVersion, true, true}, - {clparams.GloasVersion, true, true}, + {clparams.BellatrixVersion, false, false, false}, + {clparams.CapellaVersion, true, false, false}, + {clparams.DenebVersion, true, true, false}, + {clparams.FuluVersion, true, true, false}, + {clparams.GloasVersion, true, true, true}, } { t.Run(tc.version.String(), func(t *testing.T) { - attrs := payloadAttributes(tc.version, 1, common.Hash{0xbb}, common.Address{0xcc}, withdrawals, &root) + attrs := payloadAttributes(tc.version, 1, common.Hash{0xbb}, common.Address{0xcc}, + withdrawals, &root, &slotNumber, &targetGasLimit) - // The forkchoice call is versioned: V1 and V2 reject a parent beacon block root, so - // sending one below Deneb fails the request rather than being ignored. - require.Equal(t, tc.wantParentRoot, attrs.ParentBeaconBlockRoot != nil) + // A version that does not define a field must not have it populated: V1 carries no + // withdrawals, V1 and V2 no parent beacon block root, and supplying one is rejected + // rather than ignored. require.Equal(t, tc.wantWithdrawals, attrs.Withdrawals != nil) - require.Nil(t, attrs.SlotNumber, "only a Gloas proposal knows its slot number") - require.Nil(t, attrs.TargetGasLimit) + require.Equal(t, tc.wantParentRoot, attrs.ParentBeaconBlockRoot != nil) + + // The values have to arrive, not merely be non-nil: dropping either Gloas field leaves + // every Gloas proposal rejected with -38003. + if tc.wantGloasFields { + require.Equal(t, &slotNumber, attrs.SlotNumber) + require.Equal(t, &targetGasLimit, attrs.TargetGasLimit) + } else { + require.Nil(t, attrs.SlotNumber) + require.Nil(t, attrs.TargetGasLimit) + } + require.Equal(t, hexutil.Uint64(1), attrs.Timestamp) + require.Equal(t, common.Hash{0xbb}, attrs.PrevRandao) + require.Equal(t, common.Address{0xcc}, attrs.SuggestedFeeRecipient) }) } }