Initial work on validator summary.

This commit is contained in:
Jim McDonald
2022-10-16 23:35:44 +01:00
parent be2270c543
commit fd1e4a97bb
11 changed files with 905 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
dev:
- add more information to "epoch summary"
- add "validator summary"
1.25.2:
- no longer require connection parameter

View File

@@ -128,6 +128,8 @@ func includeCommandBindings(cmd *cobra.Command) {
validatorInfoBindings()
case "validator/keycheck":
validatorKeycheckBindings()
case "validator/summary":
validatorSummaryBindings()
case "validator/yield":
validatorYieldBindings()
case "validator/expectation":

View File

@@ -30,3 +30,6 @@ func init() {
func validatorFlags(cmd *cobra.Command) {
}
func validatorBindings() {
}

View File

@@ -0,0 +1,140 @@
// 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"
"time"
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"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Operation.
epoch string
validators []string
jsonOutput bool
// Data access.
eth2Client eth2client.Service
chainTime chaintime.Service
proposerDutiesProvider eth2client.ProposerDutiesProvider
attesterDutiesProvider eth2client.AttesterDutiesProvider
blocksProvider eth2client.SignedBeaconBlockProvider
syncCommitteesProvider eth2client.SyncCommitteesProvider
validatorsProvider eth2client.ValidatorsProvider
beaconCommitteesProvider eth2client.BeaconCommitteesProvider
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Processing.
validatorsByIndex map[phase0.ValidatorIndex]*apiv1.Validator
// Results.
summary *validatorSummary
}
type validatorSummary struct {
Epoch phase0.Epoch `json:"epoch"`
Validators []*apiv1.Validator `json:"validators"`
FirstSlot phase0.Slot `json:"first_slot"`
LastSlot phase0.Slot `json:"last_slot"`
ActiveValidators int `json:"active_validators"`
ParticipatingValidators int `json:"participating_validators"`
NonParticipatingValidators []*nonParticipatingValidator `json:"non_participating_validators"`
IncorrectHeadValidators []*validatorFault `json:"incorrect_head_validators"`
UntimelyHeadValidators []*validatorFault `json:"untimely_head_validators"`
UntimelySourceValidators []*validatorFault `json:"untimely_source_validators"`
IncorrectTargetValidators []*validatorFault `json:"incorrect_target_validators"`
UntimelyTargetValidators []*validatorFault `json:"untimely_target_validators"`
Slots []*slot `json:"slots"`
Proposals []*epochProposal `json:"-"`
SyncCommittee []*epochSyncCommittee `json:"-"`
}
type slot struct {
Slot phase0.Slot `json:"slot"`
Attestations *slotAttestations `json:"attestations"`
}
type slotAttestations struct {
Expected int `json:"expected"`
Included int `json:"included"`
CorrectHead int `json:"correct_head"`
TimelyHead int `json:"timely_head"`
CorrectTarget int `json:"correct_target"`
TimelyTarget int `json:"timely_target"`
TimelySource int `json:"timely_source"`
}
type epochProposal struct {
Slot phase0.Slot `json:"slot"`
Proposer phase0.ValidatorIndex `json:"proposer"`
Block bool `json:"block"`
}
type epochSyncCommittee struct {
Index phase0.ValidatorIndex `json:"index"`
Missed int `json:"missed"`
}
type validatorFault struct {
Validator phase0.ValidatorIndex `json:"validator_index"`
AttestationData *phase0.AttestationData `json:"attestation_data,omitempty"`
InclusionDistance int `json:"inclusion_delay"`
}
type nonParticipatingValidator struct {
Validator phase0.ValidatorIndex `json:"validator_index"`
Slot phase0.Slot `json:"slot"`
Committee phase0.CommitteeIndex `json:"committee_index"`
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
validatorsByIndex: make(map[phase0.ValidatorIndex]*apiv1.Validator),
summary: &validatorSummary{},
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
c.epoch = viper.GetString("epoch")
c.validators = viper.GetStringSlice("validators")
c.jsonOutput = viper.GetBool("json")
return c, nil
}

View File

@@ -0,0 +1,64 @@
// 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"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
_, err := newCommand(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,86 @@
// 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"
"encoding/json"
"fmt"
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.jsonOutput {
return c.outputJSON(ctx)
}
return c.outputTxt(ctx)
}
func (c *command) outputJSON(_ context.Context) (string, error) {
data, err := json.Marshal(c.summary)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputTxt(_ context.Context) (string, error) {
builder := strings.Builder{}
builder.WriteString("Epoch ")
builder.WriteString(fmt.Sprintf("%d:\n", c.summary.Epoch))
if len(c.summary.NonParticipatingValidators) > 0 {
builder.WriteString(" Non-participating validators:\n")
for _, validator := range c.summary.NonParticipatingValidators {
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d)\n", validator.Validator, validator.Slot, validator.Committee))
}
}
if len(c.summary.IncorrectHeadValidators) > 0 {
builder.WriteString(" Incorrect head validators:\n")
for _, validator := range c.summary.IncorrectHeadValidators {
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d)\n", validator.Validator, validator.AttestationData.Slot, validator.AttestationData.Index))
}
}
if len(c.summary.UntimelyHeadValidators) > 0 {
builder.WriteString(" Untimely head validators:\n")
for _, validator := range c.summary.UntimelyHeadValidators {
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d, inclusion distance %d)\n", validator.Validator, validator.AttestationData.Slot, validator.AttestationData.Index, validator.InclusionDistance))
}
}
if len(c.summary.UntimelySourceValidators) > 0 {
builder.WriteString(" Untimely source validators:\n")
for _, validator := range c.summary.UntimelySourceValidators {
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d, inclusion distance %d)\n", validator.Validator, validator.AttestationData.Slot, validator.AttestationData.Index, validator.InclusionDistance))
}
}
if len(c.summary.IncorrectTargetValidators) > 0 {
builder.WriteString(" Incorrect target validators:\n")
for _, validator := range c.summary.IncorrectTargetValidators {
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d)\n", validator.Validator, validator.AttestationData.Slot, validator.AttestationData.Index))
}
}
if len(c.summary.UntimelyTargetValidators) > 0 {
builder.WriteString(" Untimely target validators:\n")
for _, validator := range c.summary.UntimelyTargetValidators {
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d, inclusion distance %d)\n", validator.Validator, validator.AttestationData.Slot, validator.AttestationData.Index, validator.InclusionDistance))
}
}
return builder.String(), nil
}

View File

@@ -0,0 +1,425 @@
// 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.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
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
}

View File

@@ -0,0 +1,62 @@
// 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"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "InvalidData",
vars: map[string]interface{}{
"timeout": "60s",
"data": "[[",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
cmd, err := newCommand(context.Background())
require.NoError(t, err)
err = cmd.process(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,50 @@
// 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"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

66
cmd/validatorsummary.go Normal file
View File

@@ -0,0 +1,66 @@
// 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 cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
validatorsummary "github.com/wealdtech/ethdo/cmd/validator/summary"
)
var validatorSummaryCmd = &cobra.Command{
Use: "summary",
Short: "Obtain summary information about validator(s) in an epoch",
Long: `Obtain summary information about one or more validators in an epoch. For example:
ethdo validator summary --validators=1,2,3 --epoch=12345
In quiet mode this will return 0 if information for the epoch is found, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatorsummary.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
validatorCmd.AddCommand(validatorSummaryCmd)
validatorFlags(validatorSummaryCmd)
validatorSummaryCmd.Flags().String("epoch", "", "the epoch for which to obtain information ()")
validatorSummaryCmd.Flags().StringSlice("validators", nil, "the list of validators for which to obtain information")
validatorSummaryCmd.Flags().Bool("json", false, "output data in JSON format")
}
func validatorSummaryBindings() {
validatorBindings()
if err := viper.BindPFlag("epoch", validatorSummaryCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("validators", validatorSummaryCmd.Flags().Lookup("validators")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", validatorSummaryCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
}

View File

@@ -711,6 +711,12 @@ $ ethdo validator yield
Yield: 4.64%
```
#### `summary`
`ethdo validator summary` provides a summary of the given epoch for the given validators. Options include:
- `epoch`: the epoch for which to provide a summary; defaults to last complete epoch
- `validators`: the list of validators for which to provide a summary
- `json`: provide JSON output
### `proposer` commands
Proposer commands focus on Ethereum 2 validators' actions as proposers.