Compare commits

...

74 Commits

Author SHA1 Message Date
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
Jim McDonald
1c23926295 Add support for hoodi. 2025-03-16 08:00:45 +00:00
Jim McDonald
992a969eaf Update dependencies. 2025-03-16 07:58:10 +00:00
Jim McDonald
58d8587471 Tidy-ups. 2025-03-16 07:55:00 +00:00
Jim McDonald
b89154ada3 Add "block trail". 2025-02-26 09:40:03 +00:00
Jim McDonald
b9fff0dbde Handle blocks without blobs. 2025-02-25 12:54:58 +00:00
Jim McDonald
0238130895 Fix incorrect argument binding. 2025-02-22 07:23:40 +00:00
Jim McDonald
60b6ce44f1 Merge pull request #159 from wealdtech/update_index
Handle electra attestations for epoch summary
2025-02-22 07:14:47 +00:00
Chris Berry
24486f0175 Merge pull request #160 from wealdtech/fix_help
Update help text to match args
2025-02-21 15:10:50 +00:00
Chris Berry
503bf9a996 Update help text to match args 2025-02-21 14:46:36 +00:00
Jim McDonald
1d14a57204 Update dependencies. 2025-02-17 09:26:00 +00:00
Jim McDonald
842d603524 Update workflow. 2025-02-16 15:43:20 +00:00
Jim McDonald
c3471240a5 Update workflows. 2025-02-16 15:42:52 +00:00
Chris Berry
7b4ea7e27e Handle electra attestations for epoch summary 2025-02-14 11:54:05 +00:00
Jim McDonald
4d3bd966e0 Use standard mock from go-eth2-client. 2025-02-08 11:46:49 +00:00
Jim McDonald
0488b13ba1 Updates for electra. 2025-02-08 11:34:55 +00:00
Jim McDonald
f8a611d63d Merge pull request #157 from wealdtech/electra
Electra
2025-02-08 10:41:46 +00:00
Chris Berry
772f07f8ec Merge pull request #156 from wealdtech/electra_merge
Update go-eth2-client and merge master changes
2025-02-07 09:22:35 +00:00
Chris Berry
f6e23d803b Update go-eth2-client 2025-02-06 16:43:26 +00:00
Chris Berry
86e872294d Merge branch 'master' into electra_merge
# Conflicts:
#	CHANGELOG.md
2025-02-06 16:42:26 +00:00
Jim McDonald
d7d9c66052 Update Dockerfile. 2025-01-31 17:14:12 +00:00
Jim McDonald
3e173f141e Order deposit data from HD wallet accounts by path. 2025-01-31 17:04:50 +00:00
Jim McDonald
5d95e93b76 Allow blockid for validator info. 2025-01-31 17:03:48 +00:00
Jim McDonald
005eed1360 Avoid corner case when deriving from mnemonic. 2025-01-28 19:32:01 +00:00
Chris Berry
34b752edcc Merge pull request #154 from wealdtech/electra_updates
Update to use electra version of go-eth2-client
2025-01-27 11:10:03 +00:00
Chris Berry
f17fe2f5cb Refactor based on review 2025-01-27 10:23:30 +00:00
Chris Berry
8322353af5 Update docker file 2025-01-27 10:08:48 +00:00
Chris Berry
7d0e607f96 Update to use electra version of go-eth2-client 2025-01-27 10:01:36 +00:00
Jim McDonald
fcafa037f8 Bump version. 2024-12-20 22:54:25 +00:00
Jim McDonald
8e1c8c5300 New workflow 2024-12-20 22:53:41 +00:00
Jim McDonald
5af1476bc3 Bump version. 2024-12-20 22:47:59 +00:00
Jim McDonald
24a4220804 Update workflow. 2024-12-20 22:47:25 +00:00
Jim McDonald
fa1d4d60fa Update dependencies. 2024-12-20 22:41:00 +00:00
Jim McDonald
7d00d1261f Avoid crash when signing and verifing signatures using keys rather than accounts. 2024-10-22 22:55:08 +01:00
Jim McDonald
ac85a9539b Update workflow. 2024-10-21 20:25:41 +01:00
Jim McDonald
099e434f43 Update workflows. 2024-10-21 19:50:23 +01:00
Jim McDonald
5bab79bd79 Update workflow. 2024-10-21 19:47:03 +01:00
Jim McDonald
1defa3b121 Merge pull request #149 from wealdtech/add-trin
Add trin.
2024-10-21 19:45:18 +01:00
Jim McDonald
6cb7b034aa Update workflows. 2024-10-21 19:42:14 +01:00
Jim McDonald
bd9659d71f Add trin. 2024-10-21 19:00:36 +01:00
Jim McDonald
68ca31e034 Merge pull request #147 from polskikh/fix-validate-deposit-data-json
Fix validating deposit message root for json output data
2024-10-04 12:16:02 +01:00
Vladislav Polskikh
a37a5f4af3 Fix validating deposit message root for json output data 2024-10-04 12:10:20 +02:00
Jim McDonald
47cf033feb Update workflows. 2024-10-01 13:18:52 +01:00
Jim McDonald
72a9390f97 Linting. 2024-10-01 13:02:39 +01:00
Jim McDonald
0276a72de6 Fix crash when block info has no blobs. 2024-10-01 13:02:07 +01:00
Jim McDonald
fd0a89c258 More data for epoch summaries. 2024-09-04 11:51:16 +01:00
Jim McDonald
1ac505f0bd Disable lint rule that generates false positives 2024-09-01 22:26:59 +01:00
Jim McDonald
b4124b7a27 Add keystore wallet. 2024-09-01 22:15:58 +01:00
Jim McDonald
f3ae2baf8f Allow derived keystore to be displayed on stdout.
Fixes #143
2024-08-02 09:46:25 +01:00
Jim McDonald
8aaf16ab3f Update dependencies. 2024-07-28 18:50:47 +01:00
Jim McDonald
61179045dd Provide client info in block info output. 2024-07-28 18:19:53 +01:00
Jim McDonald
e50b4f015b Update changelog. 2024-06-23 07:35:22 +01:00
Jim McDonald
6e522d6b5a Bump version. 2024-06-23 07:29:43 +01:00
Jim McDonald
d467a8ef9c Linting. 2024-06-18 12:52:53 +01:00
Jim McDonald
d51683426c Update README for S3. 2024-06-17 08:05:09 +01:00
Jim McDonald
4879443e51 Update linter rules. 2024-05-27 09:46:47 +01:00
Jim McDonald
cf9d5718a3 Update dependencies. 2024-05-27 09:42:21 +01:00
68 changed files with 2678 additions and 918 deletions

View File

@@ -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

View File

@@ -1,23 +1,29 @@
name: golangci-lint
name: 'golangci-lint'
on:
push:
tags:
- v*
branches:
- master
pull_request:
push:
branches:
- 'master'
workflow_dispatch:
permissions:
contents: read
contents: 'read'
pull-requests: 'read'
checks: 'write'
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
name: 'lint'
runs-on: 'ubuntu-24.04'
steps:
- uses: actions/setup-go@v4
- uses: 'actions/setup-go@v5'
with:
go-version: '^1.21'
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
cache: false
go-version: '^1.22'
- uses: 'actions/checkout@v4'
- uses: 'golangci/golangci-lint-action@v6'
with:
version: 'latest'
args: '--timeout=60m'
only-new-issues: true
skip-cache: true

View File

@@ -3,8 +3,9 @@ name: Release
on:
push:
tags:
- 'v*'
- 't*'
- 'v*'
- 't*'
workflow_dispatch:
jobs:
# Set variables that will be available to all builds.
@@ -44,9 +45,10 @@ jobs:
needs: [create_release, env_vars]
steps:
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '^1.21'
cache: false
go-version: '^1.22'
- name: Check out repository into the Go module directory
uses: actions/checkout@v4
@@ -119,9 +121,10 @@ jobs:
needs: [create_release, env_vars]
steps:
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '^1.21'
cache: false
go-version: '^1.22'
- name: Check out repository into the Go module directory
uses: actions/checkout@v4
@@ -164,9 +167,10 @@ jobs:
needs: [create_release, env_vars]
steps:
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '^1.21'
cache: false
go-version: '^1.22'
- name: Check out repository into the Go module directory
uses: actions/checkout@v4

View File

@@ -1,15 +1,18 @@
name: test
on:
pull_request:
push:
branches:
- master
pull_request:
- master
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: '^1.21'
cache: false
go-version: '^1.22'
- uses: actions/checkout@v4
- uses: n8maninger/action-golang-test@v1
- uses: n8maninger/action-golang-test@v2

6
.gitignore vendored
View File

@@ -15,6 +15,12 @@ coverage.html
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
# Intellij
.idea/
# Makefile
Makefile
# Vim
*.sw?

View File

@@ -4,6 +4,16 @@
# This file is not a configuration example,
# it contains the exhaustive configuration with explanations of the options.
issues:
# Which files to exclude: they will be analyzed, but issues from them won't be reported.
# There is no need to include all autogenerated files,
# we confidently recognize autogenerated files.
# If it's not, please let us know.
# "/" will be replaced by current OS file path separator to properly work on Windows.
# Default: []
exclude-files:
- ".*_ssz\\.go$"
# Options for analysis running.
run:
# The default concurrency value is the number of available CPU.
@@ -39,15 +49,6 @@ run:
# Default: true
# skip-dirs-use-default: false
# Which files to skip: they will be analyzed, but issues from them won't be reported.
# Default value is empty list,
# but there is no need to include all autogenerated files,
# we confidently recognize autogenerated files.
# If it's not please let us know.
# "/" will be replaced by current OS file path separator to properly work on Windows.
skip-files:
- ".*_ssz\\.go$"
# If set we pass it to "go list -mod={option}". From "go help modules":
# If invoked with -mod=readonly, the go command is disallowed from the implicit
# automatic updating of go.mod described above. Instead, it fails when any changes
@@ -68,43 +69,22 @@ run:
# Define the Go version limit.
# Mainly related to generics support since go1.18.
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18
go: '1.19'
# go: '1.19'
# output configuration options
output:
# Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
#
# Multiple can be specified by separating them by comma, output can be provided
# for each of them by separating format name and path by colon symbol.
# Output path can be either `stdout`, `stderr` or path to the file to write to.
# Example: "checkstyle:report.json,colored-line-number"
#
# Default: colored-line-number
# format: json
# Print lines of code with issue.
# Default: true
# print-issued-lines: false
# Print linter name in the end of issue text.
# Default: true
# print-linter-name: false
# Make issues output unique by line.
# Default: true
# uniq-by-line: false
# Add a prefix to the output file references.
# Default is no prefix.
# path-prefix: ""
# Sort results by: filepath, line and column.
# sort-results: true
formats:
- format: colored-line-number
path: stderr
# All available settings of specific linters.
linters-settings:
gosec:
excludes:
# Flags for potentially-unsafe casting of ints, but generates a lot of false positives.
- 'G115'
lll:
line-length: 132
@@ -127,12 +107,11 @@ linters:
disable:
- contextcheck
- cyclop
- deadcode
- depguard
- dupl
- err113
- errorlint
- exhaustive
- exhaustivestruct
- exhaustruct
- forbidigo
- forcetypeassert
@@ -142,30 +121,22 @@ linters:
- gochecknoinits
- gocognit
- goconst
- goerr113
- goheader
- golint
- gomnd
- ifshort
- interfacer
- ireturn
- lll
- maintidx
- maligned
- mnd
- musttag
- nestif
- nilnil
- nlreturn
- nolintlint
- nosnakecase
- perfsprint
- promlinter
- rowserrcheck
- scopelint
- sqlclosecheck
- structcheck
- tenv
- unparam
- varcheck
- varnamelen
- wastedassign
- wrapcheck

View File

@@ -1,4 +1,57 @@
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
1.37.2:
- add "block trail"
1.37.1:
- handle missing blobs for block info
- fix `--epoch` flag for epoch summary
1.37.0:
- support Electra
- add `--compounding` flag when creating validator deposit data
1.36.6:
- allow specification of blockid for validator info
- validator depositdata orders deposits from an HD wallet by path
1.36.5:
- avoid corner case mnemonic derivation with 25th word
1.36.2:
- avoid crash when signing and verifing signatures using keys rather than accounts
1.36.1:
- more JSON data for epoch summary
- fix crash when block ifno had no blobs
1.36.0:
- support keystore wallets
1.35.6:
- provide more JSON data in "epoch summary"
1.35.5:
- allow keystore to be output to the console
1.35.4:
- provide consensus and execution client info in block info output
1.35.3:
- provide better error message on context deadlline exceeded
- update launchpad output to match latest version
- add deposit contract address to "chain info"

View File

@@ -1,4 +1,4 @@
FROM golang:1.21-bookworm as builder
FROM golang:1.23-bookworm AS builder
WORKDIR /app

View File

@@ -151,9 +151,13 @@ Amazon S3-compatible stores have additional options available, which can be conf
{
"stores": {
"s3": {
"bucket":"mybucketname",
"path":"path/in/bucket",
"passphrase":"secret"
"region": "us-west-1",
"bucket": "my-s3-store",
"path": "/wallets",
"credentials": {
"id": "ABCDEF123",
"secret": "XXXXXXXXX"
}
}
}
}

View File

@@ -22,6 +22,7 @@ import (
type dataIn struct {
quiet bool
json bool
// Derivation information.
mnemonic string
path string
@@ -37,6 +38,9 @@ func input(_ context.Context) (*dataIn, error) {
// Quiet.
data.quiet = viper.GetBool("quiet")
// JSON.
data.json = viper.GetBool("json")
// Mnemonic.
if viper.GetString("mnemonic") == "" {
return nil, errors.New("mnemonic is required")

View File

@@ -31,6 +31,7 @@ import (
)
type dataOut struct {
json bool
showPrivateKey bool
showWithdrawalCredentials bool
generateKeystore bool
@@ -94,10 +95,14 @@ func outputKeystore(_ context.Context, data *dataOut) (string, error) {
return "", errors.Wrap(err, "failed to marshal keystore JSON")
}
keystoreFilename := fmt.Sprintf("keystore-%s-%d.json", strings.ReplaceAll(data.path, "/", "_"), time.Now().Unix())
if data.json {
fmt.Fprintf(os.Stdout, "%s\n", string(out))
} else {
keystoreFilename := fmt.Sprintf("keystore-%s-%d.json", strings.ReplaceAll(data.path, "/", "_"), time.Now().Unix())
if err := os.WriteFile(keystoreFilename, out, 0o600); err != nil {
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", keystoreFilename))
if err := os.WriteFile(keystoreFilename, out, 0o600); err != nil {
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", keystoreFilename))
}
}
return "", nil
}

View File

@@ -38,6 +38,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
results := &dataOut{
json: data.json,
showPrivateKey: data.showPrivateKey,
showWithdrawalCredentials: data.showWithdrawalCredentials,
generateKeystore: data.generateKeystore,

View File

@@ -50,6 +50,7 @@ func init() {
accountDeriveCmd.Flags().Bool("show-private-key", false, "show private key for derived account")
accountDeriveCmd.Flags().Bool("show-withdrawal-credentials", false, "show withdrawal credentials for derived account")
accountDeriveCmd.Flags().Bool("generate-keystore", false, "generate a keystore for the derived account")
accountDeriveCmd.Flags().Bool("json", false, "display the JSON keystore for the derived account on stdout")
}
func accountDeriveBindings(cmd *cobra.Command) {
@@ -62,4 +63,7 @@ func accountDeriveBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("generate-keystore", cmd.Flags().Lookup("generate-keystore")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", cmd.Flags().Lookup("json")); err != nil {
panic(err)
}
}

View File

@@ -19,6 +19,7 @@ import (
"strconv"
"strings"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
@@ -27,7 +28,7 @@ type dataOut struct {
debug bool
quiet bool
verbose bool
attestation *phase0.Attestation
attestation *spec.VersionedAttestation
slot phase0.Slot
attestationIndex uint64
inclusionDelay phase0.Slot

View File

@@ -22,6 +22,7 @@ import (
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
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"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
@@ -93,9 +94,18 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.Wrap(err, "failed to obtain block attestations")
}
for i, attestation := range attestations {
if attestation.Data.Slot == duty.Slot &&
attestation.Data.Index == duty.CommitteeIndex &&
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
attestationData, err := attestation.Data()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attestation data")
}
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attestation aggregation bits")
}
if attestationData.Slot == duty.Slot &&
attestationData.Index == duty.CommitteeIndex &&
aggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
headCorrect := false
targetCorrect := false
if data.verbose {
@@ -128,8 +138,13 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *spec.VersionedAttestation) (bool, error) {
attestationData, err := attestation.Data()
if err != nil {
return false, errors.Wrap(err, "failed to obtain attestation data")
}
slot := attestationData.Slot
for {
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
@@ -149,13 +164,19 @@ func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Atte
slot--
continue
}
return bytes.Equal(response.Data.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
return bytes.Equal(response.Data.Root[:], attestationData.BeaconBlockRoot[:]), nil
}
}
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *spec.VersionedAttestation) (bool, error) {
attestationData, err := attestation.Data()
if err != nil {
return false, errors.Wrap(err, "failed to obtain attestation data")
}
// Start with first slot of the target epoch.
slot := data.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
slot := data.chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
for {
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
@@ -175,7 +196,8 @@ func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.At
slot--
continue
}
return bytes.Equal(response.Data.Root[:], attestation.Data.Target.Root[:]), nil
return bytes.Equal(response.Data.Root[:], attestationData.Target.Root[:]), nil
}
}

View File

@@ -55,7 +55,7 @@ type command struct {
weightDenominator uint64
// Processing.
priorAttestations map[string]*attestationData
priorAttestations map[string]*attestationDataInfo
// Head roots provides the root of the head slot at given slots.
headRoots map[phase0.Slot]phase0.Root
// Target roots provides the root of the target epoch at given slots.
@@ -77,20 +77,20 @@ type blockAnalysis struct {
}
type attestationAnalysis struct {
Head phase0.Root `json:"head"`
Target phase0.Root `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationData `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
Head phase0.Root `json:"head"`
Target phase0.Root `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationDataInfo `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
type syncCommitteeAnalysis struct {
@@ -100,7 +100,7 @@ type syncCommitteeAnalysis struct {
Value float64 `json:"value"`
}
type attestationData struct {
type attestationDataInfo struct {
Block phase0.Slot `json:"block"`
Index int `json:"index"`
}
@@ -110,7 +110,7 @@ func newCommand(_ context.Context) (*command, error) {
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
priorAttestations: make(map[string]*attestationData),
priorAttestations: make(map[string]*attestationDataInfo),
headRoots: make(map[phase0.Slot]phase0.Root),
targetRoots: make(map[phase0.Slot]phase0.Root),
votes: make(map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist),

View File

@@ -34,20 +34,20 @@ func (c *command) output(ctx context.Context) (string, error) {
}
type attestationAnalysisJSON struct {
Head string `json:"head"`
Target string `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationData `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
Head string `json:"head"`
Target string `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationDataInfo `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
func (a *attestationAnalysis) MarshalJSON() ([]byte, error) {

View File

@@ -17,6 +17,7 @@ import (
"bytes"
"context"
"fmt"
"net/http"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
@@ -39,7 +40,7 @@ func (c *command) process(ctx context.Context) error {
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
return errors.New("empty beacon block")
}
return errors.Wrap(err, "failed to obtain beacon block")
@@ -62,8 +63,12 @@ func (c *command) process(ctx context.Context) error {
// Calculate how many parents we need to fetch.
minSlot := slot
for _, attestation := range attestations {
if attestation.Data.Slot < minSlot {
minSlot = attestation.Data.Slot
attestationData, err := attestation.Data()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation data")
}
if attestationData.Slot < minSlot {
minSlot = attestationData.Slot
}
}
if c.debug {
@@ -102,10 +107,16 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
if c.debug {
fmt.Printf("Processing attestation %d\n", i)
}
attestationData, err := attestation.Data()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation data")
}
analysis := &attestationAnalysis{
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
Head: attestationData.BeaconBlockRoot,
Target: attestationData.Target.Root,
Distance: int(slot - attestationData.Slot),
}
root, err := attestation.HashTreeRoot()
@@ -115,45 +126,47 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return err
}
_, exists = blockVotes[data.Slot][data.Index]
_, exists := blockVotes[attestationData.Slot]
if !exists {
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
blockVotes[attestationData.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[attestationData.Slot][attestationData.Index]
if !exists {
blockVotes[attestationData.Slot][attestationData.Index] = bitfield.NewBitlist(aggregationBits.Len())
}
// Count new votes.
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.PossibleVotes = int(aggregationBits.Len())
for j := range aggregationBits.Len() {
if aggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[data.Slot][data.Index].BitAt(j) {
if blockVotes[attestationData.Slot][attestationData.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[data.Slot][data.Index].BitAt(j) {
if c.votes[attestationData.Slot][attestationData.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
blockVotes[attestationData.Slot][attestationData.Index].SetBitAt(j, true)
}
}
// Calculate head correct.
var err error
analysis.HeadCorrect, err = c.calcHeadCorrect(ctx, attestation)
if err != nil {
return err
}
// Calculate head timely.
analysis.HeadTimely = analysis.HeadCorrect && attestation.Data.Slot == slot-1
analysis.HeadTimely = analysis.HeadCorrect && attestationData.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestation.Data.Slot >= slot-5
analysis.SourceTimely = attestationData.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
@@ -163,7 +176,7 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
// Calculate target timely.
if block.Version < spec.DataVersionDeneb {
analysis.TargetTimely = attestation.Data.Slot >= slot-32
analysis.TargetTimely = attestationData.Slot >= slot-32
} else {
analysis.TargetTimely = true
}
@@ -193,7 +206,7 @@ func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedB
if err != nil {
return err
}
root, err := block.Deneb.HashTreeRoot()
root, err := block.Root()
if err != nil {
panic(err)
}
@@ -211,7 +224,7 @@ func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedB
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
return errors.New("empty beacon block")
}
return err
@@ -254,23 +267,31 @@ func (c *command) processParentBlock(_ context.Context, block *spec.VersionedSig
if err != nil {
return err
}
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationDataInfo{
Block: slot,
Index: i,
}
data := attestation.Data
_, exists := c.votes[data.Slot]
if !exists {
c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
attestationData, err := attestation.Data()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation data")
}
_, exists = c.votes[data.Slot][data.Index]
if !exists {
c.votes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation aggregation bits")
}
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
c.votes[data.Slot][data.Index].SetBitAt(j, true)
_, exists := c.votes[attestationData.Slot]
if !exists {
c.votes[attestationData.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = c.votes[attestationData.Slot][attestationData.Index]
if !exists {
c.votes[attestationData.Slot][attestationData.Index] = bitfield.NewBitlist(aggregationBits.Len())
}
for j := range aggregationBits.Len() {
if aggregationBits.BitAt(j) {
c.votes[attestationData.Slot][attestationData.Index].SetBitAt(j, true)
}
}
}
@@ -384,8 +405,13 @@ func (c *command) setup(ctx context.Context) error {
return nil
}
func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
func (c *command) calcHeadCorrect(ctx context.Context, attestation *spec.VersionedAttestation) (bool, error) {
attestationData, err := attestation.Data()
if err != nil {
return false, errors.Wrap(err, "failed to obtain attestation data")
}
slot := attestationData.Slot
root, exists := c.headRoots[slot]
if !exists {
for {
@@ -394,7 +420,7 @@ func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attes
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
if c.debug {
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
}
@@ -412,27 +438,32 @@ func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attes
slot--
continue
}
c.headRoots[attestation.Data.Slot] = response.Data.Root
c.headRoots[slot] = response.Data.Root
root = response.Data.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.BeaconBlockRoot[:]), nil
return bytes.Equal(root[:], attestationData.BeaconBlockRoot[:]), nil
}
func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
root, exists := c.targetRoots[attestation.Data.Slot]
func (c *command) calcTargetCorrect(ctx context.Context, attestation *spec.VersionedAttestation) (bool, error) {
attestationData, err := attestation.Data()
if err != nil {
return false, errors.Wrap(err, "failed to obtain attestation data")
}
root, exists := c.targetRoots[attestationData.Slot]
if !exists {
// Start with first slot of the target epoch.
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
slot := c.chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
for {
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
if c.debug {
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
}
@@ -449,12 +480,12 @@ func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Att
slot--
continue
}
c.targetRoots[attestation.Data.Slot] = response.Data.Root
c.targetRoots[attestationData.Slot] = response.Data.Root
root = response.Data.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
return bytes.Equal(root[:], attestationData.Target.Root[:]), nil
}
func (c *command) analyzeSyncCommittees(_ context.Context, block *spec.VersionedSignedBeaconBlock) error {
@@ -490,6 +521,20 @@ 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.DataVersionElectra:
c.analysis.SyncCommitee.Contributions = int(block.Electra.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Electra.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
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)
}

View File

@@ -19,6 +19,7 @@ import (
"encoding/hex"
"fmt"
"math/big"
"regexp"
"sort"
"strconv"
"strings"
@@ -31,6 +32,7 @@ import (
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/capella"
"github.com/attestantio/go-eth2-client/spec/deneb"
"github.com/attestantio/go-eth2-client/spec/electra"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
@@ -54,7 +56,7 @@ func output(_ context.Context, data *dataOut) (string, error) {
return "", nil
}
func outputBlockGeneral(_ context.Context,
func outputBlockGeneral(ctx context.Context,
verbose bool,
slot phase0.Slot,
proposerIndex phase0.ValidatorIndex,
@@ -77,19 +79,12 @@ func outputBlockGeneral(_ context.Context,
res.WriteString(fmt.Sprintf("Epoch: %d\n", phase0.Epoch(uint64(slot)/slotsPerEpoch)))
res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(slot)*int64(slotDuration.Seconds()), 0)))
res.WriteString(fmt.Sprintf("Block root: %#x\n", blockRoot))
res.WriteString(fmt.Sprintf("Parent root: %#x\n", parentRoot))
if verbose {
res.WriteString(fmt.Sprintf("Body root: %#x\n", bodyRoot))
res.WriteString(fmt.Sprintf("Parent root: %#x\n", parentRoot))
res.WriteString(fmt.Sprintf("State root: %#x\n", stateRoot))
}
if len(graffiti) > 0 && hex.EncodeToString(graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
graffiti = bytes.TrimRight(graffiti, "\u0000")
if utf8.Valid(graffiti) {
res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(graffiti)))
} else {
res.WriteString(fmt.Sprintf("Graffiti: %#x\n", graffiti))
}
}
res.WriteString(blockGraffiti(ctx, graffiti))
return res.String(), nil
}
@@ -139,7 +134,61 @@ func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service,
res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()))
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitlistToString(att.AggregationBits)))
if _, exists := committees[att.Data.Index]; exists {
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index])))
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees, []int{int(att.Data.Index)})))
}
res.WriteString(fmt.Sprintf(" Slot: %d\n", att.Data.Slot))
res.WriteString(fmt.Sprintf(" Beacon block root: %#x\n", att.Data.BeaconBlockRoot))
res.WriteString(fmt.Sprintf(" Source epoch: %d\n", att.Data.Source.Epoch))
res.WriteString(fmt.Sprintf(" Source root: %#x\n", att.Data.Source.Root))
res.WriteString(fmt.Sprintf(" Target epoch: %d\n", att.Data.Target.Epoch))
res.WriteString(fmt.Sprintf(" Target root: %#x\n", att.Data.Target.Root))
}
}
}
return res.String(), nil
}
func outputElectraBlockAttestations(ctx context.Context, eth2Client eth2client.Service, verbose bool, attestations []*electra.Attestation) (string, error) {
res := strings.Builder{}
validatorCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
res.WriteString(fmt.Sprintf("Attestations: %d\n", len(attestations)))
if verbose {
beaconCommitteesProvider, isProvider := eth2Client.(eth2client.BeaconCommitteesProvider)
if isProvider {
for i, att := range attestations {
res.WriteString(fmt.Sprintf(" %d:\n", i))
// Fetch committees for this epoch if not already obtained.
committees, exists := validatorCommittees[att.Data.Slot]
if !exists {
response, err := beaconCommitteesProvider.BeaconCommittees(ctx, &api.BeaconCommitteesOpts{
State: fmt.Sprintf("%d", att.Data.Slot),
})
if err != nil {
// Failed to get it; create an empty committee to stop us continually attempting to re-fetch.
validatorCommittees[att.Data.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
} else {
for _, beaconCommittee := range response.Data {
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
}
validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
}
}
committees = validatorCommittees[att.Data.Slot]
}
committeeIndices := make([]phase0.CommitteeIndex, 0)
for _, committeeIndex := range att.CommitteeBits.BitIndices() {
committeeIndices = append(committeeIndices, phase0.CommitteeIndex(committeeIndex))
}
res.WriteString(fmt.Sprintf(" Committee indices: %d\n", committeeIndices))
res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()))
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitlistToString(att.AggregationBits)))
if _, exists := committees[att.Data.Index]; exists {
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees, att.CommitteeBits.BitIndices())))
}
res.WriteString(fmt.Sprintf(" Slot: %d\n", att.Data.Slot))
res.WriteString(fmt.Sprintf(" Beacon block root: %#x\n", att.Data.BeaconBlockRoot))
@@ -204,6 +253,56 @@ func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Ser
return res.String(), nil
}
func outputElectraBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Service, verbose bool, attesterSlashings []*electra.AttesterSlashing) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Attester slashings: %d\n", len(attesterSlashings)))
if verbose {
for i, slashing := range attesterSlashings {
// Say what was slashed.
att1 := slashing.Attestation1
att2 := slashing.Attestation2
slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices)
if len(slashedIndices) == 0 {
continue
}
res.WriteString(fmt.Sprintf(" %d:\n", i))
res.WriteString(fmt.Sprintln(" Slashed validators:"))
response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{
State: "head",
Indices: slashedIndices,
})
if err != nil {
return "", errors.Wrap(err, "failed to obtain beacon committees")
}
for k, v := range response.Data {
res.WriteString(fmt.Sprintf(" %#x (%d)\n", v.Validator.PublicKey[:], k))
}
// Say what caused the slashing.
if att1.Data.Target.Epoch == att2.Data.Target.Epoch {
res.WriteString(fmt.Sprintf(" Double voted for same target epoch (%d):\n", att1.Data.Target.Epoch))
if !bytes.Equal(att1.Data.Target.Root[:], att2.Data.Target.Root[:]) {
res.WriteString(fmt.Sprintf(" Attestation 1 target epoch root: %#x\n", att1.Data.Target.Root))
res.WriteString(fmt.Sprintf(" Attestation 2target epoch root: %#x\n", att2.Data.Target.Root))
}
if !bytes.Equal(att1.Data.BeaconBlockRoot[:], att2.Data.BeaconBlockRoot[:]) {
res.WriteString(fmt.Sprintf(" Attestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot))
res.WriteString(fmt.Sprintf(" Attestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot))
}
} else if att1.Data.Source.Epoch < att2.Data.Source.Epoch &&
att1.Data.Target.Epoch > att2.Data.Target.Epoch {
res.WriteString(" Surround voted:\n")
res.WriteString(fmt.Sprintf(" Attestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch))
res.WriteString(fmt.Sprintf(" Attestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch))
}
}
}
return res.String(), nil
}
func outputBlockDeposits(_ context.Context, verbose bool, deposits []*phase0.Deposit) (string, error) {
res := strings.Builder{}
@@ -295,7 +394,7 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
res.WriteString(fmt.Sprintf(" Error: failed to obtain sync committee: %v\n", err))
} else {
res.WriteString(" Contributing validators:")
for i := uint64(0); i < syncAggregate.SyncCommitteeBits.Len(); i++ {
for i := range syncAggregate.SyncCommitteeBits.Len() {
if syncAggregate.SyncCommitteeBits.BitAt(i) {
res.WriteString(fmt.Sprintf(" %d", syncCommitteeResponse.Data.Validators[i]))
}
@@ -508,7 +607,117 @@ func outputDenebBlockText(ctx context.Context,
}
res.WriteString(tmp)
tmp, err = outputDenebBlobInfo(ctx, data.verbose, signedBlock.Message.Body, blobs)
tmp, err = outputBlobInfo(ctx, data.verbose, signedBlock.Message.Body.BlobKZGCommitments, blobs)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputElectraBlockText(ctx context.Context,
data *dataOut,
signedBlock *electra.SignedBeaconBlock,
blobs []*deneb.BlobSidecar,
) (
string,
error,
) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
blockRoot, err := signedBlock.Message.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to obtain block root")
}
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate body root")
}
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
signedBlock.Message.ProposerIndex,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti[:],
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Eth1 data.
if data.verbose {
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
if err != nil {
return "", err
}
res.WriteString(tmp)
}
// Sync aggregate.
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attestations.
tmp, err = outputElectraBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputElectraBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
if err != nil {
return "", err
}
res.WriteString(tmp)
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
// Add verbose proposer slashings.
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputBlockBLSToExecutionChanges(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.BLSToExecutionChanges)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputDenebBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputElectraBlockExecutionRequests(ctx, data.verbose, signedBlock.Message.Body.ExecutionRequests)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputBlobInfo(ctx, data.verbose, signedBlock.Message.Body.BlobKZGCommitments, blobs)
if err != nil {
return "", err
}
@@ -797,7 +1006,7 @@ func outputCapellaBlockExecutionPayload(_ context.Context,
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas))
for i := 0; i < 32; i++ {
for i := range 32 {
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
}
baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBEBytes)
@@ -903,30 +1112,73 @@ func outputDenebBlockExecutionPayload(_ context.Context,
return res.String(), nil
}
func outputDenebBlobInfo(_ context.Context,
func outputElectraBlockExecutionRequests(_ context.Context,
verbose bool,
body *deneb.BeaconBlockBody,
executionRequests *electra.ExecutionRequests,
) (
string,
error,
) {
if executionRequests == nil {
return "", nil
}
res := strings.Builder{}
res.WriteString("Deposit requests: ")
res.WriteString(fmt.Sprintf("%d\n", len(executionRequests.Deposits)))
if verbose {
for i, deposit := range executionRequests.Deposits {
res.WriteString(fmt.Sprintf("%3d:\n", i))
res.WriteString(fmt.Sprintf(" Public key: %#x\n", deposit.Pubkey))
res.WriteString(fmt.Sprintf(" Withdrawal credentials: %#x\n", deposit.WithdrawalCredentials))
res.WriteString(fmt.Sprintf(" Amount: %s\n", string2eth.GWeiToString(uint64(deposit.Amount), true)))
}
}
res.WriteString("Withdrawal requests: ")
res.WriteString(fmt.Sprintf("%d\n", len(executionRequests.Withdrawals)))
if verbose {
for i, withdrawal := range executionRequests.Withdrawals {
res.WriteString(fmt.Sprintf("%3d:\n", i))
res.WriteString(fmt.Sprintf(" Source address: %#x\n", withdrawal.SourceAddress))
res.WriteString(fmt.Sprintf(" Validator public key: %#x\n", withdrawal.ValidatorPubkey))
res.WriteString(fmt.Sprintf(" Amount: %s\n", string2eth.GWeiToString(uint64(withdrawal.Amount), true)))
}
}
res.WriteString("Consolidation requests: ")
res.WriteString(fmt.Sprintf("%d\n", len(executionRequests.Consolidations)))
if verbose {
for i, consolidation := range executionRequests.Consolidations {
res.WriteString(fmt.Sprintf("%3d:\n", i))
res.WriteString(fmt.Sprintf(" Source address: %#x\n", consolidation.SourceAddress))
res.WriteString(fmt.Sprintf(" Source public key: %#x\n", consolidation.SourcePubkey))
res.WriteString(fmt.Sprintf(" Target public key: %#x\n", consolidation.TargetPubkey))
}
}
return res.String(), nil
}
func outputBlobInfo(_ context.Context,
verbose bool,
commitments []deneb.KZGCommitment,
blobs []*deneb.BlobSidecar,
) (
string,
error,
) {
if body == nil {
return "", nil
}
if !verbose {
return fmt.Sprintf("Blobs: %d\n", len(body.BlobKZGCommitments)), nil
}
res := strings.Builder{}
for i, blob := range blobs {
if i == 0 {
res.WriteString("Blobs\n")
if len(blobs) == 0 && len(commitments) > 0 {
res.WriteString(fmt.Sprintf("Blobs: %d (but no blobs obtained from the beacon node)\n", len(commitments)))
} else {
res.WriteString(fmt.Sprintf("Blobs: %d\n", len(blobs)))
if verbose {
for i, blob := range blobs {
res.WriteString(fmt.Sprintf("%3d:\n", i))
res.WriteString(fmt.Sprintf(" KZG proof: %s\n", blob.KZGProof.String()))
res.WriteString(fmt.Sprintf(" KZG commitment: %s\n", blob.KZGCommitment.String()))
}
}
res.WriteString(fmt.Sprintf(" Index: %d\n", blob.Index))
res.WriteString(fmt.Sprintf(" KZG commitment: %s\n", body.BlobKZGCommitments[i].String()))
}
return res.String(), nil
@@ -959,7 +1211,7 @@ func outputBellatrixBlockExecutionPayload(_ context.Context,
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas))
for i := 0; i < 32; i++ {
for i := range 32 {
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
}
baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBEBytes)
@@ -1022,7 +1274,7 @@ func bitlistToString(input bitfield.Bitlist) string {
bits := int(input.Len())
res := ""
for i := 0; i < bits; i++ {
for i := range bits {
if input.BitAt(uint64(i)) {
res = fmt.Sprintf("%s✓", res)
} else {
@@ -1039,7 +1291,7 @@ func bitvectorToString(input bitfield.Bitvector512) string {
bits := int(input.Len())
res := strings.Builder{}
for i := 0; i < bits; i++ {
for i := range bits {
if input.BitAt(uint64(i)) {
res.WriteString("✓")
} else {
@@ -1052,13 +1304,118 @@ func bitvectorToString(input bitfield.Bitvector512) string {
return res.String()
}
func attestingIndices(input bitfield.Bitlist, indices []phase0.ValidatorIndex) string {
func attestingIndices(input bitfield.Bitlist,
committees map[phase0.CommitteeIndex][]phase0.ValidatorIndex,
includedCommittees []int,
) string {
bits := int(input.Len())
res := ""
for i := 0; i < bits; i++ {
// Build up the validator list from the included committees.
validatorIndices := make([]phase0.ValidatorIndex, 0)
for _, committeeIndex := range includedCommittees {
validatorIndices = append(validatorIndices, committees[phase0.CommitteeIndex(committeeIndex)]...)
}
res := strings.Builder{}
for i := range bits {
if input.BitAt(uint64(i)) {
res = fmt.Sprintf("%s%d ", res, indices[i])
// Work out the committee and offset given the index.
res.WriteString(fmt.Sprintf("%d ", validatorIndices[i]))
}
}
return strings.TrimSpace(res)
return strings.TrimSpace(res.String())
}
func blockGraffiti(_ context.Context, graffiti []byte) string {
if len(graffiti) == 0 || hex.EncodeToString(graffiti) == "0000000000000000000000000000000000000000000000000000000000000000" {
// No graffiti.
return ""
}
// Remove any trailing null characters.
graffiti = bytes.TrimRight(graffiti, "\u0000")
if !utf8.Valid(graffiti) {
// Graffiti is not valid UTF-8, return hex.
return fmt.Sprintf("Graffiti: %#x\n", graffiti)
}
// See if there is client identification information present in the graffiti.
// The client identification will always be the last entry in the graffiti, with a space beforehand.
parts := bytes.Split(graffiti, []byte{' '})
// Consensus and execution client values come from
// https://github.com/ethereum/execution-apis/blob/main/src/engine/identification.md
consensusClients := map[string]string{
"GR": "grandine",
"LH": "lighthouse",
"LS": "lodestar",
"NB": "nimbus",
"PM": "prysm",
"TK": "teku",
}
consensusRegex := regexp.MustCompile(`(GR|LH|LS|NB|PM|TK)([0-9a-f]*)`)
consensusData := consensusRegex.Find(parts[len(parts)-1])
executionClients := map[string]string{
"BU": "besu",
"EG": "erigon",
"EJ": "ethereumJS",
"GE": "go-ethereum",
"NM": "nethermind",
"RH": "reth",
"TR": "trin-execution",
}
executionRegex := regexp.MustCompile(`(BU|EG|EJ|GE|NM|RH|TR)([0-9a-f]*)`)
executionData := executionRegex.Find(parts[len(parts)-1])
if len(consensusData) == 0 && len(executionData) == 0 {
// There is no identifier; return the graffiti as-is.
return fmt.Sprintf("Graffiti: %s\n", string(graffiti))
}
res := strings.Builder{}
truncatedGraffiti := bytes.Join(parts[0:len(parts)-1], []byte(" "))
if len(truncatedGraffiti) > 0 {
res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(truncatedGraffiti)))
}
if len(consensusData) > 0 {
consensusClient := consensusData[0:2]
consensusHash := ""
if len(consensusData) > 2 {
consensusHash = string(consensusData[2:])
}
res.WriteString("Consensus client: ")
res.WriteString(consensusClients[string(consensusClient)])
if consensusHash != "" {
res.WriteString(" (version hash ")
res.WriteString(consensusHash)
res.WriteString(")")
}
res.WriteString("\n")
}
if len(executionData) > 0 {
executionClient := executionData[0:2]
executionHash := ""
if len(executionData) > 2 {
executionHash = string(executionData[2:])
}
res.WriteString("Execution client: ")
res.WriteString(executionClients[string(executionClient)])
if executionHash != "" {
res.WriteString(" (version hash ")
res.WriteString(executionHash)
res.WriteString(")")
}
res.WriteString("\n")
}
return res.String()
}

View File

@@ -175,3 +175,58 @@ func TestOutputBlockETH1Data(t *testing.T) {
})
}
}
func TestBlockGraffiti(t *testing.T) {
tests := []struct {
name string
graffiti []byte
res string
}{
{
name: "Empty",
graffiti: []byte(""),
},
{
name: "NoID",
graffiti: []byte("No identifier"),
res: "Graffiti: No identifier\n",
},
{
name: "SingleClient",
graffiti: []byte("Graffiti TK"),
res: "Graffiti: Graffiti\nConsensus client: teku\n",
},
{
name: "SingleClientImmediate",
graffiti: []byte("TK"),
res: "Consensus client: teku\n",
},
{
name: "SingleClientAndHashImmediate",
graffiti: []byte("TKa9f98260"),
res: "Consensus client: teku (version hash a9f98260)\n",
},
{
name: "DualClients",
graffiti: []byte("LHGE"),
res: "Consensus client: lighthouse\nExecution client: go-ethereum\n",
},
{
name: "DualClientsReverseOrder",
graffiti: []byte("GELH"),
res: "Consensus client: lighthouse\nExecution client: go-ethereum\n",
},
{
name: "DualClientsTruncatedHash",
graffiti: []byte("Freedom To Transact TKa9f9NM220b"),
res: "Graffiti: Freedom To Transact\nConsensus client: teku (version hash a9f9)\nExecution client: nethermind (version hash 220b)\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res := blockGraffiti(context.Background(), test.graffiti)
require.Equal(t, test.res, res)
})
}
}

View File

@@ -31,6 +31,7 @@ import (
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/capella"
"github.com/attestantio/go-eth2-client/spec/deneb"
"github.com/attestantio/go-eth2-client/spec/electra"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
@@ -56,18 +57,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
eth2Client: data.eth2Client,
}
specResponse, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
err := populateResults(ctx, results)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
return nil, err
}
genesisResponse, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
}
genesis := genesisResponse.Data
results.genesisTime = genesis.GenesisTime
results.slotDuration = specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
results.slotsPerEpoch = specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
if data.blockTime != "" {
data.blockID, err = timeToBlockID(ctx, data.eth2Client, data.blockTime)
@@ -76,6 +69,77 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
}
block, err := obtainBlock(ctx, data, results)
if err != nil {
return nil, err
}
if data.quiet {
os.Exit(0)
}
switch block.Version {
case spec.DataVersionPhase0:
err = outputPhase0Block(ctx, data.jsonOutput, block.Phase0)
case spec.DataVersionAltair:
err = outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, block.Altair)
case spec.DataVersionBellatrix:
err = outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, block.Bellatrix)
case spec.DataVersionCapella:
err = outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, block.Capella)
case spec.DataVersionDeneb:
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")
}
if err != nil {
return nil, errors.Wrap(err, "failed to process block")
}
if data.stream {
jsonOutput = data.jsonOutput
sszOutput = data.sszOutput
if !jsonOutput && !sszOutput {
fmt.Println("")
}
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")
}
<-ctx.Done()
}
return &dataOut{}, nil
}
func populateResults(ctx context.Context, results *dataOut) error {
specResponse, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
if err != nil {
return errors.Wrap(err, "failed to connect to obtain configuration information")
}
genesisResponse, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
if err != nil {
return errors.Wrap(err, "failed to connect to obtain genesis information")
}
genesis := genesisResponse.Data
results.genesisTime = genesis.GenesisTime
results.slotDuration = specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
results.slotsPerEpoch = specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
return nil
}
func obtainBlock(ctx context.Context, data *dataIn, results *dataOut,
) (
*spec.VersionedSignedBeaconBlock,
error,
) {
blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: data.blockID,
})
@@ -91,68 +155,99 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.Wrap(err, "failed to obtain beacon block")
}
block := blockResponse.Data
if data.quiet {
os.Exit(0)
}
switch block.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(ctx, data.jsonOutput, block.Phase0); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionAltair:
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, block.Altair); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, block.Bellatrix); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionCapella:
if err := outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, block.Capella); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionDeneb:
return blockResponse.Data, nil
}
func processDenebBlock(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 {
return nil, errors.Wrap(err, "failed to obtain blob sidecars")
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
}
blobSidecars := blobSidecarsResponse.Data
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, block.Deneb, blobSidecars); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
default:
return nil, errors.New("unknown block version")
}
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, block.Deneb, blobSidecars); err != nil {
return errors.Wrap(err, "failed to output block")
}
if data.stream {
jsonOutput = data.jsonOutput
sszOutput = data.sszOutput
if !jsonOutput && !sszOutput {
fmt.Println("")
}
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
if err != nil {
return nil, errors.Wrap(err, "failed to start block stream")
}
<-ctx.Done()
}
return &dataOut{}, nil
return nil
}
func headEventHandler(event *apiv1.Event) {
ctx := context.Background()
// Only interested in head events.
if event.Topic != "head" {
return
func processElectraBlock(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 := outputElectraBlock(ctx, data.jsonOutput, data.sszOutput, block.Electra, blobSidecars); err != nil {
return errors.Wrap(err, "failed to output block")
}
blockID := fmt.Sprintf("%#x", event.Data.(*apiv1.HeadEvent).Block[:])
return nil
}
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")
}
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,
})
@@ -180,13 +275,65 @@ func headEventHandler(event *apiv1.Event) {
case spec.DataVersionCapella:
err = outputCapellaBlock(ctx, jsonOutput, sszOutput, block.Capella)
case spec.DataVersionDeneb:
var blobSidecarsResponse *api.Response[[]*deneb.BlobSidecar]
blobSidecarsResponse, err = results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
Block: blockID,
})
if err == nil {
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, block.Deneb, blobSidecarsResponse.Data)
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 = 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")
}
@@ -319,6 +466,64 @@ func outputDenebBlock(ctx context.Context,
return nil
}
func outputElectraBlock(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 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

View File

@@ -0,0 +1,89 @@
// Copyright © 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blocktrail
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Operation.
blockID string
jsonOutput bool
target string
maxBlocks int
// Data access.
consensusClient eth2client.Service
chainTime chaintime.Service
blocksProvider eth2client.SignedBeaconBlockProvider
blockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Processing.
justifiedCheckpoint *phase0.Checkpoint
finalizedCheckpoint *phase0.Checkpoint
// Results.
steps []*step
found bool
}
type step struct {
Slot phase0.Slot `json:"slot"`
Root phase0.Root `json:"root"`
ParentRoot phase0.Root `json:"parent_root"`
State string `json:"state,omitempty"`
// Not a slot, but we're using it to steal the JSON processing.
ExecutionBlock phase0.Slot `json:"execution_block"`
ExecutionHash phase0.Hash32 `json:"execution_hash"`
}
func newCommand(_ context.Context) (*command, error) {
c := &command{
timeout: viper.GetDuration("timeout"),
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
jsonOutput: viper.GetBool("json"),
connection: viper.GetString("connection"),
allowInsecureConnections: viper.GetBool("allow-insecure-connections"),
blockID: viper.GetString("blockid"),
target: viper.GetString("target"),
maxBlocks: viper.GetInt("max-blocks"),
steps: make([]*step, 0),
}
// Timeout.
if c.timeout == 0 {
return nil, errors.New("timeout is required")
}
return c, nil
}

74
cmd/block/trail/output.go Normal file
View File

@@ -0,0 +1,74 @@
// Copyright © 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blocktrail
import (
"context"
"encoding/json"
"fmt"
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.jsonOutput {
return c.outputJSON(ctx)
}
return c.outputTxt(ctx)
}
type simpleOut struct {
Start *step `json:"start"`
End *step `json:"end"`
Steps int `json:"distance"`
}
func (c *command) outputJSON(_ context.Context) (string, error) {
var err error
var data []byte
if c.verbose {
data, err = json.Marshal(c.steps)
} else {
basic := &simpleOut{
Start: c.steps[0],
End: c.steps[len(c.steps)-1],
Steps: len(c.steps) - 1,
}
data, err = json.Marshal(basic)
}
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputTxt(_ context.Context) (string, error) {
if !c.found {
return "Target not found", nil
}
builder := strings.Builder{}
builder.WriteString("Target '")
builder.WriteString(c.target)
builder.WriteString("' found at a distance of ")
builder.WriteString(fmt.Sprintf("%d", len(c.steps)-1))
builder.WriteString(" block(s)")
return builder.String(), nil
}

182
cmd/block/trail/process.go Normal file
View File

@@ -0,0 +1,182 @@
// Copyright © 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blocktrail
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
untilRoot := phase0.Root{}
var untilBlock phase0.Slot
switch {
case strings.ToLower(c.target) == "justified", strings.ToLower(c.target) == "finalized":
// Nothing to do.
case strings.HasPrefix(c.target, "0x"):
// Assume a root.
if err := json.Unmarshal([]byte(fmt.Sprintf("%q", c.target)), &untilRoot); err != nil {
return err
}
default:
// Assume a block number.
tmp, err := strconv.ParseUint(c.target, 10, 64)
if err != nil {
return err
}
untilBlock = phase0.Slot(tmp)
}
blockID := c.blockID
for range c.maxBlocks {
step := &step{}
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: blockID,
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
return errors.New("empty beacon block")
}
return errors.Wrap(err, "failed to obtain beacon block")
}
block := blockResponse.Data
step.Slot, err = block.Slot()
if err != nil {
return err
}
step.Root, err = block.Root()
if err != nil {
return err
}
step.ParentRoot, err = block.ParentRoot()
if err != nil {
return err
}
executionBlock, err := block.ExecutionBlockNumber()
if err != nil {
return err
}
step.ExecutionBlock = phase0.Slot(executionBlock)
step.ExecutionHash, err = block.ExecutionBlockHash()
if err != nil {
return err
}
if c.debug {
data, err := json.Marshal(step)
if err == nil {
fmt.Fprintf(os.Stderr, "Step is %s\n", string(data))
}
}
c.steps = append(c.steps, step)
blockID = step.ParentRoot.String()
if c.target == "justified" && bytes.Equal(step.Root[:], c.justifiedCheckpoint.Root[:]) {
c.found = true
break
}
if c.target == "finalized" && bytes.Equal(step.Root[:], c.finalizedCheckpoint.Root[:]) {
c.found = true
break
}
if untilBlock > 0 && step.Slot == untilBlock {
c.found = true
break
}
if (!untilRoot.IsZero()) && bytes.Equal(step.Root[:], untilRoot[:]) {
c.found = true
break
}
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.consensusClient, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.consensusClient.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(c.consensusClient.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
var isProvider bool
c.blocksProvider, isProvider = c.consensusClient.(eth2client.SignedBeaconBlockProvider)
if !isProvider {
return errors.New("connection does not provide signed beacon block information")
}
c.blockHeadersProvider, isProvider = c.consensusClient.(eth2client.BeaconBlockHeadersProvider)
if !isProvider {
return errors.New("connection does not provide beacon block header information")
}
finalityProvider, isProvider := c.consensusClient.(eth2client.FinalityProvider)
if !isProvider {
return errors.New("connection does not provide finality information")
}
finalityResponse, err := finalityProvider.Finality(ctx, &api.FinalityOpts{
State: "head",
})
if err != nil {
return errors.Wrap(err, "failed to obtain finality")
}
finality := finalityResponse.Data
c.justifiedCheckpoint = finality.Justified
if c.debug {
fmt.Fprintf(os.Stderr, "Justified checkpoint is %d / %#x\n", c.justifiedCheckpoint.Epoch, c.justifiedCheckpoint.Root)
}
c.finalizedCheckpoint = finality.Finalized
if c.debug {
fmt.Fprintf(os.Stderr, "Finalized checkpoint is %d / %#x\n", c.finalizedCheckpoint.Epoch, c.finalizedCheckpoint.Root)
}
return nil
}

View File

@@ -0,0 +1,63 @@
// Copyright © 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blocktrail
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "NoBlock",
vars: map[string]interface{}{
"timeout": "60s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"blockid": "invalid",
},
err: "failed to obtain beacon block: failed to request signed beacon block: GET failed with status 400: {\"code\":400,\"message\":\"BAD_REQUEST: Unsupported endpoint version: v2\",\"stacktraces\":[]}",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
cmd, err := newCommand(context.Background())
require.NoError(t, err)
err = cmd.process(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

59
cmd/block/trail/run.go Normal file
View File

@@ -0,0 +1,59 @@
// Copyright © 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blocktrail
import (
"context"
"errors"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
c, err := newCommand(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if viper.GetBool("quiet") {
if c.found {
return "", nil
}
os.Exit(1)
}
results, err := c.output(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil
}

65
cmd/blocktrail.go Normal file
View File

@@ -0,0 +1,65 @@
// Copyright © 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
blocktrail "github.com/wealdtech/ethdo/cmd/block/trail"
)
var blockTrailCmd = &cobra.Command{
Use: "trail",
Short: "Trail back in the chain from a given block.",
Long: `Trail back in the chain for a given block. For example:
ethdo block trail --blockid=12345 --target=finalized
In quiet mode this will return 0 if the block trail ends up at the finalized state, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := blocktrail.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
blockCmd.AddCommand(blockTrailCmd)
blockFlags(blockTrailCmd)
blockTrailCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockTrailCmd.Flags().String("target", "justified", "the target block (block number, hash, justified or finalized)")
blockTrailCmd.Flags().Int("max-blocks", 16384, "the maximum number of blocks to look at before halting")
}
func blockTrailBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("target", cmd.Flags().Lookup("target")); err != nil {
panic(err)
}
if err := viper.BindPFlag("max-blocks", cmd.Flags().Lookup("max-blocks")); err != nil {
panic(err)
}
}

View File

@@ -97,6 +97,12 @@ func (c *command) process(ctx context.Context) error {
case spec.DataVersionDeneb:
c.incumbent = state.Deneb.ETH1Data
c.eth1DataVotes = state.Deneb.ETH1DataVotes
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)
}

View File

@@ -221,7 +221,7 @@ func (c *command) confirmContributionSignature(ctx context.Context) error {
subCommittee := c.syncCommittee.ValidatorAggregates[c.item.Message.Contribution.SubcommitteeIndex]
includedIndices := make([]phase0.ValidatorIndex, 0, len(subCommittee))
for i := uint64(0); i < c.item.Message.Contribution.AggregationBits.Len(); i++ {
for i := range c.item.Message.Contribution.AggregationBits.Len() {
if c.item.Message.Contribution.AggregationBits.BitAt(i) {
includedIndices = append(includedIndices, subCommittee[int(i)])
}

View File

@@ -65,6 +65,12 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
errCheck(err, "Failed to obtain finality information")
finality := finalityResponse.Data
blockProvider, isProvider := eth2Client.(eth2client.SignedBeaconBlockProvider)
assert(isProvider, "beacon node does not provide signed beacon blocks; cannot report on chain status")
blockResponse, err := blockProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{Block: "head"})
errCheck(err, "Failed to obtain block information")
block := blockResponse.Data
slot := chainTime.CurrentSlot()
nextSlot := slot + 1
@@ -78,12 +84,28 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
nextEpochStartSlot := chainTime.FirstSlotOfEpoch(nextEpoch)
nextEpochTimestamp := chainTime.StartOfEpoch(nextEpoch)
headSlot, err := block.Slot()
errCheck(err, "Failed to obtain block slot")
res := strings.Builder{}
res.WriteString("Current slot: ")
res.WriteString(fmt.Sprintf("%d", slot))
res.WriteString("\n")
res.WriteString("Head slot: ")
res.WriteString(fmt.Sprintf("%d", headSlot))
if headSlot != slot {
if slot-headSlot == 1 {
res.WriteString(" (1 slot behind)")
} else {
res.WriteString(" (")
res.WriteString(fmt.Sprintf("%d", slot-headSlot))
res.WriteString(" slots behind)")
}
}
res.WriteString("\n")
res.WriteString("Current epoch: ")
res.WriteString(fmt.Sprintf("%d", epoch))
res.WriteString("\n")

View File

@@ -45,7 +45,7 @@ var depositVerifyCmd = &cobra.Command{
Short: "Verify deposit data matches the provided data",
Long: `Verify deposit data matches the provided input data. For example:
ethdo deposit verify --data=depositdata.json --withdrawalaccount=primary/current --value="32 Ether"
ethdo deposit verify --data=depositdata.json --withdrawalaccount=primary/current --depositvalue="32 Ether"
The deposit data is compared to the supplied withdrawal account/public key, validator public key, and value to ensure they match.

View File

@@ -29,8 +29,8 @@ func init() {
RootCmd.AddCommand(epochCmd)
}
func epochFlags(_ *cobra.Command) {
epochSummaryCmd.Flags().String("epoch", "", "the epoch for which to obtain information (default current, can be 'current', 'last' or a number)")
func epochFlags(cmd *cobra.Command) {
cmd.Flags().String("epoch", "", "the epoch for which to obtain information (default current, can be 'current', 'last' or a number)")
}
func epochBindings(cmd *cobra.Command) {

View File

@@ -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"
@@ -36,9 +38,11 @@ type command struct {
allowInsecureConnections bool
// Operation.
epoch string
stream bool
jsonOutput bool
epoch string
validatorsStr []string
validators map[phase0.ValidatorIndex]struct{}
stream bool
jsonOutput bool
// Data access.
eth2Client eth2client.Service
@@ -50,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
@@ -58,46 +72,84 @@ type command struct {
}
type epochSummary struct {
Epoch phase0.Epoch `json:"epoch"`
FirstSlot phase0.Slot `json:"first_slot"`
LastSlot phase0.Slot `json:"last_slot"`
Proposals []*epochProposal `json:"proposals"`
SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
ActiveValidators int `json:"active_validators"`
ParticipatingValidators int `json:"participating_validators"`
HeadCorrectValidators int `json:"head_correct_validators"`
HeadTimelyValidators int `json:"head_timely_validators"`
SourceTimelyValidators int `json:"source_timely_validators"`
TargetCorrectValidators int `json:"target_correct_validators"`
TargetTimelyValidators int `json:"target_timely_validators"`
NonParticipatingValidators []*nonParticipatingValidator `json:"nonparticipating_validators"`
Blobs int `json:"blobs"`
Epoch phase0.Epoch `json:"epoch"`
FirstSlot phase0.Slot `json:"first_slot"`
LastSlot phase0.Slot `json:"last_slot"`
Blocks int `json:"blocks"`
Proposals []*epochProposal `json:"proposals"`
SyncCommitteeValidators int `json:"sync_committee_validators"`
SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
ActiveValidators int `json:"active_validators"`
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"`
NonTargetCorrectValidators []*attestingValidator `json:"nontargetcorrect_validators"`
NonSourceTimelyValidators []*attestingValidator `json:"nonsourcetimely_validators"`
Blobs int `json:"blobs"`
}
type epochProposal struct {
Slot phase0.Slot `json:"slot"`
Proposer phase0.ValidatorIndex `json:"proposer"`
Block bool `json:"block"`
ValidatorIndex phase0.ValidatorIndex `json:"validator_index"`
Slot phase0.Slot `json:"slot"`
Block bool `json:"block"`
}
type epochSyncCommittee struct {
Index phase0.ValidatorIndex `json:"index"`
Missed int `json:"missed"`
ValidatorIndex phase0.ValidatorIndex `json:"validator_index"`
Missed int `json:"missed"`
MissedSlots []phase0.Slot `json:"missed_slots"`
}
type nonParticipatingValidator struct {
Validator phase0.ValidatorIndex `json:"validator_index"`
Slot phase0.Slot `json:"slot"`
Committee phase0.CommitteeIndex `json:"committee_index"`
type attestingValidator struct {
Validator phase0.ValidatorIndex `json:"validator_index"`
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) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
summary: &epochSummary{},
blocksCache: make(map[string]*spec.VersionedSignedBeaconBlock),
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
validatorsStr: viper.GetStringSlice("validators"),
summary: &epochSummary{
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{}),
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.

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");
// 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"
)
@@ -50,7 +51,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
missedProposals := make([]string, 0, len(c.summary.Proposals))
for _, proposal := range c.summary.Proposals {
if !proposal.Block {
missedProposals = append(missedProposals, fmt.Sprintf("\n Slot %d (validator %d)", proposal.Slot, proposal.Proposer))
missedProposals = append(missedProposals, fmt.Sprintf("\n Slot %d (validator %d)", proposal.Slot, proposal.ValidatorIndex))
} else {
proposedBlocks++
}
@@ -64,17 +65,85 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
builder.WriteString("\n Slot ")
builder.WriteString(fmt.Sprintf("%d (%d/%d)", proposal.Slot, uint64(proposal.Slot)%uint64(len(c.summary.Proposals)), len(c.summary.Proposals)))
builder.WriteString(" validator ")
builder.WriteString(fmt.Sprintf("%d", proposal.Proposer))
builder.WriteString(fmt.Sprintf("%d", proposal.ValidatorIndex))
builder.WriteString(" not proposed or not included")
}
}
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 {
@@ -98,7 +167,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
if c.verbose {
for _, syncCommittee := range c.summary.SyncCommittee {
builder.WriteString("\n Validator ")
builder.WriteString(fmt.Sprintf("%d", syncCommittee.Index))
builder.WriteString(fmt.Sprintf("%d", syncCommittee.ValidatorIndex))
builder.WriteString(" included ")
builder.WriteString(fmt.Sprintf("%d/%d", proposedBlocks-syncCommittee.Missed, proposedBlocks))
builder.WriteString(fmt.Sprintf(" (%0.2f%%)", 100.0*float64(proposedBlocks-syncCommittee.Missed)/float64(proposedBlocks)))

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");
// 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"
@@ -43,6 +44,14 @@ func (c *command) process(ctx context.Context) error {
c.summary.FirstSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)
c.summary.LastSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) - 1
validators, err := util.ParseValidators(ctx, c.validatorsProvider, c.validatorsStr, "head")
if err != nil {
return errors.Wrap(err, "failed to parse validators")
}
for _, validator := range validators {
c.validators[validator.Index] = struct{}{}
}
if err := c.processProposerDuties(ctx); err != nil {
return err
}
@@ -52,6 +61,7 @@ func (c *command) process(ctx context.Context) error {
if err := c.processSyncCommitteeDuties(ctx); err != nil {
return err
}
return c.processBlobs(ctx)
}
@@ -69,10 +79,20 @@ func (c *command) processProposerDuties(ctx context.Context) error {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", duty.Slot))
}
present := block != nil
if present {
c.summary.Blocks++
}
_, exists := c.validators[duty.ValidatorIndex]
if len(c.validators) > 0 && !exists {
// Not one of ours.
continue
}
c.summary.Proposals = append(c.summary.Proposals, &epochProposal{
Slot: duty.Slot,
Proposer: duty.ValidatorIndex,
Block: present,
Slot: duty.Slot,
ValidatorIndex: duty.ValidatorIndex,
Block: present,
})
}
@@ -80,14 +100,27 @@ func (c *command) processProposerDuties(ctx context.Context) error {
}
func (c *command) activeValidators(ctx context.Context) (map[phase0.ValidatorIndex]*apiv1.Validator, error) {
validatorIndices := make([]phase0.ValidatorIndex, 0, len(c.validators))
for validator := range c.validators {
validatorIndices = append(validatorIndices, validator)
}
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)),
State: 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]
if len(c.validators) > 0 && !exists {
continue
}
if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch {
activeValidators[validator.Index] = validator
}
@@ -103,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.
@@ -112,18 +149,42 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
lastSlot = c.chainTime.CurrentSlot()
}
var votes map[phase0.ValidatorIndex]struct{}
var participations map[phase0.ValidatorIndex]*nonParticipatingValidator
c.summary.ParticipatingValidators, c.summary.HeadCorrectValidators, c.summary.HeadTimelyValidators, c.summary.SourceTimelyValidators, c.summary.TargetCorrectValidators, c.summary.TargetTimelyValidators, votes, participations, err = c.processSlots(ctx, firstSlot, lastSlot)
if err != nil {
if err := c.processSlots(ctx, firstSlot, lastSlot); err != nil {
return err
}
c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0, len(activeValidators)-len(votes))
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(c.participatingValidators))
for activeValidatorIndex := range activeValidators {
if _, exists := votes[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 := c.headCorrectValidators[activeValidatorIndex]; !exists {
if _, exists := c.participations[activeValidatorIndex]; exists {
c.summary.NonHeadCorrectValidators = append(c.summary.NonHeadCorrectValidators, c.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 := c.targetCorrectValidators[activeValidatorIndex]; !exists {
if _, exists := c.participations[activeValidatorIndex]; exists {
c.summary.NonTargetCorrectValidators = append(c.summary.NonTargetCorrectValidators, c.participations[activeValidatorIndex])
}
}
if _, exists := c.sourceTimelyValidators[activeValidatorIndex]; !exists {
if _, exists := c.participations[activeValidatorIndex]; exists {
c.summary.NonSourceTimelyValidators = append(c.summary.NonSourceTimelyValidators, c.participations[activeValidatorIndex])
}
}
}
@@ -140,28 +201,12 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
return nil
}
//nolint:gocyclo
func (c *command) processSlots(ctx context.Context,
firstSlot phase0.Slot,
lastSlot phase0.Slot,
) (
int,
int,
int,
int,
int,
int,
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]*nonParticipatingValidator,
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]*nonParticipatingValidator)
// Need a cache of beacon block headers to reduce lookup times.
headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider)
@@ -169,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 0, 0, 0, 0, 0, 0, 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.
@@ -177,83 +222,154 @@ func (c *command) processSlots(ctx context.Context,
}
slot, err := block.Slot()
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
return err
}
attestations, err := block.Attestations()
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
return err
}
for _, attestation := range attestations {
if attestation.Data.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestation.Data.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
attestationData, err := attestation.Data()
if err != nil {
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.
continue
}
slotCommittees, exists := allCommittees[attestation.Data.Slot]
slotCommittees, exists := allCommittees[attestationData.Slot]
if !exists {
response, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, &api.BeaconCommitteesOpts{
State: fmt.Sprintf("%d", attestation.Data.Slot),
State: fmt.Sprintf("%d", attestationData.Slot),
})
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
return 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 {
allCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
}
allCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
for _, index := range beaconCommittee.Validators {
participations[index] = &nonParticipatingValidator{
Validator: index,
Slot: beaconCommittee.Slot,
Committee: beaconCommittee.Index,
if len(c.validators) > 0 {
if _, exists := c.validators[index]; !exists {
// Not one of our validators.
continue
}
}
if _, exists := c.participations[index]; !exists {
c.participations[index] = &attestingValidator{
Validator: index,
EffectiveBalance: c.validatorInfo[index].Validator.EffectiveBalance,
Slot: beaconCommittee.Slot,
Committee: beaconCommittee.Index,
}
}
}
}
slotCommittees = allCommittees[attestation.Data.Slot]
slotCommittees = allCommittees[attestationData.Slot]
}
committee := slotCommittees[attestation.Data.Index]
inclusionDistance := slot - attestation.Data.Slot
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
for i := uint64(0); i < attestation.AggregationBits.Len(); i++ {
if attestation.AggregationBits.BitAt(i) {
votes[committee[int(i)]] = struct{}{}
if _, exists := headCorrects[committee[int(i)]]; !exists && headCorrect {
headCorrects[committee[int(i)]] = struct{}{}
}
if _, exists := headTimelys[committee[int(i)]]; !exists && headCorrect && inclusionDistance == 1 {
headTimelys[committee[int(i)]] = struct{}{}
}
if _, exists := sourceTimelys[committee[int(i)]]; !exists && inclusionDistance <= 5 {
sourceTimelys[committee[int(i)]] = struct{}{}
}
if _, exists := targetCorrects[committee[int(i)]]; !exists && targetCorrect {
targetCorrects[committee[int(i)]] = struct{}{}
}
if _, exists := targetTimelys[committee[int(i)]]; !exists && targetCorrect && inclusionDistance <= 32 {
targetTimelys[committee[int(i)]] = struct{}{}
}
}
if err := c.extractAttestationData(ctx, attestation, attestationData, slotCommittees, slot, headersCache); err != nil {
return err
}
}
}
return len(votes),
len(headCorrects),
len(headTimelys),
len(sourceTimelys),
len(targetCorrects),
len(targetTimelys),
votes,
participations,
nil
return nil
}
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,
) error {
inclusionDistance := slot - attestationData.Slot
head, err := util.AttestationHead(ctx, headersCache, attestation)
if err != nil {
return err
}
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
if err != nil {
return err
}
target, err := util.AttestationTarget(ctx, headersCache, c.chainTime, attestation)
if err != nil {
return err
}
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
if err != nil {
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 errors.Wrap(err, "failed to obtain aggregation bits")
}
for i := range aggregationBits.Len() {
if aggregationBits.BitAt(i) {
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 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
}
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 := c.headCorrectValidators[validatorIndex]; !exists && headCorrect {
c.headCorrectValidators[validatorIndex] = struct{}{}
c.summary.HeadCorrectBalance = c.summary.HeadCorrectBalance.Add(c.summary.HeadCorrectBalance, validatorBalance)
}
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 := c.sourceTimelyValidators[validatorIndex]; !exists && inclusionDistance <= 5 {
c.sourceTimelyValidators[validatorIndex] = struct{}{}
c.summary.SourceTimelyBalance = c.summary.SourceTimelyBalance.Add(c.summary.SourceTimelyBalance, validatorBalance)
}
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 nil
}
func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
@@ -273,7 +389,18 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
return errors.Wrap(err, "empty sync committee")
}
for _, validatorIndex := range committee.Validators {
if len(c.validators) == 0 {
c.summary.SyncCommitteeValidators++
} else {
if _, exists := c.validators[validatorIndex]; exists {
c.summary.SyncCommitteeValidators++
}
}
}
missed := make(map[phase0.ValidatorIndex]int)
missedSlots := make(map[phase0.ValidatorIndex][]phase0.Slot)
for _, index := range committee.Validators {
missed[index] = 0
}
@@ -296,9 +423,17 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
if err != nil {
return errors.Wrapf(err, "failed to obtain sync aggregate for slot %d", slot)
}
for i := uint64(0); i < aggregate.SyncCommitteeBits.Len(); i++ {
for i := range aggregate.SyncCommitteeBits.Len() {
validatorIndex := committee.Validators[int(i)]
if _, exists := c.validators[validatorIndex]; !exists {
if len(c.validators) > 0 {
// Not one of ours.
continue
}
}
if !aggregate.SyncCommitteeBits.BitAt(i) {
missed[committee.Validators[int(i)]]++
missed[validatorIndex]++
missedSlots[validatorIndex] = append(missedSlots[validatorIndex], slot)
}
}
}
@@ -307,8 +442,9 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
for index, count := range missed {
if count > 0 {
c.summary.SyncCommittee = append(c.summary.SyncCommittee, &epochSyncCommittee{
Index: index,
Missed: count,
ValidatorIndex: index,
Missed: count,
MissedSlots: missedSlots[index],
})
}
}
@@ -320,7 +456,7 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
return missedDiff > 0
}
// Then order by validator index.
return c.summary.SyncCommittee[i].Index < c.summary.SyncCommittee[j].Index
return c.summary.SyncCommittee[i].ValidatorIndex < c.summary.SyncCommittee[j].ValidatorIndex
})
return nil
@@ -378,10 +514,10 @@ func (c *command) setup(ctx context.Context) error {
}
func (c *command) processBlobs(ctx context.Context) error {
for slot := c.summary.FirstSlot; slot <= c.summary.LastSlot; slot++ {
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", slot))
for _, proposal := range c.summary.Proposals {
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", proposal.Slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", proposal.Slot))
}
if block == nil {
continue
@@ -391,6 +527,10 @@ func (c *command) processBlobs(ctx context.Context) error {
// No blobs in these forks.
case spec.DataVersionDeneb:
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)
}

View File

@@ -46,9 +46,17 @@ In quiet mode this will return 0 if information for the epoch is found, otherwis
func init() {
epochCmd.AddCommand(epochSummaryCmd)
epochFlags(epochSummaryCmd)
epochSummaryFlags(epochSummaryCmd)
}
func epochSummaryFlags(cmd *cobra.Command) {
epochFlags(cmd)
cmd.Flags().StringSlice("validators", nil, "the validators for which to obtain a summary")
}
func epochSummaryBindings(cmd *cobra.Command) {
epochBindings(cmd)
if err := viper.BindPFlag("validators", cmd.Flags().Lookup("validators")); err != nil {
panic(err)
}
}

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")
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")

View File

@@ -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
}

View File

@@ -52,6 +52,7 @@ var bindings = map[string]func(cmd *cobra.Command){
"attester/inclusion": attesterInclusionBindings,
"block/analyze": blockAnalyzeBindings,
"block/info": blockInfoBindings,
"block/trail": blockTrailBindings,
"chain/eth1votes": chainEth1VotesBindings,
"chain/info": chainInfoBindings,
"chain/queues": chainQueuesBindings,

View File

@@ -19,6 +19,7 @@ import (
"os"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
@@ -59,6 +60,8 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
account, err = util.ParseAccount(ctx, viper.GetString("account"), util.GetPassphrases(), true)
case viper.GetString("private-key") != "":
account, err = util.ParseAccount(ctx, viper.GetString("private-key"), nil, true)
default:
err = errors.New("account or private key must be supplied")
}
errCheck(err, "Failed to obtain account")

View File

@@ -19,6 +19,7 @@ import (
"os"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
@@ -71,6 +72,10 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
account, err = util.ParseAccount(ctx, viper.GetString("private-key"), nil, false)
case viper.GetString("public-key") != "":
account, err = util.ParseAccount(ctx, viper.GetString("public-key"), nil, false)
case signatureVerifySigner != "":
account, err = util.ParseAccount(ctx, signatureVerifySigner, nil, false)
default:
err = errors.New("one of account, private-key, public-key, or signer must be supplied")
}
errCheck(err, "Failed to obtain account")
outputIf(viper.GetBool("debug"), fmt.Sprintf("Public key is %#x", account.PublicKey().Marshal()))

View File

@@ -120,6 +120,20 @@ func (c *command) process(ctx context.Context) error {
} else {
c.inclusions = append(c.inclusions, 2)
}
case spec.DataVersionElectra:
aggregate = block.Electra.Message.Body.SyncAggregate
if aggregate.SyncCommitteeBits.BitAt(c.committeeIndex) {
c.inclusions = append(c.inclusions, 1)
} 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)
}

View File

@@ -16,6 +16,8 @@ package depositdata
import (
"context"
"encoding/hex"
"fmt"
"os"
"strings"
"time"
@@ -29,6 +31,7 @@ import (
)
type dataIn struct {
debug bool
format string
timeout time.Duration
withdrawalAccount string
@@ -39,13 +42,16 @@ type dataIn struct {
forkVersion *spec.Version
domain *spec.Domain
passphrases []string
compounding bool
}
func input() (*dataIn, error) {
var err error
data := &dataIn{
debug: viper.GetBool("debug"),
forkVersion: &spec.Version{},
domain: &spec.Domain{},
compounding: viper.GetBool("compounding"),
}
if viper.GetString("validatoraccount") == "" {
@@ -97,6 +103,9 @@ func input() (*dataIn, error) {
if withdrawalDetailsPresent > 1 {
return nil, errors.New("only one of withdrawal account, public key or address is allowed")
}
if data.compounding && data.withdrawalAddress == "" {
return nil, errors.New("a compounding validator must be created with a withdrawal address")
}
if viper.GetString("depositvalue") == "" {
return nil, errors.New("deposit value is required")
@@ -106,10 +115,6 @@ func input() (*dataIn, error) {
return nil, errors.Wrap(err, "deposit value is invalid")
}
data.amount = spec.Gwei(amount)
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
if data.amount < 1000000000 { // MIN_DEPOSIT_AMOUNT
return nil, errors.New("deposit value must be at least 1 Ether")
}
data.forkVersion, err = inputForkVersion(ctx)
if err != nil {
@@ -117,6 +122,12 @@ func input() (*dataIn, error) {
}
copy(data.domain[:], e2types.Domain(e2types.DomainDeposit, data.forkVersion[:], e2types.ZeroGenesisValidatorsRoot))
if data.debug {
fmt.Fprintf(os.Stderr, "Deposit domain is %#x\n", e2types.DomainDeposit)
fmt.Fprintf(os.Stderr, "Fork version is %#x\n", *data.forkVersion)
fmt.Fprintf(os.Stderr, "Genesis validators root is %#x\n", e2types.ZeroGenesisValidatorsRoot)
fmt.Fprintf(os.Stderr, "Signature domain is %#x\n", *data.domain)
}
return data, nil
}

View File

@@ -190,17 +190,6 @@ func TestInput(t *testing.T) {
},
err: "deposit value is required",
},
{
name: "DepositValueTooSmall",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "1000 Wei",
"forkversion": "0x01020304",
},
err: "deposit value must be at least 1 Ether",
},
{
name: "DepositValueInvalid",
vars: map[string]interface{}{

View File

@@ -24,6 +24,7 @@ import (
type dataOut struct {
format string
account string
path string
validatorPubKey *spec.BLSPubKey
withdrawalCredentials []byte
amount spec.Gwei
@@ -113,6 +114,7 @@ func validatorDepositDataOutputLaunchpad(datum *dataOut) (string, error) {
[4]byte{0x80, 0x00, 0x00, 0x69}: "ropsten",
[4]byte{0x90, 0x00, 0x00, 0x69}: "sepolia",
[4]byte{0x01, 0x01, 0x70, 0x00}: "holesky",
[4]byte{0x10, 0x00, 0x09, 0x10}: "hoodi",
}
if datum.validatorPubKey == nil {
@@ -170,7 +172,7 @@ func validatorDepositDataOutputJSON(datum *dataOut) (string, error) {
if datum.depositDataRoot == nil {
return "", errors.New("deposit data root required")
}
if datum.depositDataRoot == nil {
if datum.depositMessageRoot == nil {
return "", errors.New("deposit message root required")
}
if datum.forkVersion == nil {

View File

@@ -17,6 +17,8 @@ import (
"context"
"encoding/hex"
"fmt"
"sort"
"strconv"
"strings"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
@@ -40,6 +42,21 @@ func process(data *dataIn) ([]*dataOut, error) {
return nil, err
}
// These values are hard-coded, to allow deposit data to be generated without a connection to the beacon node.
if data.amount < 1000000000 { // MIN_DEPOSIT_AMOUNT
return nil, errors.New("deposit value must be at least 1 Ether")
}
switch data.compounding {
case false:
if data.amount > 32000000000 {
return nil, errors.New("deposit value exceeds maximum for a non-compounding validator")
}
case true:
if data.amount > 2048000000000 {
return nil, errors.New("deposit value exceeds maximum for a compounding validator")
}
}
for _, validatorAccount := range data.validatorAccounts {
validatorPubKey, err := ethdoutil.BestPublicKey(validatorAccount)
if err != nil {
@@ -80,7 +97,7 @@ func process(data *dataIn) ([]*dataOut, error) {
copy(depositDataRoot[:], root[:])
validatorWallet := validatorAccount.(e2wtypes.AccountWalletProvider).Wallet()
results = append(results, &dataOut{
result := &dataOut{
format: data.format,
account: fmt.Sprintf("%s/%s", validatorWallet.Name(), validatorAccount.Name()),
validatorPubKey: &pubKey,
@@ -90,8 +107,53 @@ func process(data *dataIn) ([]*dataOut, error) {
forkVersion: data.forkVersion,
depositMessageRoot: &depositMessageRoot,
depositDataRoot: &depositDataRoot,
}
if pathProvider, isPathProvider := validatorAccount.(e2wtypes.AccountPathProvider); isPathProvider {
result.path = pathProvider.Path()
}
results = append(results, result)
}
if len(results) == 0 {
return results, nil
}
// Order the results
if results[0].path != "" {
// Order accounts by their path components.
sort.Slice(results, func(i int, j int) bool {
iBits := strings.Split(results[i].path, "/")
jBits := strings.Split(results[j].path, "/")
for index := range iBits {
if iBits[index] == "m" && jBits[index] == "m" {
continue
}
if len(jBits) <= index {
return false
}
iBit, err := strconv.ParseUint(iBits[index], 10, 64)
if err != nil {
return true
}
jBit, err := strconv.ParseUint(jBits[index], 10, 64)
if err != nil {
return false
}
if iBit < jBit {
return true
}
if iBit > jBit {
return false
}
}
return len(jBits) > len(iBits)
})
} else {
// Order accounts by their name.
sort.Slice(results, func(i int, j int) bool {
return strings.Compare(results[i].account, results[j].account) < 0
})
}
return results, nil
}
@@ -145,7 +207,11 @@ func createWithdrawalCredentials(data *dataIn) ([]byte, error) {
withdrawalCredentials = make([]byte, 32)
copy(withdrawalCredentials[12:32], withdrawalAddressBytes)
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
withdrawalCredentials[0] = byte(1) // ETH1_ADDRESS_WITHDRAWAL_PREFIX
if data.compounding {
withdrawalCredentials[0] = byte(2) // COMPOUNDING_WITHDRAWAL_PREFIX
} else {
withdrawalCredentials[0] = byte(1) // ETH1_ADDRESS_WITHDRAWAL_PREFIX
}
default:
return nil, errors.New("withdrawal account, public key or address is required")
}

View File

@@ -37,7 +37,7 @@ func Run(cmd *cobra.Command) (string, error) {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
return "", err
}
}

View File

@@ -101,7 +101,7 @@ func checkMnemonic(ctx context.Context, debug bool, validatorWithdrawalCredentia
// Create seed from mnemonic and passphrase.
seed := bip39.NewSeed(mnemonic, mnemonicPassphrase)
// Check first 1024 indices.
for i := 0; i < 1024; i++ {
for i := range 1024 {
path := fmt.Sprintf("m/12381/3600/%d/0", i)
if debug {
fmt.Printf("Checking path %s\n", path)

View File

@@ -243,16 +243,24 @@ func (c *command) processAttesterDutiesSlot(ctx context.Context,
return err
}
for _, attestation := range attestations {
if _, exists := dutiesBySlot[attestation.Data.Slot]; !exists {
attestationData, err := attestation.Data()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation data")
}
if _, exists := dutiesBySlot[attestationData.Slot]; !exists {
// We do not have any attestations for this slot.
continue
}
if _, exists := dutiesBySlot[attestation.Data.Slot][attestation.Data.Index]; !exists {
if _, exists := dutiesBySlot[attestationData.Slot][attestationData.Index]; !exists {
// We do not have any attestations for this committee.
continue
}
for _, duty := range dutiesBySlot[attestation.Data.Slot][attestation.Data.Index] {
if attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
for _, duty := range dutiesBySlot[attestationData.Slot][attestationData.Index] {
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return errors.Wrap(err, "failed to obtain aggregation bits")
}
if aggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
// Found it.
if _, exists := votes[duty.ValidatorIndex]; exists {
// Duplicate; ignore.
@@ -261,13 +269,13 @@ func (c *command) processAttesterDutiesSlot(ctx context.Context,
votes[duty.ValidatorIndex] = struct{}{}
// Update the metrics for the attestation.
index := int(attestation.Data.Slot - c.chainTime.FirstSlotOfEpoch(c.summary.Epoch))
index := int(attestationData.Slot - c.chainTime.FirstSlotOfEpoch(c.summary.Epoch))
c.summary.Slots[index].Attestations.Included++
inclusionDelay := slot - duty.Slot
fault := &validatorFault{
Validator: duty.ValidatorIndex,
AttestationData: attestation.Data,
AttestationData: attestationData,
InclusionDistance: int(inclusionDelay),
}

View File

@@ -28,14 +28,16 @@ var validatorCredentialsSetCmd = &cobra.Command{
ethdo validator credentials set --validator=primary/validator --withdrawal-address=0x00...13 --private-key=0x00...1f
Note that to change credentials the private key to the existing BLS withdrawal credentials must be available, either directly as an account or private key, or via the mnemonic that was used when generating the initial deposits.
The validator account can be specified in one of a number of ways:
- mnemonic using --mnemonic; this will scan the mnemonic and generate all applicable operations
- mnemonic and path to the validator key using --mnemonic and --path; this will generate a single operation
- mnemonic and validator index or public key --mnemonic and --validator; this will generate a single operation
- mnemonic and withdrawal private key using --mnemonic and --private-key; this will generate all applicable operations
- validator and withdrawal private key using --validator and --private-key; this will generate a single operation
- account and withdrawal account using --account and --withdrawal-account; this will generate a single operation
- mnemonic and existing BLS withdrawal private key using --mnemonic and --private-key; this will generate all applicable operations
- validator and existing BLS withdrawal private key using --validator and --private-key; this will generate a single operation
- account and existing BLS withdrawal account using --account and --withdrawal-account; this will generate a single operation
In quiet mode this will return 0 if the credentials operation has been generated (and successfully broadcast if online), otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2022 Weald Technology Trading
// Copyright © 2019 - 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
@@ -26,7 +26,7 @@ var validatorDepositDataCmd = &cobra.Command{
Short: "Generate deposit data for one or more validators",
Long: `Generate data for deposits to the Ethereum 1 validator contract. For example:
ethdo validator depositdata --validatoraccount=primary/validator --withdrawalaccount=primary/current --value="32 Ether"
ethdo validator depositdata --validatoraccount=primary/validator --withdrawalaccount=primary/current --depositvalue="32 Ether"
If validatoraccount is provided with an account path it will generate deposit data for all matching accounts.
@@ -57,6 +57,7 @@ func init() {
validatorDepositDataCmd.Flags().Bool("raw", false, "Print raw deposit data transaction data")
validatorDepositDataCmd.Flags().String("forkversion", "", "Use a hard-coded fork version (default is to use mainnet value)")
validatorDepositDataCmd.Flags().Bool("launchpad", false, "Print launchpad-compatible JSON")
validatorDepositDataCmd.Flags().Bool("compounding", false, "Create a compounding (max 2048 ETH) validator")
}
func validatorDepositdataBindings(cmd *cobra.Command) {
@@ -84,4 +85,7 @@ func validatorDepositdataBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("launchpad", cmd.Flags().Lookup("launchpad")); err != nil {
panic(err)
}
if err := viper.BindPFlag("compounding", cmd.Flags().Lookup("compounding")); err != nil {
panic(err)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 - 2022 Weald Technology Trading
// Copyright © 2020 - 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
@@ -67,9 +67,17 @@ In quiet mode this will return 0 if the validator information can be obtained, o
os.Exit(_exitFailure)
}
validator, err := util.ParseValidator(ctx, eth2Client.(eth2client.ValidatorsProvider), viper.GetString("validator"), "head")
validator, err := util.ParseValidator(ctx, eth2Client.(eth2client.ValidatorsProvider), viper.GetString("validator"), viper.GetString("blockid"))
errCheck(err, "Failed to obtain validator")
if viper.GetBool("json") {
data, err := json.Marshal(validator)
errCheck(err, "failed to marshal JSON")
fmt.Fprintf(os.Stdout, "%s", string(data))
return
}
if viper.GetBool("verbose") {
network, err := util.Network(ctx, eth2Client)
errCheck(err, "Failed to obtain network")
@@ -185,7 +193,8 @@ func graphData(network string, validatorPubKey []byte) (uint64, spec.Gwei, error
func init() {
validatorCmd.AddCommand(validatorInfoCmd)
validatorInfoCmd.Flags().String("validator", "", "Public key for which to obtain status")
validatorInfoCmd.Flags().String("validator", "", "ID of the validator")
validatorInfoCmd.Flags().String("blockid", "head", "the block at which to fetch the information")
validatorFlags(validatorInfoCmd)
}
@@ -193,4 +202,7 @@ func validatorInfoBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
panic(err)
}
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2024 Weald Technology Trading.
// Copyright © 2019 - 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
@@ -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.35.2)"
var ReleaseVersion = "local build (latest release 1.39.0)"
// versionCmd represents the version command.
var versionCmd = &cobra.Command{

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 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
@@ -24,6 +24,7 @@ import (
distributed "github.com/wealdtech/go-eth2-wallet-distributed"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
hd "github.com/wealdtech/go-eth2-wallet-hd/v2"
keystore "github.com/wealdtech/go-eth2-wallet-keystore"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
"golang.org/x/text/unicode/norm"
)
@@ -40,6 +41,8 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return processHD(ctx, data)
case "distributed":
return processDistributed(ctx, data)
case "keystore":
return processKeystore(ctx, data)
default:
return nil, errors.New("wallet type not supported")
}
@@ -132,3 +135,16 @@ func processDistributed(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
func processKeystore(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
results := &dataOut{}
if _, err := keystore.CreateWallet(ctx, data.walletName, data.store, keystorev4.New()); err != nil {
return nil, err
}
return results, nil
}

View File

@@ -47,7 +47,7 @@ $ ethdo wallet batch --wallet="Validators" ---passphrase="my account secret" --b
`ethdo wallet create` creates a new wallet with the given parameters. Options for creating a wallet include:
- `wallet`: the name of the wallet to create
- `type`: the type of wallet to create. This can be either "nd" for a non-deterministic wallet, where private keys are generated randomly, or "hd" for a hierarchical deterministic wallet, where private keys are generated from a seed and path as per [EIP-2333](https://eips.ethereum.org/EIPS/eip-2333) (defaults to "nd")
- `type`: the type of wallet to create. This can be either "nd" for a non-deterministic wallet, where private keys are generated randomly, "hd" for a hierarchical deterministic wallet, where private keys are generated from a seed and path as per [EIP-2333](https://eips.ethereum.org/EIPS/eip-2333), "keystore" for a wallet where data is written in the [EIP=2335](https://eips.ethereum.org/EIPS/eip-2335) format, or "distributed" for a wallet used by [Dirk](https://github.com/wealdtech/dirk) (defaults to "nd")
- `wallet-passphrase`: the passphrase for of the wallet. This is required for hierarchical deterministic wallets, to protect the seed
- `mnemonic`: for hierarchical deterministic wallets only, use a pre-defined 24-word [BIP-39 seed phrase](https://en.bitcoin.it/wiki/Seed_phrase) to create the wallet, along with an additional "seed extension" phrase if required. **Warning** The same mnemonic can be used to create multiple wallets, in which case they will generate the same keys.
@@ -183,6 +183,7 @@ $ ethdo account create --account="Personal wallet/Operations" --wallet-passphras
- `show-private-key`: show the private of the derived account. **Warning** displaying private keys, especially those derived from seeds held on hardware wallets, can expose your Ether to risk of being stolen. Only use this option if you are sure you understand the risks involved
- `show-withdrawal-credentials`: show the withdrawal credentials of the derived account
- `generate-keystore`: generate a keystore for the account
- `json`: output the generated keystore to stdout
```sh
$ ethdo account derive --mnemonic="abandon ... abandon art" --path="m/12381/3600/0/0"
@@ -370,6 +371,18 @@ Deposits: 0
Voluntary exits: 0
```
#### `trail`
`ethdo block trail` tracks back from the provided block to see if it is in a chain descending from the a target block. Options include:
- `blockid`: the ID (slot, root, 'head') of the block to trail from; defaults to head
- `target`: the target block (slot, block hash, 'justified', 'finalized') to check; defaults to 'justified'
- `max-blocks`: the maximum number of blocks to look at to find the target
```sh
$ ethdo block trail
Target 'justified' found at a distance of 54 block(s)
```
### `chain` commands
Chain commands focus on providing information about Ethereum consensus chains.
@@ -658,6 +671,7 @@ $ ethdo validator credentials set --validator=Validators/1 --withdrawal-address=
- `withdrawaladdress` specify the Ethereum execution address to be used for the withdrawal credentials (if withdrawalpubkey is not supplied)
- `withdrawalpubkey` specify the public key to be used for the withdrawal credentials (if withdrawalaccount is not supplied)
- `validatoraccount` specify the account to be used for the validator
- `compounding` specify if this validator should be compounding, retaining Ether above 32 ETH by default to compound rewards
- `depositvalue` specify the amount of the deposit
- `forkversion` specify the fork version for the deposit signature; this defaults to mainnet. Note that supplying an incorrect value could result in the loss of your deposit, so only supply this value if you are sure you know what you are doing. You can find the value for other chains by fetching the value supplied in "Genesis fork version" of the `ethdo chain info` command
- `raw` generate raw hex output that can be supplied as the data to an Ethereum 1 deposit transaction
@@ -681,6 +695,7 @@ $ ethdo validator exit --private-key=0x01e748d098d3bcb477d636f19d510399ae18205fa
`ethdo validator info` provides information for a given validator. Options include:
- `validator`: the validator for which to obtain information, as a [validator specifier](https://github.com/wealdtech/ethdo#validator-specifier)
- `blockid`: the ID (slot, root, 'head') of the block at which to obtain information
```sh
$ ethdo validator info --validator=Validators/1

116
go.mod
View File

@@ -1,107 +1,105 @@
module github.com/wealdtech/ethdo
go 1.21
go 1.23.0
toolchain go1.21.6
toolchain go1.23.2
require (
github.com/attestantio/go-eth2-client v0.20.0
github.com/ferranbt/fastssz v0.1.3
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
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/herumi/bls-eth-go-binary v1.33.0
github.com/herumi/bls-eth-go-binary v1.36.4
github.com/mitchellh/go-homedir v1.1.0
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/pkg/errors v0.9.1
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7
github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388
github.com/rs/zerolog v1.32.0
github.com/shopspring/decimal v1.3.1
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.8.4
github.com/rs/zerolog v1.33.0
github.com/shopspring/decimal v1.4.0
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/spf13/viper v1.20.0
github.com/stretchr/testify v1.10.0
github.com/tyler-smith/go-bip39 v1.1.0
github.com/wealdtech/go-bytesutil v1.2.1
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.16.0
github.com/wealdtech/go-eth2-wallet-dirk v1.4.7
github.com/wealdtech/go-eth2-wallet-distributed v1.2.1
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.2
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.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
github.com/wealdtech/go-eth2-wallet-types/v2 v2.11.0
github.com/wealdtech/go-eth2-wallet-types/v2 v2.12.0
github.com/wealdtech/go-string2eth v1.2.1
golang.org/x/text v0.14.0
golang.org/x/text v0.23.0
)
require (
github.com/aws/aws-sdk-go v1.51.1 // indirect
github.com/aws/aws-sdk-go v1.55.6 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/emicklei/dot v1.8.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-yaml v1.9.2 // indirect
github.com/golang/glog v1.2.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-yaml v1.16.0 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/huandu/go-clone v1.7.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/highwayhash v1.0.2 // indirect
github.com/minio/highwayhash v1.0.3 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pk910/dynamic-ssz v0.0.6 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.50.0 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/prometheus/client_golang v1.21.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/protolambda/zssz v0.1.5 // indirect
github.com/r3labs/sse/v2 v2.10.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sagikazarmark/locafero v0.8.0 // indirect
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/wealdtech/eth2-signer-api v1.7.2 // indirect
github.com/wealdtech/go-indexer v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

273
go.sum
View File

@@ -1,70 +1,57 @@
github.com/attestantio/go-eth2-client v0.20.0 h1:5UA/qkABDasmdiZtaqK60sEdFw0vUGQU2c0NS/0IhfU=
github.com/attestantio/go-eth2-client v0.20.0/go.mod h1:Tb412NpzhsC0sbtpXS4D51y5se6nDkWAi6amsJrqX9c=
github.com/aws/aws-sdk-go v1.51.1 h1:AFvTihcDPanvptoKS09a4yYmNtPm3+pXlk6uYHmZiFk=
github.com/aws/aws-sdk-go v1.51.1/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
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=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/ferranbt/fastssz v0.1.3 h1:ZI+z3JH05h4kgmFXdHuR1aWYsgrg7o+Fw7/NCzM16Mo=
github.com/ferranbt/fastssz v0.1.3/go.mod h1:0Y9TEd/9XuFlh7mskMPfXiI2Dkw4Ddg9EyXt1W7MRvE=
github.com/emicklei/dot v1.8.0 h1:HnD60yAKFAevNeT+TPYr9pb8VB9bqdeSo0nzwIW6IOI=
github.com/emicklei/dot v1.8.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/goccy/go-yaml v1.9.2 h1:2Njwzw+0+pjU2gb805ZC1B/uBuAs2VcZ3K+ZgHwDs7w=
github.com/goccy/go-yaml v1.9.2/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-yaml v1.16.0 h1:d7m1G7A0t+logajVtklHfDYJs2Et9g3gHwdBNNFou0w=
github.com/goccy/go-yaml v1.16.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/herumi/bls-eth-go-binary v1.33.0 h1:fHoysK+WbL/FQIJoVGECGd2lBLa2De7YjAGZljI2vzQ=
github.com/herumi/bls-eth-go-binary v1.33.0/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U=
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/herumi/bls-eth-go-binary v1.36.4 h1:yff41RSbfyZwfE1NF/qddP5nXhgdU0c3RGOpYOoM7YM=
github.com/herumi/bls-eth-go-binary v1.36.4/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c=
github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
github.com/huandu/go-clone v1.7.2 h1:3+Aq0Ed8XK+zKkLjE2dfHg0XrpIfcohBE1K+c8Usxoo=
@@ -73,105 +60,100 @@ github.com/huandu/go-clone/generic v1.6.0 h1:Wgmt/fUZ28r16F2Y3APotFD59sHk1p78K0X
github.com/huandu/go-clone/generic v1.6.0/go.mod h1:xgd9ZebcMsBWWcBx5mVMCoqMX24gLWr5lQicr+nVXNs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pk910/dynamic-ssz v0.0.6 h1:Tu97LSc2TtCyqRfoSbhG9XuR/FbA7CkKeAnlkgUydFY=
github.com/pk910/dynamic-ssz v0.0.6/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/protolambda/zssz v0.1.5 h1:7fjJjissZIIaa2QcvmhS/pZISMX21zVITt49sW1ouek=
github.com/protolambda/zssz v0.1.5/go.mod h1:a4iwOX5FE7/JkKA+J/PH0Mjo9oXftN6P8NZyL28gpag=
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7 h1:0tVE4tdWQK9ZpYygoV7+vS6QkDvQVySboMVEIxBJmXw=
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7/go.mod h1:wmuf/mdK4VMD+jA9ThwcUKjg3a2XWM9cVfFYjDyY4j4=
github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 h1:lC8kiphgdOBTcbTvo8MwkvpKjO0SlAgjv4xIK5FGJ94=
github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15/go.mod h1:8svFBIKKu31YriBG/pNizo9N0Jr9i5PQ+dFkxWg3x5k=
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388 h1:4bD+ujqGfY4zoDUF3q9MhdmpPXzdp03DYUIlXeQ72kk=
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388/go.mod h1:VecIJZrewdAuhVckySLFt2wAAHRME934bSDurP8ftkc=
github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4=
github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs=
github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0=
github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ=
github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/umbracle/gohashtree v0.0.2-alpha.0.20230207094856-5b775a815c10 h1:CQh33pStIp/E30b7TxDlXfM0145bn2e8boI30IxAhTg=
github.com/umbracle/gohashtree v0.0.2-alpha.0.20230207094856-5b775a815c10/go.mod h1:x/Pa0FF5Te9kdrlZKJK82YmAkvL8+f989USgz6Jiw7M=
github.com/wealdtech/eth2-signer-api v1.7.2 h1:9wmwWEstUwukyZmh0OhQfSHm9KrqFHF7oLSlrk0l2Uk=
github.com/wealdtech/eth2-signer-api v1.7.2/go.mod h1:HOdnGSKi9z6OkV/UgpKpbsF3HcOAJkIjjjSWTXisnWI=
github.com/wealdtech/go-bytesutil v1.2.1 h1:TjuRzcG5KaPwaR5JB7L/OgJqMQWvlrblA1n0GfcXFSY=
@@ -182,89 +164,86 @@ 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.16.0 h1:syD1xDYB7emk4x+6bTYm5VZp9nx5FLab5Fgm09Eq1Kg=
github.com/wealdtech/go-eth2-wallet v1.16.0/go.mod h1:JFA2P7PpPR8quQ/T6Gsr/4VLj5sQVnyzKgfPA+eqmYE=
github.com/wealdtech/go-eth2-wallet-dirk v1.4.7 h1:P9xgaoBB9h1r0OUEE6LHffZ+IbiGE4uniMaNjvP1+D8=
github.com/wealdtech/go-eth2-wallet-dirk v1.4.7/go.mod h1:fP/g7OQQTYCFRg+ZIv6eoTvDdjkZlHnLcf41UgCvrM8=
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 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.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.0 h1:5g4emFacTf+sX6zx6SbZIZGR7Jx5Xr/Xdb7sXnEXlWk=
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.7.0/go.mod h1:aWgnEi07w1L9wMBRB69sYvoEONppAUly6FDQRWQGqH8=
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=
github.com/wealdtech/go-eth2-wallet-store-s3 v1.12.0/go.mod h1:1lSVxfQynUAd5u46rCeAI8wCl8S44lZsNYYXlxVAvwU=
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.2 h1:TwOt7bEHsVe6dKJb7XuUG7m06gaBGPCQlBk24Ql8Mws=
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.2/go.mod h1:rtIoB34tqL3kUOK+LsLTAHfynxLR8pGScy0lmQmpbKc=
github.com/wealdtech/go-eth2-wallet-types/v2 v2.11.0 h1:yX9+FfUXvPDvZ8Q5bhF+64AWrQwh4a3/HpfTx99DnZc=
github.com/wealdtech/go-eth2-wallet-types/v2 v2.11.0/go.mod h1:UVP9YFcnPiIzHqbmCMW3qrQ3TK5FOqr1fmKqNT9JGr8=
github.com/wealdtech/go-eth2-wallet-types/v2 v2.12.0 h1:w0OrVImtQfjN7XCqALfgVtcmH8FChaUZjwNGKGzqp0E=
github.com/wealdtech/go-eth2-wallet-types/v2 v2.12.0/go.mod h1:m8xsnPZLq1vt7bnMveTc4xxVJUv8mBL21iPU4kALYGE=
github.com/wealdtech/go-indexer v1.1.0 h1:vn4gY7nSYSLe0sXVauJgyHvK4NXiDrLKBYYYKWypahk=
github.com/wealdtech/go-indexer v1.1.0/go.mod h1:lEFTda1rul1EwWIX3QqXq/KW0tnEEhC41Lup06V7Tlo=
github.com/wealdtech/go-string2eth v1.2.1 h1:u9sofvGFkp+uvTg4Nvsvy5xBaiw8AibGLLngfC4F76g=
github.com/wealdtech/go-string2eth v1.2.1/go.mod h1:9uwxm18zKZfrReXrGIbdiRYJtbE91iGcj6TezKKEx80=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc=
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY=
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

View File

@@ -62,4 +62,6 @@ type Service interface {
CapellaInitialEpoch() phase0.Epoch
// DenebInitialEpoch provides the epoch at which the Deneb hard fork takes place.
DenebInitialEpoch() phase0.Epoch
// ElectraInitialEpoch provides the epoch at which the Electra hard fork takes place.
ElectraInitialEpoch() phase0.Epoch
}

View File

@@ -35,6 +35,7 @@ type Service struct {
bellatrixForkEpoch phase0.Epoch
capellaForkEpoch phase0.Epoch
denebForkEpoch phase0.Epoch
electraForkEpoch phase0.Epoch
}
// module-wide log.
@@ -116,6 +117,13 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
}
log.Trace().Uint64("epoch", uint64(denebForkEpoch)).Msg("Obtained Deneb fork epoch")
electraForkEpoch, err := fetchElectraForkEpoch(ctx, parameters.specProvider)
if err != nil {
// Set to far future epoch.
electraForkEpoch = 0xffffffffffffffff
}
log.Trace().Uint64("epoch", uint64(electraForkEpoch)).Msg("Obtained Electra fork epoch")
s := &Service{
genesisTime: genesisResponse.Data.GenesisTime,
slotDuration: slotDuration,
@@ -125,6 +133,7 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
bellatrixForkEpoch: bellatrixForkEpoch,
capellaForkEpoch: capellaForkEpoch,
denebForkEpoch: denebForkEpoch,
electraForkEpoch: electraForkEpoch,
}
return s, nil
@@ -341,3 +350,32 @@ func fetchDenebForkEpoch(ctx context.Context,
return phase0.Epoch(epoch), nil
}
// ElectraInitialEpoch provides the epoch at which the Electra hard fork takes place.
func (s *Service) ElectraInitialEpoch() phase0.Epoch {
return s.electraForkEpoch
}
func fetchElectraForkEpoch(ctx context.Context,
specProvider eth2client.SpecProvider,
) (
phase0.Epoch,
error,
) {
// Fetch the fork version.
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
if err != nil {
return 0, errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := specResponse.Data["ELECTRA_FORK_EPOCH"]
if !exists {
return 0, errors.New("deneb fork version not known by chain")
}
epoch, isEpoch := tmp.(uint64)
if !isEpoch {
//nolint:revive
return 0, errors.New("ELECTRA_FORK_EPOCH is not a uint64!")
}
return phase0.Epoch(epoch), nil
}

View File

@@ -18,22 +18,41 @@ import (
"testing"
"time"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/mock"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/services/chaintime"
"github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/testing/mock"
)
func TestService(t *testing.T) {
genesisTime := time.Now()
slotDuration := 12 * time.Second
slotsPerEpoch := uint64(32)
epochsPerSyncCommitteePeriod := uint64(256)
ctx := context.Background()
mockGenesisProvider := mock.NewGenesisProvider(genesisTime)
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
mockClient, err := mock.New(ctx)
require.NoError(t, err)
// genesis is 1 day ago.
genesisTime := time.Now().AddDate(0, 0, -1)
mockClient.GenesisFunc = func(context.Context, *api.GenesisOpts) (*api.Response[*apiv1.Genesis], error) {
return &api.Response[*apiv1.Genesis]{
Data: &apiv1.Genesis{
GenesisTime: genesisTime,
},
Metadata: make(map[string]any),
}, nil
}
mockClient.SpecFunc = func(context.Context, *api.SpecOpts) (*api.Response[map[string]any], error) {
return &api.Response[map[string]any]{
Data: map[string]any{
"SECONDS_PER_SLOT": time.Second * 12,
"SLOTS_PER_EPOCH": uint64(32),
"EPOCHS_PER_SYNC_COMMITTEE_PERIOD": uint64(256),
},
Metadata: make(map[string]any),
}, nil
}
tests := []struct {
name string
@@ -44,7 +63,7 @@ func TestService(t *testing.T) {
name: "GenesisProviderMissing",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithSpecProvider(mockSpecProvider),
standard.WithSpecProvider(mockClient),
},
err: "problem with parameters: no genesis provider specified",
},
@@ -52,7 +71,7 @@ func TestService(t *testing.T) {
name: "SpecProviderMissing",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithGenesisProvider(mockGenesisProvider),
standard.WithGenesisProvider(mockClient),
},
err: "problem with parameters: no spec provider specified",
},
@@ -60,8 +79,8 @@ func TestService(t *testing.T) {
name: "Good",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithGenesisProvider(mockGenesisProvider),
standard.WithSpecProvider(mockSpecProvider),
standard.WithGenesisProvider(mockClient),
standard.WithSpecProvider(mockClient),
},
},
}
@@ -80,7 +99,14 @@ func TestService(t *testing.T) {
// createService is a helper that creates a mock chaintime service.
func createService(genesisTime time.Time) (chaintime.Service, time.Duration, uint64, uint64, []*phase0.Fork, error) {
slotDuration := 12 * time.Second
ctx := context.Background()
mockClient, err := mock.New(ctx)
if err != nil {
return nil, 0, 0, 0, nil, err
}
secondsPerSlot := time.Second * 12
slotsPerEpoch := uint64(32)
epochsPerSyncCommitteePeriod := uint64(256)
forkSchedule := []*phase0.Fork{
@@ -96,13 +122,36 @@ func createService(genesisTime time.Time) (chaintime.Service, time.Duration, uin
},
}
mockGenesisProvider := mock.NewGenesisProvider(genesisTime)
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
s, err := standard.New(context.Background(),
standard.WithGenesisProvider(mockGenesisProvider),
standard.WithSpecProvider(mockSpecProvider),
mockClient.GenesisFunc = func(context.Context, *api.GenesisOpts) (*api.Response[*apiv1.Genesis], error) {
return &api.Response[*apiv1.Genesis]{
Data: &apiv1.Genesis{
GenesisTime: genesisTime,
},
Metadata: make(map[string]any),
}, nil
}
mockClient.SpecFunc = func(context.Context, *api.SpecOpts) (*api.Response[map[string]any], error) {
return &api.Response[map[string]any]{
Data: map[string]any{
"SECONDS_PER_SLOT": secondsPerSlot,
"SLOTS_PER_EPOCH": slotsPerEpoch,
"EPOCHS_PER_SYNC_COMMITTEE_PERIOD": epochsPerSyncCommitteePeriod,
},
Metadata: make(map[string]any),
}, nil
}
mockClient.ForkScheduleFunc = func(context.Context, *api.ForkScheduleOpts) (*api.Response[[]*phase0.Fork], error) {
return &api.Response[[]*phase0.Fork]{
Data: forkSchedule,
Metadata: make(map[string]any),
}, nil
}
s, err := standard.New(ctx,
standard.WithGenesisProvider(mockClient),
standard.WithSpecProvider(mockClient),
)
return s, slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod, forkSchedule, err
return s, secondsPerSlot, slotsPerEpoch, epochsPerSyncCommitteePeriod, forkSchedule, err
}
func TestGenesisTime(t *testing.T) {

View File

@@ -75,9 +75,9 @@ func (p *polynomial) evaluate(x uint8) uint8 {
func interpolatePolynomial(xSamples, ySamples []uint8, x uint8) uint8 {
limit := len(xSamples)
var result, basis uint8
for i := 0; i < limit; i++ {
for i := range limit {
basis = 1
for j := 0; j < limit; j++ {
for j := range limit {
if i == j {
continue
}
@@ -183,7 +183,7 @@ func Split(secret []byte, parts, threshold int) ([][]byte, error) {
// Generate a `parts` number of (x,y) pairs
// We cheat by encoding the x value once as the final index,
// so that it only needs to be stored once.
for i := 0; i < parts; i++ {
for i := range parts {
x := uint8(xCoordinates[i]) + 1
y := p.evaluate(x)
out[i][idx] = y

View File

@@ -1,163 +0,0 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mock
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
)
// GenesisProvider is a mock for eth2client.GenesisProvider.
type GenesisProvider struct {
genesisTime time.Time
}
// NewGenesisProvider returns a mock genesis provider with the provided value.
func NewGenesisProvider(genesisTime time.Time) eth2client.GenesisProvider {
return &GenesisProvider{
genesisTime: genesisTime,
}
}
// Genesisis a mock.
func (m *GenesisProvider) Genesis(_ context.Context, _ *api.GenesisOpts) (*api.Response[*apiv1.Genesis], error) {
return &api.Response[*apiv1.Genesis]{
Data: &apiv1.Genesis{
GenesisTime: m.genesisTime,
},
Metadata: make(map[string]any),
}, nil
}
// SpecProvider is a mock for eth2client.SpecProvider.
type SpecProvider struct {
spec map[string]any
}
// NewSpecProvider returns a mock spec provider with the provided values.
func NewSpecProvider(slotDuration time.Duration,
slotsPerEpoch uint64,
epochsPerSyncCommitteePeriod uint64,
) eth2client.SpecProvider {
return &SpecProvider{
spec: map[string]any{
"SECONDS_PER_SLOT": slotDuration,
"SLOTS_PER_EPOCH": slotsPerEpoch,
"EPOCHS_PER_SYNC_COMMITTEE_PERIOD": epochsPerSyncCommitteePeriod,
},
}
}
// Spec is a mock.
func (m *SpecProvider) Spec(_ context.Context, _ *api.SpecOpts) (*api.Response[map[string]any], error) {
return &api.Response[map[string]any]{
Data: m.spec,
Metadata: make(map[string]any),
}, nil
}
// ForkScheduleProvider is a mock for eth2client.ForkScheduleProvider.
type ForkScheduleProvider struct {
schedule []*phase0.Fork
}
// NewForkScheduleProvider returns a mock spec provider with the provided values.
func NewForkScheduleProvider(schedule []*phase0.Fork) eth2client.ForkScheduleProvider {
return &ForkScheduleProvider{
schedule: schedule,
}
}
// ForkSchedule is a mock.
func (m *ForkScheduleProvider) ForkSchedule(_ context.Context, _ *api.ForkScheduleOpts) (*api.Response[[]*phase0.Fork], error) {
return &api.Response[[]*phase0.Fork]{
Data: m.schedule,
Metadata: make(map[string]any),
}, nil
}
// SlotsPerEpochProvider is a mock for eth2client.SlotsPerEpochProvider.
type SlotsPerEpochProvider struct {
slotsPerEpoch uint64
}
// NewSlotsPerEpochProvider returns a mock slots per epoch provider with the provided value.
func NewSlotsPerEpochProvider(slotsPerEpoch uint64) eth2client.SlotsPerEpochProvider {
return &SlotsPerEpochProvider{
slotsPerEpoch: slotsPerEpoch,
}
}
// SlotsPerEpoch is a mock.
func (m *SlotsPerEpochProvider) SlotsPerEpoch(_ context.Context) (uint64, error) {
return m.slotsPerEpoch, nil
}
// AttestationsSubmitter is a mock for eth2client.AttestationsSubmitter.
type AttestationsSubmitter struct{}
// NewAttestationSubmitter returns a mock attestations submitter with the provided value.
func NewAttestationSubmitter() eth2client.AttestationsSubmitter {
return &AttestationsSubmitter{}
}
// SubmitAttestations is a mock.
func (m *AttestationsSubmitter) SubmitAttestations(_ context.Context, _ []*phase0.Attestation) error {
return nil
}
// BeaconBlockSubmitter is a mock for eth2client.BeaconBlockSubmitter.
type BeaconBlockSubmitter struct{}
// NewBeaconBlockSubmitter returns a mock beacon block submitter with the provided value.
func NewBeaconBlockSubmitter() eth2client.BeaconBlockSubmitter {
return &BeaconBlockSubmitter{}
}
// SubmitBeaconBlock is a mock.
func (m *BeaconBlockSubmitter) SubmitBeaconBlock(_ context.Context, _ *spec.VersionedSignedBeaconBlock) error {
return nil
}
// AggregateAttestationsSubmitter is a mock for eth2client.AggregateAttestationsSubmitter.
type AggregateAttestationsSubmitter struct{}
// NewAggregateAttestationsSubmitter returns a mock aggregate attestation submitter with the provided value.
func NewAggregateAttestationsSubmitter() eth2client.AggregateAttestationsSubmitter {
return &AggregateAttestationsSubmitter{}
}
// SubmitAggregateAttestations is a mock.
func (m *AggregateAttestationsSubmitter) SubmitAggregateAttestations(_ context.Context, _ []*phase0.SignedAggregateAndProof) error {
return nil
}
// BeaconCommitteeSubscriptionsSubmitter is a mock for eth2client.BeaconCommitteeSubscriptionsSubmitter.
type BeaconCommitteeSubscriptionsSubmitter struct{}
// NewBeaconCommitteeSubscriptionsSubmitter returns a mock beacon committee subscription submitter with the provided value.
func NewBeaconCommitteeSubscriptionsSubmitter() eth2client.BeaconCommitteeSubscriptionsSubmitter {
return &BeaconCommitteeSubscriptionsSubmitter{}
}
// SubmitBeaconCommitteeSubscriptions is a mock.
func (m *BeaconCommitteeSubscriptionsSubmitter) SubmitBeaconCommitteeSubscriptions(_ context.Context, _ []*apiv1.BeaconCommitteeSubscription) error {
return nil
}

View File

@@ -1,3 +1,4 @@
// Copyright © 2020 - 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
@@ -56,6 +57,12 @@ func ParseAccount(ctx context.Context,
account, err = parseAccountFromKeystorePath(ctx, accountStr, supplementary, unlock)
}
}
if err != nil {
if strings.Count(accountStr, " ") > 7 {
// It is also possible that this is a mnemonic with "/" in the additional word, so try that as well.
account, err = parseAccountFromMnemonic(ctx, accountStr, supplementary, unlock)
}
}
if err != nil {
return nil, err
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020 - 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
@@ -75,6 +75,18 @@ func TestParseAccount(t *testing.T) {
supplementary: []string{"m/12381/3600/0/0"},
expectedPubkey: "0x99b1f1d84d76185466d86c34bde1101316afddae76217aa86cd066979b19858c2c9d9e56eebc1e067ac54277a61790db",
},
{
name: "ShortMnemonic",
accountStr: "aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban art",
supplementary: []string{"m/12381/3600/0/0"},
expectedPubkey: "0x99b1f1d84d76185466d86c34bde1101316afddae76217aa86cd066979b19858c2c9d9e56eebc1e067ac54277a61790db",
},
{
name: "ShortMnemonicWith25th",
accountStr: `aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban aban art !"£$%^&*()<>,./?;:'@#~]}[{-_=+`,
supplementary: []string{"m/12381/3600/0/0"},
expectedPubkey: "0xa9264986cbde1f05d4c37ed57b03c476f360f0c64cf4a41752c3d352f60caebb6555c5522f0f962962a619336e1539f2",
},
{
name: "MnemonicUnlocked",
accountStr: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",

View File

@@ -17,19 +17,60 @@ import (
"bytes"
"context"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/services/chaintime"
)
// AttestationHead returns the head for which the attestation should have voted.
func AttestationHead(ctx context.Context,
headersCache *BeaconBlockHeaderCache,
attestation *spec.VersionedAttestation,
) (
phase0.Root,
error,
) {
attestationData, err := attestation.Data()
if err != nil {
return phase0.Root{}, errors.Wrap(err, "failed to obtain attestation data")
}
slot := attestationData.Slot
for {
header, err := headersCache.Fetch(ctx, slot)
if err != nil {
return phase0.Root{}, err
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
return header.Root, nil
}
}
// AttestationHeadCorrect returns true if the given attestation had the correct head.
func AttestationHeadCorrect(ctx context.Context,
headersCache *BeaconBlockHeaderCache,
attestation *phase0.Attestation,
attestation *spec.VersionedAttestation,
) (
bool,
error,
) {
slot := attestation.Data.Slot
attestationData, err := attestation.Data()
if err != nil {
return false, errors.Wrap(err, "failed to obtain attestation data")
}
slot := attestationData.Slot
for {
header, err := headersCache.Fetch(ctx, slot)
if err != nil {
@@ -45,7 +86,44 @@ func AttestationHeadCorrect(ctx context.Context,
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
return bytes.Equal(header.Root[:], attestationData.BeaconBlockRoot[:]), nil
}
}
// AttestationTarget returns the target for which the attestation should have voted.
func AttestationTarget(ctx context.Context,
headersCache *BeaconBlockHeaderCache,
chainTime chaintime.Service,
attestation *spec.VersionedAttestation,
) (
phase0.Root,
error,
) {
attestationData, err := attestation.Data()
if err != nil {
return phase0.Root{}, errors.Wrap(err, "failed to obtain attestation data")
}
// Start with first slot of the target epoch.
slot := chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
for {
header, err := headersCache.Fetch(ctx, slot)
if err != nil {
return phase0.Root{}, err
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
return header.Root, nil
}
}
@@ -53,13 +131,18 @@ func AttestationHeadCorrect(ctx context.Context,
func AttestationTargetCorrect(ctx context.Context,
headersCache *BeaconBlockHeaderCache,
chainTime chaintime.Service,
attestation *phase0.Attestation,
attestation *spec.VersionedAttestation,
) (
bool,
error,
) {
attestationData, err := attestation.Data()
if err != nil {
return false, errors.Wrap(err, "failed to obtain attestation data")
}
// Start with first slot of the target epoch.
slot := chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
slot := chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
for {
header, err := headersCache.Fetch(ctx, slot)
if err != nil {
@@ -75,6 +158,7 @@ func AttestationTargetCorrect(ctx context.Context,
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.Target.Root[:]), nil
return bytes.Equal(header.Root[:], attestationData.Target.Root[:]), nil
}
}

View File

@@ -18,28 +18,44 @@ import (
"testing"
"time"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/mock"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/testing/mock"
"github.com/wealdtech/ethdo/util"
)
func TestParseEpoch(t *testing.T) {
ctx := context.Background()
mockClient, err := mock.New(ctx)
require.NoError(t, err)
// genesis is 1 day ago.
genesisTime := time.Now().AddDate(0, 0, -1)
slotDuration := 12 * time.Second
slotsPerEpoch := uint64(32)
epochsPerSyncCommitteePeriod := uint64(256)
mockGenesisProvider := mock.NewGenesisProvider(genesisTime)
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
mockClient.GenesisFunc = func(context.Context, *api.GenesisOpts) (*api.Response[*apiv1.Genesis], error) {
return &api.Response[*apiv1.Genesis]{
Data: &apiv1.Genesis{
GenesisTime: genesisTime,
},
Metadata: make(map[string]any),
}, nil
}
mockClient.SpecFunc = func(context.Context, *api.SpecOpts) (*api.Response[map[string]any], error) {
return &api.Response[map[string]any]{
Data: map[string]any{
"SECONDS_PER_SLOT": time.Second * 12,
"SLOTS_PER_EPOCH": uint64(32),
},
Metadata: make(map[string]any),
}, nil
}
chainTime, err := standardchaintime.New(context.Background(),
standardchaintime.WithLogLevel(zerolog.Disabled),
standardchaintime.WithGenesisProvider(mockGenesisProvider),
standardchaintime.WithSpecProvider(mockSpecProvider),
standardchaintime.WithGenesisProvider(mockClient),
standardchaintime.WithSpecProvider(mockClient),
)
require.NoError(t, err)

View File

@@ -18,28 +18,44 @@ import (
"testing"
"time"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/mock"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/testing/mock"
"github.com/wealdtech/ethdo/util"
)
func TestParseSlot(t *testing.T) {
ctx := context.Background()
mockClient, err := mock.New(ctx)
require.NoError(t, err)
// genesis is 1 day ago.
genesisTime := time.Now().AddDate(0, 0, -1)
slotDuration := 12 * time.Second
slotsPerSlot := uint64(32)
epochsPerSyncCommitteePeriod := uint64(256)
mockGenesisProvider := mock.NewGenesisProvider(genesisTime)
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerSlot, epochsPerSyncCommitteePeriod)
mockClient.GenesisFunc = func(context.Context, *api.GenesisOpts) (*api.Response[*apiv1.Genesis], error) {
return &api.Response[*apiv1.Genesis]{
Data: &apiv1.Genesis{
GenesisTime: genesisTime,
},
Metadata: make(map[string]any),
}, nil
}
mockClient.SpecFunc = func(context.Context, *api.SpecOpts) (*api.Response[map[string]any], error) {
return &api.Response[map[string]any]{
Data: map[string]any{
"SECONDS_PER_SLOT": time.Second * 12,
"SLOTS_PER_EPOCH": uint64(32),
},
Metadata: make(map[string]any),
}, nil
}
chainTime, err := standardchaintime.New(context.Background(),
standardchaintime.WithLogLevel(zerolog.Disabled),
standardchaintime.WithGenesisProvider(mockGenesisProvider),
standardchaintime.WithSpecProvider(mockSpecProvider),
standardchaintime.WithGenesisProvider(mockClient),
standardchaintime.WithSpecProvider(mockClient),
)
require.NoError(t, err)

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})
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to obtain validators %v", indices))