mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-09 14:07:56 -05:00
425 lines
15 KiB
Go
425 lines
15 KiB
Go
// Copyright © 2022 Weald Technology Trading.
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package validatorsummary
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
eth2client "github.com/attestantio/go-eth2-client"
|
|
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
|
"github.com/attestantio/go-eth2-client/spec/phase0"
|
|
"github.com/pkg/errors"
|
|
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
|
"github.com/wealdtech/ethdo/util"
|
|
)
|
|
|
|
func (c *command) process(ctx context.Context) error {
|
|
// Obtain information we need to process.
|
|
err := c.setup(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.summary.Epoch, err = util.ParseEpoch(ctx, c.chainTime, c.epoch)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to parse epoch")
|
|
}
|
|
c.summary.FirstSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)
|
|
c.summary.LastSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) - 1
|
|
c.summary.Slots = make([]*slot, 1+int(c.summary.LastSlot)-int(c.summary.FirstSlot))
|
|
for i := range c.summary.Slots {
|
|
c.summary.Slots[i] = &slot{
|
|
Slot: c.summary.FirstSlot + phase0.Slot(i),
|
|
}
|
|
}
|
|
|
|
c.summary.Validators, err = util.ParseValidators(ctx, c.validatorsProvider, c.validators, fmt.Sprintf("%d", c.summary.FirstSlot))
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to parse validators")
|
|
}
|
|
// Reorder validators by index.
|
|
sort.Slice(c.summary.Validators, func(i int, j int) bool {
|
|
return c.summary.Validators[i].Index < c.summary.Validators[j].Index
|
|
})
|
|
|
|
// Create a map for validator indices for easy lookup.
|
|
c.validatorsByIndex = make(map[phase0.ValidatorIndex]*apiv1.Validator)
|
|
for _, validator := range c.summary.Validators {
|
|
c.validatorsByIndex[validator.Index] = validator
|
|
}
|
|
|
|
if err := c.processProposerDuties(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.processAttesterDuties(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
// if err := c.processSyncCommitteeDuties(ctx); err != nil {
|
|
// return err
|
|
// }
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *command) processProposerDuties(ctx context.Context) error {
|
|
duties, err := c.proposerDutiesProvider.ProposerDuties(ctx, c.summary.Epoch, nil)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to obtain proposer duties")
|
|
}
|
|
if duties == nil {
|
|
return errors.New("empty proposer duties")
|
|
}
|
|
for _, duty := range duties {
|
|
if _, exists := c.validatorsByIndex[duty.ValidatorIndex]; !exists {
|
|
continue
|
|
}
|
|
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", duty.Slot))
|
|
if err != nil {
|
|
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", duty.Slot))
|
|
}
|
|
present := block != nil
|
|
c.summary.Proposals = append(c.summary.Proposals, &epochProposal{
|
|
Slot: duty.Slot,
|
|
Proposer: duty.ValidatorIndex,
|
|
Block: present,
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *command) activeValidators() (map[phase0.ValidatorIndex]*apiv1.Validator, []phase0.ValidatorIndex) {
|
|
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
|
|
activeValidatorIndices := make([]phase0.ValidatorIndex, 0, len(c.validatorsByIndex))
|
|
for _, validator := range c.summary.Validators {
|
|
if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch {
|
|
activeValidators[validator.Index] = validator
|
|
activeValidatorIndices = append(activeValidatorIndices, validator.Index)
|
|
}
|
|
}
|
|
|
|
return activeValidators, activeValidatorIndices
|
|
}
|
|
|
|
func (c *command) processAttesterDuties(ctx context.Context) error {
|
|
activeValidators, activeValidatorIndices := c.activeValidators()
|
|
|
|
// Obtain number of validators that voted for blocks in the epoch.
|
|
// These votes can be included anywhere from the second slot of
|
|
// the epoch to the first slot of the next-but-one epoch.
|
|
firstSlot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) + 1
|
|
lastSlot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch + 2)
|
|
if lastSlot > c.chainTime.CurrentSlot() {
|
|
lastSlot = c.chainTime.CurrentSlot()
|
|
}
|
|
|
|
// Obtain the duties for the validators to know where they should be attesting.
|
|
duties, err := c.attesterDutiesProvider.AttesterDuties(ctx, c.summary.Epoch, activeValidatorIndices)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to obtain attester duties")
|
|
}
|
|
for slot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch); slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1); slot++ {
|
|
index := int(slot - c.chainTime.FirstSlotOfEpoch(c.summary.Epoch))
|
|
c.summary.Slots[index].Attestations = &slotAttestations{}
|
|
}
|
|
|
|
// Need a cache of beacon block headers to reduce lookup times.
|
|
headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider)
|
|
|
|
// Need a map of duties to easily find the attestations we care about.
|
|
dutiesBySlot := make(map[phase0.Slot]map[phase0.CommitteeIndex][]*apiv1.AttesterDuty)
|
|
dutiesByValidatorIndex := make(map[phase0.ValidatorIndex]*apiv1.AttesterDuty)
|
|
for _, duty := range duties {
|
|
index := int(duty.Slot - c.chainTime.FirstSlotOfEpoch(c.summary.Epoch))
|
|
dutiesByValidatorIndex[duty.ValidatorIndex] = duty
|
|
c.summary.Slots[index].Attestations.Expected++
|
|
if _, exists := dutiesBySlot[duty.Slot]; !exists {
|
|
dutiesBySlot[duty.Slot] = make(map[phase0.CommitteeIndex][]*apiv1.AttesterDuty)
|
|
}
|
|
if _, exists := dutiesBySlot[duty.Slot][duty.CommitteeIndex]; !exists {
|
|
dutiesBySlot[duty.Slot][duty.CommitteeIndex] = make([]*apiv1.AttesterDuty, 0)
|
|
}
|
|
dutiesBySlot[duty.Slot][duty.CommitteeIndex] = append(dutiesBySlot[duty.Slot][duty.CommitteeIndex], duty)
|
|
}
|
|
|
|
c.summary.IncorrectHeadValidators = make([]*validatorFault, 0)
|
|
c.summary.UntimelyHeadValidators = make([]*validatorFault, 0)
|
|
c.summary.UntimelySourceValidators = make([]*validatorFault, 0)
|
|
c.summary.IncorrectTargetValidators = make([]*validatorFault, 0)
|
|
c.summary.UntimelyTargetValidators = make([]*validatorFault, 0)
|
|
|
|
// Hunt through the blocks looking for attestations from the validators.
|
|
votes := make(map[phase0.ValidatorIndex]struct{})
|
|
for slot := firstSlot; slot <= lastSlot; slot++ {
|
|
if err := c.processAttesterDutiesSlot(ctx, slot, dutiesBySlot, votes, headersCache, activeValidatorIndices); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Use dutiesMap and votes to work out which validators didn't participate.
|
|
c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0)
|
|
for _, index := range activeValidatorIndices {
|
|
if _, exists := votes[index]; !exists {
|
|
// Didn't vote.
|
|
duty := dutiesByValidatorIndex[index]
|
|
c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, &nonParticipatingValidator{
|
|
Validator: index,
|
|
Slot: duty.Slot,
|
|
Committee: duty.CommitteeIndex,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sort the non-participating validators list.
|
|
sort.Slice(c.summary.NonParticipatingValidators, func(i int, j int) bool {
|
|
if c.summary.NonParticipatingValidators[i].Slot != c.summary.NonParticipatingValidators[j].Slot {
|
|
return c.summary.NonParticipatingValidators[i].Slot < c.summary.NonParticipatingValidators[j].Slot
|
|
}
|
|
if c.summary.NonParticipatingValidators[i].Committee != c.summary.NonParticipatingValidators[j].Committee {
|
|
return c.summary.NonParticipatingValidators[i].Committee < c.summary.NonParticipatingValidators[j].Committee
|
|
}
|
|
return c.summary.NonParticipatingValidators[i].Validator < c.summary.NonParticipatingValidators[j].Validator
|
|
})
|
|
|
|
c.summary.ActiveValidators = len(activeValidators)
|
|
c.summary.ParticipatingValidators = len(votes)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *command) processAttesterDutiesSlot(ctx context.Context,
|
|
slot phase0.Slot,
|
|
dutiesBySlot map[phase0.Slot]map[phase0.CommitteeIndex][]*apiv1.AttesterDuty,
|
|
votes map[phase0.ValidatorIndex]struct{},
|
|
headersCache *util.BeaconBlockHeaderCache,
|
|
activeValidatorIndices []phase0.ValidatorIndex,
|
|
) error {
|
|
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
|
if err != nil {
|
|
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
|
|
}
|
|
if block == nil {
|
|
// No block at this slot; that's fine.
|
|
return nil
|
|
}
|
|
attestations, err := block.Attestations()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, attestation := range attestations {
|
|
if _, exists := dutiesBySlot[attestation.Data.Slot]; !exists {
|
|
// We do not have any attestations for this slot.
|
|
continue
|
|
}
|
|
if _, exists := dutiesBySlot[attestation.Data.Slot][attestation.Data.Index]; !exists {
|
|
// We do not have any attestations for this committee.
|
|
continue
|
|
}
|
|
for _, duty := range dutiesBySlot[attestation.Data.Slot][attestation.Data.Index] {
|
|
if attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
|
|
// Found it.
|
|
if _, exists := votes[duty.ValidatorIndex]; exists {
|
|
// Duplicate; ignore.
|
|
continue
|
|
}
|
|
votes[duty.ValidatorIndex] = struct{}{}
|
|
|
|
// Update the metrics for the attestation.
|
|
index := int(attestation.Data.Slot - c.chainTime.FirstSlotOfEpoch(c.summary.Epoch))
|
|
c.summary.Slots[index].Attestations.Included++
|
|
inclusionDelay := slot - duty.Slot
|
|
|
|
fault := &validatorFault{
|
|
Validator: duty.ValidatorIndex,
|
|
AttestationData: attestation.Data,
|
|
InclusionDistance: int(inclusionDelay),
|
|
}
|
|
|
|
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to calculate if attestation had correct head vote")
|
|
}
|
|
if headCorrect {
|
|
c.summary.Slots[index].Attestations.CorrectHead++
|
|
if inclusionDelay == 1 {
|
|
c.summary.Slots[index].Attestations.TimelyHead++
|
|
} else {
|
|
c.summary.UntimelyHeadValidators = append(c.summary.UntimelyHeadValidators, fault)
|
|
}
|
|
} else {
|
|
c.summary.IncorrectHeadValidators = append(c.summary.IncorrectHeadValidators, fault)
|
|
if inclusionDelay > 1 {
|
|
c.summary.UntimelyHeadValidators = append(c.summary.UntimelyHeadValidators, fault)
|
|
}
|
|
}
|
|
|
|
if inclusionDelay <= 5 {
|
|
c.summary.Slots[index].Attestations.TimelySource++
|
|
} else {
|
|
c.summary.UntimelySourceValidators = append(c.summary.UntimelySourceValidators, fault)
|
|
}
|
|
|
|
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to calculate if attestation had correct target vote")
|
|
}
|
|
if targetCorrect {
|
|
c.summary.Slots[index].Attestations.CorrectTarget++
|
|
if inclusionDelay <= 32 {
|
|
c.summary.Slots[index].Attestations.TimelyTarget++
|
|
} else {
|
|
c.summary.UntimelyTargetValidators = append(c.summary.UntimelyTargetValidators, fault)
|
|
}
|
|
} else {
|
|
c.summary.IncorrectTargetValidators = append(c.summary.IncorrectTargetValidators, fault)
|
|
if inclusionDelay > 32 {
|
|
c.summary.UntimelyTargetValidators = append(c.summary.UntimelyTargetValidators, fault)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(votes) == len(activeValidatorIndices) {
|
|
// Found them all.
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
|
|
// if c.summary.Epoch < c.chainTime.AltairInitialEpoch() {
|
|
// // The epoch is pre-Altair. No info but no error.
|
|
// return nil
|
|
// }
|
|
//
|
|
// committee, err := c.syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", c.summary.FirstSlot))
|
|
// if err != nil {
|
|
// return errors.Wrap(err, "failed to obtain sync committee")
|
|
// }
|
|
// if len(committee.Validators) == 0 {
|
|
// return errors.Wrap(err, "empty sync committee")
|
|
// }
|
|
//
|
|
// missed := make(map[phase0.ValidatorIndex]int)
|
|
// for _, index := range committee.Validators {
|
|
// missed[index] = 0
|
|
// }
|
|
//
|
|
// for slot := c.summary.FirstSlot; slot <= c.summary.LastSlot; slot++ {
|
|
// block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
|
// if err != nil {
|
|
// return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
|
|
// }
|
|
// if block == nil {
|
|
// // If the block is missed we don't count the sync aggregate miss.
|
|
// continue
|
|
// }
|
|
// var aggregate *altair.SyncAggregate
|
|
// switch block.Version {
|
|
// case spec.DataVersionPhase0:
|
|
// // No sync committees in this fork.
|
|
// return nil
|
|
// case spec.DataVersionAltair:
|
|
// aggregate = block.Altair.Message.Body.SyncAggregate
|
|
// case spec.DataVersionBellatrix:
|
|
// aggregate = block.Bellatrix.Message.Body.SyncAggregate
|
|
// default:
|
|
// return fmt.Errorf("unhandled block version %v", block.Version)
|
|
// }
|
|
// for i := uint64(0); i < aggregate.SyncCommitteeBits.Len(); i++ {
|
|
// if !aggregate.SyncCommitteeBits.BitAt(i) {
|
|
// missed[committee.Validators[int(i)]]++
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// c.summary.SyncCommittee = make([]*epochSyncCommittee, 0, len(missed))
|
|
// for index, count := range missed {
|
|
// if count > 0 {
|
|
// c.summary.SyncCommittee = append(c.summary.SyncCommittee, &epochSyncCommittee{
|
|
// Index: index,
|
|
// Missed: count,
|
|
// })
|
|
// }
|
|
// }
|
|
//
|
|
// sort.Slice(c.summary.SyncCommittee, func(i int, j int) bool {
|
|
// missedDiff := c.summary.SyncCommittee[i].Missed - c.summary.SyncCommittee[j].Missed
|
|
// if missedDiff != 0 {
|
|
// // Actually want to order by missed descending, so invert the expected condition.
|
|
// return missedDiff > 0
|
|
// }
|
|
// // Then order by validator index.
|
|
// return c.summary.SyncCommittee[i].Index < c.summary.SyncCommittee[j].Index
|
|
// })
|
|
//
|
|
// return nil
|
|
// }
|
|
|
|
func (c *command) setup(ctx context.Context) error {
|
|
var err error
|
|
|
|
// Connect to the client.
|
|
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to connect to beacon node")
|
|
}
|
|
|
|
c.chainTime, err = standardchaintime.New(ctx,
|
|
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
|
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
|
)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to set up chaintime service")
|
|
}
|
|
|
|
var isProvider bool
|
|
c.proposerDutiesProvider, isProvider = c.eth2Client.(eth2client.ProposerDutiesProvider)
|
|
if !isProvider {
|
|
return errors.New("connection does not provide proposer duties")
|
|
}
|
|
c.attesterDutiesProvider, isProvider = c.eth2Client.(eth2client.AttesterDutiesProvider)
|
|
if !isProvider {
|
|
return errors.New("connection does not provide attester duties")
|
|
}
|
|
c.blocksProvider, isProvider = c.eth2Client.(eth2client.SignedBeaconBlockProvider)
|
|
if !isProvider {
|
|
return errors.New("connection does not provide signed beacon blocks")
|
|
}
|
|
c.syncCommitteesProvider, isProvider = c.eth2Client.(eth2client.SyncCommitteesProvider)
|
|
if !isProvider {
|
|
return errors.New("connection does not provide sync committee duties")
|
|
}
|
|
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
|
|
if !isProvider {
|
|
return errors.New("connection does not provide validators")
|
|
}
|
|
c.beaconCommitteesProvider, isProvider = c.eth2Client.(eth2client.BeaconCommitteesProvider)
|
|
if !isProvider {
|
|
return errors.New("connection does not provide beacon committees")
|
|
}
|
|
c.beaconBlockHeadersProvider, isProvider = c.eth2Client.(eth2client.BeaconBlockHeadersProvider)
|
|
if !isProvider {
|
|
return errors.New("connection does not provide beacon block headers")
|
|
}
|
|
|
|
return nil
|
|
}
|