Add more information to "epoch summary".

This commit is contained in:
Jim McDonald
2022-10-01 22:52:20 +01:00
parent 290413f115
commit fe0bfd4f87
7 changed files with 438 additions and 63 deletions

View File

@@ -1,3 +1,6 @@
dev:
- add more information to "epoch summary"
1.25.2: 1.25.2:
- no longer require connection parameter - no longer require connection parameter
- support "block analyze" on bellatrix (thanks @tcrossland) - support "block analyze" on bellatrix (thanks @tcrossland)

View File

@@ -40,13 +40,14 @@ type command struct {
jsonOutput bool jsonOutput bool
// Data access. // Data access.
eth2Client eth2client.Service eth2Client eth2client.Service
chainTime chaintime.Service chainTime chaintime.Service
proposerDutiesProvider eth2client.ProposerDutiesProvider proposerDutiesProvider eth2client.ProposerDutiesProvider
blocksProvider eth2client.SignedBeaconBlockProvider blocksProvider eth2client.SignedBeaconBlockProvider
syncCommitteesProvider eth2client.SyncCommitteesProvider syncCommitteesProvider eth2client.SyncCommitteesProvider
validatorsProvider eth2client.ValidatorsProvider validatorsProvider eth2client.ValidatorsProvider
beaconCommitteesProvider eth2client.BeaconCommitteesProvider beaconCommitteesProvider eth2client.BeaconCommitteesProvider
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Results. // Results.
summary *epochSummary summary *epochSummary
@@ -60,6 +61,11 @@ type epochSummary struct {
SyncCommittee []*epochSyncCommittee `json:"sync_committees"` SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
ActiveValidators int `json:"active_validators"` ActiveValidators int `json:"active_validators"`
ParticipatingValidators int `json:"participating_validators"` ParticipatingValidators int `json:"participating_validators"`
HeadCorrectValidators int `json:"head_correct_validators"`
HeadTimelyValidators int `json:"head_timely_validators"`
SourceTimelyValidators int `json:"source_timely_validators"`
TargetCorrectValidators int `json:"target_correct_validators"`
TargetTimelyValidators int `json:"target_timely_validators"`
NonParticipatingValidators []*nonParticipatingValidator `json:"nonparticipating_validators"` NonParticipatingValidators []*nonParticipatingValidator `json:"nonparticipating_validators"`
} }

View File

@@ -70,6 +70,11 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
} }
builder.WriteString(fmt.Sprintf("\n Attestations: %d/%d (%0.2f%%)", c.summary.ParticipatingValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.ParticipatingValidators)/float64(c.summary.ActiveValidators))) builder.WriteString(fmt.Sprintf("\n Attestations: %d/%d (%0.2f%%)", c.summary.ParticipatingValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.ParticipatingValidators)/float64(c.summary.ActiveValidators)))
builder.WriteString(fmt.Sprintf("\n Source timely: %d/%d (%0.2f%%)", c.summary.SourceTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.SourceTimelyValidators)/float64(c.summary.ActiveValidators)))
builder.WriteString(fmt.Sprintf("\n Target correct: %d/%d (%0.2f%%)", c.summary.TargetCorrectValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.TargetCorrectValidators)/float64(c.summary.ActiveValidators)))
builder.WriteString(fmt.Sprintf("\n Target timely: %d/%d (%0.2f%%)", c.summary.TargetTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.TargetTimelyValidators)/float64(c.summary.ActiveValidators)))
builder.WriteString(fmt.Sprintf("\n Head correct: %d/%d (%0.2f%%)", c.summary.HeadCorrectValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.HeadCorrectValidators)/float64(c.summary.ActiveValidators)))
builder.WriteString(fmt.Sprintf("\n Head timely: %d/%d (%0.2f%%)", c.summary.HeadTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.HeadTimelyValidators)/float64(c.summary.ActiveValidators)))
if c.verbose { if c.verbose {
// Sort list by validator index. // Sort list by validator index.
for _, validator := range c.summary.NonParticipatingValidators { for _, validator := range c.summary.NonParticipatingValidators {

View File

@@ -79,11 +79,10 @@ func (c *command) processProposerDuties(ctx context.Context) error {
return nil return nil
} }
func (c *command) processAttesterDuties(ctx context.Context) error { func (c *command) activeValidators(ctx context.Context) (map[phase0.ValidatorIndex]*apiv1.Validator, error) {
// Obtain all active validators for the given epoch.
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), nil) validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), nil)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to obtain validators for epoch") return nil, errors.Wrap(err, "failed to obtain validators for epoch")
} }
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator) activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
for _, validator := range validators { for _, validator := range validators {
@@ -92,6 +91,15 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
} }
} }
return activeValidators, nil
}
func (c *command) processAttesterDuties(ctx context.Context) error {
activeValidators, err := c.activeValidators(ctx)
if err != nil {
return err
}
c.summary.ActiveValidators = len(activeValidators)
// Obtain number of validators that voted for blocks in the epoch. // Obtain number of validators that voted for blocks in the epoch.
// These votes can be included anywhere from the second slot of // These votes can be included anywhere from the second slot of
// the epoch to the first slot of the next-but-one epoch. // the epoch to the first slot of the next-but-one epoch.
@@ -101,61 +109,13 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
lastSlot = c.chainTime.CurrentSlot() lastSlot = c.chainTime.CurrentSlot()
} }
votes := make(map[phase0.ValidatorIndex]struct{}) var votes map[phase0.ValidatorIndex]struct{}
allCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex) var participations map[phase0.ValidatorIndex]*nonParticipatingValidator
participations := make(map[phase0.ValidatorIndex]*nonParticipatingValidator) c.summary.ParticipatingValidators, c.summary.HeadCorrectValidators, c.summary.HeadTimelyValidators, c.summary.SourceTimelyValidators, c.summary.TargetCorrectValidators, c.summary.TargetTimelyValidators, votes, participations, err = c.processSlots(ctx, firstSlot, lastSlot)
if err != nil {
for slot := firstSlot; slot <= lastSlot; slot++ { return err
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.
continue
}
attestations, err := block.Attestations()
if err != nil {
return err
}
for _, attestation := range attestations {
if attestation.Data.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestation.Data.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
// Outside of this epoch's range.
continue
}
slotCommittees, exists := allCommittees[attestation.Data.Slot]
if !exists {
beaconCommittees, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", attestation.Data.Slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
}
for _, beaconCommittee := range beaconCommittees {
if _, exists := allCommittees[beaconCommittee.Slot]; !exists {
allCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
}
allCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
for _, index := range beaconCommittee.Validators {
participations[index] = &nonParticipatingValidator{
Validator: index,
Slot: beaconCommittee.Slot,
Committee: beaconCommittee.Index,
}
}
}
slotCommittees = allCommittees[attestation.Data.Slot]
}
committee := slotCommittees[attestation.Data.Index]
for i := uint64(0); i < attestation.AggregationBits.Len(); i++ {
if attestation.AggregationBits.BitAt(i) {
votes[committee[int(i)]] = struct{}{}
}
}
}
} }
c.summary.ActiveValidators = len(activeValidators)
c.summary.ParticipatingValidators = len(votes)
c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0, len(activeValidators)-len(votes)) c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0, len(activeValidators)-len(votes))
for activeValidatorIndex := range activeValidators { for activeValidatorIndex := range activeValidators {
if _, exists := votes[activeValidatorIndex]; !exists { if _, exists := votes[activeValidatorIndex]; !exists {
@@ -177,6 +137,121 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
return nil return nil
} }
func (c *command) processSlots(ctx context.Context,
firstSlot phase0.Slot,
lastSlot phase0.Slot,
) (
int,
int,
int,
int,
int,
int,
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]*nonParticipatingValidator,
error,
) {
votes := make(map[phase0.ValidatorIndex]struct{})
headCorrects := make(map[phase0.ValidatorIndex]struct{})
headTimelys := make(map[phase0.ValidatorIndex]struct{})
sourceTimelys := make(map[phase0.ValidatorIndex]struct{})
targetCorrects := make(map[phase0.ValidatorIndex]struct{})
targetTimelys := make(map[phase0.ValidatorIndex]struct{})
allCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
participations := make(map[phase0.ValidatorIndex]*nonParticipatingValidator)
// Need a cache of beacon block headers to reduce lookup times.
headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider)
for slot := firstSlot; slot <= lastSlot; slot++ {
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
}
if block == nil {
// No block at this slot; that's fine.
continue
}
slot, err := block.Slot()
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
attestations, err := block.Attestations()
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
for _, attestation := range attestations {
if attestation.Data.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestation.Data.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
// Outside of this epoch's range.
continue
}
slotCommittees, exists := allCommittees[attestation.Data.Slot]
if !exists {
beaconCommittees, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", attestation.Data.Slot))
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
}
for _, beaconCommittee := range beaconCommittees {
if _, exists := allCommittees[beaconCommittee.Slot]; !exists {
allCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
}
allCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
for _, index := range beaconCommittee.Validators {
participations[index] = &nonParticipatingValidator{
Validator: index,
Slot: beaconCommittee.Slot,
Committee: beaconCommittee.Index,
}
}
}
slotCommittees = allCommittees[attestation.Data.Slot]
}
committee := slotCommittees[attestation.Data.Index]
inclusionDistance := slot - attestation.Data.Slot
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
for i := uint64(0); i < attestation.AggregationBits.Len(); i++ {
if attestation.AggregationBits.BitAt(i) {
votes[committee[int(i)]] = struct{}{}
if _, exists := headCorrects[committee[int(i)]]; !exists && headCorrect {
headCorrects[committee[int(i)]] = struct{}{}
}
if _, exists := headTimelys[committee[int(i)]]; !exists && headCorrect && inclusionDistance == 1 {
headTimelys[committee[int(i)]] = struct{}{}
}
if _, exists := sourceTimelys[committee[int(i)]]; !exists && inclusionDistance <= 5 {
sourceTimelys[committee[int(i)]] = struct{}{}
}
if _, exists := targetCorrects[committee[int(i)]]; !exists && targetCorrect {
targetCorrects[committee[int(i)]] = struct{}{}
}
if _, exists := targetTimelys[committee[int(i)]]; !exists && targetCorrect && inclusionDistance <= 32 {
targetTimelys[committee[int(i)]] = struct{}{}
}
}
}
}
}
return len(votes),
len(headCorrects),
len(headTimelys),
len(sourceTimelys),
len(targetCorrects),
len(targetTimelys),
votes,
participations,
nil
}
func (c *command) processSyncCommitteeDuties(ctx context.Context) error { func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
if c.summary.Epoch < c.chainTime.AltairInitialEpoch() { if c.summary.Epoch < c.chainTime.AltairInitialEpoch() {
// The epoch is pre-Altair. No info but no error. // The epoch is pre-Altair. No info but no error.
@@ -286,6 +361,10 @@ func (c *command) setup(ctx context.Context) error {
if !isProvider { if !isProvider {
return errors.New("connection does not provide beacon committees") 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 return nil
} }

80
util/attestations.go Normal file
View File

@@ -0,0 +1,80 @@
// 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 util
import (
"bytes"
"context"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/wealdtech/ethdo/services/chaintime"
)
// AttestationHeadCorrect returns true if the given attestation had the correct head.
func AttestationHeadCorrect(ctx context.Context,
headersCache *BeaconBlockHeaderCache,
attestation *phase0.Attestation,
) (
bool,
error,
) {
slot := attestation.Data.Slot
for {
header, err := headersCache.Fetch(ctx, slot)
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
}
}
// AttestationTargetCorrect returns true if the given attestation had the correct target.
func AttestationTargetCorrect(ctx context.Context,
headersCache *BeaconBlockHeaderCache,
chainTime chaintime.Service,
attestation *phase0.Attestation,
) (
bool,
error,
) {
// Start with first slot of the target epoch.
slot := chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
for {
header, err := headersCache.Fetch(ctx, slot)
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.Target.Root[:]), nil
}
}

70
util/beaconheadercache.go Normal file
View File

@@ -0,0 +1,70 @@
// 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 util
import (
"context"
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
)
// BeaconBlockHeaderCache is a cache of beacon block headers.
type BeaconBlockHeaderCache struct {
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
entries map[phase0.Slot]*beaconBlockHeaderEntry
}
// NewBeaconBlockHeaderCache makes a new beacon block header cache.
func NewBeaconBlockHeaderCache(provider eth2client.BeaconBlockHeadersProvider) *BeaconBlockHeaderCache {
return &BeaconBlockHeaderCache{
beaconBlockHeadersProvider: provider,
entries: make(map[phase0.Slot]*beaconBlockHeaderEntry),
}
}
type beaconBlockHeaderEntry struct {
present bool
value *apiv1.BeaconBlockHeader
}
// Fetch the beacon block header for the given slot.
func (b *BeaconBlockHeaderCache) Fetch(ctx context.Context,
slot phase0.Slot,
) (
*apiv1.BeaconBlockHeader,
error,
) {
entry, exists := b.entries[slot]
if !exists {
header, err := b.beaconBlockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return nil, err
}
if header == nil {
entry = &beaconBlockHeaderEntry{
present: false,
}
} else {
entry = &beaconBlockHeaderEntry{
present: true,
value: header,
}
}
b.entries[slot] = entry
}
return entry.value, nil
}

132
util/validators.go Normal file
View File

@@ -0,0 +1,132 @@
// 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 util
import (
"context"
"encoding/hex"
"fmt"
"strconv"
"strings"
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"
)
// ParseValidators parses input to obtain the list of validators.
func ParseValidators(ctx context.Context, validatorsProvider eth2client.ValidatorsProvider, validatorsStr []string, stateID string) ([]*apiv1.Validator, error) {
validators := make([]*apiv1.Validator, 0, len(validatorsStr))
for i := range validatorsStr {
if strings.Contains(validatorsStr[i], "-") {
// Range.
bits := strings.Split(validatorsStr[i], "-")
if len(bits) != 2 {
return nil, fmt.Errorf("invalid range %s", validatorsStr[i])
}
low, err := strconv.ParseUint(bits[0], 10, 64)
if err != nil {
return nil, errors.Wrap(err, "invalid range start")
}
high, err := strconv.ParseUint(bits[1], 10, 64)
if err != nil {
return nil, errors.Wrap(err, "invalid range end")
}
indices := make([]phase0.ValidatorIndex, 0)
for index := low; index <= high; index++ {
indices = append(indices, phase0.ValidatorIndex(index))
}
rangeValidators, err := validatorsProvider.Validators(ctx, stateID, indices)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to obtain validators %s", validatorsStr[i]))
}
for _, validator := range rangeValidators {
validators = append(validators, validator)
}
} else {
validator, err := ParseValidator(ctx, validatorsProvider, validatorsStr[i], stateID)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("unknown validator %s", validatorsStr[i]))
}
validators = append(validators, validator)
}
}
return validators, nil
}
// ParseValidator parses input to obtain the validator.
func ParseValidator(ctx context.Context,
validatorsProvider eth2client.ValidatorsProvider,
validatorStr string,
stateID string,
) (
*apiv1.Validator,
error,
) {
var validators map[phase0.ValidatorIndex]*apiv1.Validator
switch {
case strings.HasPrefix(validatorStr, "0x"):
// A public key.
data, err := hex.DecodeString(strings.TrimPrefix(validatorStr, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to parse validator public key")
}
pubKey := phase0.BLSPubKey{}
copy(pubKey[:], data)
validators, err = validatorsProvider.ValidatorsByPubKey(ctx,
stateID,
[]phase0.BLSPubKey{pubKey},
)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator information")
}
case strings.Contains(validatorStr, "/"):
// An account.
_, account, err := WalletAndAccountFromPath(ctx, validatorStr)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain account")
}
accPubKey, err := BestPublicKey(account)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain public key for account")
}
pubKey := phase0.BLSPubKey{}
copy(pubKey[:], accPubKey.Marshal())
validators, err = validatorsProvider.ValidatorsByPubKey(ctx,
stateID,
[]phase0.BLSPubKey{pubKey},
)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator information")
}
default:
// An index.
index, err := strconv.ParseUint(validatorStr, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse validator index")
}
validators, err = validatorsProvider.Validators(ctx, stateID, []phase0.ValidatorIndex{phase0.ValidatorIndex(index)})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator information")
}
}
// Validator is first entry in the map.
for _, validator := range validators {
return validator, nil
}
return nil, errors.New("unknown validator")
}