Compare commits

...

19 Commits

Author SHA1 Message Date
Chris Berry
b450e96dde Merge pull request #174 from wealdtech/update-docker-workflow
Update docker build and push workflow
2025-10-14 13:23:16 +03:00
Miguel Tenorio
30d7f6989a Update docker build and push workflow 2025-10-13 22:55:32 +08:00
Chris Berry
00ea75e5c8 Merge pull request #173 from wealdtech/fulu
Fulu
2025-10-13 17:41:09 +03:00
Chris Berry
34647927ab Merge branch 'chris_fulu' into fulu
# Conflicts:
#	go.mod
#	go.sum
2025-10-13 15:24:36 +01:00
Chris Berry
44cfb68e2c Update for fulu release. 2025-10-13 15:23:11 +01:00
Hoanh An
70df67e6ab Bump go-eth2-client fulu version 2025-09-03 17:56:38 -07:00
Chris Berry
08fb16ff9e Fulu changes. 2025-08-06 16:48:32 +01:00
Chris Berry
903ecc8581 Merge pull request #170 from ahmohamed/master
Update version to 1.38.0
2025-07-08 12:25:38 +01:00
Ahmed Mohamed
93f4f6d68c update version 2025-07-08 21:23:05 +10:00
Chris Berry
1eee5a1349 Merge pull request #169 from ahmohamed/patch-1
Update changelog
2025-07-08 11:52:53 +01:00
Ahmed Mohamed
9c4e9bcb2f Update changelog 2025-07-08 20:27:32 +10:00
Chris Berry
83a950e5d1 Merge pull request #166 from samcm/feat/bump-goeth2
fix: update go-eth2-client to v0.26.0
2025-07-04 10:12:30 +01:00
Sam Calder-Mason
b2c0ae3fa2 fix: update go-eth2-client to v0.26.0 and adapt event handling 2025-07-04 18:26:49 +10:00
Jim McDonald
3059aa270f Update version. 2025-05-13 20:58:32 +01:00
Jim McDonald
8b7aab7180 Merge pull request #165 from aimxhaisse/fix/eip-7044
Support of EIP-7044 in exit verify cmd
2025-04-07 09:56:39 +01:00
Jim McDonald
c46483586d Add ETH values to epoch summary. 2025-04-03 10:13:45 +01:00
s. rannou
e4f0b934d7 Support of EIP-7044 in exit verify cmd 2025-04-01 23:28:23 +02:00
Jim McDonald
25a5bd917f Support 0 validators for epoch summary. 2025-03-18 10:42:39 +00:00
Jim McDonald
889a884f6e Do not fetch all validators if no validators to parse. 2025-03-18 10:39:57 +00:00
15 changed files with 427 additions and 325 deletions

View File

@@ -1,58 +1,45 @@
name: Docker 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: on:
push: push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+**'
jobs: jobs:
# Set variables that will be available to all builds. # Build and push the Docker image
env_vars: build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
release_version: ${{ steps.release_version.outputs.release_version }}
binary: ${{ steps.binary.outputs.binary }}
steps: steps:
- id: release_version - name: Check out repository
run: | uses: actions/checkout@v4
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
# Build. # This step extracts the version number from the tag
build: # e.g., if the tag is 'v1.2.3', this will output '1.2.3'
runs-on: ubuntu-latest - name: Extract release version
needs: [env_vars] id: release_version
steps: run: |
- name: Check out repository into the Go module directory echo "version=$(echo ${{ github.ref_name }} | sed -e 's/^v//')" >> $GITHUB_OUTPUT
uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
push: true push: true
tags: wealdtech/ethdo:latest tags: |
wealdtech/ethdo:${{ steps.release_version.outputs.version }}
- name: build and push on release wealdtech/ethdo:latest
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 }}

View File

@@ -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: 1.37.3:
- add "hoodi" to the list of supported networks - add "hoodi" to the list of supported networks

View File

@@ -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.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value c.analysis.Value += c.analysis.SyncCommitee.Value
return nil 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: default:
return fmt.Errorf("unsupported block version %d", block.Version) return fmt.Errorf("unsupported block version %d", block.Version)
} }

View File

@@ -90,6 +90,8 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
err = processDenebBlock(ctx, data, block) err = processDenebBlock(ctx, data, block)
case spec.DataVersionElectra: case spec.DataVersionElectra:
err = processElectraBlock(ctx, data, block) err = processElectraBlock(ctx, data, block)
case spec.DataVersionFulu:
err = processFuluBlock(ctx, data, block)
default: default:
return nil, errors.New("unknown block version") return nil, errors.New("unknown block version")
} }
@@ -103,7 +105,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if !jsonOutput && !sszOutput { if !jsonOutput && !sszOutput {
fmt.Println("") 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 { if err != nil {
return nil, errors.Wrap(err, "failed to start block stream") return nil, errors.Wrap(err, "failed to start block stream")
} }
@@ -212,15 +217,37 @@ func processElectraBlock(ctx context.Context,
return nil return nil
} }
func headEventHandler(event *apiv1.Event) { func processFuluBlock(ctx context.Context,
ctx := context.Background() data *dataIn,
block *spec.VersionedSignedBeaconBlock,
// Only interested in head events. ) error {
if event.Topic != "head" { var blobSidecars []*deneb.BlobSidecar
return 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{ blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: blockID, Block: blockID,
}) })
@@ -267,6 +294,46 @@ func headEventHandler(event *apiv1.Event) {
blobSidecars = blobSidecarsResponse.Data blobSidecars = blobSidecarsResponse.Data
} }
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, block.Deneb, blobSidecars) 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: default:
err = errors.New("unknown block version") err = errors.New("unknown block version")
} }
@@ -428,6 +495,35 @@ func outputElectraBlock(ctx context.Context,
return nil 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) { func timeToBlockID(ctx context.Context, eth2Client eth2client.Service, input string) (string, error) {
var timestamp time.Time var timestamp time.Time

View File

@@ -100,6 +100,9 @@ func (c *command) process(ctx context.Context) error {
case spec.DataVersionElectra: case spec.DataVersionElectra:
c.incumbent = state.Electra.ETH1Data c.incumbent = state.Electra.ETH1Data
c.eth1DataVotes = state.Electra.ETH1DataVotes c.eth1DataVotes = state.Electra.ETH1DataVotes
case spec.DataVersionFulu:
c.incumbent = state.Fulu.ETH1Data
c.eth1DataVotes = state.Fulu.ETH1DataVotes
default: default:
return fmt.Errorf("unhandled beacon state version %v", state.Version) return fmt.Errorf("unhandled beacon state version %v", state.Version)
} }

View File

@@ -15,9 +15,11 @@ package epochsummary
import ( import (
"context" "context"
"math/big"
"time" "time"
eth2client "github.com/attestantio/go-eth2-client" 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"
"github.com/attestantio/go-eth2-client/spec/phase0" "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -52,6 +54,16 @@ type command struct {
beaconCommitteesProvider eth2client.BeaconCommitteesProvider beaconCommitteesProvider eth2client.BeaconCommitteesProvider
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider 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. // Caches.
blocksCache map[string]*spec.VersionedSignedBeaconBlock blocksCache map[string]*spec.VersionedSignedBeaconBlock
@@ -68,12 +80,19 @@ type epochSummary struct {
SyncCommitteeValidators int `json:"sync_committee_validators"` SyncCommitteeValidators int `json:"sync_committee_validators"`
SyncCommittee []*epochSyncCommittee `json:"sync_committees"` SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
ActiveValidators int `json:"active_validators"` ActiveValidators int `json:"active_validators"`
ActiveBalance *big.Int `json:"active_balance"`
ParticipatingValidators int `json:"participating_validators"` ParticipatingValidators int `json:"participating_validators"`
ParticipatingBalance *big.Int `json:"participating_balance"`
HeadCorrectValidators int `json:"head_correct_validators"` HeadCorrectValidators int `json:"head_correct_validators"`
HeadCorrectBalance *big.Int `json:"head_correct_balance"`
HeadTimelyValidators int `json:"head_timely_validators"` HeadTimelyValidators int `json:"head_timely_validators"`
HeadTimelyBalance *big.Int `json:"head_timely_balance"`
SourceTimelyValidators int `json:"source_timely_validators"` SourceTimelyValidators int `json:"source_timely_validators"`
SourceTimelyBalance *big.Int `json:"source_timely_balance"`
TargetCorrectValidators int `json:"target_correct_validators"` TargetCorrectValidators int `json:"target_correct_validators"`
TargetCorrectBalance *big.Int `json:"target_correct_balance"`
TargetTimelyValidators int `json:"target_timely_validators"` TargetTimelyValidators int `json:"target_timely_validators"`
TargetTimelyBalance *big.Int `json:"target_timely_balance"`
NonParticipatingValidators []*attestingValidator `json:"nonparticipating_validators"` NonParticipatingValidators []*attestingValidator `json:"nonparticipating_validators"`
NonHeadCorrectValidators []*attestingValidator `json:"nonheadcorrect_validators"` NonHeadCorrectValidators []*attestingValidator `json:"nonheadcorrect_validators"`
NonHeadTimelyValidators []*attestingValidator `json:"nonheadtimely_validators"` NonHeadTimelyValidators []*attestingValidator `json:"nonheadtimely_validators"`
@@ -95,14 +114,15 @@ type epochSyncCommittee struct {
} }
type attestingValidator struct { type attestingValidator struct {
Validator phase0.ValidatorIndex `json:"validator_index"` Validator phase0.ValidatorIndex `json:"validator_index"`
Slot phase0.Slot `json:"slot"` EffectiveBalance phase0.Gwei `json:"effective_balance"`
Committee phase0.CommitteeIndex `json:"committee_index"` Slot phase0.Slot `json:"slot"`
HeadVote *phase0.Root `json:"head_vote,omitempty"` Committee phase0.CommitteeIndex `json:"committee_index"`
Head *phase0.Root `json:"head,omitempty"` HeadVote *phase0.Root `json:"head_vote,omitempty"`
TargetVote *phase0.Root `json:"target_vote,omitempty"` Head *phase0.Root `json:"head,omitempty"`
Target *phase0.Root `json:"target,omitempty"` TargetVote *phase0.Root `json:"target_vote,omitempty"`
InclusionSlot phase0.Slot `json:"inclusion_slot,omitempty"` Target *phase0.Root `json:"target,omitempty"`
InclusionSlot phase0.Slot `json:"inclusion_slot,omitempty"`
} }
func newCommand(_ context.Context) (*command, error) { func newCommand(_ context.Context) (*command, error) {
@@ -112,10 +132,24 @@ func newCommand(_ context.Context) (*command, error) {
debug: viper.GetBool("debug"), debug: viper.GetBool("debug"),
validatorsStr: viper.GetStringSlice("validators"), validatorsStr: viper.GetStringSlice("validators"),
summary: &epochSummary{ 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{}), validators: make(map[phase0.ValidatorIndex]struct{}),
blocksCache: make(map[string]*spec.VersionedSignedBeaconBlock), 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. // Timeout.

View File

@@ -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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at
@@ -17,6 +17,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/big"
"strings" "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))) gweiToEth := big.NewInt(1e9)
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))) mul := big.NewInt(10000)
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))) participatingBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.ParticipatingBalance, mul), c.summary.ActiveBalance)
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 Attesting balance: %s/%s (%0.2f%%)",
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))) new(big.Int).Div(c.summary.ParticipatingBalance, gweiToEth).String(),
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))) 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 { if c.verbose {
// Sort list by validator index. // Sort list by validator index.
for _, validator := range c.summary.NonParticipatingValidators { for _, validator := range c.summary.NonParticipatingValidators {

View File

@@ -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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at
@@ -16,6 +16,7 @@ package epochsummary
import ( import (
"context" "context"
"fmt" "fmt"
"math/big"
"net/http" "net/http"
"sort" "sort"
@@ -105,12 +106,14 @@ func (c *command) activeValidators(ctx context.Context) (map[phase0.ValidatorInd
} }
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{ response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: "head", State: fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)),
Indices: validatorIndices, Indices: validatorIndices,
}) })
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to obtain validators for epoch") return nil, errors.Wrap(err, "failed to obtain validators for epoch")
} }
c.validatorInfo = response.Data
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator) activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
for _, validator := range response.Data { for _, validator := range response.Data {
_, exists := c.validators[validator.Index] _, exists := c.validators[validator.Index]
@@ -133,6 +136,10 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
} }
c.summary.ActiveValidators = len(activeValidators) 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. // Obtain number of validators that voted for blocks in the epoch.
// These votes can be included anywhere from the second slot of // These votes can be included anywhere from the second slot of
// the epoch to the first slot of the next-but-one epoch. // 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() lastSlot = c.chainTime.CurrentSlot()
} }
participatingValidators, headCorrectValidators, headTimelyValidators, sourceTimelyValidators, targetCorrectValidators, targetTimelyValidators, participations, err := c.processSlots(ctx, firstSlot, lastSlot) if err := c.processSlots(ctx, firstSlot, lastSlot); err != nil {
if err != nil {
return err return err
} }
c.summary.ParticipatingValidators = len(participatingValidators) c.summary.ParticipatingValidators = len(c.participatingValidators)
c.summary.HeadCorrectValidators = len(headCorrectValidators) c.summary.HeadCorrectValidators = len(c.headCorrectValidators)
c.summary.HeadTimelyValidators = len(headTimelyValidators) c.summary.HeadTimelyValidators = len(c.headTimelyValidators)
c.summary.SourceTimelyValidators = len(sourceTimelyValidators) c.summary.SourceTimelyValidators = len(c.sourceTimelyValidators)
c.summary.TargetCorrectValidators = len(targetCorrectValidators) c.summary.TargetCorrectValidators = len(c.targetCorrectValidators)
c.summary.TargetTimelyValidators = len(targetTimelyValidators) 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 { for activeValidatorIndex := range activeValidators {
if _, exists := participatingValidators[activeValidatorIndex]; !exists { if _, exists := c.participatingValidators[activeValidatorIndex]; !exists {
if _, exists := participations[activeValidatorIndex]; exists { if _, exists := c.participations[activeValidatorIndex]; exists {
c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, participations[activeValidatorIndex]) c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, c.participations[activeValidatorIndex])
} }
} }
if _, exists := headCorrectValidators[activeValidatorIndex]; !exists { if _, exists := c.headCorrectValidators[activeValidatorIndex]; !exists {
if _, exists := participations[activeValidatorIndex]; exists { if _, exists := c.participations[activeValidatorIndex]; exists {
c.summary.NonHeadCorrectValidators = append(c.summary.NonHeadCorrectValidators, participations[activeValidatorIndex]) c.summary.NonHeadCorrectValidators = append(c.summary.NonHeadCorrectValidators, c.participations[activeValidatorIndex])
} }
} }
if _, exists := headTimelyValidators[activeValidatorIndex]; !exists { if _, exists := c.headTimelyValidators[activeValidatorIndex]; !exists {
if _, exists := participations[activeValidatorIndex]; exists { if _, exists := c.participations[activeValidatorIndex]; exists {
c.summary.NonHeadTimelyValidators = append(c.summary.NonHeadTimelyValidators, participations[activeValidatorIndex]) c.summary.NonHeadTimelyValidators = append(c.summary.NonHeadTimelyValidators, c.participations[activeValidatorIndex])
} }
} }
if _, exists := targetCorrectValidators[activeValidatorIndex]; !exists { if _, exists := c.targetCorrectValidators[activeValidatorIndex]; !exists {
if _, exists := participations[activeValidatorIndex]; exists { if _, exists := c.participations[activeValidatorIndex]; exists {
c.summary.NonTargetCorrectValidators = append(c.summary.NonTargetCorrectValidators, participations[activeValidatorIndex]) c.summary.NonTargetCorrectValidators = append(c.summary.NonTargetCorrectValidators, c.participations[activeValidatorIndex])
} }
} }
if _, exists := sourceTimelyValidators[activeValidatorIndex]; !exists { if _, exists := c.sourceTimelyValidators[activeValidatorIndex]; !exists {
if _, exists := participations[activeValidatorIndex]; exists { if _, exists := c.participations[activeValidatorIndex]; exists {
c.summary.NonSourceTimelyValidators = append(c.summary.NonSourceTimelyValidators, participations[activeValidatorIndex]) 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, func (c *command) processSlots(ctx context.Context,
firstSlot phase0.Slot, firstSlot phase0.Slot,
lastSlot phase0.Slot, lastSlot phase0.Slot,
) ( ) error {
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{})
allCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex) 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. // Need a cache of beacon block headers to reduce lookup times.
headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider) headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider)
@@ -224,7 +214,7 @@ func (c *command) processSlots(ctx context.Context,
for slot := firstSlot; slot <= lastSlot; slot++ { for slot := firstSlot; slot <= lastSlot; slot++ {
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", slot)) block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil { 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 { if block == nil {
// No block at this slot; that's fine. // No block at this slot; that's fine.
@@ -232,16 +222,16 @@ func (c *command) processSlots(ctx context.Context,
} }
slot, err := block.Slot() slot, err := block.Slot()
if err != nil { if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err return err
} }
attestations, err := block.Attestations() attestations, err := block.Attestations()
if err != nil { if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err return err
} }
for _, attestation := range attestations { for _, attestation := range attestations {
attestationData, err := attestation.Data() attestationData, err := attestation.Data()
if err != nil { 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) { if attestationData.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestationData.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
// Outside of this epoch's range. // Outside of this epoch's range.
@@ -253,7 +243,7 @@ func (c *command) processSlots(ctx context.Context,
State: fmt.Sprintf("%d", attestationData.Slot), State: fmt.Sprintf("%d", attestationData.Slot),
}) })
if err != nil { 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 { for _, beaconCommittee := range response.Data {
if _, exists := allCommittees[beaconCommittee.Slot]; !exists { if _, exists := allCommittees[beaconCommittee.Slot]; !exists {
@@ -270,94 +260,71 @@ func (c *command) processSlots(ctx context.Context,
} }
} }
if _, exists := participations[index]; !exists { if _, exists := c.participations[index]; !exists {
participations[index] = &attestingValidator{ c.participations[index] = &attestingValidator{
Validator: index, Validator: index,
Slot: beaconCommittee.Slot, EffectiveBalance: c.validatorInfo[index].Validator.EffectiveBalance,
Committee: beaconCommittee.Index, Slot: beaconCommittee.Slot,
Committee: beaconCommittee.Index,
} }
} }
} }
} }
slotCommittees = allCommittees[attestationData.Slot] slotCommittees = allCommittees[attestationData.Slot]
} }
if attestation.Version >= spec.DataVersionElectra { if err := c.extractAttestationData(ctx, attestation, attestationData, slotCommittees, slot, headersCache); err != nil {
participations, votes, headCorrects, headTimelys, sourceTimelys, targetCorrects, targetTimelys, err = c.extractElectraAttestationData( return err
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
}
} }
} }
} }
return votes, return nil
headCorrects,
headTimelys,
sourceTimelys,
targetCorrects,
targetTimelys,
participations,
nil
} }
func (c *command) extractPhase0AttestationData(ctx context.Context, func (c *command) extractAttestationData(ctx context.Context,
attestation *spec.VersionedAttestation, attestation *spec.VersionedAttestation,
attestationData *phase0.AttestationData, attestationData *phase0.AttestationData,
slotCommittees map[phase0.CommitteeIndex][]phase0.ValidatorIndex, slotCommittees map[phase0.CommitteeIndex][]phase0.ValidatorIndex,
slot phase0.Slot, slot phase0.Slot,
headersCache *util.BeaconBlockHeaderCache, headersCache *util.BeaconBlockHeaderCache,
participations map[phase0.ValidatorIndex]*attestingValidator, ) error {
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]
inclusionDistance := slot - attestationData.Slot inclusionDistance := slot - attestationData.Slot
head, err := util.AttestationHead(ctx, headersCache, attestation) head, err := util.AttestationHead(ctx, headersCache, attestation)
if err != nil { if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err return err
} }
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation) headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
if err != nil { if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err return err
} }
target, err := util.AttestationTarget(ctx, headersCache, c.chainTime, attestation) target, err := util.AttestationTarget(ctx, headersCache, c.chainTime, attestation)
if err != nil { if err != nil {
return nil, nil, nil, nil, nil, nil, nil, err return err
} }
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation) targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
if err != nil { 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() aggregationBits, err := attestation.AggregationBits()
if err != nil { 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() { for i := range aggregationBits.Len() {
if aggregationBits.BitAt(i) { if aggregationBits.BitAt(i) {
validatorIndex := committee[int(i)] validatorIndex := committee[i]
if len(c.validators) > 0 { if len(c.validators) > 0 {
if _, exists := c.validators[validatorIndex]; !exists { if _, exists := c.validators[validatorIndex]; !exists {
// Not one of our validators. // 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. // Only set the information from the first attestation we find for this validator.
if participations[validatorIndex].InclusionSlot == 0 { if c.participations[validatorIndex].InclusionSlot == 0 {
participations[validatorIndex].HeadVote = &attestationData.BeaconBlockRoot c.participations[validatorIndex].HeadVote = &attestationData.BeaconBlockRoot
participations[validatorIndex].Head = &head c.participations[validatorIndex].Head = &head
participations[validatorIndex].TargetVote = &attestationData.Target.Root c.participations[validatorIndex].TargetVote = &attestationData.Target.Root
participations[validatorIndex].Target = &target c.participations[validatorIndex].Target = &target
participations[validatorIndex].InclusionSlot = slot c.participations[validatorIndex].InclusionSlot = slot
} }
votes[validatorIndex] = struct{}{} validatorBalance := big.NewInt(int64(c.validatorInfo[validatorIndex].Validator.EffectiveBalance))
if _, exists := headCorrects[validatorIndex]; !exists && headCorrect { if _, exists := c.participatingValidators[validatorIndex]; !exists {
headCorrects[validatorIndex] = struct{}{} c.summary.ParticipatingBalance = c.summary.ParticipatingBalance.Add(c.summary.ParticipatingBalance, validatorBalance)
c.participatingValidators[validatorIndex] = struct{}{}
} }
if _, exists := headTimelys[validatorIndex]; !exists && headCorrect && inclusionDistance == 1 { if _, exists := c.headCorrectValidators[validatorIndex]; !exists && headCorrect {
headTimelys[validatorIndex] = struct{}{} c.headCorrectValidators[validatorIndex] = struct{}{}
c.summary.HeadCorrectBalance = c.summary.HeadCorrectBalance.Add(c.summary.HeadCorrectBalance, validatorBalance)
} }
if _, exists := sourceTimelys[validatorIndex]; !exists && inclusionDistance <= 5 { if _, exists := c.headTimelyValidators[validatorIndex]; !exists && headCorrect && inclusionDistance == 1 {
sourceTimelys[validatorIndex] = struct{}{} c.headTimelyValidators[validatorIndex] = struct{}{}
c.summary.HeadTimelyBalance = c.summary.HeadTimelyBalance.Add(c.summary.HeadTimelyBalance, validatorBalance)
} }
if _, exists := targetCorrects[validatorIndex]; !exists && targetCorrect { if _, exists := c.sourceTimelyValidators[validatorIndex]; !exists && inclusionDistance <= 5 {
targetCorrects[validatorIndex] = struct{}{} c.sourceTimelyValidators[validatorIndex] = struct{}{}
c.summary.SourceTimelyBalance = c.summary.SourceTimelyBalance.Add(c.summary.SourceTimelyBalance, validatorBalance)
} }
if _, exists := targetTimelys[validatorIndex]; !exists && targetCorrect && inclusionDistance <= 32 { if _, exists := c.targetCorrectValidators[validatorIndex]; !exists && targetCorrect {
targetTimelys[validatorIndex] = struct{}{} 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, return nil
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
} }
func (c *command) processSyncCommitteeDuties(ctx context.Context) error { 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() { for i := range aggregate.SyncCommitteeBits.Len() {
validatorIndex := committee.Validators[int(i)] validatorIndex := committee.Validators[int(i)]
if _, exists := c.validators[validatorIndex]; !exists { if _, exists := c.validators[validatorIndex]; !exists {
// Not one of ours. if len(c.validators) > 0 {
continue // Not one of ours.
continue
}
} }
if !aggregate.SyncCommitteeBits.BitAt(i) { if !aggregate.SyncCommitteeBits.BitAt(i) {
missed[validatorIndex]++ missed[validatorIndex]++
@@ -657,6 +529,8 @@ func (c *command) processBlobs(ctx context.Context) error {
c.summary.Blobs += len(block.Deneb.Message.Body.BlobKZGCommitments) c.summary.Blobs += len(block.Deneb.Message.Body.BlobKZGCommitments)
case spec.DataVersionElectra: case spec.DataVersionElectra:
c.summary.Blobs += len(block.Electra.Message.Body.BlobKZGCommitments) c.summary.Blobs += len(block.Electra.Message.Body.BlobKZGCommitments)
case spec.DataVersionFulu:
c.summary.Blobs += len(block.Fulu.Message.Body.BlobKZGCommitments)
default: default:
return fmt.Errorf("unhandled block version %v", block.Version) return fmt.Errorf("unhandled block version %v", block.Version)
} }

View File

@@ -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") errCheck(err, "Failed to obtain beacon chain genesis")
genesis := genesisResponse.Data genesis := genesisResponse.Data
response, err := eth2Client.(consensusclient.ForkProvider).Fork(ctx, &api.ForkOpts{State: "head"}) response, err := eth2Client.(consensusclient.SpecProvider).Spec(ctx, &api.SpecOpts{})
errCheck(err, "Failed to obtain fork information") 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) signatureBytes := make([]byte, 96)
copy(signatureBytes, signedOp.Signature[:]) copy(signatureBytes, signedOp.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(signatureBytes) sig, err := e2types.BLSSignatureFromBytes(signatureBytes)
errCheck(err, "Invalid signature") errCheck(err, "Invalid signature")
verified := false
// Try with the current fork.
domain := phase0.Domain{} domain := phase0.Domain{}
currentExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, response.Data.CurrentVersion[:], genesis.GenesisValidatorsRoot[:]) forkRaw, ok := response.Data["CAPELLA_FORK_VERSION"]
errCheck(err, "Failed to compute domain") if !ok {
copy(domain[:], currentExitDomain) err = errors.New("failed to obtain Capella fork version")
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")
} }
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") assert(verified, "Voluntary exit failed to verify against current and previous fork versions")
outputIf(viper.GetBool("verbose"), "Verified") outputIf(viper.GetBool("verbose"), "Verified")

View File

@@ -19,7 +19,8 @@ import (
"fmt" "fmt"
eth2client "github.com/attestantio/go-eth2-client" 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" "github.com/pkg/errors"
) )
@@ -28,7 +29,10 @@ func process(ctx context.Context, data *dataIn) error {
return errors.New("no data") 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 { if err != nil {
return errors.Wrap(err, "failed to connect for events") return errors.Wrap(err, "failed to connect for events")
} }
@@ -38,7 +42,7 @@ func process(ctx context.Context, data *dataIn) error {
return nil return nil
} }
func eventHandler(event *api.Event) { func eventHandler(event *apiv1.Event) {
if event.Data == nil { if event.Data == nil {
return return
} }

View File

@@ -127,6 +127,13 @@ func (c *command) process(ctx context.Context) error {
} else { } else {
c.inclusions = append(c.inclusions, 2) 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: default:
return fmt.Errorf("unhandled block version %v", block.Version) return fmt.Errorf("unhandled block version %v", block.Version)
} }

View File

@@ -24,7 +24,7 @@ import (
// ReleaseVersion is the release version of the codebase. // ReleaseVersion is the release version of the codebase.
// Usually overridden by tag names when building binaries. // 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. // versionCmd represents the version command.
var versionCmd = &cobra.Command{ var versionCmd = &cobra.Command{

12
go.mod
View File

@@ -5,7 +5,7 @@ go 1.23.0
toolchain go1.23.2 toolchain go1.23.2
require ( 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/ferranbt/fastssz v0.1.4
github.com/gofrs/uuid v4.4.0+incompatible github.com/gofrs/uuid v4.4.0+incompatible
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
@@ -27,13 +27,13 @@ require (
github.com/wealdtech/go-ecodec v1.1.4 github.com/wealdtech/go-ecodec v1.1.4
github.com/wealdtech/go-eth2-types/v2 v2.8.2 github.com/wealdtech/go-eth2-types/v2 v2.8.2
github.com/wealdtech/go-eth2-util v1.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-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-encryptor-keystorev4 v1.4.1
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.1 github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.2
github.com/wealdtech/go-eth2-wallet-keystore v1.0.0 github.com/wealdtech/go-eth2-wallet-keystore v1.0.2
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.5.0 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-filesystem v1.18.1
github.com/wealdtech/go-eth2-wallet-store-s3 v1.12.0 github.com/wealdtech/go-eth2-wallet-store-s3 v1.12.0
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.2 github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.2

24
go.sum
View File

@@ -1,5 +1,5 @@
github.com/attestantio/go-eth2-client v0.24.1 h1:DZ/2O83eUcSfPPs63xF6fdXDe4afA4nlt5j0y2cweOI= github.com/attestantio/go-eth2-client v0.27.1 h1:g7bm+gG/p+gfzYdEuxuAepVWYb8EO+2KojV5/Lo2BxM=
github.com/attestantio/go-eth2-client v0.24.1/go.mod h1:/KTLN3WuH1xrJL7ZZrpBoWM1xCCihnFbzequD5L+83o= 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 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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-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 h1:gq+JMrnadifyKadUr75wmfP7+usiqMu9t3VVoob5Dvo=
github.com/wealdtech/go-eth2-util v1.8.2/go.mod h1:/80GAK0K/3+PqUBZHvaOPd3b1sjHeimxQh1nrJzgaPk= 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.2 h1:tFkWddJwH8Iq3H9K1Fnp4avxNn+4qbE3Go7k81a/c1U=
github.com/wealdtech/go-eth2-wallet v1.17.0/go.mod h1:qMmDrx//GrdZ3q+0Jf9SNwCaLsFOxOmXgr1yptpSMIE= 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 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-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.2 h1:O6nfhMRTUpblOzj8KiCLFgQAyAqrv2dweorzss/V6PU=
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/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 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-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 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-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.2 h1:7wB8j12LVdUR/IFLmwTxdXfuTvpXSn4yj+ZD1OhDSJY=
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.1/go.mod h1:aWgnEi07w1L9wMBRB69sYvoEONppAUly6FDQRWQGqH8= github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.2/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.2 h1:OseWEBvr13voALVCdg7ojsU3Kly/FPR9sCadnsx3/tM=
github.com/wealdtech/go-eth2-wallet-keystore v1.0.0/go.mod h1:6DGINunnasS9y9F7KH3ya2h74fHWgSCfP3dAJWe4A6U= 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.0 h1:vphAFklkYMRJVo9f5rVWly7PECHrLS4yarjemBa7fRM= github.com/wealdtech/go-eth2-wallet-nd/v2 v2.5.1 h1:bSdDCn+o4wq5MHogGkUtqbPp6Z7Tndt2qBb1zjof96Y=
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.5.0/go.mod h1:kBZUZogqwvvxulEvXi5l6OjZyd7EBmCKxce5Q+lW7fs= 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 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-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= github.com/wealdtech/go-eth2-wallet-store-s3 v1.12.0 h1:noknYCbHw2soPhwke1LvC99Kk/2CLN787KcgxdZ7OGo=

View File

@@ -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}) response, err := validatorsProvider.Validators(ctx, &api.ValidatorsOpts{State: stateID, Indices: indices})
if err != nil { if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to obtain validators %v", indices)) return nil, errors.Wrap(err, fmt.Sprintf("failed to obtain validators %v", indices))