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:
- no longer require connection parameter
- support "block analyze" on bellatrix (thanks @tcrossland)

View File

@@ -47,6 +47,7 @@ type command struct {
syncCommitteesProvider eth2client.SyncCommitteesProvider
validatorsProvider eth2client.ValidatorsProvider
beaconCommitteesProvider eth2client.BeaconCommitteesProvider
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Results.
summary *epochSummary
@@ -60,6 +61,11 @@ type epochSummary struct {
SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
ActiveValidators int `json:"active_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"`
}

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 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 {
// Sort list by validator index.
for _, validator := range c.summary.NonParticipatingValidators {

View File

@@ -79,11 +79,10 @@ func (c *command) processProposerDuties(ctx context.Context) error {
return nil
}
func (c *command) processAttesterDuties(ctx context.Context) error {
// Obtain all active validators for the given epoch.
func (c *command) activeValidators(ctx context.Context) (map[phase0.ValidatorIndex]*apiv1.Validator, error) {
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), 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)
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.
// These votes can be included anywhere from the second slot of
// 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()
}
votes := make(map[phase0.ValidatorIndex]struct{})
allCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
participations := make(map[phase0.ValidatorIndex]*nonParticipatingValidator)
for slot := firstSlot; slot <= 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 {
// No block at this slot; that's fine.
continue
}
attestations, err := block.Attestations()
var votes map[phase0.ValidatorIndex]struct{}
var participations 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 {
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))
for activeValidatorIndex := range activeValidators {
if _, exists := votes[activeValidatorIndex]; !exists {
@@ -177,6 +137,121 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
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 {
if c.summary.Epoch < c.chainTime.AltairInitialEpoch() {
// The epoch is pre-Altair. No info but no error.
@@ -286,6 +361,10 @@ func (c *command) setup(ctx context.Context) error {
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
}

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")
}