Skip to content

Commit

Permalink
Add PTC assignment support for Duty endpoint (#14032)
Browse files Browse the repository at this point in the history
  • Loading branch information
potuz committed Nov 4, 2024
1 parent ad16d4a commit 8bc3793
Show file tree
Hide file tree
Showing 8 changed files with 947 additions and 1,050 deletions.
49 changes: 49 additions & 0 deletions beacon-chain/core/helpers/beacon_committee.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ type CommitteeAssignment struct {
Committee []primitives.ValidatorIndex
AttesterSlot primitives.Slot
CommitteeIndex primitives.CommitteeIndex
PtcSlot primitives.Slot
}

// verifyAssignmentEpoch verifies if the given epoch is valid for assignment based on the provided state.
Expand Down Expand Up @@ -303,6 +304,13 @@ func CommitteeAssignments(ctx context.Context, state state.BeaconState, epoch pr
vals[v] = struct{}{}
}
assignments := make(map[primitives.ValidatorIndex]*CommitteeAssignment)

activeValidatorCount, err := ActiveValidatorCount(ctx, state, epoch)
if err != nil {
return nil, err
}
ptcPerSlot, PtcMembersPerCommittee := PtcAllocation(activeValidatorCount)

// Compute committee assignments for each slot in the epoch.
for slot := startSlot; slot < startSlot+params.BeaconConfig().SlotsPerEpoch; slot++ {
committees, err := BeaconCommittees(ctx, state, slot)
Expand All @@ -321,11 +329,52 @@ func CommitteeAssignments(ctx context.Context, state state.BeaconState, epoch pr
assignments[vIndex].AttesterSlot = slot
assignments[vIndex].CommitteeIndex = primitives.CommitteeIndex(j)
}

// We only need to assign PTC slots for the first `PTCPerSlot` committees of a given slot.
if uint64(j) < ptcPerSlot {
assignments = PTCAssignments(committee, assignments, PtcMembersPerCommittee, slot)
}
}
}
return assignments, nil
}

// PTCAssignments updates the PTC slot assignments for the given committee members.
// committee: a slice of ValidatorIndex representing committee members.
// assignments: a map of ValidatorIndex to CommitteeAssignment where assignments will be updated.
// membersPerCommittee: the number of members to be assigned to the PTC committee.
// slot: the slot to be assigned for PTC assignment.
// Returns the updated assignments map.
func PTCAssignments(committee []primitives.ValidatorIndex,
assignments map[primitives.ValidatorIndex]*CommitteeAssignment,
membersPerCommittee uint64,
slot primitives.Slot) map[primitives.ValidatorIndex]*CommitteeAssignment {
committeeLength := uint64(len(committee))
// If the number of PTC members is greater than Beacon members,
// return the current assignments without changes.
if membersPerCommittee > committeeLength {
return assignments
}

// Calculate the starting index for PTC committee.
ptcStartIndex := committeeLength - membersPerCommittee

// Loop through the selected committee members for PTC assignments.
for i := ptcStartIndex; i < committeeLength; i++ {
vIndex := committee[i]

assignment, exists := assignments[vIndex]
if !exists {
assignment = &CommitteeAssignment{}
assignments[vIndex] = assignment
}

assignment.PtcSlot = slot
}

return assignments
}

// VerifyBitfieldLength verifies that a bitfield length matches the given committee size.
func VerifyBitfieldLength(bf bitfield.Bitfield, committeeSize uint64) error {
if bf.Len() != committeeSize {
Expand Down
46 changes: 42 additions & 4 deletions beacon-chain/core/helpers/beacon_committee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package helpers_test
import (
"context"
"fmt"
"slices"
"strconv"
"testing"

"github.com/prysmaticlabs/go-bitfield"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/time"
state_native "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/state-native"
field_params "github.com/prysmaticlabs/prysm/v5/config/fieldparams"
"github.com/prysmaticlabs/prysm/v5/config/params"
"github.com/prysmaticlabs/prysm/v5/consensus-types/primitives"
"github.com/prysmaticlabs/prysm/v5/container/slice"
Expand Down Expand Up @@ -722,15 +724,26 @@ func TestCommitteeIndices(t *testing.T) {
assert.DeepEqual(t, []primitives.CommitteeIndex{0, 1, 3}, indices)
}

func TestAttestationCommittees(t *testing.T) {
validators := make([]*ethpb.Validator, params.BeaconConfig().SlotsPerEpoch.Mul(params.BeaconConfig().TargetCommitteeSize))
func TestCommitteeAssignments_PTC(t *testing.T) {
helpers.ClearCache()
// Create 10 committees. Total 40960 validators.
committeeCount := uint64(10)
validatorCount := committeeCount * params.BeaconConfig().TargetCommitteeSize * uint64(params.BeaconConfig().SlotsPerEpoch)
validators := make([]*ethpb.Validator, validatorCount)
validatorIndices := make([]primitives.ValidatorIndex, validatorCount)

for i := 0; i < len(validators); i++ {
k := make([]byte, 48)
copy(k, strconv.Itoa(i))
validators[i] = &ethpb.Validator{
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
PublicKey: k,
WithdrawalCredentials: make([]byte, 32),
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
}
validatorIndices[i] = primitives.ValidatorIndex(i)
}

state, err := state_native.InitializeFromProtoPhase0(&ethpb.BeaconState{
state, err := state_native.InitializeFromProtoEpbs(&ethpb.BeaconStateEPBS{
Validators: validators,
RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector),
})
Expand All @@ -754,6 +767,31 @@ func TestAttestationCommittees(t *testing.T) {
assert.Equal(t, params.BeaconConfig().TargetCommitteeSize, uint64(len(committees[0])))
assert.Equal(t, params.BeaconConfig().TargetCommitteeSize, uint64(len(committees[1])))
})
as, err := helpers.CommitteeAssignments(context.Background(), state, 1, validatorIndices)
require.NoError(t, err)

// Capture all the slots and all the validator index that belonged in a PTC using a map for verification later.
slotValidatorMap := make(map[primitives.Slot][]primitives.ValidatorIndex)
for i, a := range as {
slotValidatorMap[a.PtcSlot] = append(slotValidatorMap[a.PtcSlot], i)
}

// Verify that all the slots have the correct number of PTC.
for s, v := range slotValidatorMap {
if s == 0 {
continue
}
// Make sure all the PTC are the correct size from the map.
require.Equal(t, len(v), field_params.PTCSize)

// Get the actual PTC from the beacon state using the helper function
ptc, err := helpers.GetPayloadTimelinessCommittee(context.Background(), state, s)
require.NoError(t, err)
for _, index := range ptc {
i := slices.Index(v, index)
require.NotEqual(t, -1, i) // PTC not found from the assignment map
}
}
}

func TestBeaconCommittees(t *testing.T) {
Expand Down
13 changes: 11 additions & 2 deletions beacon-chain/core/helpers/payload_attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ func GetPayloadTimelinessCommittee(ctx context.Context, state state.ReadOnlyBeac
if err != nil {
return nil, errors.Wrap(err, "could not compute active validator count")
}
committeesPerSlot := math.LargestPowerOfTwo(math.Min(SlotCommitteeCount(activeCount), fieldparams.PTCSize))
membersPerCommittee := fieldparams.PTCSize / committeesPerSlot
committeesPerSlot, membersPerCommittee := PtcAllocation(activeCount)
for i := uint64(0); i < committeesPerSlot; i++ {
committee, err := BeaconCommitteeFromState(ctx, state, slot, primitives.CommitteeIndex(i))
if err != nil {
Expand All @@ -89,3 +88,13 @@ func GetPayloadTimelinessCommittee(ctx context.Context, state state.ReadOnlyBeac
}
return
}

// PtcAllocation returns:
// 1. The number of beacon committees that PTC will borrow from in a slot.
// 2. The number of validators that PTC will borrow from in a beacon committee.
func PtcAllocation(totalActive uint64) (committeesPerSlot, membersPerCommittee uint64) {
slotCommittees := SlotCommitteeCount(totalActive)
committeesPerSlot = math.LargestPowerOfTwo(math.Min(slotCommittees, fieldparams.PTCSize))
membersPerCommittee = fieldparams.PTCSize / committeesPerSlot
return
}
24 changes: 24 additions & 0 deletions beacon-chain/core/helpers/payload_attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,27 @@ func TestGetPayloadTimelinessCommittee(t *testing.T) {

require.DeepEqual(t, committee1[len(committee1)-64:], ptc[:64])
}

func Test_PtcAllocation(t *testing.T) {
tests := []struct {
totalActive uint64
memberPerCommittee uint64
committeesPerSlot uint64
}{
{64, 512, 1},
{params.BeaconConfig().MinGenesisActiveValidatorCount, 128, 4},
{25600, 128, 4},
{256000, 16, 32},
{1024000, 8, 64},
}

for _, test := range tests {
committeesPerSlot, memberPerCommittee := helpers.PtcAllocation(test.totalActive)
if memberPerCommittee != test.memberPerCommittee {
t.Errorf("memberPerCommittee(%d) = %d; expected %d", test.totalActive, memberPerCommittee, test.memberPerCommittee)
}
if committeesPerSlot != test.committeesPerSlot {
t.Errorf("committeesPerSlot(%d) = %d; expected %d", test.totalActive, committeesPerSlot, test.committeesPerSlot)
}
}
}
2 changes: 2 additions & 0 deletions beacon-chain/rpc/prysm/v1alpha1/validator/duties.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,15 @@ func (vs *Server) duties(ctx context.Context, req *ethpb.DutiesRequest) (*ethpb.
assignment.Committee = ca.Committee
assignment.AttesterSlot = ca.AttesterSlot
assignment.CommitteeIndex = ca.CommitteeIndex
assignment.PtcSlot = ca.PtcSlot
}
// Save the next epoch assignments.
ca, ok = nextEpochAssignments[idx]
if ok {
nextAssignment.Committee = ca.Committee
nextAssignment.AttesterSlot = ca.AttesterSlot
nextAssignment.CommitteeIndex = ca.CommitteeIndex
nextAssignment.PtcSlot = ca.PtcSlot
}
} else {
// If the validator isn't in the beacon state, try finding their deposit to determine their status.
Expand Down
Loading

0 comments on commit 8bc3793

Please sign in to comment.