mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-08 21:48:05 -05:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b450e96dde | ||
|
|
30d7f6989a | ||
|
|
00ea75e5c8 | ||
|
|
34647927ab | ||
|
|
44cfb68e2c | ||
|
|
70df67e6ab | ||
|
|
08fb16ff9e | ||
|
|
903ecc8581 | ||
|
|
93f4f6d68c | ||
|
|
1eee5a1349 | ||
|
|
9c4e9bcb2f | ||
|
|
83a950e5d1 | ||
|
|
b2c0ae3fa2 | ||
|
|
3059aa270f | ||
|
|
8b7aab7180 | ||
|
|
c46483586d | ||
|
|
e4f0b934d7 | ||
|
|
25a5bd917f | ||
|
|
889a884f6e |
55
.github/workflows/docker.yml
vendored
55
.github/workflows/docker.yml
vendored
@@ -1,58 +1,45 @@
|
||||
name: Docker
|
||||
|
||||
# This workflow is triggered on a push to a tag that follows semantic versioning
|
||||
# e.g., v1.2.3, v2.0.0-rc1
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+**'
|
||||
|
||||
jobs:
|
||||
# Set variables that will be available to all builds.
|
||||
env_vars:
|
||||
# Build and push the Docker image
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_version: ${{ steps.release_version.outputs.release_version }}
|
||||
binary: ${{ steps.binary.outputs.binary }}
|
||||
steps:
|
||||
- id: release_version
|
||||
run: |
|
||||
RELEASE_VERSION=$(echo ${{ github.ref_name }} | sed -e 's/^[vt]//')
|
||||
echo "release_version=${RELEASE_VERSION}" >> $GITHUB_OUTPUT
|
||||
- id: binary
|
||||
run: |
|
||||
BINARY=$(basename ${{ github.repository }})
|
||||
echo "binary=${BINARY}" >> $GITHUB_OUTPUT
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Build.
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [env_vars]
|
||||
steps:
|
||||
- name: Check out repository into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
# This step extracts the version number from the tag
|
||||
# e.g., if the tag is 'v1.2.3', this will output '1.2.3'
|
||||
- name: Extract release version
|
||||
id: release_version
|
||||
run: |
|
||||
echo "version=$(echo ${{ github.ref_name }} | sed -e 's/^v//')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: wealdtech/ethdo:latest
|
||||
|
||||
- name: build and push on release
|
||||
uses: docker/build-push-action@v4
|
||||
if: ${{ github.event.release.tag_name != '' }}
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: wealdtech/ethdo:${{ github.event.release.tag_name }}
|
||||
tags: |
|
||||
wealdtech/ethdo:${{ steps.release_version.outputs.version }}
|
||||
wealdtech/ethdo:latest
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,3 +1,16 @@
|
||||
dev:
|
||||
|
||||
1.39.0:
|
||||
- support Fulu
|
||||
|
||||
1.38.0:
|
||||
- update latest version of go-eth2-client to support complex Spec types
|
||||
- adapt event handling to use new event handler structures in go-eth2-client
|
||||
|
||||
1.37.4:
|
||||
- add support for eip-7044 in exit verify command
|
||||
- provide ETH values as well as validator numbers in "epoch summary"
|
||||
|
||||
1.37.3:
|
||||
- add "hoodi" to the list of supported networks
|
||||
|
||||
|
||||
@@ -528,6 +528,13 @@ func (c *command) analyzeSyncCommittees(_ context.Context, block *spec.Versioned
|
||||
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
|
||||
c.analysis.Value += c.analysis.SyncCommitee.Value
|
||||
return nil
|
||||
case spec.DataVersionFulu:
|
||||
c.analysis.SyncCommitee.Contributions = int(block.Fulu.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
|
||||
c.analysis.SyncCommitee.PossibleContributions = int(block.Fulu.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
|
||||
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
|
||||
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
|
||||
c.analysis.Value += c.analysis.SyncCommitee.Value
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported block version %d", block.Version)
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
err = processDenebBlock(ctx, data, block)
|
||||
case spec.DataVersionElectra:
|
||||
err = processElectraBlock(ctx, data, block)
|
||||
case spec.DataVersionFulu:
|
||||
err = processFuluBlock(ctx, data, block)
|
||||
default:
|
||||
return nil, errors.New("unknown block version")
|
||||
}
|
||||
@@ -103,7 +105,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Println("")
|
||||
}
|
||||
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
|
||||
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, &api.EventsOpts{
|
||||
Topics: []string{"head"},
|
||||
HeadHandler: headEventHandler,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to start block stream")
|
||||
}
|
||||
@@ -212,15 +217,37 @@ func processElectraBlock(ctx context.Context,
|
||||
return nil
|
||||
}
|
||||
|
||||
func headEventHandler(event *apiv1.Event) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Only interested in head events.
|
||||
if event.Topic != "head" {
|
||||
return
|
||||
func processFuluBlock(ctx context.Context,
|
||||
data *dataIn,
|
||||
block *spec.VersionedSignedBeaconBlock,
|
||||
) error {
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
kzgCommitments, err := block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
var apiErr *api.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode != http.StatusNotFound {
|
||||
return errors.Wrap(err, "failed to obtain blob sidecars")
|
||||
}
|
||||
} else {
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
}
|
||||
if err := outputFuluBlock(ctx, data.jsonOutput, data.sszOutput, block.Fulu, blobSidecars); err != nil {
|
||||
return errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
|
||||
blockID := fmt.Sprintf("%#x", event.Data.(*apiv1.HeadEvent).Block[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func headEventHandler(ctx context.Context, headEvent *apiv1.HeadEvent) {
|
||||
blockID := fmt.Sprintf("%#x", headEvent.Block[:])
|
||||
blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: blockID,
|
||||
})
|
||||
@@ -267,6 +294,46 @@ func headEventHandler(event *apiv1.Event) {
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, block.Deneb, blobSidecars)
|
||||
case spec.DataVersionElectra:
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
var kzgCommitments []deneb.KZGCommitment
|
||||
kzgCommitments, err = block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to obtain KZG commitments: %v\n", err)
|
||||
return
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
var blobSidecarsResponse *api.Response[[]*deneb.BlobSidecar]
|
||||
blobSidecarsResponse, err = results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: blockID,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to obtain blob sidecars: %v\n", err)
|
||||
return
|
||||
}
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
err = outputElectraBlock(context.Background(), jsonOutput, sszOutput, block.Electra, blobSidecars)
|
||||
case spec.DataVersionFulu:
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
var kzgCommitments []deneb.KZGCommitment
|
||||
kzgCommitments, err = block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to obtain KZG commitments: %v\n", err)
|
||||
return
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
var blobSidecarsResponse *api.Response[[]*deneb.BlobSidecar]
|
||||
blobSidecarsResponse, err = results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: blockID,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to obtain blob sidecars: %v\n", err)
|
||||
return
|
||||
}
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
err = outputFuluBlock(context.Background(), jsonOutput, sszOutput, block.Fulu, blobSidecars)
|
||||
default:
|
||||
err = errors.New("unknown block version")
|
||||
}
|
||||
@@ -428,6 +495,35 @@ func outputElectraBlock(ctx context.Context,
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputFuluBlock(ctx context.Context,
|
||||
jsonOutput bool,
|
||||
sszOutput bool,
|
||||
signedBlock *electra.SignedBeaconBlock,
|
||||
blobs []*deneb.BlobSidecar,
|
||||
) error {
|
||||
switch {
|
||||
case jsonOutput:
|
||||
data, err := json.Marshal(signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate JSON")
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
case sszOutput:
|
||||
data, err := signedBlock.MarshalSSZ()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate SSZ")
|
||||
}
|
||||
fmt.Printf("%x\n", data)
|
||||
default:
|
||||
data, err := outputElectraBlockText(ctx, results, signedBlock, blobs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func timeToBlockID(ctx context.Context, eth2Client eth2client.Service, input string) (string, error) {
|
||||
var timestamp time.Time
|
||||
|
||||
|
||||
@@ -100,6 +100,9 @@ func (c *command) process(ctx context.Context) error {
|
||||
case spec.DataVersionElectra:
|
||||
c.incumbent = state.Electra.ETH1Data
|
||||
c.eth1DataVotes = state.Electra.ETH1DataVotes
|
||||
case spec.DataVersionFulu:
|
||||
c.incumbent = state.Fulu.ETH1Data
|
||||
c.eth1DataVotes = state.Fulu.ETH1DataVotes
|
||||
default:
|
||||
return fmt.Errorf("unhandled beacon state version %v", state.Version)
|
||||
}
|
||||
|
||||
@@ -15,9 +15,11 @@ package epochsummary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
@@ -52,6 +54,16 @@ type command struct {
|
||||
beaconCommitteesProvider eth2client.BeaconCommitteesProvider
|
||||
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
|
||||
|
||||
// Intermediate data.
|
||||
validatorInfo map[phase0.ValidatorIndex]*apiv1.Validator
|
||||
participatingValidators map[phase0.ValidatorIndex]struct{}
|
||||
headCorrectValidators map[phase0.ValidatorIndex]struct{}
|
||||
headTimelyValidators map[phase0.ValidatorIndex]struct{}
|
||||
sourceTimelyValidators map[phase0.ValidatorIndex]struct{}
|
||||
targetCorrectValidators map[phase0.ValidatorIndex]struct{}
|
||||
targetTimelyValidators map[phase0.ValidatorIndex]struct{}
|
||||
participations map[phase0.ValidatorIndex]*attestingValidator
|
||||
|
||||
// Caches.
|
||||
blocksCache map[string]*spec.VersionedSignedBeaconBlock
|
||||
|
||||
@@ -68,12 +80,19 @@ type epochSummary struct {
|
||||
SyncCommitteeValidators int `json:"sync_committee_validators"`
|
||||
SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
|
||||
ActiveValidators int `json:"active_validators"`
|
||||
ActiveBalance *big.Int `json:"active_balance"`
|
||||
ParticipatingValidators int `json:"participating_validators"`
|
||||
ParticipatingBalance *big.Int `json:"participating_balance"`
|
||||
HeadCorrectValidators int `json:"head_correct_validators"`
|
||||
HeadCorrectBalance *big.Int `json:"head_correct_balance"`
|
||||
HeadTimelyValidators int `json:"head_timely_validators"`
|
||||
HeadTimelyBalance *big.Int `json:"head_timely_balance"`
|
||||
SourceTimelyValidators int `json:"source_timely_validators"`
|
||||
SourceTimelyBalance *big.Int `json:"source_timely_balance"`
|
||||
TargetCorrectValidators int `json:"target_correct_validators"`
|
||||
TargetCorrectBalance *big.Int `json:"target_correct_balance"`
|
||||
TargetTimelyValidators int `json:"target_timely_validators"`
|
||||
TargetTimelyBalance *big.Int `json:"target_timely_balance"`
|
||||
NonParticipatingValidators []*attestingValidator `json:"nonparticipating_validators"`
|
||||
NonHeadCorrectValidators []*attestingValidator `json:"nonheadcorrect_validators"`
|
||||
NonHeadTimelyValidators []*attestingValidator `json:"nonheadtimely_validators"`
|
||||
@@ -95,14 +114,15 @@ type epochSyncCommittee struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
Validator phase0.ValidatorIndex `json:"validator_index"`
|
||||
EffectiveBalance phase0.Gwei `json:"effective_balance"`
|
||||
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) {
|
||||
@@ -112,10 +132,24 @@ func newCommand(_ context.Context) (*command, error) {
|
||||
debug: viper.GetBool("debug"),
|
||||
validatorsStr: viper.GetStringSlice("validators"),
|
||||
summary: &epochSummary{
|
||||
Proposals: make([]*epochProposal, 0),
|
||||
Proposals: make([]*epochProposal, 0),
|
||||
ActiveBalance: big.NewInt(0),
|
||||
ParticipatingBalance: big.NewInt(0),
|
||||
HeadCorrectBalance: big.NewInt(0),
|
||||
HeadTimelyBalance: big.NewInt(0),
|
||||
SourceTimelyBalance: big.NewInt(0),
|
||||
TargetCorrectBalance: big.NewInt(0),
|
||||
TargetTimelyBalance: big.NewInt(0),
|
||||
},
|
||||
validators: make(map[phase0.ValidatorIndex]struct{}),
|
||||
blocksCache: make(map[string]*spec.VersionedSignedBeaconBlock),
|
||||
validators: make(map[phase0.ValidatorIndex]struct{}),
|
||||
participatingValidators: make(map[phase0.ValidatorIndex]struct{}),
|
||||
headCorrectValidators: make(map[phase0.ValidatorIndex]struct{}),
|
||||
headTimelyValidators: make(map[phase0.ValidatorIndex]struct{}),
|
||||
sourceTimelyValidators: make(map[phase0.ValidatorIndex]struct{}),
|
||||
targetCorrectValidators: make(map[phase0.ValidatorIndex]struct{}),
|
||||
targetTimelyValidators: make(map[phase0.ValidatorIndex]struct{}),
|
||||
participations: make(map[phase0.ValidatorIndex]*attestingValidator),
|
||||
blocksCache: make(map[string]*spec.VersionedSignedBeaconBlock),
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2022 Weald Technology Trading.
|
||||
// Copyright © 2022, 2025 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
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -69,12 +70,80 @@ 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)))
|
||||
gweiToEth := big.NewInt(1e9)
|
||||
mul := big.NewInt(10000)
|
||||
participatingBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.ParticipatingBalance, mul), c.summary.ActiveBalance)
|
||||
builder.WriteString(fmt.Sprintf("\n Attesting balance: %s/%s (%0.2f%%)",
|
||||
new(big.Int).Div(c.summary.ParticipatingBalance, gweiToEth).String(),
|
||||
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
|
||||
float64(participatingBalancePct.Uint64())/100.0,
|
||||
))
|
||||
|
||||
sourceTimelyBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.SourceTimelyBalance, mul), c.summary.ActiveBalance)
|
||||
builder.WriteString(fmt.Sprintf("\n Source timely: %s/%s (%0.2f%%)",
|
||||
new(big.Int).Div(c.summary.SourceTimelyBalance, gweiToEth).String(),
|
||||
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
|
||||
float64(sourceTimelyBalancePct.Uint64())/100.0,
|
||||
))
|
||||
|
||||
targetCorrectBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.TargetCorrectBalance, mul), c.summary.ActiveBalance)
|
||||
builder.WriteString(fmt.Sprintf("\n Target correct: %s/%s (%0.2f%%)",
|
||||
new(big.Int).Div(c.summary.TargetCorrectBalance, gweiToEth).String(),
|
||||
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
|
||||
float64(targetCorrectBalancePct.Uint64())/100.0,
|
||||
))
|
||||
|
||||
targetTimelyBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.TargetTimelyBalance, mul), c.summary.ActiveBalance)
|
||||
builder.WriteString(fmt.Sprintf("\n Target timely: %s/%s (%0.2f%%)",
|
||||
new(big.Int).Div(c.summary.TargetTimelyBalance, gweiToEth).String(),
|
||||
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
|
||||
float64(targetTimelyBalancePct.Uint64())/100.0,
|
||||
))
|
||||
|
||||
headCorrectBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.HeadCorrectBalance, mul), c.summary.ActiveBalance)
|
||||
builder.WriteString(fmt.Sprintf("\n Head correct: %s/%s (%0.2f%%)",
|
||||
new(big.Int).Div(c.summary.HeadCorrectBalance, gweiToEth).String(),
|
||||
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
|
||||
float64(headCorrectBalancePct.Uint64())/100.0,
|
||||
))
|
||||
|
||||
headTimelyBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.HeadTimelyBalance, mul), c.summary.ActiveBalance)
|
||||
builder.WriteString(fmt.Sprintf("\n Head timely: %s/%s (%0.2f%%)",
|
||||
new(big.Int).Div(c.summary.HeadTimelyBalance, gweiToEth).String(),
|
||||
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
|
||||
float64(headTimelyBalancePct.Uint64())/100.0,
|
||||
))
|
||||
|
||||
builder.WriteString(fmt.Sprintf("\n Attesting validators: %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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2022, 2023 Weald Technology Trading.
|
||||
// Copyright © 2022 - 2025 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
|
||||
@@ -16,6 +16,7 @@ package epochsummary
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
@@ -105,12 +106,14 @@ func (c *command) activeValidators(ctx context.Context) (map[phase0.ValidatorInd
|
||||
}
|
||||
|
||||
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
|
||||
State: "head",
|
||||
State: fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)),
|
||||
Indices: validatorIndices,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain validators for epoch")
|
||||
}
|
||||
c.validatorInfo = response.Data
|
||||
|
||||
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
|
||||
for _, validator := range response.Data {
|
||||
_, exists := c.validators[validator.Index]
|
||||
@@ -133,6 +136,10 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
}
|
||||
c.summary.ActiveValidators = len(activeValidators)
|
||||
|
||||
for _, validator := range activeValidators {
|
||||
c.summary.ActiveBalance = c.summary.ActiveBalance.Add(c.summary.ActiveBalance, big.NewInt(int64(validator.Validator.EffectiveBalance)))
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -142,43 +149,42 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
lastSlot = c.chainTime.CurrentSlot()
|
||||
}
|
||||
|
||||
participatingValidators, headCorrectValidators, headTimelyValidators, sourceTimelyValidators, targetCorrectValidators, targetTimelyValidators, participations, err := c.processSlots(ctx, firstSlot, lastSlot)
|
||||
if err != nil {
|
||||
if err := c.processSlots(ctx, firstSlot, lastSlot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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.ParticipatingValidators = len(c.participatingValidators)
|
||||
c.summary.HeadCorrectValidators = len(c.headCorrectValidators)
|
||||
c.summary.HeadTimelyValidators = len(c.headTimelyValidators)
|
||||
c.summary.SourceTimelyValidators = len(c.sourceTimelyValidators)
|
||||
c.summary.TargetCorrectValidators = len(c.targetCorrectValidators)
|
||||
c.summary.TargetTimelyValidators = len(c.targetTimelyValidators)
|
||||
|
||||
c.summary.NonParticipatingValidators = make([]*attestingValidator, 0, len(activeValidators)-len(participatingValidators))
|
||||
c.summary.NonParticipatingValidators = make([]*attestingValidator, 0, len(activeValidators)-len(c.participatingValidators))
|
||||
for activeValidatorIndex := range activeValidators {
|
||||
if _, exists := participatingValidators[activeValidatorIndex]; !exists {
|
||||
if _, exists := participations[activeValidatorIndex]; exists {
|
||||
c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, participations[activeValidatorIndex])
|
||||
if _, exists := c.participatingValidators[activeValidatorIndex]; !exists {
|
||||
if _, exists := c.participations[activeValidatorIndex]; exists {
|
||||
c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, c.participations[activeValidatorIndex])
|
||||
}
|
||||
}
|
||||
if _, exists := headCorrectValidators[activeValidatorIndex]; !exists {
|
||||
if _, exists := participations[activeValidatorIndex]; exists {
|
||||
c.summary.NonHeadCorrectValidators = append(c.summary.NonHeadCorrectValidators, participations[activeValidatorIndex])
|
||||
if _, exists := c.headCorrectValidators[activeValidatorIndex]; !exists {
|
||||
if _, exists := c.participations[activeValidatorIndex]; exists {
|
||||
c.summary.NonHeadCorrectValidators = append(c.summary.NonHeadCorrectValidators, c.participations[activeValidatorIndex])
|
||||
}
|
||||
}
|
||||
if _, exists := headTimelyValidators[activeValidatorIndex]; !exists {
|
||||
if _, exists := participations[activeValidatorIndex]; exists {
|
||||
c.summary.NonHeadTimelyValidators = append(c.summary.NonHeadTimelyValidators, participations[activeValidatorIndex])
|
||||
if _, exists := c.headTimelyValidators[activeValidatorIndex]; !exists {
|
||||
if _, exists := c.participations[activeValidatorIndex]; exists {
|
||||
c.summary.NonHeadTimelyValidators = append(c.summary.NonHeadTimelyValidators, c.participations[activeValidatorIndex])
|
||||
}
|
||||
}
|
||||
if _, exists := targetCorrectValidators[activeValidatorIndex]; !exists {
|
||||
if _, exists := participations[activeValidatorIndex]; exists {
|
||||
c.summary.NonTargetCorrectValidators = append(c.summary.NonTargetCorrectValidators, participations[activeValidatorIndex])
|
||||
if _, exists := c.targetCorrectValidators[activeValidatorIndex]; !exists {
|
||||
if _, exists := c.participations[activeValidatorIndex]; exists {
|
||||
c.summary.NonTargetCorrectValidators = append(c.summary.NonTargetCorrectValidators, c.participations[activeValidatorIndex])
|
||||
}
|
||||
}
|
||||
if _, exists := sourceTimelyValidators[activeValidatorIndex]; !exists {
|
||||
if _, exists := participations[activeValidatorIndex]; exists {
|
||||
c.summary.NonSourceTimelyValidators = append(c.summary.NonSourceTimelyValidators, participations[activeValidatorIndex])
|
||||
if _, exists := c.sourceTimelyValidators[activeValidatorIndex]; !exists {
|
||||
if _, exists := c.participations[activeValidatorIndex]; exists {
|
||||
c.summary.NonSourceTimelyValidators = append(c.summary.NonSourceTimelyValidators, c.participations[activeValidatorIndex])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,24 +205,8 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
func (c *command) processSlots(ctx context.Context,
|
||||
firstSlot phase0.Slot,
|
||||
lastSlot phase0.Slot,
|
||||
) (
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
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{})
|
||||
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{})
|
||||
) error {
|
||||
allCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
participations := make(map[phase0.ValidatorIndex]*attestingValidator)
|
||||
|
||||
// Need a cache of beacon block headers to reduce lookup times.
|
||||
headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider)
|
||||
@@ -224,7 +214,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 nil, nil, nil, nil, nil, nil, nil, 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", slot))
|
||||
}
|
||||
if block == nil {
|
||||
// No block at this slot; that's fine.
|
||||
@@ -232,16 +222,16 @@ func (c *command) processSlots(ctx context.Context,
|
||||
}
|
||||
slot, err := block.Slot()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, err
|
||||
return err
|
||||
}
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, err
|
||||
return err
|
||||
}
|
||||
for _, attestation := range attestations {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "failed to obtain attestation data")
|
||||
return errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
if attestationData.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestationData.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
|
||||
// Outside of this epoch's range.
|
||||
@@ -253,7 +243,7 @@ func (c *command) processSlots(ctx context.Context,
|
||||
State: fmt.Sprintf("%d", attestationData.Slot),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestationData.Slot))
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestationData.Slot))
|
||||
}
|
||||
for _, beaconCommittee := range response.Data {
|
||||
if _, exists := allCommittees[beaconCommittee.Slot]; !exists {
|
||||
@@ -270,94 +260,71 @@ func (c *command) processSlots(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := participations[index]; !exists {
|
||||
participations[index] = &attestingValidator{
|
||||
Validator: index,
|
||||
Slot: beaconCommittee.Slot,
|
||||
Committee: beaconCommittee.Index,
|
||||
if _, exists := c.participations[index]; !exists {
|
||||
c.participations[index] = &attestingValidator{
|
||||
Validator: index,
|
||||
EffectiveBalance: c.validatorInfo[index].Validator.EffectiveBalance,
|
||||
Slot: beaconCommittee.Slot,
|
||||
Committee: beaconCommittee.Index,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
slotCommittees = allCommittees[attestationData.Slot]
|
||||
}
|
||||
if attestation.Version >= spec.DataVersionElectra {
|
||||
participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys, err = c.extractElectraAttestationData(
|
||||
ctx, attestation, attestationData, slotCommittees, slot, headersCache, participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
} else {
|
||||
participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys, err = c.extractPhase0AttestationData(
|
||||
ctx, attestation, attestationData, slotCommittees, slot, headersCache, participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
if err := c.extractAttestationData(ctx, attestation, attestationData, slotCommittees, slot, headersCache); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return votes,
|
||||
headCorrects,
|
||||
headTimelys,
|
||||
sourceTimelys,
|
||||
targetCorrects,
|
||||
targetTimelys,
|
||||
participations,
|
||||
nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) extractPhase0AttestationData(ctx context.Context,
|
||||
func (c *command) extractAttestationData(ctx context.Context,
|
||||
attestation *spec.VersionedAttestation,
|
||||
attestationData *phase0.AttestationData,
|
||||
slotCommittees map[phase0.CommitteeIndex][]phase0.ValidatorIndex,
|
||||
slot phase0.Slot,
|
||||
headersCache *util.BeaconBlockHeaderCache,
|
||||
participations map[phase0.ValidatorIndex]*attestingValidator,
|
||||
votes map[phase0.ValidatorIndex]struct{},
|
||||
headCorrects map[phase0.ValidatorIndex]struct{},
|
||||
headTimelys map[phase0.ValidatorIndex]struct{},
|
||||
sourceTimelys map[phase0.ValidatorIndex]struct{},
|
||||
targetCorrects map[phase0.ValidatorIndex]struct{},
|
||||
targetTimelys map[phase0.ValidatorIndex]struct{},
|
||||
) (
|
||||
map[phase0.ValidatorIndex]*attestingValidator,
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
error,
|
||||
) {
|
||||
committee := slotCommittees[attestationData.Index]
|
||||
|
||||
) error {
|
||||
inclusionDistance := slot - attestationData.Slot
|
||||
|
||||
head, err := util.AttestationHead(ctx, headersCache, attestation)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, err
|
||||
return err
|
||||
}
|
||||
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, err
|
||||
return err
|
||||
}
|
||||
target, err := util.AttestationTarget(ctx, headersCache, c.chainTime, attestation)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, err
|
||||
return err
|
||||
}
|
||||
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
committee := slotCommittees[attestationData.Index]
|
||||
// Update with all of the committees if we have committee bits (from Electra onwards).
|
||||
committeeBits, err := attestation.CommitteeBits()
|
||||
if err == nil {
|
||||
committee = make([]phase0.ValidatorIndex, 0)
|
||||
for _, index := range committeeBits.BitIndices() {
|
||||
committee = append(committee, slotCommittees[phase0.CommitteeIndex(index)]...)
|
||||
}
|
||||
}
|
||||
|
||||
aggregationBits, err := attestation.AggregationBits()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "failed to obtain aggregation bits")
|
||||
return errors.Wrap(err, "failed to obtain aggregation bits")
|
||||
}
|
||||
|
||||
for i := range aggregationBits.Len() {
|
||||
if aggregationBits.BitAt(i) {
|
||||
validatorIndex := committee[int(i)]
|
||||
validatorIndex := committee[i]
|
||||
if len(c.validators) > 0 {
|
||||
if _, exists := c.validators[validatorIndex]; !exists {
|
||||
// Not one of our validators.
|
||||
@@ -366,140 +333,43 @@ func (c *command) extractPhase0AttestationData(ctx context.Context,
|
||||
}
|
||||
|
||||
// Only set the information from the first attestation we find for this validator.
|
||||
if participations[validatorIndex].InclusionSlot == 0 {
|
||||
participations[validatorIndex].HeadVote = &attestationData.BeaconBlockRoot
|
||||
participations[validatorIndex].Head = &head
|
||||
participations[validatorIndex].TargetVote = &attestationData.Target.Root
|
||||
participations[validatorIndex].Target = &target
|
||||
participations[validatorIndex].InclusionSlot = slot
|
||||
if c.participations[validatorIndex].InclusionSlot == 0 {
|
||||
c.participations[validatorIndex].HeadVote = &attestationData.BeaconBlockRoot
|
||||
c.participations[validatorIndex].Head = &head
|
||||
c.participations[validatorIndex].TargetVote = &attestationData.Target.Root
|
||||
c.participations[validatorIndex].Target = &target
|
||||
c.participations[validatorIndex].InclusionSlot = slot
|
||||
}
|
||||
|
||||
votes[validatorIndex] = struct{}{}
|
||||
if _, exists := headCorrects[validatorIndex]; !exists && headCorrect {
|
||||
headCorrects[validatorIndex] = struct{}{}
|
||||
validatorBalance := big.NewInt(int64(c.validatorInfo[validatorIndex].Validator.EffectiveBalance))
|
||||
if _, exists := c.participatingValidators[validatorIndex]; !exists {
|
||||
c.summary.ParticipatingBalance = c.summary.ParticipatingBalance.Add(c.summary.ParticipatingBalance, validatorBalance)
|
||||
c.participatingValidators[validatorIndex] = struct{}{}
|
||||
}
|
||||
if _, exists := headTimelys[validatorIndex]; !exists && headCorrect && inclusionDistance == 1 {
|
||||
headTimelys[validatorIndex] = struct{}{}
|
||||
if _, exists := c.headCorrectValidators[validatorIndex]; !exists && headCorrect {
|
||||
c.headCorrectValidators[validatorIndex] = struct{}{}
|
||||
c.summary.HeadCorrectBalance = c.summary.HeadCorrectBalance.Add(c.summary.HeadCorrectBalance, validatorBalance)
|
||||
}
|
||||
if _, exists := sourceTimelys[validatorIndex]; !exists && inclusionDistance <= 5 {
|
||||
sourceTimelys[validatorIndex] = struct{}{}
|
||||
if _, exists := c.headTimelyValidators[validatorIndex]; !exists && headCorrect && inclusionDistance == 1 {
|
||||
c.headTimelyValidators[validatorIndex] = struct{}{}
|
||||
c.summary.HeadTimelyBalance = c.summary.HeadTimelyBalance.Add(c.summary.HeadTimelyBalance, validatorBalance)
|
||||
}
|
||||
if _, exists := targetCorrects[validatorIndex]; !exists && targetCorrect {
|
||||
targetCorrects[validatorIndex] = struct{}{}
|
||||
if _, exists := c.sourceTimelyValidators[validatorIndex]; !exists && inclusionDistance <= 5 {
|
||||
c.sourceTimelyValidators[validatorIndex] = struct{}{}
|
||||
c.summary.SourceTimelyBalance = c.summary.SourceTimelyBalance.Add(c.summary.SourceTimelyBalance, validatorBalance)
|
||||
}
|
||||
if _, exists := targetTimelys[validatorIndex]; !exists && targetCorrect && inclusionDistance <= 32 {
|
||||
targetTimelys[validatorIndex] = struct{}{}
|
||||
if _, exists := c.targetCorrectValidators[validatorIndex]; !exists && targetCorrect {
|
||||
c.targetCorrectValidators[validatorIndex] = struct{}{}
|
||||
c.summary.TargetCorrectBalance = c.summary.TargetCorrectBalance.Add(c.summary.TargetCorrectBalance, validatorBalance)
|
||||
}
|
||||
if _, exists := c.targetTimelyValidators[validatorIndex]; !exists && targetCorrect && inclusionDistance <= 32 {
|
||||
c.targetTimelyValidators[validatorIndex] = struct{}{}
|
||||
c.summary.TargetTimelyBalance = c.summary.TargetTimelyBalance.Add(c.summary.TargetTimelyBalance, validatorBalance)
|
||||
}
|
||||
}
|
||||
}
|
||||
return participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys, err
|
||||
}
|
||||
|
||||
func (c *command) extractElectraAttestationData(ctx context.Context,
|
||||
attestation *spec.VersionedAttestation,
|
||||
attestationData *phase0.AttestationData,
|
||||
slotCommittees map[phase0.CommitteeIndex][]phase0.ValidatorIndex,
|
||||
slot phase0.Slot,
|
||||
headersCache *util.BeaconBlockHeaderCache,
|
||||
participations map[phase0.ValidatorIndex]*attestingValidator,
|
||||
votes map[phase0.ValidatorIndex]struct{},
|
||||
headCorrects map[phase0.ValidatorIndex]struct{},
|
||||
headTimelys map[phase0.ValidatorIndex]struct{},
|
||||
sourceTimelys map[phase0.ValidatorIndex]struct{},
|
||||
targetCorrects map[phase0.ValidatorIndex]struct{},
|
||||
targetTimelys map[phase0.ValidatorIndex]struct{},
|
||||
) (
|
||||
map[phase0.ValidatorIndex]*attestingValidator,
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
error,
|
||||
) {
|
||||
committeeBits, err := attestation.CommitteeBits()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "failed to obtain committee bits")
|
||||
}
|
||||
for _, committeeIndex := range committeeBits.BitIndices() {
|
||||
committee := slotCommittees[phase0.CommitteeIndex(committeeIndex)]
|
||||
|
||||
inclusionDistance := slot - attestationData.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 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 nil, nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
aggregationBits, err := attestation.AggregationBits()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, errors.Wrap(err, "failed to obtain aggregation bits")
|
||||
}
|
||||
// Calculate the offset for the committee so we can extract the validator from the aggregate_bits.
|
||||
committeeOffset := calcCommitteeOffset(phase0.CommitteeIndex(committeeIndex), slotCommittees)
|
||||
|
||||
// Range over the committee rather than the aggregate_bits as it's the smaller set.
|
||||
for i := range committee {
|
||||
aggregateIndex := committeeOffset + uint64(i)
|
||||
if aggregationBits.BitAt(aggregateIndex) {
|
||||
validatorIndex := committee[i]
|
||||
if len(c.validators) > 0 {
|
||||
if _, exists := c.validators[validatorIndex]; !exists {
|
||||
// Not one of our validators.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Only set the information from the first attestation we find for this validator.
|
||||
if participations[validatorIndex].InclusionSlot == 0 {
|
||||
participations[validatorIndex].HeadVote = &attestationData.BeaconBlockRoot
|
||||
participations[validatorIndex].Head = &head
|
||||
participations[validatorIndex].TargetVote = &attestationData.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[validatorIndex]; !exists && inclusionDistance <= 5 {
|
||||
sourceTimelys[validatorIndex] = struct{}{}
|
||||
}
|
||||
if _, exists := targetCorrects[validatorIndex]; !exists && targetCorrect {
|
||||
targetCorrects[validatorIndex] = struct{}{}
|
||||
}
|
||||
if _, exists := targetTimelys[validatorIndex]; !exists && targetCorrect && inclusionDistance <= 32 {
|
||||
targetTimelys[validatorIndex] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys, err
|
||||
}
|
||||
|
||||
func calcCommitteeOffset(committeeIndex phase0.CommitteeIndex, slotCommittees map[phase0.CommitteeIndex][]phase0.ValidatorIndex) uint64 {
|
||||
var total uint64
|
||||
for i := range committeeIndex {
|
||||
total += uint64(len(slotCommittees[i]))
|
||||
}
|
||||
return total
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
|
||||
@@ -556,8 +426,10 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
|
||||
for i := range aggregate.SyncCommitteeBits.Len() {
|
||||
validatorIndex := committee.Validators[int(i)]
|
||||
if _, exists := c.validators[validatorIndex]; !exists {
|
||||
// Not one of ours.
|
||||
continue
|
||||
if len(c.validators) > 0 {
|
||||
// Not one of ours.
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !aggregate.SyncCommitteeBits.BitAt(i) {
|
||||
missed[validatorIndex]++
|
||||
@@ -657,6 +529,8 @@ func (c *command) processBlobs(ctx context.Context) error {
|
||||
c.summary.Blobs += len(block.Deneb.Message.Body.BlobKZGCommitments)
|
||||
case spec.DataVersionElectra:
|
||||
c.summary.Blobs += len(block.Electra.Message.Body.BlobKZGCommitments)
|
||||
case spec.DataVersionFulu:
|
||||
c.summary.Blobs += len(block.Fulu.Message.Body.BlobKZGCommitments)
|
||||
default:
|
||||
return fmt.Errorf("unhandled block version %v", block.Version)
|
||||
}
|
||||
|
||||
@@ -72,32 +72,35 @@ In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.
|
||||
errCheck(err, "Failed to obtain beacon chain genesis")
|
||||
genesis := genesisResponse.Data
|
||||
|
||||
response, err := eth2Client.(consensusclient.ForkProvider).Fork(ctx, &api.ForkOpts{State: "head"})
|
||||
errCheck(err, "Failed to obtain fork information")
|
||||
response, err := eth2Client.(consensusclient.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
errCheck(err, "Failed to obtain spec information")
|
||||
|
||||
// Check against current and prior fork versions.
|
||||
// Check against Capella fork version (EIP-7044)
|
||||
signatureBytes := make([]byte, 96)
|
||||
copy(signatureBytes, signedOp.Signature[:])
|
||||
sig, err := e2types.BLSSignatureFromBytes(signatureBytes)
|
||||
errCheck(err, "Invalid signature")
|
||||
|
||||
verified := false
|
||||
|
||||
// Try with the current fork.
|
||||
domain := phase0.Domain{}
|
||||
currentExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, response.Data.CurrentVersion[:], genesis.GenesisValidatorsRoot[:])
|
||||
errCheck(err, "Failed to compute domain")
|
||||
copy(domain[:], currentExitDomain)
|
||||
verified, err = util.VerifyRoot(account, opRoot, domain, sig)
|
||||
errCheck(err, "Failed to verify voluntary exit")
|
||||
if !verified {
|
||||
// Try again with the previous fork.
|
||||
previousExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, response.Data.PreviousVersion[:], genesis.GenesisValidatorsRoot[:])
|
||||
copy(domain[:], previousExitDomain)
|
||||
errCheck(err, "Failed to compute domain")
|
||||
verified, err = util.VerifyRoot(account, opRoot, domain, sig)
|
||||
errCheck(err, "Failed to verify voluntary exit")
|
||||
forkRaw, ok := response.Data["CAPELLA_FORK_VERSION"]
|
||||
if !ok {
|
||||
err = errors.New("failed to obtain Capella fork version")
|
||||
}
|
||||
errCheck(err, "Failed to obtain fork version")
|
||||
|
||||
fork, ok := forkRaw.(phase0.Version)
|
||||
if !ok {
|
||||
err = errors.New("fork version is not of a valid type")
|
||||
}
|
||||
errCheck(err, "Failed to obtain fork version")
|
||||
|
||||
exitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork[:], genesis.GenesisValidatorsRoot[:])
|
||||
errCheck(err, "Failed to compute domain")
|
||||
|
||||
copy(domain[:], exitDomain)
|
||||
verified, err := util.VerifyRoot(account, opRoot, domain, sig)
|
||||
errCheck(err, "Failed to verify voluntary exit")
|
||||
|
||||
assert(verified, "Voluntary exit failed to verify against current and previous fork versions")
|
||||
|
||||
outputIf(viper.GetBool("verbose"), "Verified")
|
||||
|
||||
@@ -19,7 +19,8 @@ import (
|
||||
"fmt"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -28,7 +29,10 @@ func process(ctx context.Context, data *dataIn) error {
|
||||
return errors.New("no data")
|
||||
}
|
||||
|
||||
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, data.topics, eventHandler)
|
||||
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, &api.EventsOpts{
|
||||
Topics: data.topics,
|
||||
Handler: eventHandler,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect for events")
|
||||
}
|
||||
@@ -38,7 +42,7 @@ func process(ctx context.Context, data *dataIn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func eventHandler(event *api.Event) {
|
||||
func eventHandler(event *apiv1.Event) {
|
||||
if event.Data == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,6 +127,13 @@ func (c *command) process(ctx context.Context) error {
|
||||
} else {
|
||||
c.inclusions = append(c.inclusions, 2)
|
||||
}
|
||||
case spec.DataVersionFulu:
|
||||
aggregate = block.Fulu.Message.Body.SyncAggregate
|
||||
if aggregate.SyncCommitteeBits.BitAt(c.committeeIndex) {
|
||||
c.inclusions = append(c.inclusions, 1)
|
||||
} else {
|
||||
c.inclusions = append(c.inclusions, 2)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unhandled block version %v", block.Version)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
|
||||
// ReleaseVersion is the release version of the codebase.
|
||||
// Usually overridden by tag names when building binaries.
|
||||
var ReleaseVersion = "local build (latest release 1.37.3)"
|
||||
var ReleaseVersion = "local build (latest release 1.39.0)"
|
||||
|
||||
// versionCmd represents the version command.
|
||||
var versionCmd = &cobra.Command{
|
||||
|
||||
12
go.mod
12
go.mod
@@ -5,7 +5,7 @@ go 1.23.0
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
github.com/attestantio/go-eth2-client v0.24.1
|
||||
github.com/attestantio/go-eth2-client v0.27.1
|
||||
github.com/ferranbt/fastssz v0.1.4
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -27,13 +27,13 @@ require (
|
||||
github.com/wealdtech/go-ecodec v1.1.4
|
||||
github.com/wealdtech/go-eth2-types/v2 v2.8.2
|
||||
github.com/wealdtech/go-eth2-util v1.8.2
|
||||
github.com/wealdtech/go-eth2-wallet v1.17.0
|
||||
github.com/wealdtech/go-eth2-wallet v1.17.2
|
||||
github.com/wealdtech/go-eth2-wallet-dirk v1.5.1
|
||||
github.com/wealdtech/go-eth2-wallet-distributed v1.2.1
|
||||
github.com/wealdtech/go-eth2-wallet-distributed v1.2.2
|
||||
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1
|
||||
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.1
|
||||
github.com/wealdtech/go-eth2-wallet-keystore v1.0.0
|
||||
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.5.0
|
||||
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.2
|
||||
github.com/wealdtech/go-eth2-wallet-keystore v1.0.2
|
||||
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.5.1
|
||||
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.18.1
|
||||
github.com/wealdtech/go-eth2-wallet-store-s3 v1.12.0
|
||||
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.2
|
||||
|
||||
24
go.sum
24
go.sum
@@ -1,5 +1,5 @@
|
||||
github.com/attestantio/go-eth2-client v0.24.1 h1:DZ/2O83eUcSfPPs63xF6fdXDe4afA4nlt5j0y2cweOI=
|
||||
github.com/attestantio/go-eth2-client v0.24.1/go.mod h1:/KTLN3WuH1xrJL7ZZrpBoWM1xCCihnFbzequD5L+83o=
|
||||
github.com/attestantio/go-eth2-client v0.27.1 h1:g7bm+gG/p+gfzYdEuxuAepVWYb8EO+2KojV5/Lo2BxM=
|
||||
github.com/attestantio/go-eth2-client v0.27.1/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY=
|
||||
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
|
||||
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -164,22 +164,22 @@ github.com/wealdtech/go-eth2-types/v2 v2.8.2 h1:b5aXlNBLKgjAg/Fft9VvGlqAUCQMP5Lz
|
||||
github.com/wealdtech/go-eth2-types/v2 v2.8.2/go.mod h1:IAz9Lz1NVTaHabQa+4zjk2QDKMv8LVYo0n46M9o/TXw=
|
||||
github.com/wealdtech/go-eth2-util v1.8.2 h1:gq+JMrnadifyKadUr75wmfP7+usiqMu9t3VVoob5Dvo=
|
||||
github.com/wealdtech/go-eth2-util v1.8.2/go.mod h1:/80GAK0K/3+PqUBZHvaOPd3b1sjHeimxQh1nrJzgaPk=
|
||||
github.com/wealdtech/go-eth2-wallet v1.17.0 h1:hMjGRjvpk95gguW6UXFDkRHWjYqE0cdrO7cOClF9Ubo=
|
||||
github.com/wealdtech/go-eth2-wallet v1.17.0/go.mod h1:qMmDrx//GrdZ3q+0Jf9SNwCaLsFOxOmXgr1yptpSMIE=
|
||||
github.com/wealdtech/go-eth2-wallet v1.17.2 h1:tFkWddJwH8Iq3H9K1Fnp4avxNn+4qbE3Go7k81a/c1U=
|
||||
github.com/wealdtech/go-eth2-wallet v1.17.2/go.mod h1:CMtJ9IpvrkW2lD3B6ZAn3q/uALcxCBPBliFpIovV8+4=
|
||||
github.com/wealdtech/go-eth2-wallet-dirk v1.5.1 h1:h1wZK31yonLkwddajg+Prhhd2rrvIIxQ3HxwZ3udnaY=
|
||||
github.com/wealdtech/go-eth2-wallet-dirk v1.5.1/go.mod h1:Yz1Mc+HfbG1CODeBpAQ++/Us76OdXzI5kVs1qGvUiBM=
|
||||
github.com/wealdtech/go-eth2-wallet-distributed v1.2.1 h1:+pbG9i9b5TrWd7GDRX8yq4FKA+D7k7aI6uySEvAZ+Kk=
|
||||
github.com/wealdtech/go-eth2-wallet-distributed v1.2.1/go.mod h1:jYkDax2VhUNKIct6TVlgxAagvR56/eg7y7J+JFq+gDo=
|
||||
github.com/wealdtech/go-eth2-wallet-distributed v1.2.2 h1:O6nfhMRTUpblOzj8KiCLFgQAyAqrv2dweorzss/V6PU=
|
||||
github.com/wealdtech/go-eth2-wallet-distributed v1.2.2/go.mod h1:jYkDax2VhUNKIct6TVlgxAagvR56/eg7y7J+JFq+gDo=
|
||||
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1 h1:9j7bpwjT9wmwBb54ZkBhTm1uNIlFFcCJXefd/YskZPw=
|
||||
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1/go.mod h1:+tI1VD76E1WINI+Nstg7RVGpUolL5ql10nu2YztMO/4=
|
||||
github.com/wealdtech/go-eth2-wallet-encryptor-unencrypted v1.0.2 h1:IMIyl70hbJlxOkgTcCK//3vKe5ylhGIk6oUlIlK9xp0=
|
||||
github.com/wealdtech/go-eth2-wallet-encryptor-unencrypted v1.0.2/go.mod h1:T8nyAscWIWNcNa6EG/19PwH/OCt2Ly7Orn5okmiuSP4=
|
||||
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.1 h1:CrcPeJhMcNxSW+GAJwtpXz3mtGJjx4p9ykLlKvwZZZ4=
|
||||
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.1/go.mod h1:aWgnEi07w1L9wMBRB69sYvoEONppAUly6FDQRWQGqH8=
|
||||
github.com/wealdtech/go-eth2-wallet-keystore v1.0.0 h1:DYR6TAyi7RxXoAanLSPdiufGxCX617BQwWOdCxHqHX4=
|
||||
github.com/wealdtech/go-eth2-wallet-keystore v1.0.0/go.mod h1:6DGINunnasS9y9F7KH3ya2h74fHWgSCfP3dAJWe4A6U=
|
||||
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.5.0 h1:vphAFklkYMRJVo9f5rVWly7PECHrLS4yarjemBa7fRM=
|
||||
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.5.0/go.mod h1:kBZUZogqwvvxulEvXi5l6OjZyd7EBmCKxce5Q+lW7fs=
|
||||
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.2 h1:7wB8j12LVdUR/IFLmwTxdXfuTvpXSn4yj+ZD1OhDSJY=
|
||||
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.2/go.mod h1:aWgnEi07w1L9wMBRB69sYvoEONppAUly6FDQRWQGqH8=
|
||||
github.com/wealdtech/go-eth2-wallet-keystore v1.0.2 h1:OseWEBvr13voALVCdg7ojsU3Kly/FPR9sCadnsx3/tM=
|
||||
github.com/wealdtech/go-eth2-wallet-keystore v1.0.2/go.mod h1:SjHHqYS0IragcGcOkbFqjX0lIxRe4d0mE7tPiR+R7HI=
|
||||
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.5.1 h1:bSdDCn+o4wq5MHogGkUtqbPp6Z7Tndt2qBb1zjof96Y=
|
||||
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.5.1/go.mod h1:kBZUZogqwvvxulEvXi5l6OjZyd7EBmCKxce5Q+lW7fs=
|
||||
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.18.1 h1:Ceq74WL57jdBQnrZJFJyGRBKOOFI5wwq9VoxeAbjoEk=
|
||||
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.18.1/go.mod h1:woTpldN8qThnmya/0yeD+a3u/3Zj42u6/ijgF9CGaz8=
|
||||
github.com/wealdtech/go-eth2-wallet-store-s3 v1.12.0 h1:noknYCbHw2soPhwke1LvC99Kk/2CLN787KcgxdZ7OGo=
|
||||
|
||||
@@ -57,6 +57,11 @@ func ParseValidators(ctx context.Context, validatorsProvider eth2client.Validato
|
||||
}
|
||||
}
|
||||
|
||||
if len(validators) == 0 && len(indices) == 0 {
|
||||
// Nothing to obtain.
|
||||
return validators, nil
|
||||
}
|
||||
|
||||
response, err := validatorsProvider.Validators(ctx, &api.ValidatorsOpts{State: stateID, Indices: indices})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to obtain validators %v", indices))
|
||||
|
||||
Reference in New Issue
Block a user