Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow fetching sync committee duties for current and next period's epochs #9728

Merged
merged 9 commits into from
Oct 5, 2021
Merged
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) {
Copy link
Contributor Author

@rkapka rkapka Oct 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't particularly like this test because it duplicates a part of the implementation. But because I fetch a config value, which changes between regular and minimal configs, I cannot simply hardcode a number here.

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