Skip to content

Commit

Permalink
Allow fetching sync committee duties for current and next period's ep…
Browse files Browse the repository at this point in the history
…ochs (#9728)
  • Loading branch information
rkapka authored Oct 5, 2021
1 parent 362dfa6 commit 9aa5035
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 30 deletions.
104 changes: 80 additions & 24 deletions beacon-chain/rpc/eth/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import (
"google.golang.org/protobuf/types/known/emptypb"
)

var errInvalidValIndex = errors.New("invalid validator index")

// GetAttesterDuties requests the beacon node to provide a set of attestation duties,
// which should be performed by validators, for a particular epoch.
func (vs *Server) GetAttesterDuties(ctx context.Context, req *ethpbv1.AttesterDutiesRequest) (*ethpbv1.AttesterDutiesResponse, error) {
Expand Down Expand Up @@ -167,6 +169,16 @@ func (vs *Server) GetProposerDuties(ctx context.Context, req *ethpbv1.ProposerDu
}

// GetSyncCommitteeDuties provides a set of sync committee duties for a particular epoch.
//
// The logic for calculating epoch validity comes from https://ethereum.github.io/beacon-APIs/?urls.primaryName=dev#/Validator/getSyncCommitteeDuties
// where `epoch` is described as `epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD <= current_epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + 1`.
//
// Algorithm:
// - Get the last valid epoch. This is the last epoch of the next sync committee period.
// - Get the state for the requested epoch. If it's a future epoch from the current sync committee period
// or an epoch from the next sync committee period, then get the current state.
// - Get the state's current sync committee. If it's an epoch from the next sync committee period, then get the next sync committee.
// - Get duties.
func (vs *Server) GetSyncCommitteeDuties(ctx context.Context, req *ethpbv2.SyncCommitteeDutiesRequest) (*ethpbv2.SyncCommitteeDutiesResponse, error) {
ctx, span := trace.StartSpan(ctx, "validator.GetSyncCommitteeDuties")
defer span.End()
Expand All @@ -175,45 +187,54 @@ func (vs *Server) GetSyncCommitteeDuties(ctx context.Context, req *ethpbv2.SyncC
return nil, status.Error(codes.Unavailable, "Syncing to latest head, not ready to respond")
}

slot, err := slots.EpochStart(req.Epoch)
currentEpoch := slots.ToEpoch(vs.TimeFetcher.CurrentSlot())
lastValidEpoch := syncCommitteeDutiesLastValidEpoch(currentEpoch)
if req.Epoch > lastValidEpoch {
return nil, status.Errorf(codes.InvalidArgument, "Epoch is too far in the future. Maximum valid epoch is %v.", lastValidEpoch)
}

requestedEpoch := req.Epoch
if requestedEpoch > currentEpoch {
requestedEpoch = currentEpoch
}
slot, err := slots.EpochStart(requestedEpoch)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not get sync committee slot: %v", err)
}
st, err := vs.StateFetcher.State(ctx, []byte(strconv.FormatUint(uint64(slot), 10)))
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not get sync committee state: %v", err)
}
committee, err := st.CurrentSyncCommittee()

currentSyncCommitteeFirstEpoch, err := slots.SyncCommitteePeriodStartEpoch(currentEpoch / params.BeaconConfig().EpochsPerSyncCommitteePeriod)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not get sync committee: %v", err)
return nil, status.Errorf(codes.InvalidArgument, "Could not get sync committee period start epoch: %v.", err)
}
nextSyncCommitteeFirstEpoch := currentSyncCommitteeFirstEpoch + params.BeaconConfig().EpochsPerSyncCommitteePeriod
var committee *ethpbalpha.SyncCommittee
if req.Epoch >= nextSyncCommitteeFirstEpoch {
committee, err = st.NextSyncCommittee()
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not get sync committee: %v", err)
}
} else {
committee, err = st.CurrentSyncCommittee()
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not get sync committee: %v", err)
}
}

committeePubkeys := make(map[[48]byte][]uint64)
for j, pubkey := range committee.Pubkeys {
pubkey48 := bytesutil.ToBytes48(pubkey)
committeePubkeys[pubkey48] = append(committeePubkeys[pubkey48], uint64(j))
}
duties := make([]*ethpbv2.SyncCommitteeDuty, len(req.Index))
for i, index := range req.Index {
duty := &ethpbv2.SyncCommitteeDuty{
ValidatorIndex: index,
}
valPubkey48 := st.PubkeyAtIndex(index)
zeroPubkey := [48]byte{}
if bytes.Equal(valPubkey48[:], zeroPubkey[:]) {
return nil, status.Errorf(codes.InvalidArgument, "Invalid validator index")
}
valPubkey := valPubkey48[:]
duty.Pubkey = valPubkey
indices, ok := committeePubkeys[valPubkey48]
if ok {
duty.ValidatorSyncCommitteeIndices = indices
} else {
duty.ValidatorSyncCommitteeIndices = make([]uint64, 0)
}
duties[i] = duty
}

duties, err := syncCommitteeDuties(req.Index, st, committeePubkeys)
if errors.Is(err, errInvalidValIndex) {
return nil, status.Error(codes.InvalidArgument, "Invalid validator index")
} else if err != nil {
return nil, status.Errorf(codes.Internal, "Could not get duties: %v", err)
}
return &ethpbv2.SyncCommitteeDutiesResponse{
Data: duties,
}, nil
Expand Down Expand Up @@ -669,3 +690,38 @@ func (vs *Server) v1BeaconBlock(ctx context.Context, req *ethpbv1.ProduceBlockRe
}
return migration.V1Alpha1ToV1Block(v1alpha1resp)
}

func syncCommitteeDutiesLastValidEpoch(currentEpoch types.Epoch) types.Epoch {
currentSyncPeriodIndex := currentEpoch / params.BeaconConfig().EpochsPerSyncCommitteePeriod
// Return the last epoch of the next sync committee.
// To do this we go two periods ahead to find the first invalid epoch, and then subtract 1.
return (currentSyncPeriodIndex+2)*params.BeaconConfig().EpochsPerSyncCommitteePeriod - 1
}

func syncCommitteeDuties(
valIndices []types.ValidatorIndex,
st state.BeaconState,
committeePubkeys map[[48]byte][]uint64,
) ([]*ethpbv2.SyncCommitteeDuty, error) {
duties := make([]*ethpbv2.SyncCommitteeDuty, len(valIndices))
for i, index := range valIndices {
duty := &ethpbv2.SyncCommitteeDuty{
ValidatorIndex: index,
}
valPubkey48 := st.PubkeyAtIndex(index)
zeroPubkey := [48]byte{}
if bytes.Equal(valPubkey48[:], zeroPubkey[:]) {
return nil, errInvalidValIndex
}
valPubkey := valPubkey48[:]
duty.Pubkey = valPubkey
indices, ok := committeePubkeys[valPubkey48]
if ok {
duty.ValidatorSyncCommitteeIndices = indices
} else {
duty.ValidatorSyncCommitteeIndices = make([]uint64, 0)
}
duties[i] = duty
}
return duties, nil
}
78 changes: 72 additions & 6 deletions beacon-chain/rpc/eth/validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,19 +325,28 @@ func TestGetProposerDuties_SyncNotReady(t *testing.T) {

func TestGetSyncCommitteeDuties(t *testing.T) {
ctx := context.Background()
genesisTime := time.Now()
numVals := uint64(10)
st, _ := util.DeterministicGenesisStateAltair(t, numVals)
require.NoError(t, st.SetGenesisTime(uint64(genesisTime.Unix())))
vals := st.Validators()
committee := &ethpbalpha.SyncCommittee{}
for _, v := range vals {
committee.Pubkeys = append(committee.Pubkeys, v.PublicKey)
currCommittee := &ethpbalpha.SyncCommittee{}
for i := 0; i < 5; i++ {
currCommittee.Pubkeys = append(currCommittee.Pubkeys, vals[i].PublicKey)
}
// add one public key twice - this is needed for one of the test cases
committee.Pubkeys = append(committee.Pubkeys, vals[0].PublicKey)
require.NoError(t, st.SetCurrentSyncCommittee(committee))
currCommittee.Pubkeys = append(currCommittee.Pubkeys, vals[0].PublicKey)
require.NoError(t, st.SetCurrentSyncCommittee(currCommittee))
nextCommittee := &ethpbalpha.SyncCommittee{}
for i := 5; i < 10; i++ {
nextCommittee.Pubkeys = append(nextCommittee.Pubkeys, vals[i].PublicKey)
}
require.NoError(t, st.SetNextSyncCommittee(nextCommittee))

vs := &Server{
StateFetcher: &testutil.MockFetcher{BeaconState: st},
SyncChecker: &mockSync.Sync{IsSyncing: false},
TimeFetcher: &mockChain.ChainService{Genesis: genesisTime},
}

t.Run("Single validator", func(t *testing.T) {
Expand All @@ -357,6 +366,23 @@ func TestGetSyncCommitteeDuties(t *testing.T) {
assert.Equal(t, uint64(1), duty.ValidatorSyncCommitteeIndices[0])
})

t.Run("Epoch not at period start", func(t *testing.T) {
req := &ethpbv2.SyncCommitteeDutiesRequest{
Epoch: 1,
Index: []types.ValidatorIndex{1},
}
resp, err := vs.GetSyncCommitteeDuties(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.Equal(t, 1, len(resp.Data))
duty := resp.Data[0]
assert.DeepEqual(t, vals[1].PublicKey, duty.Pubkey)
assert.Equal(t, types.ValidatorIndex(1), duty.ValidatorIndex)
require.Equal(t, 1, len(duty.ValidatorSyncCommitteeIndices))
assert.Equal(t, uint64(1), duty.ValidatorSyncCommitteeIndices[0])
})

t.Run("Multiple validators", func(t *testing.T) {
req := &ethpbv2.SyncCommitteeDutiesRequest{
Epoch: 0,
Expand All @@ -376,7 +402,7 @@ func TestGetSyncCommitteeDuties(t *testing.T) {
require.NoError(t, err)
duty := resp.Data[0]
require.Equal(t, 2, len(duty.ValidatorSyncCommitteeIndices))
assert.DeepEqual(t, []uint64{0, 10}, duty.ValidatorSyncCommitteeIndices)
assert.DeepEqual(t, []uint64{0, 5}, duty.ValidatorSyncCommitteeIndices)
})

t.Run("Validator index out of bound", func(t *testing.T) {
Expand All @@ -388,6 +414,33 @@ func TestGetSyncCommitteeDuties(t *testing.T) {
require.NotNil(t, err)
assert.ErrorContains(t, "Invalid validator index", err)
})

t.Run("next sync committee period", func(t *testing.T) {
req := &ethpbv2.SyncCommitteeDutiesRequest{
Epoch: params.BeaconConfig().EpochsPerSyncCommitteePeriod,
Index: []types.ValidatorIndex{5},
}
resp, err := vs.GetSyncCommitteeDuties(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.Equal(t, 1, len(resp.Data))
duty := resp.Data[0]
assert.DeepEqual(t, vals[5].PublicKey, duty.Pubkey)
assert.Equal(t, types.ValidatorIndex(5), duty.ValidatorIndex)
require.Equal(t, 1, len(duty.ValidatorSyncCommitteeIndices))
assert.Equal(t, uint64(0), duty.ValidatorSyncCommitteeIndices[0])
})

t.Run("epoch too far in the future", func(t *testing.T) {
req := &ethpbv2.SyncCommitteeDutiesRequest{
Epoch: params.BeaconConfig().EpochsPerSyncCommitteePeriod * 2,
Index: []types.ValidatorIndex{5},
}
_, err := vs.GetSyncCommitteeDuties(ctx, req)
require.NotNil(t, err)
assert.ErrorContains(t, "Epoch is too far in the future", err)
})
}

func TestGetSyncCommitteeDuties_SyncNotReady(t *testing.T) {
Expand All @@ -398,6 +451,19 @@ func TestGetSyncCommitteeDuties_SyncNotReady(t *testing.T) {
assert.ErrorContains(t, "Syncing to latest head, not ready to respond", err)
}

func TestSyncCommitteeDutiesLastValidEpoch(t *testing.T) {
t.Run("first epoch of current period", func(t *testing.T) {
assert.Equal(t, params.BeaconConfig().EpochsPerSyncCommitteePeriod*2-1, syncCommitteeDutiesLastValidEpoch(0))
})
t.Run("last epoch of current period", func(t *testing.T) {
assert.Equal(
t,
params.BeaconConfig().EpochsPerSyncCommitteePeriod*2-1,
syncCommitteeDutiesLastValidEpoch(params.BeaconConfig().EpochsPerSyncCommitteePeriod-1),
)
})
}

func TestProduceBlock(t *testing.T) {
db := dbutil.SetupDB(t)
ctx := context.Background()
Expand Down

0 comments on commit 9aa5035

Please sign in to comment.