Fix PTC duties endpoint state selection and dependent root handling

This commit is contained in:
satushh
2026-02-04 20:46:48 +05:30
parent af2a6c29a6
commit 0076fc78bd
2 changed files with 46 additions and 21 deletions

View File

@@ -1260,16 +1260,25 @@ func (s *Server) GetPTCDuties(w http.ResponseWriter, r *http.Request) {
}
// Limit how far in the future we can query (current + 1 epoch).
currentEpoch := slots.ToEpoch(s.TimeFetcher.CurrentSlot())
if requestedEpoch > currentEpoch+1 {
cs := s.TimeFetcher.CurrentSlot()
currentEpoch := slots.ToEpoch(cs)
nextEpoch := currentEpoch + 1
if requestedEpoch > nextEpoch {
httputil.HandleError(w,
fmt.Sprintf("Request epoch %d can not be greater than next epoch %d", requestedEpoch, currentEpoch+1),
fmt.Sprintf("Request epoch %d can not be greater than next epoch %d", requestedEpoch, nextEpoch),
http.StatusBadRequest)
return
}
// Get state for the epoch.
st, err := s.Stater.StateByEpoch(ctx, requestedEpoch)
// For next epoch requests, we use the current epoch's state since PTC
// assignments for next epoch can be computed from current epoch's state.
// This mirrors the spec's get_ptc_assignment which asserts epoch <= next_epoch
// and uses the current state to compute assignments.
epochForState := requestedEpoch
if requestedEpoch == nextEpoch {
epochForState = currentEpoch
}
st, err := s.Stater.StateByEpoch(ctx, epochForState)
if err != nil {
shared.WriteStateFetchError(w, err)
return
@@ -1304,7 +1313,13 @@ func (s *Server) GetPTCDuties(w http.ResponseWriter, r *http.Request) {
if !requestedSet[valIdx] {
continue
}
// Validate the validator index by checking for zero pubkey.
pubkey := st.PubkeyAtIndex(valIdx)
var zeroPubkey [fieldparams.BLSPubkeyLength]byte
if bytes.Equal(pubkey[:], zeroPubkey[:]) {
httputil.HandleError(w, fmt.Sprintf("Invalid validator index %d", valIdx), http.StatusBadRequest)
return
}
duties = append(duties, &structs.PTCDuty{
Pubkey: hexutil.Encode(pubkey[:]),
ValidatorIndex: strconv.FormatUint(uint64(valIdx), 10),
@@ -1313,11 +1328,22 @@ func (s *Server) GetPTCDuties(w http.ResponseWriter, r *http.Request) {
}
}
// Get dependent root.
dependentRoot, err := ptcDependentRoot(st, requestedEpoch)
if err != nil {
httputil.HandleError(w, "Could not get dependent root: "+err.Error(), http.StatusInternalServerError)
return
// Get dependent root. For epoch 0 or 1, use genesis block root.
// For other epochs, use the block root at the last slot of the previous epoch.
var dependentRoot []byte
if requestedEpoch <= 1 {
r, err := s.BeaconDB.GenesisBlockRoot(ctx)
if err != nil {
httputil.HandleError(w, "Could not get genesis block root: "+err.Error(), http.StatusInternalServerError)
return
}
dependentRoot = r[:]
} else {
dependentRoot, err = ptcDependentRoot(st, requestedEpoch)
if err != nil {
httputil.HandleError(w, "Could not get dependent root: "+err.Error(), http.StatusInternalServerError)
return
}
}
isOptimistic, err := s.OptimisticModeFetcher.IsOptimistic(ctx)
@@ -1327,7 +1353,7 @@ func (s *Server) GetPTCDuties(w http.ResponseWriter, r *http.Request) {
}
resp := &structs.GetPTCDutiesResponse{
DependentRoot: hexutil.Encode(dependentRoot[:]),
DependentRoot: hexutil.Encode(dependentRoot),
ExecutionOptimistic: isOptimistic,
Data: duties,
}
@@ -1336,19 +1362,12 @@ func (s *Server) GetPTCDuties(w http.ResponseWriter, r *http.Request) {
// ptcDependentRoot returns the block root that PTC assignments depend on.
// PTC depends on the shuffling, which is determined by RANDAO at epoch boundary.
func ptcDependentRoot(st state.BeaconState, epoch primitives.Epoch) ([32]byte, error) {
func ptcDependentRoot(st state.BeaconState, epoch primitives.Epoch) ([]byte, error) {
epochStartSlot, err := slots.EpochStart(epoch)
if err != nil {
return [32]byte{}, err
return nil, err
}
if epochStartSlot == 0 {
return bytesutil.ToBytes32(st.GenesisValidatorsRoot()), nil
}
root, err := helpers.BlockRootAtSlot(st, epochStartSlot-1)
if err != nil {
return [32]byte{}, err
}
return bytesutil.ToBytes32(root), nil
return helpers.BlockRootAtSlot(st, epochStartSlot-1)
}
// GetLiveness requests the beacon node to indicate if a validator has been observed to be live in a given epoch.

View File

@@ -2972,6 +2972,11 @@ func TestGetPTCDuties(t *testing.T) {
st, _ := util.DeterministicGenesisStateFulu(t, numVals)
require.NoError(t, st.SetGenesisTime(genesisTime))
// Set up a genesis block root for dependent_root calculation.
genesisRoot := [32]byte{1, 2, 3}
db := dbutil.SetupDB(t)
require.NoError(t, db.SaveGenesisBlockRoot(t.Context(), genesisRoot))
mockChainService := &mockChain.ChainService{Genesis: genesisTime, State: st}
s := &Server{
Stater: &testutil.MockStater{BeaconState: st},
@@ -2979,6 +2984,7 @@ func TestGetPTCDuties(t *testing.T) {
TimeFetcher: mockChainService,
HeadFetcher: mockChainService,
OptimisticModeFetcher: mockChainService,
BeaconDB: db,
}
t.Run("single validator in PTC", func(t *testing.T) {