Core: sync committee helpers (#9269)

* Add sync committee helpers

* Create BUILD.bazel

* Update BUILD.bazel

* Update BUILD.bazel

* Fix visibility

* Preston's review
This commit is contained in:
terence tsao
2021-07-29 16:19:32 -07:00
committed by GitHub
parent 012d279663
commit 560fe69425
3 changed files with 489 additions and 1 deletions

View File

@@ -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",
],
)

View File

@@ -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 &ethpb.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
}

View File

@@ -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] = &ethpb.Validator{
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
EffectiveBalance: params.BeaconConfig().MinDepositAmount,
}
}
state, err := stateAltair.InitializeFromProto(&ethpb.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] = &ethpb.Validator{
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
EffectiveBalance: params.BeaconConfig().MinDepositAmount,
}
}
state, err := stateAltair.InitializeFromProto(&ethpb.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] = &ethpb.Validator{
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
EffectiveBalance: params.BeaconConfig().MinDepositAmount,
PublicKey: blsKey.PublicKey().Marshal(),
}
}
state, err := stateAltair.InitializeFromProto(&ethpb.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: &ethpb.SignedContributionAndProof{},
wantErr: true,
},
{
name: "nil contribution",
s: &ethpb.SignedContributionAndProof{Message: &ethpb.ContributionAndProof{}},
wantErr: true,
},
{
name: "nil bitfield",
s: &ethpb.SignedContributionAndProof{
Message: &ethpb.ContributionAndProof{
Contribution: &ethpb.SyncCommitteeContribution{},
}},
wantErr: true,
},
{
name: "non nil sync contribution",
s: &ethpb.SignedContributionAndProof{
Message: &ethpb.ContributionAndProof{
Contribution: &ethpb.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] = &ethpb.Validator{
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
EffectiveBalance: params.BeaconConfig().MinDepositAmount,
PublicKey: blsKey.PublicKey().Marshal(),
}
}
state, err := stateAltair.InitializeFromProto(&ethpb.BeaconStateAltair{
Validators: validators,
RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector),
})
require.NoError(t, err)
return state
}