Compare commits

..

7 Commits

Author SHA1 Message Date
james-prysm
afba3a4bbd add deprecation 2026-02-18 15:44:35 -06:00
james-prysm
7f8e1f56b4 self review 2026-02-18 15:33:17 -06:00
james-prysm
4075f64e15 Merge branch 'develop' into proposer-duties-v2 2026-02-18 13:02:29 -08:00
terence
a28c6c8145 Add gloas block gossip changes (#16368)
This updates beacon-block gossip validation for Gloas to use the signed
execution payload bid instead of the execution payload. It removes
execution‑payload‑based checks and introduces bid‑based checks, plus
stubs for parent‑payload checks pending blockchain package support

Reference:
https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#beacon_block

fixes #https://github.com/OffchainLabs/prysm/issues/16372
2026-02-18 20:14:21 +00:00
james-prysm
2bc43bd201 Merge branch 'develop' into proposer-duties-v2 2026-02-02 13:25:00 -08:00
james-prysm
36cb0e6c95 self review 2026-01-29 15:23:08 -06:00
james-prysm
ae6600fe2d implementing /eth/v2/validator/duties/proposer/{epoch} 2026-01-29 13:54:19 -06:00
16 changed files with 484 additions and 58 deletions

View File

@@ -56,7 +56,7 @@ type fcuConfig struct {
// sendFCU handles the logic to notify the engine of a forckhoice update
// when processing an incoming block during regular sync. It
// always updates the shuffling caches and handles epoch transitions .
func (s *Service) sendFCU(cfg *postBlockProcessConfig) {
func (s *Service) sendFCU(cfg *postBlockProcessConfig, fcuArgs *fcuConfig) {
if cfg.postState.Version() < version.Fulu {
// update the caches to compute the right proposer index
// this function is called under a forkchoice lock which we need to release.
@@ -64,8 +64,7 @@ func (s *Service) sendFCU(cfg *postBlockProcessConfig) {
s.updateCachesPostBlockProcessing(cfg)
s.ForkChoicer().Lock()
}
fcuArgs, err := s.getFCUArgs(cfg)
if err != nil {
if err := s.getFCUArgs(cfg, fcuArgs); err != nil {
log.WithError(err).Error("Could not get forkchoice update argument")
return
}

View File

@@ -64,6 +64,7 @@ func (s *Service) postBlockProcess(cfg *postBlockProcessConfig) error {
return invalidBlock{error: err}
}
startTime := time.Now()
fcuArgs := &fcuConfig{}
if features.Get().EnableLightClient && slots.ToEpoch(s.CurrentSlot()) >= params.BeaconConfig().AltairForkEpoch {
defer s.processLightClientUpdates(cfg)
@@ -101,9 +102,7 @@ func (s *Service) postBlockProcess(cfg *postBlockProcessConfig) error {
s.logNonCanonicalBlockReceived(cfg.roblock.Root(), cfg.headRoot)
return nil
}
if cfg.roblock.Version() <= version.Gloas {
s.sendFCU(cfg)
}
s.sendFCU(cfg, fcuArgs)
// Pre-Fulu the caches are updated when computing the payload attributes
if cfg.postState.Version() >= version.Fulu {

View File

@@ -38,26 +38,23 @@ func (s *Service) CurrentSlot() primitives.Slot {
}
// getFCUArgs returns the arguments to call forkchoice update
func (s *Service) getFCUArgs(cfg *postBlockProcessConfig) (*fcuConfig, error) {
fcuArgs, err := s.getFCUArgsEarlyBlock(cfg)
if err != nil {
return nil, err
func (s *Service) getFCUArgs(cfg *postBlockProcessConfig, fcuArgs *fcuConfig) error {
if err := s.getFCUArgsEarlyBlock(cfg, fcuArgs); err != nil {
return err
}
fcuArgs.attributes = s.getPayloadAttribute(cfg.ctx, fcuArgs.headState, fcuArgs.proposingSlot, cfg.headRoot[:])
return fcuArgs, nil
return nil
}
func (s *Service) getFCUArgsEarlyBlock(cfg *postBlockProcessConfig) (*fcuConfig, error) {
func (s *Service) getFCUArgsEarlyBlock(cfg *postBlockProcessConfig, fcuArgs *fcuConfig) error {
if cfg.roblock.Root() == cfg.headRoot {
return &fcuConfig{
headState: cfg.postState,
headBlock: cfg.roblock,
headRoot: cfg.headRoot,
proposingSlot: s.CurrentSlot() + 1,
}, nil
fcuArgs.headState = cfg.postState
fcuArgs.headBlock = cfg.roblock
fcuArgs.headRoot = cfg.headRoot
fcuArgs.proposingSlot = s.CurrentSlot() + 1
return nil
}
return s.fcuArgsNonCanonicalBlock(cfg)
return s.fcuArgsNonCanonicalBlock(cfg, fcuArgs)
}
// logNonCanonicalBlockReceived prints a message informing that the received
@@ -82,17 +79,16 @@ func (s *Service) logNonCanonicalBlockReceived(blockRoot [32]byte, headRoot [32]
// fcuArgsNonCanonicalBlock returns the arguments to the FCU call when the
// incoming block is non-canonical, that is, based on the head root.
func (s *Service) fcuArgsNonCanonicalBlock(cfg *postBlockProcessConfig) (*fcuConfig, error) {
func (s *Service) fcuArgsNonCanonicalBlock(cfg *postBlockProcessConfig, fcuArgs *fcuConfig) error {
headState, headBlock, err := s.getStateAndBlock(cfg.ctx, cfg.headRoot)
if err != nil {
return nil, err
return err
}
return &fcuConfig{
headState: headState,
headBlock: headBlock,
headRoot: cfg.headRoot,
proposingSlot: s.CurrentSlot() + 1,
}, nil
fcuArgs.headState = headState
fcuArgs.headBlock = headBlock
fcuArgs.headRoot = cfg.headRoot
fcuArgs.proposingSlot = s.CurrentSlot() + 1
return nil
}
// sendStateFeedOnBlock sends an event that a new block has been synced

View File

@@ -2417,12 +2417,14 @@ func Test_getFCUArgs(t *testing.T) {
isValidPayload: true,
}
// error branch
_, err = s.getFCUArgs(cfg)
fcuArgs := &fcuConfig{}
err = s.getFCUArgs(cfg, fcuArgs)
require.ErrorContains(t, "block does not exist", err)
// canonical branch
cfg.headRoot = cfg.roblock.Root()
fcuArgs, err := s.getFCUArgs(cfg)
fcuArgs = &fcuConfig{}
err = s.getFCUArgs(cfg, fcuArgs)
require.NoError(t, err)
require.Equal(t, cfg.roblock.Root(), fcuArgs.headRoot)
}

View File

@@ -77,6 +77,7 @@ type ChainService struct {
DataColumns []blocks.VerifiedRODataColumn
TargetRoot [32]byte
MockHeadSlot *primitives.Slot
DependentRootCB func([32]byte, primitives.Epoch) ([32]byte, error)
}
func (s *ChainService) Ancestor(ctx context.Context, root []byte, slot primitives.Slot) ([]byte, error) {
@@ -768,7 +769,10 @@ func (c *ChainService) ReceiveExecutionPayloadEnvelope(_ context.Context, _ inte
}
// DependentRootForEpoch mocks the same method in the chain service
func (c *ChainService) DependentRootForEpoch(_ [32]byte, _ primitives.Epoch) ([32]byte, error) {
func (c *ChainService) DependentRootForEpoch(root [32]byte, epoch primitives.Epoch) ([32]byte, error) {
if c.DependentRootCB != nil {
return c.DependentRootCB(root, epoch)
}
return c.TargetRoot, nil
}

View File

@@ -320,7 +320,7 @@ func (s *Service) validatorEndpoints(
methods: []string{http.MethodPost},
},
{
template: "/eth/v1/validator/duties/proposer/{epoch}",
template: "/eth/v1/validator/duties/proposer/{epoch}", // Deprecated: use /eth/v2/validator/duties/proposer/{epoch}
name: namespace + ".GetProposerDuties",
middleware: []middleware.Middleware{
middleware.AcceptHeaderHandler([]string{api.JsonMediaType}),
@@ -329,6 +329,16 @@ func (s *Service) validatorEndpoints(
handler: server.GetProposerDuties,
methods: []string{http.MethodGet},
},
{
template: "/eth/v2/validator/duties/proposer/{epoch}",
name: namespace + ".GetProposerDutiesV2",
middleware: []middleware.Middleware{
middleware.AcceptHeaderHandler([]string{api.JsonMediaType}),
middleware.AcceptEncodingHeaderHandler(),
},
handler: server.GetProposerDutiesV2,
methods: []string{http.MethodGet},
},
{
template: "/eth/v1/validator/duties/sync/{epoch}",
name: namespace + ".GetSyncCommitteeDuties",

View File

@@ -94,6 +94,7 @@ func Test_endpoints(t *testing.T) {
validatorRoutes := map[string][]string{
"/eth/v1/validator/duties/attester/{epoch}": {http.MethodPost},
"/eth/v1/validator/duties/proposer/{epoch}": {http.MethodGet},
"/eth/v2/validator/duties/proposer/{epoch}": {http.MethodGet},
"/eth/v1/validator/duties/sync/{epoch}": {http.MethodPost},
"/eth/v3/validator/blocks/{slot}": {http.MethodGet},
"/eth/v1/validator/attestation_data": {http.MethodGet},

View File

@@ -982,20 +982,29 @@ func (s *Server) GetAttesterDuties(w http.ResponseWriter, r *http.Request) {
httputil.WriteJson(w, response)
}
// GetProposerDuties requests beacon node to provide all validators that are scheduled to propose a block in the given epoch.
func (s *Server) GetProposerDuties(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.GetProposerDuties")
defer span.End()
// proposerDutiesInfo holds the computed proposer duties and associated metadata.
type proposerDutiesInfo struct {
duties []*structs.ProposerDuty
isOptimistic bool
lookupEpoch primitives.Epoch // epoch used for state lookup (adjusted for lookahead)
dutiesEpoch primitives.Epoch // actual epoch duties are for
st state.BeaconState
}
// computeProposerDuties computes proposer duties for the given epoch. It handles sync checking,
// epoch parsing/validation, next-epoch lookahead, state fetch, assignment computation, duty building,
// sorting, and optimistic check. It writes errors directly to w and returns nil if an error occurred.
func (s *Server) computeProposerDuties(ctx context.Context, w http.ResponseWriter, r *http.Request) *proposerDutiesInfo {
if shared.IsSyncing(ctx, w, s.SyncChecker, s.HeadFetcher, s.TimeFetcher, s.OptimisticModeFetcher) {
return
return nil
}
_, requestedEpochUint, ok := shared.UintFromRoute(w, r, "epoch")
if !ok {
return
return nil
}
requestedEpoch := primitives.Epoch(requestedEpochUint)
dutiesEpoch := requestedEpoch
cs := s.TimeFetcher.CurrentSlot()
currentEpoch := slots.ToEpoch(cs)
@@ -1007,7 +1016,7 @@ func (s *Server) GetProposerDuties(w http.ResponseWriter, r *http.Request) {
fmt.Sprintf("Request epoch %d can not be greater than next epoch %d", requestedEpoch, currentEpoch+1),
http.StatusBadRequest,
)
return
return nil
} else if requestedEpoch == nextEpoch {
// If the request is for the next epoch, we use the current epoch's state to compute duties.
requestedEpoch = currentEpoch
@@ -1017,18 +1026,18 @@ func (s *Server) GetProposerDuties(w http.ResponseWriter, r *http.Request) {
st, err := s.Stater.StateByEpoch(ctx, requestedEpoch)
if err != nil {
shared.WriteStateFetchError(w, err)
return
return nil
}
var assignments map[primitives.ValidatorIndex][]primitives.Slot
if nextEpochLookahead {
assignments, err = helpers.ProposerAssignments(ctx, st, nextEpoch)
assignments, err = helpers.ProposerAssignments(ctx, st, dutiesEpoch)
} else {
assignments, err = helpers.ProposerAssignments(ctx, st, requestedEpoch)
}
if err != nil {
httputil.HandleError(w, "Could not compute committee assignments: "+err.Error(), http.StatusInternalServerError)
return
return nil
}
duties := make([]*structs.ProposerDuty, 0)
@@ -1036,7 +1045,7 @@ func (s *Server) GetProposerDuties(w http.ResponseWriter, r *http.Request) {
val, err := st.ValidatorAtIndexReadOnly(index)
if err != nil {
httputil.HandleError(w, fmt.Sprintf("Could not get validator at index %d: %v", index, err), http.StatusInternalServerError)
return
return nil
}
pubkey48 := val.PublicKey()
pubkey := pubkey48[:]
@@ -1049,35 +1058,104 @@ func (s *Server) GetProposerDuties(w http.ResponseWriter, r *http.Request) {
}
}
if err = sortProposerDuties(duties); err != nil {
httputil.HandleError(w, "Could not sort proposer duties: "+err.Error(), http.StatusInternalServerError)
return nil
}
isOptimistic, err := s.OptimisticModeFetcher.IsOptimistic(ctx)
if err != nil {
httputil.HandleError(w, "Could not check optimistic status: "+err.Error(), http.StatusInternalServerError)
return nil
}
return &proposerDutiesInfo{
duties: duties,
isOptimistic: isOptimistic,
lookupEpoch: requestedEpoch,
dutiesEpoch: dutiesEpoch,
st: st,
}
}
// Deprecated: GetProposerDuties requests beacon node to provide all validators that are scheduled to propose a block in the given epoch.
// Use GetProposerDutiesV2 instead, which computes a fork-aware dependent root.
func (s *Server) GetProposerDuties(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.GetProposerDuties")
defer span.End()
info := s.computeProposerDuties(ctx, w, r)
if info == nil {
return
}
var dependentRoot []byte
if requestedEpoch == 0 {
r, err := s.BeaconDB.GenesisBlockRoot(ctx)
if info.lookupEpoch == 0 {
root, err := s.BeaconDB.GenesisBlockRoot(ctx)
if err != nil {
httputil.HandleError(w, "Could not get genesis block root: "+err.Error(), http.StatusInternalServerError)
return
}
dependentRoot = r[:]
dependentRoot = root[:]
} else {
dependentRoot, err = proposalDependentRoot(st, requestedEpoch)
var err error
dependentRoot, err = proposalDependentRoot(info.st, info.lookupEpoch)
if err != nil {
httputil.HandleError(w, "Could not get dependent root: "+err.Error(), http.StatusInternalServerError)
return
}
}
isOptimistic, err := s.OptimisticModeFetcher.IsOptimistic(ctx)
if err != nil {
httputil.HandleError(w, "Could not check optimistic status: "+err.Error(), http.StatusInternalServerError)
resp := &structs.GetProposerDutiesResponse{
DependentRoot: hexutil.Encode(dependentRoot),
Data: info.duties,
ExecutionOptimistic: info.isOptimistic,
}
httputil.WriteJson(w, resp)
}
// GetProposerDutiesV2 requests beacon node to provide all validators that are scheduled to propose a block in the given epoch.
// V2 computes a fork-aware dependent root: post-Fulu uses DependentRootForEpoch(headRoot, epoch-1) to account for
// the deterministic proposer lookahead, while pre-Fulu uses DependentRootForEpoch(headRoot, epoch) matching v1 semantics.
func (s *Server) GetProposerDutiesV2(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.GetProposerDutiesV2")
defer span.End()
info := s.computeProposerDuties(ctx, w, r)
if info == nil {
return
}
if err = sortProposerDuties(duties); err != nil {
httputil.HandleError(w, "Could not sort proposer duties: "+err.Error(), http.StatusInternalServerError)
return
var dependentRoot []byte
if info.dutiesEpoch == 0 {
root, err := s.BeaconDB.GenesisBlockRoot(ctx)
if err != nil {
httputil.HandleError(w, "Could not get genesis block root: "+err.Error(), http.StatusInternalServerError)
return
}
dependentRoot = root[:]
} else {
headRoot, err := s.HeadFetcher.HeadRoot(ctx)
if err != nil {
httputil.HandleError(w, "Could not get head root: "+err.Error(), http.StatusInternalServerError)
return
}
depEpoch := info.dutiesEpoch
if depEpoch >= params.BeaconConfig().FuluForkEpoch {
depEpoch = info.dutiesEpoch.Sub(1)
}
root, err := s.HeadFetcher.DependentRootForEpoch(bytesutil.ToBytes32(headRoot), depEpoch)
if err != nil {
httputil.HandleError(w, "Could not get dependent root: "+err.Error(), http.StatusInternalServerError)
return
}
dependentRoot = root[:]
}
resp := &structs.GetProposerDutiesResponse{
DependentRoot: hexutil.Encode(dependentRoot),
Data: duties,
ExecutionOptimistic: isOptimistic,
Data: info.duties,
ExecutionOptimistic: info.isOptimistic,
}
httputil.WriteJson(w, resp)
}

View File

@@ -2542,6 +2542,184 @@ func TestGetProposerDuties(t *testing.T) {
})
}
func TestGetProposerDutiesV2(t *testing.T) {
helpers.ClearCache()
genesis := util.NewBeaconBlock()
depChainStart := params.BeaconConfig().MinGenesisActiveValidatorCount
deposits, _, err := util.DeterministicDepositsAndKeys(depChainStart)
require.NoError(t, err)
eth1Data, err := util.DeterministicEth1Data(len(deposits))
require.NoError(t, err)
genesisRoot, err := genesis.Block.HashTreeRoot()
require.NoError(t, err)
db := dbutil.SetupDB(t)
require.NoError(t, db.SaveGenesisBlockRoot(t.Context(), genesisRoot))
t.Run("epoch 0 returns genesis block root", func(t *testing.T) {
bs, err := transition.GenesisBeaconState(t.Context(), deposits, 0, eth1Data)
require.NoError(t, err, "Could not set up genesis state")
require.NoError(t, bs.SetSlot(params.BeaconConfig().SlotsPerEpoch))
chainSlot := primitives.Slot(0)
chain := &mockChain.ChainService{
State: bs, Root: genesisRoot[:], Slot: &chainSlot,
}
s := &Server{
Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{0: bs}},
HeadFetcher: chain,
TimeFetcher: chain,
OptimisticModeFetcher: chain,
SyncChecker: &mockSync.Sync{IsSyncing: false},
PayloadIDCache: cache.NewPayloadIDCache(),
TrackedValidatorsCache: cache.NewTrackedValidatorsCache(),
BeaconDB: db,
}
request := httptest.NewRequest(http.MethodGet, "http://www.example.com/eth/v2/validator/duties/proposer/{epoch}", nil)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetProposerDutiesV2(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
resp := &structs.GetProposerDutiesResponse{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
assert.Equal(t, hexutil.Encode(genesisRoot[:]), resp.DependentRoot)
assert.Equal(t, 31, len(resp.Data))
})
t.Run("pre-fulu uses DependentRootForEpoch with dutiesEpoch", func(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.FuluForkEpoch = 100 // well beyond our test epoch
params.OverrideBeaconConfig(cfg)
bs, err := transition.GenesisBeaconState(t.Context(), deposits, 0, eth1Data)
require.NoError(t, err, "Could not set up genesis state")
require.NoError(t, bs.SetSlot(params.BeaconConfig().SlotsPerEpoch))
chainSlot := primitives.Slot(0)
preFuluRoot := [32]byte{'p', 'r', 'e'}
var capturedEpoch primitives.Epoch
chain := &mockChain.ChainService{
State: bs, Root: genesisRoot[:], Slot: &chainSlot,
DependentRootCB: func(_ [32]byte, epoch primitives.Epoch) ([32]byte, error) {
capturedEpoch = epoch
return preFuluRoot, nil
},
}
s := &Server{
Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{0: bs}},
HeadFetcher: chain,
TimeFetcher: chain,
OptimisticModeFetcher: chain,
SyncChecker: &mockSync.Sync{IsSyncing: false},
PayloadIDCache: cache.NewPayloadIDCache(),
TrackedValidatorsCache: cache.NewTrackedValidatorsCache(),
BeaconDB: db,
}
// Request epoch 1 (pre-Fulu since FuluForkEpoch=100).
// V2 pre-Fulu calls DependentRootForEpoch(headRoot, dutiesEpoch=1).
request := httptest.NewRequest(http.MethodGet, "http://www.example.com/eth/v2/validator/duties/proposer/{epoch}", nil)
request.SetPathValue("epoch", "1")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetProposerDutiesV2(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
resp := &structs.GetProposerDutiesResponse{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
assert.Equal(t, hexutil.Encode(preFuluRoot[:]), resp.DependentRoot)
assert.Equal(t, primitives.Epoch(1), capturedEpoch, "pre-Fulu should pass dutiesEpoch to DependentRootForEpoch")
assert.Equal(t, 32, len(resp.Data))
})
t.Run("post-fulu uses DependentRootForEpoch with dutiesEpoch-1", func(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.FuluForkEpoch = 0 // Fulu active from genesis
params.OverrideBeaconConfig(cfg)
bs, err := transition.GenesisBeaconState(t.Context(), deposits, 0, eth1Data)
require.NoError(t, err, "Could not set up genesis state")
require.NoError(t, bs.SetSlot(params.BeaconConfig().SlotsPerEpoch))
chainSlot := primitives.Slot(0)
postFuluRoot := [32]byte{'p', 'o', 's', 't'}
var capturedEpoch primitives.Epoch
chain := &mockChain.ChainService{
State: bs, Root: genesisRoot[:], Slot: &chainSlot,
DependentRootCB: func(_ [32]byte, epoch primitives.Epoch) ([32]byte, error) {
capturedEpoch = epoch
return postFuluRoot, nil
},
}
s := &Server{
Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{0: bs}},
HeadFetcher: chain,
TimeFetcher: chain,
OptimisticModeFetcher: chain,
SyncChecker: &mockSync.Sync{IsSyncing: false},
PayloadIDCache: cache.NewPayloadIDCache(),
TrackedValidatorsCache: cache.NewTrackedValidatorsCache(),
BeaconDB: db,
}
// Request epoch 1 (post-Fulu since FuluForkEpoch=0).
// V2 post-Fulu calls DependentRootForEpoch(headRoot, dutiesEpoch-1=0).
request := httptest.NewRequest(http.MethodGet, "http://www.example.com/eth/v2/validator/duties/proposer/{epoch}", nil)
request.SetPathValue("epoch", "1")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetProposerDutiesV2(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
resp := &structs.GetProposerDutiesResponse{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
assert.Equal(t, hexutil.Encode(postFuluRoot[:]), resp.DependentRoot)
assert.Equal(t, primitives.Epoch(0), capturedEpoch, "post-Fulu should pass dutiesEpoch-1 to DependentRootForEpoch")
assert.Equal(t, 32, len(resp.Data))
})
t.Run("next epoch lookahead", func(t *testing.T) {
bs, err := transition.GenesisBeaconState(t.Context(), deposits, 0, eth1Data)
require.NoError(t, err, "Could not set up genesis state")
require.NoError(t, bs.SetSlot(params.BeaconConfig().SlotsPerEpoch))
chainSlot := primitives.Slot(0)
lookaheadRoot := [32]byte{'l', 'o', 'o', 'k'}
var capturedEpoch primitives.Epoch
chain := &mockChain.ChainService{
State: bs, Root: genesisRoot[:], Slot: &chainSlot,
DependentRootCB: func(_ [32]byte, epoch primitives.Epoch) ([32]byte, error) {
capturedEpoch = epoch
return lookaheadRoot, nil
},
}
s := &Server{
Stater: &testutil.MockStater{StatesBySlot: map[primitives.Slot]state.BeaconState{0: bs}},
HeadFetcher: chain,
TimeFetcher: chain,
OptimisticModeFetcher: chain,
SyncChecker: &mockSync.Sync{IsSyncing: false},
PayloadIDCache: cache.NewPayloadIDCache(),
TrackedValidatorsCache: cache.NewTrackedValidatorsCache(),
BeaconDB: db,
}
// Request epoch 1 when current epoch is 0, triggering next-epoch lookahead.
// dutiesEpoch should remain 1, and DependentRootForEpoch should be called with epoch 1.
request := httptest.NewRequest(http.MethodGet, "http://www.example.com/eth/v2/validator/duties/proposer/{epoch}", nil)
request.SetPathValue("epoch", "1")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetProposerDutiesV2(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
resp := &structs.GetProposerDutiesResponse{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
assert.Equal(t, hexutil.Encode(lookaheadRoot[:]), resp.DependentRoot)
assert.Equal(t, primitives.Epoch(1), capturedEpoch, "next-epoch lookahead should pass dutiesEpoch to DependentRootForEpoch")
assert.Equal(t, 32, len(resp.Data))
})
}
func TestGetSyncCommitteeDuties(t *testing.T) {
helpers.ClearCache()
params.SetupTestConfigCleanup(t)

View File

@@ -54,6 +54,7 @@ go_library(
"validate_aggregate_proof.go",
"validate_attester_slashing.go",
"validate_beacon_attestation.go",
"validate_beacon_block_gloas.go",
"validate_beacon_blocks.go",
"validate_blob.go",
"validate_bls_to_execution_change.go",
@@ -211,6 +212,7 @@ go_test(
"validate_aggregate_proof_test.go",
"validate_attester_slashing_test.go",
"validate_beacon_attestation_test.go",
"validate_beacon_block_gloas_test.go",
"validate_beacon_blocks_test.go",
"validate_blob_test.go",
"validate_bls_to_execution_change_test.go",

View File

@@ -0,0 +1,61 @@
package sync
import (
"context"
"github.com/OffchainLabs/prysm/v7/config/params"
consensusblocks "github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/time/slots"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/pkg/errors"
)
// validateExecutionPayloadBid validates execution payload bid gossip rules.
// [REJECT] The bid's parent (defined by bid.parent_block_root) equals the block's parent (defined by block.parent_root).
// [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the consensus layer --
// i.e. validate that len(bid.blob_kzg_commitments) <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block
func (s *Service) validateExecutionPayloadBid(ctx context.Context, blk interfaces.ReadOnlyBeaconBlock) (pubsub.ValidationResult, error) {
if blk.Version() < version.Gloas {
return pubsub.ValidationAccept, nil
}
signedBid, err := blk.Body().SignedExecutionPayloadBid()
if err != nil {
return pubsub.ValidationIgnore, errors.Wrap(err, "unable to read bid from block")
}
wrappedBid, err := consensusblocks.WrappedROSignedExecutionPayloadBid(signedBid)
if err != nil {
return pubsub.ValidationIgnore, errors.Wrap(err, "unable to wrap signed execution payload bid")
}
bid, err := wrappedBid.Bid()
if err != nil {
return pubsub.ValidationIgnore, errors.Wrap(err, "unable to read bid from signed execution payload bid")
}
if bid.ParentBlockRoot() != blk.ParentRoot() {
return pubsub.ValidationReject, errors.New("bid parent block root does not match block parent root")
}
maxBlobsPerBlock := params.BeaconConfig().MaxBlobsPerBlockAtEpoch(slots.ToEpoch(blk.Slot()))
if bid.BlobKzgCommitmentCount() > uint64(maxBlobsPerBlock) {
return pubsub.ValidationReject, errors.Wrapf(errRejectCommitmentLen, "%d > %d", bid.BlobKzgCommitmentCount(), maxBlobsPerBlock)
}
return pubsub.ValidationAccept, nil
}
// validateExecutionPayloadBidParentSeen validates parent payload gossip rules.
// [IGNORE] The block's parent execution payload (defined by bid.parent_block_hash) has been seen
// (via gossip or non-gossip sources) (a client MAY queue blocks for processing once the parent payload is retrieved).
func (s *Service) validateExecutionPayloadBidParentSeen(ctx context.Context, blk interfaces.ReadOnlyBeaconBlock) (pubsub.ValidationResult, error) {
// TODO: Requires blockchain service changes to expose parent payload seen status
return pubsub.ValidationAccept, nil
}
// validateExecutionPayloadBidParentValid validates parent payload verification status.
// If execution_payload verification of block's execution payload parent by an execution node is complete:
// [REJECT] The block's execution payload parent (defined by bid.parent_block_hash) passes all validation.
func (s *Service) validateExecutionPayloadBidParentValid(ctx context.Context, blk interfaces.ReadOnlyBeaconBlock) (pubsub.ValidationResult, error) {
// TODO: Requires blockchain service changes to expose execution payload parent validation status.
return pubsub.ValidationAccept, nil
}

View File

@@ -0,0 +1,75 @@
package sync
import (
"context"
"testing"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/testing/util"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/stretchr/testify/require"
)
func TestValidateExecutionPayloadBid_Accept(t *testing.T) {
params.SetupTestConfigCleanup(t)
ctx := context.Background()
parentRoot := bytesutil.PadTo([]byte{0x01}, fieldparams.RootLength)
block := util.NewBeaconBlockGloas()
block.Block.ParentRoot = parentRoot
block.Block.Body.SignedExecutionPayloadBid.Message.ParentBlockRoot = parentRoot
block.Block.Body.SignedExecutionPayloadBid.Message.BlobKzgCommitments = nil
wsb, err := blocks.NewSignedBeaconBlock(block)
require.NoError(t, err)
s := &Service{}
res, err := s.validateExecutionPayloadBid(ctx, wsb.Block())
require.NoError(t, err)
require.Equal(t, pubsub.ValidationAccept, res)
}
func TestValidateExecutionPayloadBid_RejectParentRootMismatch(t *testing.T) {
params.SetupTestConfigCleanup(t)
ctx := context.Background()
block := util.NewBeaconBlockGloas()
block.Block.ParentRoot = bytesutil.PadTo([]byte{0x01}, fieldparams.RootLength)
block.Block.Body.SignedExecutionPayloadBid.Message.ParentBlockRoot = bytesutil.PadTo([]byte{0x02}, fieldparams.RootLength)
wsb, err := blocks.NewSignedBeaconBlock(block)
require.NoError(t, err)
s := &Service{}
res, err := s.validateExecutionPayloadBid(ctx, wsb.Block())
require.Error(t, err)
require.Equal(t, pubsub.ValidationReject, res)
}
func TestValidateExecutionPayloadBid_RejectTooManyCommitments(t *testing.T) {
params.SetupTestConfigCleanup(t)
ctx := context.Background()
parentRoot := bytesutil.PadTo([]byte{0x01}, fieldparams.RootLength)
block := util.NewBeaconBlockGloas()
block.Block.ParentRoot = parentRoot
block.Block.Body.SignedExecutionPayloadBid.Message.ParentBlockRoot = parentRoot
maxBlobs := params.BeaconConfig().MaxBlobsPerBlockAtEpoch(0)
commitments := make([][]byte, maxBlobs+1)
for i := range commitments {
commitments[i] = bytesutil.PadTo([]byte{0x02}, fieldparams.BLSPubkeyLength)
}
block.Block.Body.SignedExecutionPayloadBid.Message.BlobKzgCommitments = commitments
wsb, err := blocks.NewSignedBeaconBlock(block)
require.NoError(t, err)
s := &Service{}
res, err := s.validateExecutionPayloadBid(ctx, wsb.Block())
require.Error(t, err)
require.Equal(t, pubsub.ValidationReject, res)
}

View File

@@ -127,6 +127,9 @@ func (s *Service) validateBeaconBlockPubSub(ctx context.Context, pid peer.ID, ms
log.WithError(err).WithFields(getBlockFields(blk)).Debug("Received block with an invalid parent")
return pubsub.ValidationReject, err
}
if res, err := s.validateExecutionPayloadBidParentValid(ctx, blk.Block()); err != nil {
return res, err
}
s.pendingQueueLock.RLock()
if s.seenPendingBlocks[blockRoot] {
@@ -198,6 +201,16 @@ func (s *Service) validateBeaconBlockPubSub(ctx context.Context, pid peer.ID, ms
log.WithError(err).WithFields(getBlockFields(blk)).Debug("Could not identify parent for block")
return pubsub.ValidationIgnore, err
}
if res, err := s.validateExecutionPayloadBidParentSeen(ctx, blk.Block()); err != nil {
return res, err
}
if res, err := s.validateExecutionPayloadBid(ctx, blk.Block()); err != nil {
if res == pubsub.ValidationReject {
s.setBadBlock(ctx, blockRoot)
}
return res, err
}
err = s.validateBeaconBlock(ctx, blk, blockRoot)
if err != nil {
@@ -365,7 +378,7 @@ func (s *Service) blockVerifyingState(ctx context.Context, blk interfaces.ReadOn
}
func validateDenebBeaconBlock(blk interfaces.ReadOnlyBeaconBlock) error {
if blk.Version() < version.Deneb {
if blk.Version() < version.Deneb || blk.Version() >= version.Gloas {
return nil
}
commits, err := blk.Body().BlobKzgCommitments()
@@ -398,6 +411,10 @@ func validateDenebBeaconBlock(blk interfaces.ReadOnlyBeaconBlock) error {
// [IGNORE] The block's parent (defined by block.parent_root) passes all validation (including execution
// node verification of the block.body.execution_payload).
func (s *Service) validateBellatrixBeaconBlock(ctx context.Context, verifyingState state.ReadOnlyBeaconState, blk interfaces.ReadOnlyBeaconBlock) error {
if blk.Version() >= version.Gloas {
return nil
}
// Error if block and state are not the same version
if verifyingState.Version() != blk.Version() {
return errors.New("block and state are not the same version")

View File

@@ -0,0 +1,3 @@
### Added
- /eth/v2/validator/duties/proposer/{epoch} implementation with updated dependent root info.

View File

@@ -1,2 +0,0 @@
### Changed
- Changed fcuArgs parameters passing and only call FCU pre-Gloas

View File

@@ -0,0 +1,3 @@
### Added
- Add Gloas beacon block gossip validation for execution payload bids