From 560fe694259d3a223496f5f2feb5f20aa9c295e7 Mon Sep 17 00:00:00 2001 From: terence tsao Date: Thu, 29 Jul 2021 16:19:32 -0700 Subject: [PATCH] Core: sync committee helpers (#9269) * Add sync committee helpers * Create BUILD.bazel * Update BUILD.bazel * Update BUILD.bazel * Fix visibility * Preston's review --- beacon-chain/core/altair/BUILD.bazel | 11 +- beacon-chain/core/altair/sync_committee.go | 181 +++++++++++ .../core/altair/sync_committee_test.go | 298 ++++++++++++++++++ 3 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 beacon-chain/core/altair/sync_committee.go create mode 100644 beacon-chain/core/altair/sync_committee_test.go diff --git a/beacon-chain/core/altair/BUILD.bazel b/beacon-chain/core/altair/BUILD.bazel index 6b8f291053..2bdb99cb24 100644 --- a/beacon-chain/core/altair/BUILD.bazel +++ b/beacon-chain/core/altair/BUILD.bazel @@ -2,7 +2,10 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["deposit.go"], + srcs = [ + "deposit.go", + "sync_committee.go", + ], importpath = "github.com/prysmaticlabs/prysm/beacon-chain/core/altair", visibility = [ "//beacon-chain:__subpackages__", @@ -10,9 +13,13 @@ go_library( ], deps = [ "//beacon-chain/core/blocks:go_default_library", + "//beacon-chain/core/helpers:go_default_library", "//beacon-chain/state:go_default_library", "//proto/prysm/v1alpha1:go_default_library", + "//shared/bls:go_default_library", "//shared/bytesutil:go_default_library", + "//shared/hashutil:go_default_library", + "//shared/mathutil:go_default_library", "//shared/params:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_prysmaticlabs_eth2_types//:go_default_library", @@ -24,6 +31,7 @@ go_test( srcs = [ "deposit_fuzz_test.go", "deposit_test.go", + "sync_committee_test.go", ], deps = [ ":go_default_library", @@ -37,5 +45,6 @@ go_test( "//shared/testutil/require:go_default_library", "//shared/trieutil:go_default_library", "@com_github_google_gofuzz//:go_default_library", + "@com_github_prysmaticlabs_eth2_types//:go_default_library", ], ) diff --git a/beacon-chain/core/altair/sync_committee.go b/beacon-chain/core/altair/sync_committee.go new file mode 100644 index 0000000000..d4820fbd4d --- /dev/null +++ b/beacon-chain/core/altair/sync_committee.go @@ -0,0 +1,181 @@ +package altair + +import ( + "context" + + "github.com/pkg/errors" + types "github.com/prysmaticlabs/eth2-types" + "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/beacon-chain/state" + ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/shared/bls" + "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/shared/hashutil" + "github.com/prysmaticlabs/prysm/shared/mathutil" + "github.com/prysmaticlabs/prysm/shared/params" +) + +const maxRandomByte = uint64(1<<8 - 1) + +// ValidateNilSyncContribution validates the following fields are not nil: +// -the contribution and proof itself +// -the message within contribution and proof +// -the contribution within contribution and proof +// -the aggregation bits within contribution +func ValidateNilSyncContribution(s *ethpb.SignedContributionAndProof) error { + if s == nil { + return errors.New("signed message can't be nil") + } + if s.Message == nil { + return errors.New("signed contribution's message can't be nil") + } + if s.Message.Contribution == nil { + return errors.New("inner contribution can't be nil") + } + if s.Message.Contribution.AggregationBits == nil { + return errors.New("contribution's bitfield can't be nil") + } + return nil +} + +// NextSyncCommittee returns the next sync committee for a given state. +// +// Spec code: +// def get_next_sync_committee(state: BeaconState) -> SyncCommittee: +// """ +// Return the next sync committee, with possible pubkey duplicates. +// """ +// indices = get_next_sync_committee_indices(state) +// pubkeys = [state.validators[index].pubkey for index in indices] +// aggregate_pubkey = bls.AggregatePKs(pubkeys) +// return SyncCommittee(pubkeys=pubkeys, aggregate_pubkey=aggregate_pubkey) +func NextSyncCommittee(ctx context.Context, s state.BeaconStateAltair) (*ethpb.SyncCommittee, error) { + indices, err := NextSyncCommitteeIndices(ctx, s) + if err != nil { + return nil, err + } + pubkeys := make([][]byte, len(indices)) + for i, index := range indices { + p := s.PubkeyAtIndex(index) + pubkeys[i] = p[:] + } + aggregated, err := bls.AggregatePublicKeys(pubkeys) + if err != nil { + return nil, err + } + return ðpb.SyncCommittee{ + Pubkeys: pubkeys, + AggregatePubkey: aggregated.Marshal(), + }, nil +} + +// NextSyncCommitteeIndices returns the next sync committee indices for a given state. +// +// Spec code: +// def get_next_sync_committee_indices(state: BeaconState) -> Sequence[ValidatorIndex]: +// """ +// Return the sync committee indices, with possible duplicates, for the next sync committee. +// """ +// epoch = Epoch(get_current_epoch(state) + 1) +// +// MAX_RANDOM_BYTE = 2**8 - 1 +// active_validator_indices = get_active_validator_indices(state, epoch) +// active_validator_count = uint64(len(active_validator_indices)) +// seed = get_seed(state, epoch, DOMAIN_SYNC_COMMITTEE) +// i = 0 +// sync_committee_indices: List[ValidatorIndex] = [] +// while len(sync_committee_indices) < SYNC_COMMITTEE_SIZE: +// shuffled_index = compute_shuffled_index(uint64(i % active_validator_count), active_validator_count, seed) +// candidate_index = active_validator_indices[shuffled_index] +// random_byte = hash(seed + uint_to_bytes(uint64(i // 32)))[i % 32] +// effective_balance = state.validators[candidate_index].effective_balance +// if effective_balance * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE * random_byte: +// sync_committee_indices.append(candidate_index) +// i += 1 +// return sync_committee_indices +func NextSyncCommitteeIndices(ctx context.Context, s state.BeaconStateAltair) ([]types.ValidatorIndex, error) { + epoch := helpers.NextEpoch(s) + indices, err := helpers.ActiveValidatorIndices(s, epoch) + if err != nil { + return nil, err + } + seed, err := helpers.Seed(s, epoch, params.BeaconConfig().DomainSyncCommittee) + if err != nil { + return nil, err + } + count := uint64(len(indices)) + cfg := params.BeaconConfig() + syncCommitteeSize := cfg.SyncCommitteeSize + cIndices := make([]types.ValidatorIndex, 0, syncCommitteeSize) + hashFunc := hashutil.CustomSHA256Hasher() + + for i := types.ValidatorIndex(0); uint64(len(cIndices)) < params.BeaconConfig().SyncCommitteeSize; i++ { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + sIndex, err := helpers.ComputeShuffledIndex(i.Mod(count), count, seed, true) + if err != nil { + return nil, err + } + + b := append(seed[:], bytesutil.Bytes8(uint64(i.Div(32)))...) + randomByte := hashFunc(b)[i%32] + cIndex := indices[sIndex] + v, err := s.ValidatorAtIndexReadOnly(cIndex) + if err != nil { + return nil, err + } + + effectiveBal := v.EffectiveBalance() + if effectiveBal*maxRandomByte >= cfg.MaxEffectiveBalance*uint64(randomByte) { + cIndices = append(cIndices, cIndex) + } + } + + return cIndices, nil +} + +// SyncSubCommitteePubkeys returns the pubkeys participating in a sync subcommittee. +// +// def get_sync_subcommittee_pubkeys(state: BeaconState, subcommittee_index: uint64) -> Sequence[BLSPubkey]: +// # Committees assigned to `slot` sign for `slot - 1` +// # This creates the exceptional logic below when transitioning between sync committee periods +// next_slot_epoch = compute_epoch_at_slot(Slot(state.slot + 1)) +// if compute_sync_committee_period(get_current_epoch(state)) == compute_sync_committee_period(next_slot_epoch): +// sync_committee = state.current_sync_committee +// else: +// sync_committee = state.next_sync_committee +// +// # Return pubkeys for the subcommittee index +// sync_subcommittee_size = SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT +// i = subcommittee_index * sync_subcommittee_size +// return sync_committee.pubkeys[i:i + sync_subcommittee_size] +func SyncSubCommitteePubkeys(syncCommittee *ethpb.SyncCommittee, subComIdx types.CommitteeIndex) ([][]byte, error) { + cfg := params.BeaconConfig() + subCommSize := cfg.SyncCommitteeSize / cfg.SyncCommitteeSubnetCount + i := uint64(subComIdx) * subCommSize + endOfSubCom := i + subCommSize + pubkeyLen := uint64(len(syncCommittee.Pubkeys)) + if endOfSubCom > pubkeyLen { + return nil, errors.Errorf("end index is larger than array length: %d > %d", endOfSubCom, pubkeyLen) + } + return syncCommittee.Pubkeys[i:endOfSubCom], nil +} + +// IsSyncCommitteeAggregator checks whether the provided signature is for a valid +// aggregator. +// +// def is_sync_committee_aggregator(signature: BLSSignature) -> bool: +// modulo = max(1, SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT // TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE) +// return bytes_to_uint64(hash(signature)[0:8]) % modulo == 0 +func IsSyncCommitteeAggregator(sig []byte) (bool, error) { + if len(sig) != params.BeaconConfig().BLSPubkeyLength { + return false, errors.New("incorrect sig length") + } + + cfg := params.BeaconConfig() + modulo := mathutil.Max(1, cfg.SyncCommitteeSize/cfg.SyncCommitteeSubnetCount/cfg.TargetAggregatorsPerSyncSubcommittee) + hashedSig := hashutil.Hash(sig) + return bytesutil.FromBytes8(hashedSig[:8])%modulo == 0, nil +} diff --git a/beacon-chain/core/altair/sync_committee_test.go b/beacon-chain/core/altair/sync_committee_test.go new file mode 100644 index 0000000000..7cabaf2096 --- /dev/null +++ b/beacon-chain/core/altair/sync_committee_test.go @@ -0,0 +1,298 @@ +package altair_test + +import ( + "context" + "testing" + + types "github.com/prysmaticlabs/eth2-types" + "github.com/prysmaticlabs/prysm/beacon-chain/core/altair" + "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" + stateAltair "github.com/prysmaticlabs/prysm/beacon-chain/state/v2" + ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/shared/bls" + "github.com/prysmaticlabs/prysm/shared/params" + "github.com/prysmaticlabs/prysm/shared/testutil/require" +) + +func TestSyncCommitteeIndices_CanGet(t *testing.T) { + getState := func(t *testing.T, count uint64) *stateAltair.BeaconState { + validators := make([]*ethpb.Validator, count) + for i := 0; i < len(validators); i++ { + validators[i] = ðpb.Validator{ + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: params.BeaconConfig().MinDepositAmount, + } + } + state, err := stateAltair.InitializeFromProto(ðpb.BeaconStateAltair{ + Validators: validators, + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + }) + require.NoError(t, err) + return state + } + + type args struct { + state *stateAltair.BeaconState + epoch types.Epoch + } + tests := []struct { + name string + args args + wantErr bool + errString string + }{ + { + name: "nil state", + args: args{ + state: nil, + }, + wantErr: true, + errString: "nil inner state", + }, + { + name: "genesis validator count, epoch 0", + args: args{ + state: getState(t, params.BeaconConfig().MinGenesisActiveValidatorCount), + epoch: 0, + }, + wantErr: false, + }, + { + name: "genesis validator count, epoch 100", + args: args{ + state: getState(t, params.BeaconConfig().MinGenesisActiveValidatorCount), + epoch: 100, + }, + wantErr: false, + }, + { + name: "less than optimal validator count, epoch 100", + args: args{ + state: getState(t, params.BeaconConfig().MaxValidatorsPerCommittee), + epoch: 100, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + helpers.ClearCache() + got, err := altair.NextSyncCommitteeIndices(context.Background(), tt.args.state) + if tt.wantErr { + require.ErrorContains(t, tt.errString, err) + } else { + require.NoError(t, err) + require.Equal(t, int(params.BeaconConfig().SyncCommitteeSize), len(got)) + } + }) + } +} + +func TestSyncCommitteeIndices_DifferentPeriods(t *testing.T) { + helpers.ClearCache() + getState := func(t *testing.T, count uint64) *stateAltair.BeaconState { + validators := make([]*ethpb.Validator, count) + for i := 0; i < len(validators); i++ { + validators[i] = ðpb.Validator{ + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: params.BeaconConfig().MinDepositAmount, + } + } + state, err := stateAltair.InitializeFromProto(ðpb.BeaconStateAltair{ + Validators: validators, + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + }) + require.NoError(t, err) + return state + } + + state := getState(t, params.BeaconConfig().MaxValidatorsPerCommittee) + got1, err := altair.NextSyncCommitteeIndices(context.Background(), state) + require.NoError(t, err) + require.NoError(t, state.SetSlot(params.BeaconConfig().SlotsPerEpoch)) + got2, err := altair.NextSyncCommitteeIndices(context.Background(), state) + require.NoError(t, err) + require.DeepNotEqual(t, got1, got2) + require.NoError(t, state.SetSlot(params.BeaconConfig().SlotsPerEpoch*types.Slot(params.BeaconConfig().EpochsPerSyncCommitteePeriod))) + got2, err = altair.NextSyncCommitteeIndices(context.Background(), state) + require.NoError(t, err) + require.DeepNotEqual(t, got1, got2) + require.NoError(t, state.SetSlot(params.BeaconConfig().SlotsPerEpoch*types.Slot(2*params.BeaconConfig().EpochsPerSyncCommitteePeriod))) + got2, err = altair.NextSyncCommitteeIndices(context.Background(), state) + require.NoError(t, err) + require.DeepNotEqual(t, got1, got2) +} + +func TestSyncCommittee_CanGet(t *testing.T) { + getState := func(t *testing.T, count uint64) *stateAltair.BeaconState { + validators := make([]*ethpb.Validator, count) + for i := 0; i < len(validators); i++ { + blsKey, err := bls.RandKey() + require.NoError(t, err) + validators[i] = ðpb.Validator{ + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: params.BeaconConfig().MinDepositAmount, + PublicKey: blsKey.PublicKey().Marshal(), + } + } + state, err := stateAltair.InitializeFromProto(ðpb.BeaconStateAltair{ + Validators: validators, + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + }) + require.NoError(t, err) + return state + } + + type args struct { + state *stateAltair.BeaconState + epoch types.Epoch + } + tests := []struct { + name string + args args + wantErr bool + errString string + }{ + { + name: "nil state", + args: args{ + state: nil, + }, + wantErr: true, + errString: "nil inner state", + }, + { + name: "genesis validator count, epoch 0", + args: args{ + state: getState(t, params.BeaconConfig().MinGenesisActiveValidatorCount), + epoch: 0, + }, + wantErr: false, + }, + { + name: "genesis validator count, epoch 100", + args: args{ + state: getState(t, params.BeaconConfig().MinGenesisActiveValidatorCount), + epoch: 100, + }, + wantErr: false, + }, + { + name: "less than optimal validator count, epoch 100", + args: args{ + state: getState(t, params.BeaconConfig().MaxValidatorsPerCommittee), + epoch: 100, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + helpers.ClearCache() + if !tt.wantErr { + require.NoError(t, tt.args.state.SetSlot(types.Slot(tt.args.epoch)*params.BeaconConfig().SlotsPerEpoch)) + } + got, err := altair.NextSyncCommittee(context.Background(), tt.args.state) + if tt.wantErr { + require.ErrorContains(t, tt.errString, err) + } else { + require.NoError(t, err) + require.Equal(t, int(params.BeaconConfig().SyncCommitteeSize), len(got.Pubkeys)) + require.Equal(t, params.BeaconConfig().BLSPubkeyLength, len(got.AggregatePubkey)) + } + }) + } +} + +func TestValidateNilSyncContribution(t *testing.T) { + tests := []struct { + name string + s *ethpb.SignedContributionAndProof + wantErr bool + }{ + { + name: "nil object", + s: nil, + wantErr: true, + }, + { + name: "nil message", + s: ðpb.SignedContributionAndProof{}, + wantErr: true, + }, + { + name: "nil contribution", + s: ðpb.SignedContributionAndProof{Message: ðpb.ContributionAndProof{}}, + wantErr: true, + }, + { + name: "nil bitfield", + s: ðpb.SignedContributionAndProof{ + Message: ðpb.ContributionAndProof{ + Contribution: ðpb.SyncCommitteeContribution{}, + }}, + wantErr: true, + }, + { + name: "non nil sync contribution", + s: ðpb.SignedContributionAndProof{ + Message: ðpb.ContributionAndProof{ + Contribution: ðpb.SyncCommitteeContribution{ + AggregationBits: []byte{}, + }, + }}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := altair.ValidateNilSyncContribution(tt.s); (err != nil) != tt.wantErr { + t.Errorf("ValidateNilSyncContribution() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSyncSubCommitteePubkeys_CanGet(t *testing.T) { + helpers.ClearCache() + state := getState(t, params.BeaconConfig().MaxValidatorsPerCommittee) + com, err := altair.NextSyncCommittee(context.Background(), state) + require.NoError(t, err) + sub, err := altair.SyncSubCommitteePubkeys(com, 0) + require.NoError(t, err) + subCommSize := params.BeaconConfig().SyncCommitteeSize / params.BeaconConfig().SyncCommitteeSubnetCount + require.Equal(t, int(subCommSize), len(sub)) + require.DeepSSZEqual(t, com.Pubkeys[0:subCommSize], sub) + + sub, err = altair.SyncSubCommitteePubkeys(com, 1) + require.NoError(t, err) + require.DeepSSZEqual(t, com.Pubkeys[subCommSize:2*subCommSize], sub) + + sub, err = altair.SyncSubCommitteePubkeys(com, 2) + require.NoError(t, err) + require.DeepSSZEqual(t, com.Pubkeys[2*subCommSize:3*subCommSize], sub) + + sub, err = altair.SyncSubCommitteePubkeys(com, 3) + require.NoError(t, err) + require.DeepSSZEqual(t, com.Pubkeys[3*subCommSize:], sub) + +} + +func getState(t *testing.T, count uint64) *stateAltair.BeaconState { + validators := make([]*ethpb.Validator, count) + for i := 0; i < len(validators); i++ { + blsKey, err := bls.RandKey() + require.NoError(t, err) + validators[i] = ðpb.Validator{ + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: params.BeaconConfig().MinDepositAmount, + PublicKey: blsKey.PublicKey().Marshal(), + } + } + state, err := stateAltair.InitializeFromProto(ðpb.BeaconStateAltair{ + Validators: validators, + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + }) + require.NoError(t, err) + return state +}