mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-09 14:07:56 -05:00
Initial work on validator summary.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
dev:
|
||||
- add more information to "epoch summary"
|
||||
- add "validator summary"
|
||||
|
||||
1.25.2:
|
||||
- no longer require connection parameter
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -30,3 +30,6 @@ func init() {
|
||||
|
||||
func validatorFlags(cmd *cobra.Command) {
|
||||
}
|
||||
|
||||
func validatorBindings() {
|
||||
}
|
||||
|
||||
140
cmd/validator/summary/command.go
Normal file
140
cmd/validator/summary/command.go
Normal 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
|
||||
}
|
||||
64
cmd/validator/summary/command_internal_test.go
Normal file
64
cmd/validator/summary/command_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
86
cmd/validator/summary/output.go
Normal file
86
cmd/validator/summary/output.go
Normal 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
|
||||
}
|
||||
425
cmd/validator/summary/process.go
Normal file
425
cmd/validator/summary/process.go
Normal 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
|
||||
}
|
||||
62
cmd/validator/summary/process_internal_test.go
Normal file
62
cmd/validator/summary/process_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/validator/summary/run.go
Normal file
50
cmd/validator/summary/run.go
Normal 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
66
cmd/validatorsummary.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user