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

Multiple Proposer Slots Allowed Per Epoch for Validators #5344

Merged
merged 11 commits into from
Apr 8, 2020
2 changes: 1 addition & 1 deletion WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -1309,7 +1309,7 @@ go_repository(

go_repository(
name = "com_github_prysmaticlabs_ethereumapis",
commit = "59479f4a647fcec5d8dbf7c50435cc10fb5751fc",
commit = "62699dc897a9f15fb935ad9e63d553a2c12434d6",
importpath = "github.com/prysmaticlabs/ethereumapis",
patch_args = ["-p1"],
patches = [
Expand Down
95 changes: 12 additions & 83 deletions beacon-chain/core/helpers/committee.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (

var committeeCache = cache.NewCommitteesCache()

type proposerIndexToSlots map[uint64][]uint64
rauljordan marked this conversation as resolved.
Show resolved Hide resolved

rauljordan marked this conversation as resolved.
Show resolved Hide resolved
// SlotCommitteeCount returns the number of crosslink committees of a slot. The
// active validator count is provided as an argument rather than a direct implementation
// from the spec definition. Having the active validator count as an argument allows for
Expand Down Expand Up @@ -181,7 +183,10 @@ type CommitteeAssignmentContainer struct {
// 2. Compute all committees.
// 3. Determine the attesting slot for each committee.
// 4. Construct a map of validator indices pointing to the respective committees.
func CommitteeAssignments(state *stateTrie.BeaconState, epoch uint64) (map[uint64]*CommitteeAssignmentContainer, map[uint64]uint64, error) {
func CommitteeAssignments(
state *stateTrie.BeaconState,
epoch uint64,
) (map[uint64]*CommitteeAssignmentContainer, map[uint64][]uint64, error) {
nextEpoch := NextEpoch(state)
if epoch > nextEpoch {
return nil, nil, fmt.Errorf(
Expand All @@ -191,9 +196,11 @@ func CommitteeAssignments(state *stateTrie.BeaconState, epoch uint64) (map[uint6
)
}

// Track which slot has which proposer.
// We determine the slots in which proposers are supposed to act.
// Some validators may need to propose multiple times per epoch, so
// we use a map of proposer idx -> []slot to keep track of this possibility.
startSlot := StartSlot(epoch)
proposerIndexToSlot := make(map[uint64]uint64)
proposerIndexToSlots := make(map[uint64][]uint64)
for slot := startSlot; slot < startSlot+params.BeaconConfig().SlotsPerEpoch; slot++ {
if err := state.SetSlot(slot); err != nil {
return nil, nil, err
Expand All @@ -202,7 +209,7 @@ func CommitteeAssignments(state *stateTrie.BeaconState, epoch uint64) (map[uint6
if err != nil {
return nil, nil, errors.Wrapf(err, "could not check proposer at slot %d", state.Slot())
}
proposerIndexToSlot[i] = slot
proposerIndexToSlots[i] = append(proposerIndexToSlots[i], slot)
}

activeValidatorIndices, err := ActiveValidatorIndices(state, epoch)
Expand Down Expand Up @@ -235,85 +242,7 @@ func CommitteeAssignments(state *stateTrie.BeaconState, epoch uint64) (map[uint6
}
}

return validatorIndexToCommittee, proposerIndexToSlot, nil
}

// CommitteeAssignment is used to query committee assignment from
// current and previous epoch.
//
// Deprecated: Consider using CommitteeAssignments, especially when computing more than one
// validator assignment as this method is O(n^2) in computational complexity. This method exists to
// ensure spec definition conformance and otherwise should probably not be used.
//
// Spec pseudocode definition:
// def get_committee_assignment(state: BeaconState,
// epoch: Epoch,
// validator_index: ValidatorIndex
// ) -> Optional[Tuple[Sequence[ValidatorIndex], CommitteeIndex, Slot]]:
// """
// Return the committee assignment in the ``epoch`` for ``validator_index``.
// ``assignment`` returned is a tuple of the following form:
// * ``assignment[0]`` is the list of validators in the committee
// * ``assignment[1]`` is the index to which the committee is assigned
// * ``assignment[2]`` is the slot at which the committee is assigned
// Return None if no assignment.
// """
// next_epoch = get_current_epoch(state) + 1
// assert epoch <= next_epoch
//
// start_slot = compute_start_slot_at_epoch(epoch)
// for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH):
// for index in range(get_committee_count_at_slot(state, Slot(slot))):
// committee = get_beacon_committee(state, Slot(slot), CommitteeIndex(index))
// if validator_index in committee:
// return committee, CommitteeIndex(index), Slot(slot)
// return None
func CommitteeAssignment(
Copy link
Member

Choose a reason for hiding this comment

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

why remove this ? dont we need it for a single validator

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deprecated since several months, it isn't used anywhere in the codebase

state *stateTrie.BeaconState,
epoch uint64,
validatorIndex uint64,
) ([]uint64, uint64, uint64, uint64, error) {
nextEpoch := NextEpoch(state)
if epoch > nextEpoch {
return nil, 0, 0, 0, fmt.Errorf(
"epoch %d can't be greater than next epoch %d",
epoch, nextEpoch)
}

// Track which slot has which proposer.
startSlot := StartSlot(epoch)
proposerIndexToSlot := make(map[uint64]uint64)
for slot := startSlot; slot < startSlot+params.BeaconConfig().SlotsPerEpoch; slot++ {
if err := state.SetSlot(slot); err != nil {
return nil, 0, 0, 0, err
}
i, err := BeaconProposerIndex(state)
if err != nil {
return nil, 0, 0, 0, errors.Wrapf(err, "could not check proposer at slot %d", state.Slot())
}
proposerIndexToSlot[i] = slot
}

activeValidatorIndices, err := ActiveValidatorIndices(state, epoch)
if err != nil {
return nil, 0, 0, 0, err
}
for slot := startSlot; slot < startSlot+params.BeaconConfig().SlotsPerEpoch; slot++ {
countAtSlot := SlotCommitteeCount(uint64(len(activeValidatorIndices)))
for i := uint64(0); i < countAtSlot; i++ {
committee, err := BeaconCommitteeFromState(state, slot, i)
if err != nil {
return nil, 0, 0, 0, errors.Wrapf(err, "could not get crosslink committee at slot %d", slot)
}
for _, v := range committee {
if validatorIndex == v {
proposerSlot, _ := proposerIndexToSlot[v]
return committee, i, slot, proposerSlot, nil
}
}
}
}
return []uint64{}, 0, 0, 0, fmt.Errorf("validator with index %d not found in assignments", validatorIndex)
return validatorIndexToCommittee, proposerIndexToSlots, nil
}

// VerifyBitfieldLength verifies that a bitfield length matches the given committee size.
Expand Down
148 changes: 3 additions & 145 deletions beacon-chain/core/helpers/committee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import (
"fmt"
"reflect"
"strconv"
"strings"
"testing"

ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/prysmaticlabs/go-bitfield"
beaconstate "github.com/prysmaticlabs/prysm/beacon-chain/state"
pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1"

rauljordan marked this conversation as resolved.
Show resolved Hide resolved
beaconstate "github.com/prysmaticlabs/prysm/beacon-chain/state"
"github.com/prysmaticlabs/prysm/shared/attestationutil"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/featureconfig"
Expand Down Expand Up @@ -191,148 +191,6 @@ func TestVerifyBitfieldLength_OK(t *testing.T) {
}
}

func TestCommitteeAssignment_CanRetrieve(t *testing.T) {
ClearCache()
// Initialize test with 128 validators, each slot and each index gets 2 validators.
validators := make([]*ethpb.Validator, 2*params.BeaconConfig().SlotsPerEpoch)
for i := 0; i < len(validators); i++ {
validators[i] = &ethpb.Validator{
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
}
}
state, _ := beaconstate.InitializeFromProto(&pb.BeaconState{
Validators: validators,
Slot: params.BeaconConfig().SlotsPerEpoch,
RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector),
})

tests := []struct {
index uint64
slot uint64
committee []uint64
committeeIndex uint64
isProposer bool
proposerSlot uint64
}{
{
index: 0,
slot: 78,
committee: []uint64{0, 38},
committeeIndex: 0,
isProposer: false,
},
{
index: 1,
slot: 71,
committee: []uint64{1, 4},
committeeIndex: 0,
isProposer: true,
proposerSlot: 79,
},
{
index: 11,
slot: 90,
committee: []uint64{31, 11},
committeeIndex: 0,
isProposer: false,
},
}

for i, tt := range tests {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
committee, committeeIndex, slot, proposerSlot, err := CommitteeAssignment(state, tt.slot/params.BeaconConfig().SlotsPerEpoch, tt.index)
if err != nil {
t.Fatalf("failed to execute NextEpochCommitteeAssignment: %v", err)
}
if committeeIndex != tt.committeeIndex {
t.Errorf("wanted committeeIndex %d, got committeeIndex %d for validator index %d",
tt.committeeIndex, committeeIndex, tt.index)
}
if slot != tt.slot {
t.Errorf("wanted slot %d, got slot %d for validator index %d",
tt.slot, slot, tt.index)
}
if proposerSlot != tt.proposerSlot {
t.Errorf("wanted proposer slot %d, got proposer slot %d for validator index %d",
tt.proposerSlot, proposerSlot, tt.index)
}
if !reflect.DeepEqual(committee, tt.committee) {
t.Errorf("wanted committee %v, got committee %v for validator index %d",
tt.committee, committee, tt.index)
}
if proposerSlot != tt.proposerSlot {
t.Errorf("wanted proposer slot slot %d, got slot %d for validator index %d",
tt.slot, slot, tt.index)
}
})
}
}

func TestCommitteeAssignment_CantFindValidator(t *testing.T) {
ClearCache()
validators := make([]*ethpb.Validator, 1)
for i := 0; i < len(validators); i++ {
validators[i] = &ethpb.Validator{
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
}
}
state, _ := beaconstate.InitializeFromProto(&pb.BeaconState{
Validators: validators,
Slot: params.BeaconConfig().SlotsPerEpoch,
RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector),
})

index := uint64(10000)
_, _, _, _, err := CommitteeAssignment(state, 1, index)
if err != nil && !strings.Contains(err.Error(), "not found in assignments") {
t.Errorf("Wanted 'not found in assignments', received %v", err)
}
}

// Test helpers.CommitteeAssignments against the results of helpers.CommitteeAssignment by validator
// index. Warning: this test is a bit slow!
func TestCommitteeAssignments_AgreesWithSpecDefinitionMethod(t *testing.T) {
ClearCache()
// Initialize test with 256 validators, each slot and each index gets 4 validators.
validators := make([]*ethpb.Validator, 4*params.BeaconConfig().SlotsPerEpoch)
for i := 0; i < len(validators); i++ {
validators[i] = &ethpb.Validator{
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
}
}
state, _ := beaconstate.InitializeFromProto(&pb.BeaconState{
Validators: validators,
Slot: params.BeaconConfig().SlotsPerEpoch,
RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector),
})
// Test for 2 epochs.
for epoch := uint64(0); epoch < 2; epoch++ {
state, _ := beaconstate.InitializeFromProto(state.CloneInnerState())
assignments, proposers, err := CommitteeAssignments(state, epoch)
if err != nil {
t.Fatal(err)
}
for i := uint64(0); int(i) < len(validators); i++ {
committee, committeeIndex, slot, proposerSlot, err := CommitteeAssignment(state, epoch, i)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(committee, assignments[i].Committee) {
t.Errorf("Computed different committees for validator %d", i)
}
if committeeIndex != assignments[i].CommitteeIndex {
t.Errorf("Computed different committee index for validator %d", i)
}
if slot != assignments[i].AttesterSlot {
t.Errorf("Computed different attesting slot for validator %d", i)
}
if proposerSlot != proposers[i] {
t.Errorf("Computed different proposing slot for validator %d", i)
}
}
}
}

func TestCommitteeAssignments_CanRetrieve(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

can this test be run with more slots per epoch so we could have a validator that gets more then one slot in an epoch to propose for?

// Initialize test with 256 validators, each slot and each index gets 4 validators.
validators := make([]*ethpb.Validator, 4*params.BeaconConfig().SlotsPerEpoch)
Expand Down Expand Up @@ -408,7 +266,7 @@ func TestCommitteeAssignments_CanRetrieve(t *testing.T) {
t.Errorf("wanted slot %d, got slot %d for validator index %d",
tt.slot, cac.AttesterSlot, tt.index)
}
if proposerIndexToSlot[tt.index] != tt.proposerSlot {
if proposerIndexToSlot[tt.index][0] != tt.proposerSlot {
t.Errorf("wanted proposer slot %d, got proposer slot %d for validator index %d",
tt.proposerSlot, proposerIndexToSlot[tt.index], tt.index)
}
Expand Down
14 changes: 7 additions & 7 deletions beacon-chain/rpc/beacon/assignments.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ func (bs *Server) ListValidatorAssignments(

// initialize all committee related data.
committeeAssignments := map[uint64]*helpers.CommitteeAssignmentContainer{}
proposerIndexToSlot := map[uint64]uint64{}
proposerIndexToSlots := make(map[uint64][]uint64)
archivedInfo := &pb.ArchivedCommitteeInfo{}
archivedBalances := []uint64{}
archivedBalances := make([]uint64, 0)
archivedAssignments := make(map[uint64]*ethpb.ValidatorAssignments_CommitteeAssignment)

if shouldFetchFromArchive {
Expand All @@ -123,7 +123,7 @@ func (bs *Server) ListValidatorAssignments(
return nil, status.Errorf(codes.Internal, "Could not retrieve archived assignment for epoch %d: %v", requestedEpoch, err)
}
} else {
committeeAssignments, proposerIndexToSlot, err = helpers.CommitteeAssignments(headState, requestedEpoch)
committeeAssignments, proposerIndexToSlots, err = helpers.CommitteeAssignments(headState, requestedEpoch)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not compute committee assignments: %v", err)
}
Expand All @@ -150,7 +150,7 @@ func (bs *Server) ListValidatorAssignments(
BeaconCommittees: comAssignment.Committee,
CommitteeIndex: comAssignment.CommitteeIndex,
AttesterSlot: comAssignment.AttesterSlot,
ProposerSlot: proposerIndexToSlot[index],
ProposerSlots: proposerIndexToSlots[index],
PublicKey: pubkey[:],
}
res = append(res, assign)
Expand All @@ -176,7 +176,7 @@ func archivedValidatorCommittee(
attesterSeed := bytesutil.ToBytes32(archivedInfo.AttesterSeed)

startSlot := helpers.StartSlot(epoch)
proposerIndexToSlot := make(map[uint64]uint64)
proposerIndexToSlots := make(map[uint64][]uint64)
activeVals := make([]*ethpb.Validator, len(archivedBalances))
for i, bal := range archivedBalances {
activeVals[i] = &ethpb.Validator{EffectiveBalance: bal}
Expand All @@ -189,7 +189,7 @@ func archivedValidatorCommittee(
if err != nil {
return nil, errors.Wrapf(err, "could not check proposer at slot %d", slot)
}
proposerIndexToSlot[i] = slot
proposerIndexToSlots[i] = append(proposerIndexToSlots[i], slot)
}

assignmentMap := make(map[uint64]*ethpb.ValidatorAssignments_CommitteeAssignment)
Expand All @@ -211,7 +211,7 @@ func archivedValidatorCommittee(
BeaconCommittees: committee,
CommitteeIndex: i,
AttesterSlot: slot,
ProposerSlot: proposerIndexToSlot[index],
ProposerSlots: proposerIndexToSlots[index],
}
}
}
Expand Down
Loading