diff --git a/CHANGELOG.md b/CHANGELOG.md index fd982d7..ab373d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.36.1: + - more JSON data for epoch summary + +1.36.0: + - support keystore wallets + +1.35.6: + - provide more JSON data in "epoch summary" + 1.35.5: - allow keystore to be output to the console diff --git a/cmd/epoch.go b/cmd/epoch.go index e566c62..9854b32 100644 --- a/cmd/epoch.go +++ b/cmd/epoch.go @@ -29,12 +29,12 @@ func init() { RootCmd.AddCommand(epochCmd) } -func epochFlags(_ *cobra.Command) { - epochSummaryCmd.Flags().String("epoch", "", "the epoch for which to obtain information (default current, can be 'current', 'last' or a number)") +func epochFlags(cmd *cobra.Command) { + cmd.Flags().String("epoch", "", "the epoch for which to obtain information (default current, can be 'current', 'last' or a number)") } func epochBindings(cmd *cobra.Command) { - if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { + if err := viper.BindPFlag("validators", cmd.Flags().Lookup("validators")); err != nil { panic(err) } } diff --git a/cmd/epoch/summary/command.go b/cmd/epoch/summary/command.go index a21c4ea..ff1d736 100644 --- a/cmd/epoch/summary/command.go +++ b/cmd/epoch/summary/command.go @@ -36,9 +36,11 @@ type command struct { allowInsecureConnections bool // Operation. - epoch string - stream bool - jsonOutput bool + epoch string + validatorsStr []string + validators map[phase0.ValidatorIndex]struct{} + stream bool + jsonOutput bool // Data access. eth2Client eth2client.Service @@ -58,45 +60,61 @@ type command struct { } type epochSummary struct { - Epoch phase0.Epoch `json:"epoch"` - FirstSlot phase0.Slot `json:"first_slot"` - LastSlot phase0.Slot `json:"last_slot"` - Proposals []*epochProposal `json:"proposals"` - 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"` - Blobs int `json:"blobs"` + Epoch phase0.Epoch `json:"epoch"` + FirstSlot phase0.Slot `json:"first_slot"` + LastSlot phase0.Slot `json:"last_slot"` + Blocks int `json:"blocks"` + Proposals []*epochProposal `json:"proposals"` + SyncCommitteeValidators int `json:"sync_committee_validators"` + 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 []*attestingValidator `json:"nonparticipating_validators"` + NonHeadCorrectValidators []*attestingValidator `json:"nonheadcorrect_validators"` + NonHeadTimelyValidators []*attestingValidator `json:"nonheadtimely_validators"` + NonTargetCorrectValidators []*attestingValidator `json:"nontargetcorrect_validators"` + NonSourceTimelyValidators []*attestingValidator `json:"nonsourcetimely_validators"` + Blobs int `json:"blobs"` } type epochProposal struct { - Slot phase0.Slot `json:"slot"` - Proposer phase0.ValidatorIndex `json:"proposer"` - Block bool `json:"block"` + ValidatorIndex phase0.ValidatorIndex `json:"validator_index"` + Slot phase0.Slot `json:"slot"` + Block bool `json:"block"` } type epochSyncCommittee struct { - Index phase0.ValidatorIndex `json:"index"` - Missed int `json:"missed"` + ValidatorIndex phase0.ValidatorIndex `json:"validator_index"` + Missed int `json:"missed"` + MissedSlots []phase0.Slot `json:"missed_slots"` } -type nonParticipatingValidator struct { - Validator phase0.ValidatorIndex `json:"validator_index"` - Slot phase0.Slot `json:"slot"` - Committee phase0.CommitteeIndex `json:"committee_index"` +type attestingValidator struct { + Validator phase0.ValidatorIndex `json:"validator_index"` + Slot phase0.Slot `json:"slot"` + Committee phase0.CommitteeIndex `json:"committee_index"` + HeadVote *phase0.Root `json:"head_vote,omitempty"` + Head *phase0.Root `json:"head,omitempty"` + TargetVote *phase0.Root `json:"target_vote,omitempty"` + Target *phase0.Root `json:"target,omitempty"` + InclusionSlot phase0.Slot `json:"inclusion_slot,omitempty"` } func newCommand(_ context.Context) (*command, error) { c := &command{ - quiet: viper.GetBool("quiet"), - verbose: viper.GetBool("verbose"), - debug: viper.GetBool("debug"), - summary: &epochSummary{}, + quiet: viper.GetBool("quiet"), + verbose: viper.GetBool("verbose"), + debug: viper.GetBool("debug"), + validatorsStr: viper.GetStringSlice("validators"), + summary: &epochSummary{ + Proposals: make([]*epochProposal, 0), + }, + validators: make(map[phase0.ValidatorIndex]struct{}), blocksCache: make(map[string]*spec.VersionedSignedBeaconBlock), } diff --git a/cmd/epoch/summary/output.go b/cmd/epoch/summary/output.go index 6670259..6fb5022 100644 --- a/cmd/epoch/summary/output.go +++ b/cmd/epoch/summary/output.go @@ -50,7 +50,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) { missedProposals := make([]string, 0, len(c.summary.Proposals)) for _, proposal := range c.summary.Proposals { if !proposal.Block { - missedProposals = append(missedProposals, fmt.Sprintf("\n Slot %d (validator %d)", proposal.Slot, proposal.Proposer)) + missedProposals = append(missedProposals, fmt.Sprintf("\n Slot %d (validator %d)", proposal.Slot, proposal.ValidatorIndex)) } else { proposedBlocks++ } @@ -64,7 +64,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) { builder.WriteString("\n Slot ") builder.WriteString(fmt.Sprintf("%d (%d/%d)", proposal.Slot, uint64(proposal.Slot)%uint64(len(c.summary.Proposals)), len(c.summary.Proposals))) builder.WriteString(" validator ") - builder.WriteString(fmt.Sprintf("%d", proposal.Proposer)) + builder.WriteString(fmt.Sprintf("%d", proposal.ValidatorIndex)) builder.WriteString(" not proposed or not included") } } @@ -98,7 +98,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) { if c.verbose { for _, syncCommittee := range c.summary.SyncCommittee { builder.WriteString("\n Validator ") - builder.WriteString(fmt.Sprintf("%d", syncCommittee.Index)) + builder.WriteString(fmt.Sprintf("%d", syncCommittee.ValidatorIndex)) builder.WriteString(" included ") builder.WriteString(fmt.Sprintf("%d/%d", proposedBlocks-syncCommittee.Missed, proposedBlocks)) builder.WriteString(fmt.Sprintf(" (%0.2f%%)", 100.0*float64(proposedBlocks-syncCommittee.Missed)/float64(proposedBlocks))) diff --git a/cmd/epoch/summary/process.go b/cmd/epoch/summary/process.go index 834b033..bd3530f 100644 --- a/cmd/epoch/summary/process.go +++ b/cmd/epoch/summary/process.go @@ -43,6 +43,14 @@ func (c *command) process(ctx context.Context) error { c.summary.FirstSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) c.summary.LastSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) - 1 + validators, err := util.ParseValidators(ctx, c.validatorsProvider, c.validatorsStr, "head") + if err != nil { + return errors.Wrap(err, "failed to parse validators") + } + for _, validator := range validators { + c.validators[validator.Index] = struct{}{} + } + if err := c.processProposerDuties(ctx); err != nil { return err } @@ -52,6 +60,7 @@ func (c *command) process(ctx context.Context) error { if err := c.processSyncCommitteeDuties(ctx); err != nil { return err } + return c.processBlobs(ctx) } @@ -69,10 +78,20 @@ func (c *command) processProposerDuties(ctx context.Context) error { return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", duty.Slot)) } present := block != nil + if present { + c.summary.Blocks++ + } + + _, exists := c.validators[duty.ValidatorIndex] + if len(c.validators) > 0 && !exists { + // Not one of ours. + continue + } + c.summary.Proposals = append(c.summary.Proposals, &epochProposal{ - Slot: duty.Slot, - Proposer: duty.ValidatorIndex, - Block: present, + Slot: duty.Slot, + ValidatorIndex: duty.ValidatorIndex, + Block: present, }) } @@ -80,14 +99,25 @@ func (c *command) processProposerDuties(ctx context.Context) error { } func (c *command) activeValidators(ctx context.Context) (map[phase0.ValidatorIndex]*apiv1.Validator, error) { + validatorIndices := make([]phase0.ValidatorIndex, 0, len(c.validators)) + for validator := range c.validators { + validatorIndices = append(validatorIndices, validator) + } + response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{ - State: fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), + State: "head", + Indices: validatorIndices, }) if err != nil { return nil, errors.Wrap(err, "failed to obtain validators for epoch") } activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator) for _, validator := range response.Data { + _, exists := c.validators[validator.Index] + if len(c.validators) > 0 && !exists { + continue + } + if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch { activeValidators[validator.Index] = validator } @@ -112,20 +142,45 @@ func (c *command) processAttesterDuties(ctx context.Context) error { lastSlot = c.chainTime.CurrentSlot() } - 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) + participatingValidators, headCorrectValidators, headTimelyValidators, sourceTimelyValidators, targetCorrectValidators, targetTimelyValidators, participations, err := c.processSlots(ctx, firstSlot, lastSlot) if err != nil { return err } - c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0, len(activeValidators)-len(votes)) + c.summary.ParticipatingValidators = len(participatingValidators) + c.summary.HeadCorrectValidators = len(headCorrectValidators) + c.summary.HeadTimelyValidators = len(headTimelyValidators) + c.summary.SourceTimelyValidators = len(sourceTimelyValidators) + c.summary.TargetCorrectValidators = len(targetCorrectValidators) + c.summary.TargetTimelyValidators = len(targetTimelyValidators) + + c.summary.NonParticipatingValidators = make([]*attestingValidator, 0, len(activeValidators)-len(participatingValidators)) for activeValidatorIndex := range activeValidators { - if _, exists := votes[activeValidatorIndex]; !exists { + if _, exists := participatingValidators[activeValidatorIndex]; !exists { if _, exists := participations[activeValidatorIndex]; exists { c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, participations[activeValidatorIndex]) } } + if _, exists := headCorrectValidators[activeValidatorIndex]; !exists { + if _, exists := participations[activeValidatorIndex]; exists { + c.summary.NonHeadCorrectValidators = append(c.summary.NonHeadCorrectValidators, participations[activeValidatorIndex]) + } + } + if _, exists := headTimelyValidators[activeValidatorIndex]; !exists { + if _, exists := participations[activeValidatorIndex]; exists { + c.summary.NonHeadTimelyValidators = append(c.summary.NonHeadTimelyValidators, participations[activeValidatorIndex]) + } + } + if _, exists := targetCorrectValidators[activeValidatorIndex]; !exists { + if _, exists := participations[activeValidatorIndex]; exists { + c.summary.NonTargetCorrectValidators = append(c.summary.NonTargetCorrectValidators, participations[activeValidatorIndex]) + } + } + if _, exists := sourceTimelyValidators[activeValidatorIndex]; !exists { + if _, exists := participations[activeValidatorIndex]; exists { + c.summary.NonSourceTimelyValidators = append(c.summary.NonSourceTimelyValidators, participations[activeValidatorIndex]) + } + } } sort.Slice(c.summary.NonParticipatingValidators, func(i int, j int) bool { if c.summary.NonParticipatingValidators[i].Slot != c.summary.NonParticipatingValidators[j].Slot { @@ -140,18 +195,18 @@ func (c *command) processAttesterDuties(ctx context.Context) error { return nil } +//nolint:gocyclo 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, + map[phase0.ValidatorIndex]struct{}, + map[phase0.ValidatorIndex]struct{}, + map[phase0.ValidatorIndex]struct{}, + map[phase0.ValidatorIndex]struct{}, + map[phase0.ValidatorIndex]struct{}, + map[phase0.ValidatorIndex]*attestingValidator, error, ) { votes := make(map[phase0.ValidatorIndex]struct{}) @@ -161,7 +216,7 @@ func (c *command) processSlots(ctx context.Context, 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) + participations := make(map[phase0.ValidatorIndex]*attestingValidator) // Need a cache of beacon block headers to reduce lookup times. headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider) @@ -169,7 +224,7 @@ func (c *command) processSlots(ctx context.Context, for slot := firstSlot; slot <= lastSlot; slot++ { block, err := c.fetchBlock(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)) + return nil, nil, nil, nil, nil, 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. @@ -177,11 +232,11 @@ func (c *command) processSlots(ctx context.Context, } slot, err := block.Slot() if err != nil { - return 0, 0, 0, 0, 0, 0, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, err } attestations, err := block.Attestations() if err != nil { - return 0, 0, 0, 0, 0, 0, nil, nil, err + return nil, nil, nil, nil, nil, 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) { @@ -194,18 +249,29 @@ func (c *command) processSlots(ctx context.Context, State: 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)) + return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot)) } for _, beaconCommittee := range response.Data { 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, + if len(c.validators) > 0 { + if _, exists := c.validators[index]; !exists { + // Not one of our validators. + continue + } + } + + if _, exists := participations[index]; !exists { + participations[index] = &attestingValidator{ + Validator: index, + Slot: beaconCommittee.Slot, + Committee: beaconCommittee.Index, + } } } } @@ -214,44 +280,70 @@ func (c *command) processSlots(ctx context.Context, committee := slotCommittees[attestation.Data.Index] inclusionDistance := slot - attestation.Data.Slot + + head, err := util.AttestationHead(ctx, headersCache, attestation) + if err != nil { + return nil, nil, nil, nil, nil, nil, nil, err + } headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation) if err != nil { - return 0, 0, 0, 0, 0, 0, nil, nil, err + return nil, nil, nil, nil, nil, nil, nil, err + } + target, err := util.AttestationTarget(ctx, headersCache, c.chainTime, attestation) + if err != nil { + return nil, nil, nil, nil, nil, 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 + return nil, nil, nil, nil, nil, 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{}{} + validatorIndex := committee[int(i)] + if len(c.validators) > 0 { + if _, exists := c.validators[validatorIndex]; !exists { + // Not one of our validators. + continue + } } - if _, exists := headTimelys[committee[int(i)]]; !exists && headCorrect && inclusionDistance == 1 { - headTimelys[committee[int(i)]] = struct{}{} + + // Only set the information from the first attestation we find for this validator. + if participations[validatorIndex].InclusionSlot == 0 { + participations[validatorIndex].HeadVote = &attestation.Data.BeaconBlockRoot + participations[validatorIndex].Head = &head + participations[validatorIndex].TargetVote = &attestation.Data.Target.Root + participations[validatorIndex].Target = &target + participations[validatorIndex].InclusionSlot = slot + } + + votes[validatorIndex] = struct{}{} + if _, exists := headCorrects[validatorIndex]; !exists && headCorrect { + headCorrects[validatorIndex] = struct{}{} + } + if _, exists := headTimelys[validatorIndex]; !exists && headCorrect && inclusionDistance == 1 { + headTimelys[validatorIndex] = struct{}{} } - if _, exists := sourceTimelys[committee[int(i)]]; !exists && inclusionDistance <= 5 { - sourceTimelys[committee[int(i)]] = struct{}{} + if _, exists := sourceTimelys[validatorIndex]; !exists && inclusionDistance <= 5 { + sourceTimelys[validatorIndex] = struct{}{} } - if _, exists := targetCorrects[committee[int(i)]]; !exists && targetCorrect { - targetCorrects[committee[int(i)]] = struct{}{} + if _, exists := targetCorrects[validatorIndex]; !exists && targetCorrect { + targetCorrects[validatorIndex] = struct{}{} } - if _, exists := targetTimelys[committee[int(i)]]; !exists && targetCorrect && inclusionDistance <= 32 { - targetTimelys[committee[int(i)]] = struct{}{} + if _, exists := targetTimelys[validatorIndex]; !exists && targetCorrect && inclusionDistance <= 32 { + targetTimelys[validatorIndex] = struct{}{} } } } } } - return len(votes), - len(headCorrects), - len(headTimelys), - len(sourceTimelys), - len(targetCorrects), - len(targetTimelys), - votes, + + return votes, + headCorrects, + headTimelys, + sourceTimelys, + targetCorrects, + targetTimelys, participations, nil } @@ -273,7 +365,18 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error { return errors.Wrap(err, "empty sync committee") } + for _, validatorIndex := range committee.Validators { + if len(c.validators) == 0 { + c.summary.SyncCommitteeValidators++ + } else { + if _, exists := c.validators[validatorIndex]; exists { + c.summary.SyncCommitteeValidators++ + } + } + } + missed := make(map[phase0.ValidatorIndex]int) + missedSlots := make(map[phase0.ValidatorIndex][]phase0.Slot) for _, index := range committee.Validators { missed[index] = 0 } @@ -297,8 +400,14 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error { return errors.Wrapf(err, "failed to obtain sync aggregate for slot %d", slot) } for i := uint64(0); i < aggregate.SyncCommitteeBits.Len(); i++ { + validatorIndex := committee.Validators[int(i)] + if _, exists := c.validators[validatorIndex]; !exists { + // Not one of ours. + continue + } if !aggregate.SyncCommitteeBits.BitAt(i) { - missed[committee.Validators[int(i)]]++ + missed[validatorIndex]++ + missedSlots[validatorIndex] = append(missedSlots[validatorIndex], slot) } } } @@ -307,8 +416,9 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error { for index, count := range missed { if count > 0 { c.summary.SyncCommittee = append(c.summary.SyncCommittee, &epochSyncCommittee{ - Index: index, - Missed: count, + ValidatorIndex: index, + Missed: count, + MissedSlots: missedSlots[index], }) } } @@ -320,7 +430,7 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error { return missedDiff > 0 } // Then order by validator index. - return c.summary.SyncCommittee[i].Index < c.summary.SyncCommittee[j].Index + return c.summary.SyncCommittee[i].ValidatorIndex < c.summary.SyncCommittee[j].ValidatorIndex }) return nil @@ -378,10 +488,10 @@ func (c *command) setup(ctx context.Context) error { } func (c *command) processBlobs(ctx context.Context) error { - for slot := c.summary.FirstSlot; slot <= c.summary.LastSlot; slot++ { - block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", slot)) + for _, proposal := range c.summary.Proposals { + block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", proposal.Slot)) if err != nil { - return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot)) + return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", proposal.Slot)) } if block == nil { continue diff --git a/cmd/epochsummary.go b/cmd/epochsummary.go index bdfaaac..207af26 100644 --- a/cmd/epochsummary.go +++ b/cmd/epochsummary.go @@ -46,9 +46,17 @@ In quiet mode this will return 0 if information for the epoch is found, otherwis func init() { epochCmd.AddCommand(epochSummaryCmd) - epochFlags(epochSummaryCmd) + epochSummaryFlags(epochSummaryCmd) +} + +func epochSummaryFlags(cmd *cobra.Command) { + epochFlags(cmd) + cmd.Flags().StringSlice("validators", nil, "the validators for which to obtain a summary") } func epochSummaryBindings(cmd *cobra.Command) { epochBindings(cmd) + if err := viper.BindPFlag("validators", cmd.Flags().Lookup("validators")); err != nil { + panic(err) + } } diff --git a/util/attestations.go b/util/attestations.go index c73da84..0a812a3 100644 --- a/util/attestations.go +++ b/util/attestations.go @@ -21,6 +21,35 @@ import ( "github.com/wealdtech/ethdo/services/chaintime" ) +// AttestationHead returns the head for which the attestation should have voted. +func AttestationHead(ctx context.Context, + headersCache *BeaconBlockHeaderCache, + attestation *phase0.Attestation, +) ( + phase0.Root, + error, +) { + slot := attestation.Data.Slot + for { + header, err := headersCache.Fetch(ctx, slot) + if err != nil { + return phase0.Root{}, err + } + if header == nil { + // No block. + slot-- + continue + } + if !header.Canonical { + // Not canonical. + slot-- + continue + } + + return header.Root, nil + } +} + // AttestationHeadCorrect returns true if the given attestation had the correct head. func AttestationHeadCorrect(ctx context.Context, headersCache *BeaconBlockHeaderCache, @@ -49,6 +78,37 @@ func AttestationHeadCorrect(ctx context.Context, } } +// AttestationTarget returns the target for which the attestation should have voted. +func AttestationTarget(ctx context.Context, + headersCache *BeaconBlockHeaderCache, + chainTime chaintime.Service, + attestation *phase0.Attestation, +) ( + phase0.Root, + 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 phase0.Root{}, err + } + if header == nil { + // No block. + slot-- + continue + } + if !header.Canonical { + // Not canonical. + slot-- + continue + } + + return header.Root, nil + } +} + // AttestationTargetCorrect returns true if the given attestation had the correct target. func AttestationTargetCorrect(ctx context.Context, headersCache *BeaconBlockHeaderCache,