mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-02-18 16:54:26 -05:00
Compare commits
7 Commits
gloas_post
...
proposer-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afba3a4bbd | ||
|
|
7f8e1f56b4 | ||
|
|
4075f64e15 | ||
|
|
a28c6c8145 | ||
|
|
2bc43bd201 | ||
|
|
36cb0e6c95 | ||
|
|
ae6600fe2d |
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
61
beacon-chain/sync/validate_beacon_block_gloas.go
Normal file
61
beacon-chain/sync/validate_beacon_block_gloas.go
Normal 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
|
||||
}
|
||||
75
beacon-chain/sync/validate_beacon_block_gloas_test.go
Normal file
75
beacon-chain/sync/validate_beacon_block_gloas_test.go
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
3
changelog/james-prysm_proposer-duties-v2.md
Normal file
3
changelog/james-prysm_proposer-duties-v2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Added
|
||||
|
||||
- /eth/v2/validator/duties/proposer/{epoch} implementation with updated dependent root info.
|
||||
@@ -1,2 +0,0 @@
|
||||
### Changed
|
||||
- Changed fcuArgs parameters passing and only call FCU pre-Gloas
|
||||
@@ -0,0 +1,3 @@
|
||||
### Added
|
||||
|
||||
- Add Gloas beacon block gossip validation for execution payload bids
|
||||
Reference in New Issue
Block a user