Compare commits

..

1 Commits

Author SHA1 Message Date
Jim McDonald
bf1d265742 Support 12 and 18-word mnemonics with passphrases.
Only supports single-word (no whitespace) passphrases for 12 and 18 word
mnemonics.

Fixes #87
2023-05-18 23:54:26 +01:00
193 changed files with 2090 additions and 4732 deletions

View File

@@ -1,45 +0,0 @@
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:
# Build and push the Docker image
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
# 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@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64/v8
push: true
tags: |
wealdtech/ethdo:${{ steps.release_version.outputs.version }}
wealdtech/ethdo:latest

View File

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

View File

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

View File

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

6
.gitignore vendored
View File

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

View File

@@ -4,16 +4,6 @@
# 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.
@@ -49,6 +39,15 @@ 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
@@ -69,22 +68,43 @@ 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:
formats:
- format: colored-line-number
path: stderr
# 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
# 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
@@ -107,11 +127,11 @@ linters:
disable:
- contextcheck
- cyclop
- depguard
- deadcode
- dupl
- err113
- errorlint
- exhaustive
- exhaustivestruct
- exhaustruct
- forbidigo
- forcetypeassert
@@ -121,22 +141,29 @@ linters:
- gochecknoinits
- gocognit
- goconst
- goerr113
- goheader
- golint
- gomnd
- ifshort
- interfacer
- ireturn
- lll
- maintidx
- mnd
- maligned
- musttag
- nestif
- nilnil
- nlreturn
- nolintlint
- perfsprint
- nosnakecase
- promlinter
- rowserrcheck
- scopelint
- sqlclosecheck
- tenv
- structcheck
- unparam
- varcheck
- varnamelen
- wastedassign
- wrapcheck

View File

@@ -1,109 +1,8 @@
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"
1.35.2:
- update dependencies
1.35.1:
- fix output for various commands that may encounter an empty slot
1.35.0:
- support Deneb
- add start and end dates for eth1votes period
1.34.1:
- fix period parsing for "synccommittee members" command
1.34.0:
- update dependencies
- use Capella fork for all exits
- support Deneb beta 5
1.33.2:
- fix windows build
1.33.1:
- add "slot" to "proposer duties" command
- add activation epoch and time to "validator info" command where applicable
- add "holesky" to the list of supported networks
- avoid crash when requesting validators from beacon node without debug enabled
1.33.0:
- show all slots with 'synccommittee inclusion'
- add "wallet batch" command
1.32.0:
- fix incorrect error when "deposit verify" is not given a withdrawal address
- allow truncated mnemonics (first four characters of each word)
- add deneb information to "block info"
- add epoch parameter to "validator yield"
- add proposer index to "block info"
- "block info" honours "--quiet" flag
- "block info" accepts "--block-time" option
- increase default operation timeout from 10s to 30s
- "epoch summary" JSON lists number of blobs
1.31.0:
- initial support for deneb
- add "--generate-keystore" option for "account derive"
- update "validator exit" command to be able to generate multiple exits
- support for 12-word and 18-word mnemonics with single-word (no whitespace) passphrases
- add JSON output for "validator expectation"
1.30.0:
- add "chain spec" command

View File

@@ -1,4 +1,4 @@
FROM golang:1.23-bookworm AS builder
FROM golang:1.20-bullseye as builder
WORKDIR /app
@@ -10,7 +10,7 @@ COPY . .
RUN go build
FROM debian:bookworm-slim
FROM debian:bullseye-slim
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists/*

View File

@@ -38,7 +38,7 @@ docker pull wealdtech/ethdo
go install github.com/wealdtech/ethdo@latest
```
Note that `ethdo` requires at least version 1.20 of go to operate. The version of go can be found with `go version`.
Note that `ethdo` requires at least version 1.13 of go to operate. The version of go can be found with `go version`.
If this does not work please see the [troubleshooting](https://github.com/wealdtech/ethdo/blob/master/docs/troubleshooting.md) page.
@@ -151,13 +151,9 @@ Amazon S3-compatible stores have additional options available, which can be conf
{
"stores": {
"s3": {
"region": "us-west-1",
"bucket": "my-s3-store",
"path": "/wallets",
"credentials": {
"id": "ABCDEF123",
"secret": "XXXXXXXXX"
}
"bucket":"mybucketname",
"path":"path/in/bucket",
"passphrase":"secret"
}
}
}

View File

@@ -18,12 +18,10 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
consensusclient "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"
"github.com/wealdtech/ethdo/services/chaintime"
@@ -36,7 +34,6 @@ type ChainInfo struct {
GenesisValidatorsRoot phase0.Root
Epoch phase0.Epoch
GenesisForkVersion phase0.Version
ExitForkVersion phase0.Version
CurrentForkVersion phase0.Version
BLSToExecutionChangeDomainType phase0.DomainType
VoluntaryExitDomainType phase0.DomainType
@@ -48,7 +45,6 @@ type chainInfoJSON struct {
GenesisValidatorsRoot string `json:"genesis_validators_root"`
Epoch string `json:"epoch"`
GenesisForkVersion string `json:"genesis_fork_version"`
ExitForkVersion string `json:"exit_fork_version"`
CurrentForkVersion string `json:"current_fork_version"`
BLSToExecutionChangeDomainType string `json:"bls_to_execution_change_domain_type"`
VoluntaryExitDomainType string `json:"voluntary_exit_domain_type"`
@@ -61,12 +57,11 @@ type chainInfoVersionJSON struct {
// MarshalJSON implements json.Marshaler.
func (c *ChainInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(&chainInfoJSON{
Version: strconv.FormatUint(c.Version, 10),
Version: fmt.Sprintf("%d", c.Version),
Validators: c.Validators,
GenesisValidatorsRoot: fmt.Sprintf("%#x", c.GenesisValidatorsRoot),
Epoch: fmt.Sprintf("%d", c.Epoch),
GenesisForkVersion: fmt.Sprintf("%#x", c.GenesisForkVersion),
ExitForkVersion: fmt.Sprintf("%#x", c.ExitForkVersion),
CurrentForkVersion: fmt.Sprintf("%#x", c.CurrentForkVersion),
BLSToExecutionChangeDomainType: fmt.Sprintf("%#x", c.BLSToExecutionChangeDomainType),
VoluntaryExitDomainType: fmt.Sprintf("%#x", c.VoluntaryExitDomainType),
@@ -87,7 +82,7 @@ func (c *ChainInfo) UnmarshalJSON(input []byte) error {
if err != nil {
return errors.Wrap(err, "version invalid")
}
if version < 3 {
if version < 2 {
return errors.New("outdated version; please regenerate your offline data")
}
c.Version = version
@@ -135,18 +130,6 @@ func (c *ChainInfo) UnmarshalJSON(input []byte) error {
}
copy(c.GenesisForkVersion[:], genesisForkVersionBytes)
if data.ExitForkVersion == "" {
return errors.New("exit fork version missing")
}
exitForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.ExitForkVersion, "0x"))
if err != nil {
return errors.Wrap(err, "exit fork version invalid")
}
if len(exitForkVersionBytes) != phase0.ForkVersionLength {
return errors.New("exit fork version incorrect length")
}
copy(c.ExitForkVersion[:], exitForkVersionBytes)
if data.CurrentForkVersion == "" {
return errors.New("current fork version missing")
}
@@ -252,18 +235,18 @@ func ObtainChainInfoFromNode(ctx context.Context,
error,
) {
res := &ChainInfo{
Version: 3,
Version: 2,
Validators: make([]*ValidatorInfo, 0),
Epoch: chainTime.CurrentEpoch(),
}
// Obtain validators.
validatorsResponse, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{State: "head"})
validators, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, "head", nil)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validators")
}
for _, validator := range validatorsResponse.Data {
for _, validator := range validators {
res.Validators = append(res.Validators, &ValidatorInfo{
Index: validator.Index,
Pubkey: validator.Validator.PublicKey,
@@ -271,24 +254,20 @@ func ObtainChainInfoFromNode(ctx context.Context,
State: validator.Status,
})
}
// Order validators by index.
sort.Slice(res.Validators, func(i int, j int) bool {
return res.Validators[i].Index < res.Validators[j].Index
})
// Genesis validators root obtained from beacon node.
genesisResponse, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
genesis, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis information")
}
res.GenesisValidatorsRoot = genesisResponse.Data.GenesisValidatorsRoot
res.GenesisValidatorsRoot = genesis.GenesisValidatorsRoot
// Fetch the genesis fork version from the specification.
specResponse, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx, &api.SpecOpts{})
spec, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := specResponse.Data["GENESIS_FORK_VERSION"]
tmp, exists := spec["GENESIS_FORK_VERSION"]
if !exists {
return nil, errors.New("genesis fork version not known by chain")
}
@@ -298,34 +277,24 @@ func ObtainChainInfoFromNode(ctx context.Context,
return nil, errors.New("could not obtain GENESIS_FORK_VERSION")
}
// Fetch the exit fork version (Capella) from the specification.
tmp, exists = specResponse.Data["CAPELLA_FORK_VERSION"]
if !exists {
return nil, errors.New("capella fork version not known by chain")
}
res.ExitForkVersion, isForkVersion = tmp.(phase0.Version)
if !isForkVersion {
return nil, errors.New("could not obtain CAPELLA_FORK_VERSION")
}
// Fetch the current fork version from the fork schedule.
forkScheduleResponse, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx, &api.ForkScheduleOpts{})
forkSchedule, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain fork schedule")
}
for i := range forkScheduleResponse.Data {
if forkScheduleResponse.Data[i].Epoch <= res.Epoch {
res.CurrentForkVersion = forkScheduleResponse.Data[i].CurrentVersion
for i := range forkSchedule {
if forkSchedule[i].Epoch <= res.Epoch {
res.CurrentForkVersion = forkSchedule[i].CurrentVersion
}
}
blsToExecutionChangeDomainType, exists := specResponse.Data["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
blsToExecutionChangeDomainType, exists := spec["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
if !exists {
return nil, errors.New("failed to obtain DOMAIN_BLS_TO_EXECUTION_CHANGE")
}
copy(res.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType[:])
voluntaryExitDomainType, exists := specResponse.Data["DOMAIN_VOLUNTARY_EXIT"].(phase0.DomainType)
voluntaryExitDomainType, exists := spec["DOMAIN_VOLUNTARY_EXIT"].(phase0.DomainType)
if !exists {
return nil, errors.New("failed to obtain DOMAIN_VOLUNTARY_EXIT")
}

View File

@@ -86,7 +86,7 @@ func processPathed(ctx context.Context, data *dataIn) (*dataOut, error) {
if data.passphrase == "" {
return nil, errors.New("passphrase is required")
}
match, err := regexp.MatchString("^m/[0-9]+/[0-9]+(/[0-9+])+", data.path)
match, err := regexp.Match("^m/[0-9]+/[0-9]+(/[0-9+])+", []byte(data.path))
if err != nil {
return nil, errors.Wrap(err, "unable to match path to regular expression")
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2024 Weald Technology Trading.
// Copyright © 2019, 2020 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
@@ -15,8 +15,8 @@ package accountcreate
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
@@ -34,12 +34,7 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if 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)
}
return "", errors.Wrap(err, "failed to process")
}
if !viper.GetBool("verbose") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -22,7 +22,6 @@ import (
type dataIn struct {
quiet bool
json bool
// Derivation information.
mnemonic string
path string
@@ -38,9 +37,6 @@ 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

@@ -15,7 +15,6 @@ package accountderive
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
@@ -31,7 +30,6 @@ import (
)
type dataOut struct {
json bool
showPrivateKey bool
showWithdrawalCredentials bool
generateKeystore bool
@@ -86,7 +84,7 @@ func outputKeystore(_ context.Context, data *dataOut) (string, error) {
}
ks := make(map[string]interface{})
ks["uuid"] = uuid.String()
ks["pubkey"] = hex.EncodeToString(data.key.PublicKey().Marshal())
ks["pubkey"] = fmt.Sprintf("%x", data.key.PublicKey().Marshal())
ks["version"] = 4
ks["path"] = data.path
ks["crypto"] = crypto
@@ -95,14 +93,10 @@ func outputKeystore(_ context.Context, data *dataOut) (string, error) {
return "", errors.Wrap(err, "failed to marshal keystore JSON")
}
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())
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
return "", errors.New("not implemented")
}

View File

@@ -38,7 +38,6 @@ 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

@@ -1,4 +1,4 @@
// Copyright © 2020, 2024 Weald Technology Trading.
// Copyright © 2020 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
@@ -15,9 +15,9 @@ package accountderive
import (
"context"
"errors"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to obtain input"), err)
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
@@ -34,12 +34,7 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if 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)
}
return "", errors.Wrap(err, "failed to process")
}
if dataIn.quiet {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return strings.TrimSuffix(results, "\n"), nil

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2024 Weald Technology Trading.
// Copyright © 2019, 2020 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
@@ -15,8 +15,8 @@ package accountimport
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to obtain input"), err)
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
@@ -34,12 +34,7 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if 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)
}
return "", errors.Wrap(err, "failed to process")
}
if !viper.GetBool("verbose") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2024 Weald Technology Trading.
// Copyright © 2019, 2020 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
@@ -15,8 +15,8 @@ package accountkey
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
@@ -34,12 +34,7 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if 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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -29,7 +29,7 @@ var accountCreateCmd = &cobra.Command{
ethdo account create --account="primary/operations" --passphrase="my secret"
In quiet mode this will return 0 if the account is created successfully, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := accountcreate.Run(cmd)
if err != nil {
return err

View File

@@ -29,7 +29,7 @@ var accountDeriveCmd = &cobra.Command{
ethdo account derive --mnemonic="..." --path="m/12381/3600/0/0"
In quiet mode this will return 0 if the inputs can derive an account account, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := accountderive.Run(cmd)
if err != nil {
return err
@@ -50,7 +50,6 @@ 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) {
@@ -63,7 +62,4 @@ 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

@@ -29,7 +29,7 @@ var accountImportCmd = &cobra.Command{
ethdo account import --account="primary/testing" --key="0x..." --passphrase="my secret"
In quiet mode this will return 0 if the account is imported successfully, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := accountimport.Run(cmd)
if err != nil {
return err

View File

@@ -33,7 +33,7 @@ var accountInfoCmd = &cobra.Command{
ethdo account info --account="primary/my funds"
In quiet mode this will return 0 if the account exists, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()

View File

@@ -30,7 +30,7 @@ var accountKeyCmd = &cobra.Command{
ethdo account key --account="Personal wallet/Operations" --passphrase="my account passphrase"
In quiet mode this will return 0 if the key can be obtained, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := accountkey.Run(cmd)
if err != nil {
return err

View File

@@ -29,7 +29,7 @@ var accountLockCmd = &cobra.Command{
ethdo account lock --account="primary/my funds"
In quiet mode this will return 0 if the account is locked, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()

View File

@@ -30,7 +30,7 @@ var accountUnlockCmd = &cobra.Command{
ethdo account unlock --account="primary/my funds" --passphrase="secret"
In quiet mode this will return 0 if the account is unlocked, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()

View File

@@ -72,7 +72,7 @@ func input(ctx context.Context) (*dataIn, error) {
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to set up chaintime service")

View File

@@ -17,8 +17,7 @@ import (
"context"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
@@ -50,16 +49,12 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *apiv1.Validator, epoch spec.Epoch) (*apiv1.AttesterDuty, error) {
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
dutiesResponse, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{
Epoch: epoch,
Indices: []spec.ValidatorIndex{validator.Index},
})
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attester duties")
}
duties := dutiesResponse.Data
if len(duties) == 0 {
return nil, errors.New("validator does not have duty for that epoch")

View File

@@ -37,7 +37,7 @@ func TestProcess(t *testing.T) {
chainTime, err := standardchaintime.New(context.Background(),
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
)
require.NoError(t, err)

View File

@@ -1,4 +1,4 @@
// Copyright © 2021, 2024 Weald Technology Trading.
// 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
@@ -15,8 +15,8 @@ package attesterduties
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
@@ -34,12 +34,7 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if 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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -69,7 +69,7 @@ func input(ctx context.Context) (*dataIn, error) {
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to set up chaintime service")

View File

@@ -16,10 +16,8 @@ package attesterinclusion
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
@@ -28,7 +26,7 @@ type dataOut struct {
debug bool
quiet bool
verbose bool
attestation *spec.VersionedAttestation
attestation *phase0.Attestation
slot phase0.Slot
attestationIndex uint64
inclusionDelay phase0.Slot
@@ -51,7 +49,7 @@ func output(_ context.Context, data *dataOut) (string, error) {
buf.WriteString("Attestation included in block ")
buf.WriteString(fmt.Sprintf("%d", data.slot))
buf.WriteString(", index ")
buf.WriteString(strconv.FormatUint(data.attestationIndex, 10))
buf.WriteString(fmt.Sprintf("%d", data.attestationIndex))
if data.verbose {
buf.WriteString("\nInclusion delay: ")
buf.WriteString(fmt.Sprintf("%d", data.inclusionDelay))

View File

@@ -17,12 +17,9 @@ import (
"bytes"
"context"
"fmt"
"net/http"
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"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
@@ -41,7 +38,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to set up chaintime service")
@@ -64,22 +61,14 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
startSlot := duty.Slot + 1
endSlot := startSlot + 32
for slot := startSlot; slot < endSlot; slot++ {
blockResponse, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: fmt.Sprintf("%d", slot),
})
signedBlock, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
// No block for this slot, that's fine.
continue
}
return nil, errors.Wrap(err, "failed to obtain block")
}
block := blockResponse.Data
if block == nil {
if signedBlock == nil {
continue
}
blockSlot, err := block.Slot()
blockSlot, err := signedBlock.Slot()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block slot")
}
@@ -89,23 +78,14 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data.debug {
fmt.Printf("Fetched block for slot %d\n", slot)
}
attestations, err := block.Attestations()
attestations, err := signedBlock.Attestations()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block attestations")
}
for i, attestation := range attestations {
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) {
if attestation.Data.Slot == duty.Slot &&
attestation.Data.Index == duty.CommitteeIndex &&
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
headCorrect := false
targetCorrect := false
if data.verbose {
@@ -138,79 +118,55 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
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
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
for {
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
// No block.
slot--
continue
}
return false, err
}
if !response.Data.Canonical {
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(response.Data.Root[:], attestationData.BeaconBlockRoot[:]), nil
return bytes.Equal(header.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
}
}
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")
}
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
// Start with first slot of the target epoch.
slot := data.chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
slot := data.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
for {
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
// No block.
slot--
continue
}
return false, err
}
if !response.Data.Canonical {
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(response.Data.Root[:], attestationData.Target.Root[:]), nil
return bytes.Equal(header.Root[:], attestation.Data.Target.Root[:]), nil
}
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *apiv1.Validator, epoch phase0.Epoch) (*apiv1.AttesterDuty, error) {
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
dutiesResponse, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{
Epoch: epoch,
Indices: []phase0.ValidatorIndex{validator.Index},
})
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []phase0.ValidatorIndex{validator.Index})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attester duties")
}
duties := dutiesResponse.Data
if len(duties) == 0 {
return nil, errors.New("validator does not have duty for that epoch")

View File

@@ -37,7 +37,7 @@ func TestProcess(t *testing.T) {
chainTime, err := standardchaintime.New(context.Background(),
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
)
require.NoError(t, err)

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2024 Weald Technology Trading.
// Copyright © 2019, 2020 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
@@ -15,8 +15,8 @@ package attesterinclusion
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
@@ -34,12 +34,7 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if 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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -29,7 +29,7 @@ var attesterDutiesCmd = &cobra.Command{
ethdo attester duties --validator=Validators/00001 --epoch=12345
In quiet mode this will return 0 if a duty from the attester is found, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := attesterduties.Run(cmd)
if err != nil {
return err

View File

@@ -29,7 +29,7 @@ var attesterInclusionCmd = &cobra.Command{
ethdo attester inclusion --validator=Validators/00001 --epoch=12345
In quiet mode this will return 0 if an attestation from the attester is found on the block of the given epoch, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := attesterinclusion.Run(cmd)
if err != nil {
return err

View File

@@ -55,7 +55,7 @@ type command struct {
weightDenominator uint64
// Processing.
priorAttestations map[string]*attestationDataInfo
priorAttestations map[string]*attestationData
// 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 *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"`
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"`
}
type syncCommitteeAnalysis struct {
@@ -100,7 +100,7 @@ type syncCommitteeAnalysis struct {
Value float64 `json:"value"`
}
type attestationDataInfo struct {
type attestationData 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]*attestationDataInfo),
priorAttestations: make(map[string]*attestationData),
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

@@ -17,7 +17,6 @@ import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
)
@@ -34,20 +33,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 *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"`
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"`
}
func (a *attestationAnalysis) MarshalJSON() ([]byte, error) {
@@ -83,26 +82,26 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
for i, attestation := range c.analysis.Attestations {
if c.verbose {
builder.WriteString("Attestation ")
builder.WriteString(strconv.Itoa(i))
builder.WriteString(fmt.Sprintf("%d", i))
builder.WriteString(": ")
builder.WriteString("distance ")
builder.WriteString(strconv.Itoa(attestation.Distance))
builder.WriteString(fmt.Sprintf("%d", attestation.Distance))
builder.WriteString(", ")
if attestation.Duplicate != nil {
builder.WriteString("duplicate of attestation ")
builder.WriteString(strconv.Itoa(attestation.Duplicate.Index))
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Index))
builder.WriteString(" in block ")
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Block))
builder.WriteString("\n")
continue
}
builder.WriteString(strconv.Itoa(attestation.NewVotes))
builder.WriteString(fmt.Sprintf("%d", attestation.NewVotes))
builder.WriteString("/")
builder.WriteString(strconv.Itoa(attestation.Votes))
builder.WriteString(fmt.Sprintf("%d", attestation.Votes))
builder.WriteString("/")
builder.WriteString(strconv.Itoa(attestation.PossibleVotes))
builder.WriteString(fmt.Sprintf("%d", attestation.PossibleVotes))
builder.WriteString(" new/total/possible votes")
if attestation.NewVotes == 0 {
builder.WriteString("\n")
@@ -138,7 +137,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
if c.analysis.SyncCommitee.Contributions > 0 {
if c.verbose {
builder.WriteString("Sync committee contributions: ")
builder.WriteString(strconv.Itoa(c.analysis.SyncCommitee.Contributions))
builder.WriteString(fmt.Sprintf("%d", c.analysis.SyncCommitee.Contributions))
builder.WriteString(" contributions, score ")
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.SyncCommitee.Score))
builder.WriteString(", value ")

View File

@@ -17,10 +17,8 @@ import (
"bytes"
"context"
"fmt"
"net/http"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
@@ -35,17 +33,13 @@ func (c *command) process(ctx context.Context) error {
return err
}
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: c.blockID,
})
block, err := c.blocksProvider.SignedBeaconBlock(ctx, c.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
if block == nil {
return errors.New("empty beacon block")
}
slot, err := block.Slot()
if err != nil {
@@ -63,12 +57,8 @@ func (c *command) process(ctx context.Context) error {
// Calculate how many parents we need to fetch.
minSlot := slot
for _, attestation := range attestations {
attestationData, err := attestation.Data()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation data")
}
if attestationData.Slot < minSlot {
minSlot = attestationData.Slot
if attestation.Data.Slot < minSlot {
minSlot = attestation.Data.Slot
}
}
if c.debug {
@@ -107,16 +97,10 @@ 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: attestationData.BeaconBlockRoot,
Target: attestationData.Target.Root,
Distance: int(slot - attestationData.Slot),
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
}
root, err := attestation.HashTreeRoot()
@@ -126,47 +110,45 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return err
}
_, exists := blockVotes[attestationData.Slot]
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[attestationData.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[attestationData.Slot][attestationData.Index]
_, exists = blockVotes[data.Slot][data.Index]
if !exists {
blockVotes[attestationData.Slot][attestationData.Index] = bitfield.NewBitlist(aggregationBits.Len())
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
// Count new votes.
analysis.PossibleVotes = int(aggregationBits.Len())
for j := range aggregationBits.Len() {
if aggregationBits.BitAt(j) {
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[attestationData.Slot][attestationData.Index].BitAt(j) {
if blockVotes[data.Slot][data.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[attestationData.Slot][attestationData.Index].BitAt(j) {
if c.votes[data.Slot][data.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
blockVotes[attestationData.Slot][attestationData.Index].SetBitAt(j, true)
blockVotes[data.Slot][data.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 && attestationData.Slot == slot-1
analysis.HeadTimely = attestation.Data.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestationData.Slot >= slot-5
analysis.SourceTimely = attestation.Data.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
@@ -175,11 +157,7 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
}
// Calculate target timely.
if block.Version < spec.DataVersionDeneb {
analysis.TargetTimely = attestationData.Slot >= slot-32
} else {
analysis.TargetTimely = true
}
analysis.TargetTimely = attestation.Data.Slot >= slot-32
}
// Calculate score and value.
@@ -206,30 +184,12 @@ func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedB
if err != nil {
return err
}
root, err := block.Root()
if err != nil {
panic(err)
}
slot, err := block.Slot()
if err != nil {
panic(err)
}
if c.debug {
fmt.Printf("Parent root of %#x@%d is %#x\n", root, slot, parentRoot)
}
// Obtain the parent block.
parentBlockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: fmt.Sprintf("%#x", parentRoot),
})
parentBlock, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%#x", parentRoot))
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
return errors.New("empty beacon block")
}
return err
}
parentBlock := parentBlockResponse.Data
if parentBlock == nil {
return fmt.Errorf("unable to obtain parent block %s", parentBlock)
}
@@ -267,31 +227,23 @@ func (c *command) processParentBlock(_ context.Context, block *spec.VersionedSig
if err != nil {
return err
}
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationDataInfo{
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
Block: slot,
Index: i,
}
attestationData, err := attestation.Data()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation data")
}
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation aggregation bits")
}
_, exists := c.votes[attestationData.Slot]
data := attestation.Data
_, exists := c.votes[data.Slot]
if !exists {
c.votes[attestationData.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = c.votes[attestationData.Slot][attestationData.Index]
_, exists = c.votes[data.Slot][data.Index]
if !exists {
c.votes[attestationData.Slot][attestationData.Index] = bitfield.NewBitlist(aggregationBits.Len())
c.votes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
for j := range aggregationBits.Len() {
if aggregationBits.BitAt(j) {
c.votes[attestationData.Slot][attestationData.Index].SetBitAt(j, true)
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
c.votes[data.Slot][data.Index].SetBitAt(j, true)
}
}
}
@@ -315,7 +267,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
@@ -337,12 +289,12 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("connection does not provide spec information")
}
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
spec, err := specProvider.Spec(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := specResponse.Data["TIMELY_SOURCE_WEIGHT"]
tmp, exists := spec["TIMELY_SOURCE_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
@@ -353,7 +305,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("TIMELY_SOURCE_WEIGHT of unexpected type")
}
tmp, exists = specResponse.Data["TIMELY_TARGET_WEIGHT"]
tmp, exists = spec["TIMELY_TARGET_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(26)
@@ -363,7 +315,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("TIMELY_TARGET_WEIGHT of unexpected type")
}
tmp, exists = specResponse.Data["TIMELY_HEAD_WEIGHT"]
tmp, exists = spec["TIMELY_HEAD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
@@ -373,7 +325,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("TIMELY_HEAD_WEIGHT of unexpected type")
}
tmp, exists = specResponse.Data["SYNC_REWARD_WEIGHT"]
tmp, exists = spec["SYNC_REWARD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(2)
@@ -383,7 +335,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("SYNC_REWARD_WEIGHT of unexpected type")
}
tmp, exists = specResponse.Data["PROPOSER_WEIGHT"]
tmp, exists = spec["PROPOSER_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(8)
@@ -393,7 +345,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("PROPOSER_WEIGHT of unexpected type")
}
tmp, exists = specResponse.Data["WEIGHT_DENOMINATOR"]
tmp, exists = spec["WEIGHT_DENOMINATOR"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(64)
@@ -405,87 +357,60 @@ func (c *command) setup(ctx context.Context) error {
return nil
}
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
func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
root, exists := c.headRoots[slot]
if !exists {
for {
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
var apiError *api.Error
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)
}
return false, nil
}
return false, err
}
if response.Data == nil {
if header == nil {
// No block.
slot--
continue
}
if !response.Data.Canonical {
if !header.Canonical {
// Not canonical.
slot--
continue
}
c.headRoots[slot] = response.Data.Root
root = response.Data.Root
c.headRoots[attestation.Data.Slot] = header.Root
root = header.Root
break
}
}
return bytes.Equal(root[:], attestationData.BeaconBlockRoot[:]), nil
return bytes.Equal(root[:], attestation.Data.BeaconBlockRoot[:]), nil
}
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]
func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
root, exists := c.targetRoots[attestation.Data.Slot]
if !exists {
// Start with first slot of the target epoch.
slot := c.chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
for {
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
var apiError *api.Error
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)
}
return false, nil
}
return false, err
}
if response.Data == nil {
if header == nil {
// No block.
slot--
continue
}
if !response.Data.Canonical {
if !header.Canonical {
// Not canonical.
slot--
continue
}
c.targetRoots[attestationData.Slot] = response.Data.Root
root = response.Data.Root
c.targetRoots[attestation.Data.Slot] = header.Root
root = header.Root
break
}
}
return bytes.Equal(root[:], attestationData.Target.Root[:]), nil
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
}
func (c *command) analyzeSyncCommittees(_ context.Context, block *spec.VersionedSignedBeaconBlock) error {
@@ -521,20 +446,6 @@ 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

@@ -1,4 +1,4 @@
// Copyright © 2022, 2024 Weald Technology Trading.
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package blockanalyze
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,19 +27,14 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2023 Weald Technology Trading.
// Copyright © 2019, 2020 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
@@ -34,9 +34,8 @@ type dataIn struct {
jsonOutput bool
sszOutput bool
// Chain information.
blockID string
blockTime string
stream bool
blockID string
stream bool
}
func input(ctx context.Context) (*dataIn, error) {
@@ -51,8 +50,7 @@ func input(ctx context.Context) (*dataIn, error) {
data.debug = viper.GetBool("debug")
data.jsonOutput = viper.GetBool("json")
data.sszOutput = viper.GetBool("ssz")
data.blockID = viper.GetString("blockid")
data.blockTime = viper.GetString("block-time")
data.stream = viper.GetBool("stream")
var err error
@@ -66,5 +64,12 @@ func input(ctx context.Context) (*dataIn, error) {
return nil, err
}
if viper.GetString("blockid") == "" {
data.blockID = "head"
} else {
// Specific slot.
data.blockID = viper.GetString("blockid")
}
return data, nil
}

View File

@@ -19,20 +19,16 @@ import (
"encoding/hex"
"fmt"
"math/big"
"regexp"
"sort"
"strconv"
"strings"
"time"
"unicode/utf8"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec/altair"
"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"
@@ -56,10 +52,9 @@ func output(_ context.Context, data *dataOut) (string, error) {
return "", nil
}
func outputBlockGeneral(ctx context.Context,
func outputBlockGeneral(_ context.Context,
verbose bool,
slot phase0.Slot,
proposerIndex phase0.ValidatorIndex,
blockRoot phase0.Root,
bodyRoot phase0.Root,
parentRoot phase0.Root,
@@ -75,16 +70,22 @@ func outputBlockGeneral(ctx context.Context,
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Slot: %d\n", slot))
res.WriteString(fmt.Sprintf("Proposing validator index: %d\n", proposerIndex))
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))
}
res.WriteString(blockGraffiti(ctx, graffiti))
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))
}
}
return res.String(), nil
}
@@ -113,14 +114,12 @@ func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service,
// 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),
})
beaconCommittees, err := beaconCommitteesProvider.BeaconCommittees(ctx, 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 {
for _, beaconCommittee := range beaconCommittees {
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
}
@@ -134,61 +133,7 @@ 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, []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(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[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))
@@ -219,64 +164,11 @@ func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Ser
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,
})
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", 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 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 {
for k, v := range validators {
res.WriteString(fmt.Sprintf(" %#x (%d)\n", v.Validator.PublicKey[:], k))
}
@@ -329,14 +221,11 @@ func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Servic
if verbose {
for i, voluntaryExit := range voluntaryExits {
res.WriteString(fmt.Sprintf(" %d:\n", i))
response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{
State: "head",
Indices: []phase0.ValidatorIndex{voluntaryExit.Message.ValidatorIndex},
})
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []phase0.ValidatorIndex{voluntaryExit.Message.ValidatorIndex})
if err != nil {
res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err))
} else {
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", response.Data[voluntaryExit.Message.ValidatorIndex].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[voluntaryExit.Message.ValidatorIndex].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" Epoch: %d\n", voluntaryExit.Message.Epoch))
}
}
@@ -352,14 +241,11 @@ func outputBlockBLSToExecutionChanges(ctx context.Context, eth2Client eth2client
if verbose {
for i, op := range ops {
res.WriteString(fmt.Sprintf(" %d:\n", i))
response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{
State: "head",
Indices: []phase0.ValidatorIndex{op.Message.ValidatorIndex},
})
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []phase0.ValidatorIndex{op.Message.ValidatorIndex})
if err != nil {
res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err))
} else {
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", response.Data[op.Message.ValidatorIndex].Validator.PublicKey, op.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[op.Message.ValidatorIndex].Validator.PublicKey, op.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" BLS public key: %#x\n", op.Message.FromBLSPubkey))
res.WriteString(fmt.Sprintf(" Execution address: %s\n", op.Message.ToExecutionAddress.String()))
}
@@ -377,9 +263,9 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
if verbose {
specProvider, isProvider := eth2Client.(eth2client.SpecProvider)
if isProvider {
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
config, err := specProvider.Spec(ctx)
if err == nil {
slotsPerEpoch := specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
res.WriteString(" Contributions: ")
res.WriteString(bitvectorToString(syncAggregate.SyncCommitteeBits))
@@ -387,16 +273,14 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
syncCommitteesProvider, isProvider := eth2Client.(eth2client.SyncCommitteesProvider)
if isProvider {
syncCommitteeResponse, err := syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{
State: strconv.FormatUint(uint64(epoch)*slotsPerEpoch, 10),
})
syncCommittee, err := syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", uint64(epoch)*slotsPerEpoch))
if err != nil {
res.WriteString(fmt.Sprintf(" Error: failed to obtain sync committee: %v\n", err))
} else {
res.WriteString(" Contributing validators:")
for i := range syncAggregate.SyncCommitteeBits.Len() {
for i := uint64(0); i < syncAggregate.SyncCommitteeBits.Len(); i++ {
if syncAggregate.SyncCommitteeBits.BitAt(i) {
res.WriteString(fmt.Sprintf(" %d", syncCommitteeResponse.Data.Validators[i]))
res.WriteString(fmt.Sprintf(" %d", syncCommittee.Validators[i]))
}
}
res.WriteString("\n")
@@ -431,7 +315,6 @@ func outputCapellaBlockText(ctx context.Context, data *dataOut, signedBlock *cap
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
signedBlock.Message.ProposerIndex,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
@@ -506,14 +389,7 @@ func outputCapellaBlockText(ctx context.Context, data *dataOut, signedBlock *cap
return res.String(), nil
}
func outputDenebBlockText(ctx context.Context,
data *dataOut,
signedBlock *deneb.SignedBeaconBlock,
blobs []*deneb.BlobSidecar,
) (
string,
error,
) {
func outputDenebBlockText(ctx context.Context, data *dataOut, signedBlock *deneb.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
@@ -535,7 +411,6 @@ func outputDenebBlockText(ctx context.Context,
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
signedBlock.Message.ProposerIndex,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
@@ -607,117 +482,7 @@ func outputDenebBlockText(ctx context.Context,
}
res.WriteString(tmp)
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)
tmp, err = outputDenebBlobInfo(ctx, data.verbose, signedBlock.Message.Body)
if err != nil {
return "", err
}
@@ -748,7 +513,6 @@ func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *b
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
signedBlock.Message.ProposerIndex,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
@@ -839,7 +603,6 @@ func outputAltairBlockText(ctx context.Context, data *dataOut, signedBlock *alta
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
signedBlock.Message.ProposerIndex,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
@@ -923,7 +686,6 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
signedBlock.Message.ProposerIndex,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
@@ -1006,7 +768,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 := range 32 {
for i := 0; i < 32; i++ {
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
}
baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBEBytes)
@@ -1105,79 +867,33 @@ func outputDenebBlockExecutionPayload(_ context.Context,
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
res.WriteString(" Withdrawals: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Withdrawals)))
res.WriteString(" Excess blob gas: ")
res.WriteString(fmt.Sprintf("%d\n", payload.ExcessBlobGas))
res.WriteString(" Excess data gas: ")
res.WriteString(payload.ExcessDataGas.Dec())
res.WriteString("\n")
}
return res.String(), nil
}
func outputElectraBlockExecutionRequests(_ context.Context,
func outputDenebBlobInfo(_ context.Context,
verbose bool,
executionRequests *electra.ExecutionRequests,
body *deneb.BeaconBlockBody,
) (
string,
error,
) {
if executionRequests == nil {
if body == 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,
) {
res := strings.Builder{}
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()))
}
if !verbose {
res.WriteString(fmt.Sprintf("Blob KZG commitments: %d\n", len(body.BlobKzgCommitments)))
} else if len(body.BlobKzgCommitments) > 0 {
res.WriteString("Blob KZG commitments:\n")
for i := range body.BlobKzgCommitments {
res.WriteString(fmt.Sprintf(" %s\n", body.BlobKzgCommitments[i].String()))
}
}
@@ -1211,7 +927,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 := range 32 {
for i := 0; i < 32; i++ {
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
}
baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBEBytes)
@@ -1274,7 +990,7 @@ func bitlistToString(input bitfield.Bitlist) string {
bits := int(input.Len())
res := ""
for i := range bits {
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res = fmt.Sprintf("%s✓", res)
} else {
@@ -1291,7 +1007,7 @@ func bitvectorToString(input bitfield.Bitvector512) string {
bits := int(input.Len())
res := strings.Builder{}
for i := range bits {
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res.WriteString("✓")
} else {
@@ -1304,118 +1020,13 @@ func bitvectorToString(input bitfield.Bitvector512) string {
return res.String()
}
func attestingIndices(input bitfield.Bitlist,
committees map[phase0.CommitteeIndex][]phase0.ValidatorIndex,
includedCommittees []int,
) string {
func attestingIndices(input bitfield.Bitlist, indices []phase0.ValidatorIndex) string {
bits := int(input.Len())
// 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 {
res := ""
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
// Work out the committee and offset given the index.
res.WriteString(fmt.Sprintf("%d ", validatorIndices[i]))
res = fmt.Sprintf("%s%d ", res, indices[i])
}
}
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()
return strings.TrimSpace(res)
}

View File

@@ -175,58 +175,3 @@ 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

@@ -17,24 +17,17 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/altair"
"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"
)
var (
@@ -47,8 +40,8 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.blockID == "" && data.blockTime == "" {
return nil, errors.New("no block ID or block time")
if data.blockID == "" {
return nil, errors.New("no block ID")
}
results = &dataOut{
@@ -57,47 +50,49 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
eth2Client: data.eth2Client,
}
err := populateResults(ctx, results)
config, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
}
if data.blockTime != "" {
data.blockID, err = timeToBlockID(ctx, data.eth2Client, data.blockTime)
if err != nil {
return nil, err
}
}
block, err := obtainBlock(ctx, data, results)
genesis, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, err
}
if data.quiet {
os.Exit(0)
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
}
results.genesisTime = genesis.GenesisTime
results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration)
results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
switch block.Version {
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, data.blockID)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon block")
}
if signedBlock == nil {
return nil, errors.New("empty beacon block")
}
switch signedBlock.Version {
case spec.DataVersionPhase0:
err = outputPhase0Block(ctx, data.jsonOutput, block.Phase0)
if err := outputPhase0Block(ctx, data.jsonOutput, signedBlock.Phase0); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionAltair:
err = outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, block.Altair)
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Altair); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionBellatrix:
err = outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, block.Bellatrix)
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Bellatrix); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionCapella:
err = outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, block.Capella)
if err := outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Capella); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionDeneb:
err = processDenebBlock(ctx, data, block)
case spec.DataVersionElectra:
err = processElectraBlock(ctx, data, block)
case spec.DataVersionFulu:
err = processFuluBlock(ctx, data, block)
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Deneb); err != nil {
return nil, errors.Wrap(err, "failed to output 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
@@ -105,10 +100,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if !jsonOutput && !sszOutput {
fmt.Println("")
}
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, &api.EventsOpts{
Topics: []string{"head"},
HeadHandler: headEventHandler,
})
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
if err != nil {
return nil, errors.Wrap(err, "failed to start block stream")
}
@@ -118,222 +110,38 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
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,
})
if err != nil {
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
if data.quiet {
os.Exit(1)
}
return nil, errors.New("empty beacon block")
}
return nil, errors.Wrap(err, "failed to obtain beacon block")
func headEventHandler(event *api.Event) {
// Only interested in head events.
if event.Topic != "head" {
return
}
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 {
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 := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, block.Deneb, blobSidecars); err != nil {
return errors.Wrap(err, "failed to output block")
}
return nil
}
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")
}
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,
})
blockID := fmt.Sprintf("%#x", event.Data.(*api.HeadEvent).Block[:])
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID)
if err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to obtain block: %v\n", err)
}
return
}
block := blockResponse.Data
if block == nil {
if signedBlock == nil {
if !jsonOutput && !sszOutput {
fmt.Println("Empty beacon block")
}
return
}
switch block.Version {
switch signedBlock.Version {
case spec.DataVersionPhase0:
err = outputPhase0Block(ctx, jsonOutput, block.Phase0)
err = outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0)
case spec.DataVersionAltair:
err = outputAltairBlock(ctx, jsonOutput, sszOutput, block.Altair)
err = outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair)
case spec.DataVersionBellatrix:
err = outputBellatrixBlock(ctx, jsonOutput, sszOutput, block.Bellatrix)
err = outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix)
case spec.DataVersionCapella:
err = outputCapellaBlock(ctx, jsonOutput, sszOutput, block.Capella)
err = outputCapellaBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Capella)
case spec.DataVersionDeneb:
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)
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Deneb)
default:
err = errors.New("unknown block version")
}
@@ -437,12 +245,7 @@ func outputCapellaBlock(ctx context.Context, jsonOutput bool, sszOutput bool, si
return nil
}
func outputDenebBlock(ctx context.Context,
jsonOutput bool,
sszOutput bool,
signedBlock *deneb.SignedBeaconBlock,
blobs []*deneb.BlobSidecar,
) error {
func outputDenebBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *deneb.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
@@ -457,7 +260,7 @@ func outputDenebBlock(ctx context.Context,
}
fmt.Printf("%x\n", data)
default:
data, err := outputDenebBlockText(ctx, results, signedBlock, blobs)
data, err := outputDenebBlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
@@ -465,99 +268,3 @@ 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
switch {
case strings.HasPrefix(input, "0x"):
// Hex string.
hexTime, err := strconv.ParseInt(strings.TrimPrefix(input, "0x"), 16, 64)
if err != nil {
return "", errors.Wrap(err, "failed to parse block time as hex string")
}
timestamp = time.Unix(hexTime, 0)
case !strings.Contains(input, ":"):
// No colon, assume decimal string.
decTime, err := strconv.ParseInt(input, 10, 64)
if err != nil {
return "", errors.Wrap(err, "failed to parse block time as decimal string")
}
timestamp = time.Unix(decTime, 0)
default:
dateTime, err := time.Parse("2006-01-02T15:04:05", input)
if err != nil {
return "", errors.Wrap(err, "failed to parse block time as datetime")
}
timestamp = dateTime
}
// Assume timestamp.
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return "", errors.Wrap(err, "failed to set up chaintime service")
}
return fmt.Sprintf("%d", chainTime.TimestampToSlot(timestamp)), nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2024 Weald Technology Trading.
// Copyright © 2019, 2020 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
@@ -15,8 +15,8 @@ package blockinfo
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
@@ -34,12 +34,7 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if 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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -1,89 +0,0 @@
// 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
}

View File

@@ -1,74 +0,0 @@
// 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
}

View File

@@ -1,182 +0,0 @@
// 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

@@ -1,63 +0,0 @@
// 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)
}
})
}
}

View File

@@ -1,59 +0,0 @@
// 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
}

View File

@@ -29,7 +29,7 @@ var blockAnalyzeCmd = &cobra.Command{
ethdo block analyze --blockid=12345
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := blockanalyze.Run(cmd)
if err != nil {
return err

View File

@@ -29,7 +29,7 @@ var blockInfoCmd = &cobra.Command{
ethdo block info --blockid=12345
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := blockinfo.Run(cmd)
if err != nil {
return err
@@ -48,7 +48,6 @@ func init() {
blockCmd.AddCommand(blockInfoCmd)
blockFlags(blockInfoCmd)
blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockInfoCmd.Flags().String("block-time", "", "the time of the block to fetch (format YYYY-MM-DDTHH:MM:SS, or a hex or decimal timestamp")
blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockInfoCmd.Flags().Bool("ssz", false, "output data in SSZ format")
}
@@ -57,9 +56,6 @@ func blockInfoBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("block-time", cmd.Flags().Lookup("block-time")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stream", cmd.Flags().Lookup("stream")); err != nil {
panic(err)
}

View File

@@ -1,65 +0,0 @@
// 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

@@ -50,8 +50,6 @@ type command struct {
slot phase0.Slot
epoch phase0.Epoch
period uint64
periodStart time.Time
periodEnd time.Time
incumbent *phase0.ETH1Data
eth1DataVotes []*phase0.ETH1Data
votes map[string]*vote

View File

@@ -24,13 +24,11 @@ import (
)
type jsonOutput struct {
Period uint64 `json:"period"`
PeriodStart int64 `json:"period_start"`
PeriodEnd int64 `json:"period_end"`
Epoch phase0.Epoch `json:"epoch"`
Slot phase0.Slot `json:"slot"`
Incumbent *phase0.ETH1Data `json:"incumbent"`
Votes []*vote `json:"votes"`
Period uint64 `json:"period"`
Epoch phase0.Epoch `json:"epoch"`
Slot phase0.Slot `json:"slot"`
Incumbent *phase0.ETH1Data `json:"incumbent"`
Votes []*vote `json:"votes"`
}
func (c *command) output(ctx context.Context) (string, error) {
@@ -59,13 +57,11 @@ func (c *command) outputJSON(_ context.Context) (string, error) {
})
output := &jsonOutput{
Period: c.period,
PeriodStart: c.periodStart.Unix(),
PeriodEnd: c.periodEnd.Unix(),
Epoch: c.epoch,
Slot: c.slot,
Incumbent: c.incumbent,
Votes: votes,
Period: c.period,
Epoch: c.epoch,
Slot: c.slot,
Incumbent: c.incumbent,
Votes: votes,
}
data, err := json.Marshal(output)
if err != nil {
@@ -82,11 +78,6 @@ func (c *command) outputText(_ context.Context) (string, error) {
builder.WriteString(fmt.Sprintf("%d\n", c.period))
if c.verbose {
builder.WriteString("Period start: ")
builder.WriteString(fmt.Sprintf("%s\n", c.periodStart))
builder.WriteString("Period end: ")
builder.WriteString(fmt.Sprintf("%s\n", c.periodEnd))
builder.WriteString("Incumbent: ")
builder.WriteString(fmt.Sprintf("block %#x, deposit count %d\n", c.incumbent.BlockHash, c.incumbent.DepositCount))
}

View File

@@ -20,7 +20,6 @@ import (
"strconv"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
@@ -59,13 +58,10 @@ func (c *command) process(ctx context.Context) error {
if fetchSlot > c.chainTime.CurrentSlot() {
fetchSlot = c.chainTime.CurrentSlot()
}
stateResponse, err := c.beaconStateProvider.BeaconState(ctx, &api.BeaconStateOpts{
State: fmt.Sprintf("%d", fetchSlot),
})
state, err := c.beaconStateProvider.BeaconState(ctx, fmt.Sprintf("%d", fetchSlot))
if err != nil {
return errors.Wrap(err, "failed to obtain state")
}
state := stateResponse.Data
if state == nil {
return errors.New("state not returned by beacon node")
}
@@ -77,39 +73,32 @@ func (c *command) process(ctx context.Context) error {
}
}
c.slot, err = state.Slot()
if err != nil {
return errors.Wrap(err, "failed to obtain slot")
}
switch state.Version {
case spec.DataVersionPhase0:
c.slot = state.Phase0.Slot
c.incumbent = state.Phase0.ETH1Data
c.eth1DataVotes = state.Phase0.ETH1DataVotes
case spec.DataVersionAltair:
c.slot = state.Altair.Slot
c.incumbent = state.Altair.ETH1Data
c.eth1DataVotes = state.Altair.ETH1DataVotes
case spec.DataVersionBellatrix:
c.slot = state.Bellatrix.Slot
c.incumbent = state.Bellatrix.ETH1Data
c.eth1DataVotes = state.Bellatrix.ETH1DataVotes
case spec.DataVersionCapella:
c.slot = state.Capella.Slot
c.incumbent = state.Capella.ETH1Data
c.eth1DataVotes = state.Capella.ETH1DataVotes
case spec.DataVersionDeneb:
c.slot = state.Deneb.Slot
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)
}
c.period = uint64(c.epoch) / c.epochsPerEth1VotingPeriod
c.periodStart = c.chainTime.StartOfEpoch(phase0.Epoch(c.period * c.epochsPerEth1VotingPeriod))
c.periodEnd = c.chainTime.StartOfEpoch(phase0.Epoch((c.period + 1) * c.epochsPerEth1VotingPeriod))
c.votes = make(map[string]*vote)
for _, eth1Vote := range c.eth1DataVotes {
@@ -141,7 +130,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
@@ -157,12 +146,12 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("connection does not provide spec information")
}
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
spec, err := specProvider.Spec(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := specResponse.Data["SLOTS_PER_EPOCH"]
tmp, exists := spec["SLOTS_PER_EPOCH"]
if !exists {
return errors.New("spec did not contain SLOTS_PER_EPOCH")
}
@@ -171,7 +160,7 @@ func (c *command) setup(ctx context.Context) error {
if !good {
return errors.New("SLOTS_PER_EPOCH value invalid")
}
tmp, exists = specResponse.Data["EPOCHS_PER_ETH1_VOTING_PERIOD"]
tmp, exists = spec["EPOCHS_PER_ETH1_VOTING_PERIOD"]
if !exists {
return errors.New("spec did not contain EPOCHS_PER_ETH1_VOTING_PERIOD")
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2022, 2024 Weald Technology Trading.
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package chaineth1votes
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,19 +27,14 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -18,7 +18,6 @@ import (
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
@@ -35,14 +34,12 @@ func (c *command) process(ctx context.Context) error {
return err
}
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)),
})
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)), nil)
if err != nil {
return errors.Wrap(err, "failed to obtain validators")
}
for _, validator := range response.Data {
for _, validator := range validators {
if validator.Validator == nil {
continue
}
@@ -73,7 +70,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")

View File

@@ -1,4 +1,4 @@
// Copyright © 2022, 2024 Weald Technology Trading.
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package chainqueues
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,19 +27,14 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -16,7 +16,6 @@ package chaintime
import (
"context"
"fmt"
"strconv"
"strings"
"time"
@@ -60,14 +59,14 @@ func output(_ context.Context, data *dataOut) (string, error) {
builder.WriteString(data.epochStart.Format("2006-01-02 15:04:05"))
if data.verbose {
builder.WriteString(" (")
builder.WriteString(strconv.FormatInt(data.epochStart.Unix(), 10))
builder.WriteString(fmt.Sprintf("%d", data.epochStart.Unix()))
builder.WriteString(")")
}
builder.WriteString("\n Epoch end ")
builder.WriteString(data.epochEnd.Format("2006-01-02 15:04:05"))
if data.verbose {
builder.WriteString(" (")
builder.WriteString(strconv.FormatInt(data.epochEnd.Unix(), 10))
builder.WriteString(fmt.Sprintf("%d", data.epochEnd.Unix()))
builder.WriteString(")")
}
@@ -77,27 +76,27 @@ func output(_ context.Context, data *dataOut) (string, error) {
builder.WriteString(data.slotStart.Format("2006-01-02 15:04:05"))
if data.verbose {
builder.WriteString(" (")
builder.WriteString(strconv.FormatInt(data.slotStart.Unix(), 10))
builder.WriteString(fmt.Sprintf("%d", data.slotStart.Unix()))
builder.WriteString(")")
}
builder.WriteString("\n Slot end ")
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
if data.verbose {
builder.WriteString(" (")
builder.WriteString(strconv.FormatInt(data.slotEnd.Unix(), 10))
builder.WriteString(fmt.Sprintf("%d", data.slotEnd.Unix()))
builder.WriteString(")")
}
if data.hasSyncCommittees {
builder.WriteString("\nSync committee period ")
builder.WriteString(strconv.FormatUint(data.syncCommitteePeriod, 10))
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriod))
builder.WriteString("\n Sync committee period start ")
builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart))
if data.verbose {
builder.WriteString(", ")
builder.WriteString(strconv.FormatInt(data.syncCommitteePeriodStart.Unix(), 10))
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodStart.Unix()))
}
builder.WriteString(")\n Sync committee period end ")
builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05"))
@@ -105,7 +104,7 @@ func output(_ context.Context, data *dataOut) (string, error) {
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd))
if data.verbose {
builder.WriteString(", ")
builder.WriteString(strconv.FormatInt(data.syncCommitteePeriodEnd.Unix(), 10))
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEnd.Unix()))
}
builder.WriteString(")")
}

View File

@@ -42,7 +42,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to set up chaintime service")

View File

@@ -1,4 +1,4 @@
// Copyright © 2021, 2024 Weald Technology Trading.
// 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
@@ -15,8 +15,8 @@ package chaintime
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
@@ -34,12 +34,7 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if 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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -22,7 +22,6 @@ import (
"os"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
@@ -104,32 +103,29 @@ func (c *command) setup(ctx context.Context) error {
}
stateID := fmt.Sprintf("%d", c.item.Message.Contribution.Slot)
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: stateID,
Indices: []phase0.ValidatorIndex{c.item.Message.AggregatorIndex},
})
validators, err := c.validatorsProvider.Validators(ctx,
stateID,
[]phase0.ValidatorIndex{c.item.Message.AggregatorIndex},
)
if err != nil {
return errors.Wrap(err, "failed to obtain validator information")
}
if len(response.Data) == 0 || response.Data[c.item.Message.AggregatorIndex] == nil {
if len(validators) == 0 || validators[c.item.Message.AggregatorIndex] == nil {
return nil
}
c.validatorKnown = true
c.validator = response.Data[c.item.Message.AggregatorIndex]
c.validator = validators[c.item.Message.AggregatorIndex]
// Obtain the sync committee
syncCommitteesProvider, isProvider := c.eth2Client.(eth2client.SyncCommitteesProvider)
if !isProvider {
return errors.New("connection does not provide sync committee information")
}
syncCommitteeResponse, err := syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{
State: stateID,
})
c.syncCommittee, err = syncCommitteesProvider.SyncCommittee(ctx, stateID)
if err != nil {
return errors.Wrap(err, "failed to obtain sync committee information")
}
c.syncCommittee = syncCommitteeResponse.Data
return nil
}
@@ -141,11 +137,11 @@ func (c *command) isAggregator(ctx context.Context) (bool, error) {
if !isProvider {
return false, errors.New("connection does not provide spec information")
}
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
var err error
c.spec, err = specProvider.Spec(ctx)
if err != nil {
return false, errors.Wrap(err, "failed to obtain spec information")
}
c.spec = specResponse.Data
tmp, exists := c.spec["SYNC_COMMITTEE_SIZE"]
if !exists {
@@ -221,7 +217,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 := range c.item.Message.Contribution.AggregationBits.Len() {
for i := uint64(0); i < c.item.Message.Contribution.AggregationBits.Len(); i++ {
if c.item.Message.Contribution.AggregationBits.BitAt(i) {
includedIndices = append(includedIndices, subCommittee[int(i)])
}
@@ -230,19 +226,16 @@ func (c *command) confirmContributionSignature(ctx context.Context) error {
fmt.Fprintf(os.Stderr, "Contribution validator indices: %v (%d)\n", includedIndices, len(includedIndices))
}
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: "head",
Indices: includedIndices,
})
includedValidators, err := c.validatorsProvider.Validators(ctx, "head", includedIndices)
if err != nil {
return errors.Wrap(err, "failed to obtain subcommittee validators")
}
if len(response.Data) == 0 {
if len(includedValidators) == 0 {
return errors.New("obtained empty subcommittee validator list")
}
var aggregatePubKey *e2types.BLSPublicKey
for _, v := range response.Data {
for _, v := range includedValidators {
pubKeyBytes := make([]byte, 48)
copy(pubKeyBytes, v.Validator.PublicKey[:])
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)

View File

@@ -1,4 +1,4 @@
// Copyright © 2021, 2024 Weald Technology Trading.
// 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
@@ -15,8 +15,8 @@ package chainverifysignedcontributionandproof
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,19 +27,14 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -31,7 +31,7 @@ var chainEth1VotesCmd = &cobra.Command{
Note that this will fetch the votes made in blocks up to the end of the provided epoch.
In quiet mode this will return 0 if there is a majority for the votes, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := chaineth1votes.Run(cmd)
if err != nil {
return err

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 - 2024 Weald Technology Trading
// Copyright © 2020, 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -20,8 +20,6 @@ import (
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -36,7 +34,7 @@ var chainInfoCmd = &cobra.Command{
ethdo chain info
In quiet mode this will return 0 if the chain information can be obtained, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
@@ -47,33 +45,32 @@ In quiet mode this will return 0 if the chain information can be obtained, other
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
specResponse, err := eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
errCheck(err, "Failed to obtain beacon chain specification")
genesisResponse, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
errCheck(err, "Failed to obtain beacon chain genesis")
forkResponse, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, &api.ForkOpts{State: "head"})
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
errCheck(err, "Failed to obtain current fork")
if viper.GetBool("quiet") {
os.Exit(_exitSuccess)
}
if genesisResponse.Data.GenesisTime.Unix() == 0 {
if genesis.GenesisTime.Unix() == 0 {
fmt.Println("Genesis time: undefined")
} else {
fmt.Printf("Genesis time: %s\n", genesisResponse.Data.GenesisTime.Format(time.UnixDate))
outputIf(viper.GetBool("verbose"), fmt.Sprintf("Genesis timestamp: %v", genesisResponse.Data.GenesisTime.Unix()))
fmt.Printf("Genesis time: %s\n", genesis.GenesisTime.Format(time.UnixDate))
outputIf(viper.GetBool("verbose"), fmt.Sprintf("Genesis timestamp: %v", genesis.GenesisTime.Unix()))
}
fmt.Printf("Genesis validators root: %#x\n", genesisResponse.Data.GenesisValidatorsRoot)
fmt.Printf("Genesis fork version: %#x\n", specResponse.Data["GENESIS_FORK_VERSION"].(spec.Version))
fmt.Printf("Current fork version: %#x\n", forkResponse.Data.CurrentVersion)
fmt.Printf("Genesis validators root: %#x\n", genesis.GenesisValidatorsRoot)
fmt.Printf("Genesis fork version: %#x\n", config["GENESIS_FORK_VERSION"].(spec.Version))
fmt.Printf("Current fork version: %#x\n", fork.CurrentVersion)
if viper.GetBool("verbose") {
forkData := &spec.ForkData{
CurrentVersion: forkResponse.Data.CurrentVersion,
GenesisValidatorsRoot: genesisResponse.Data.GenesisValidatorsRoot,
CurrentVersion: fork.CurrentVersion,
GenesisValidatorsRoot: genesis.GenesisValidatorsRoot,
}
forkDataRoot, err := forkData.HashTreeRoot()
if err == nil {
@@ -82,12 +79,8 @@ In quiet mode this will return 0 if the chain information can be obtained, other
fmt.Printf("Fork digest: %#x\n", forkDigest)
}
}
fmt.Printf("Seconds per slot: %d\n", int(specResponse.Data["SECONDS_PER_SLOT"].(time.Duration).Seconds()))
fmt.Printf("Slots per epoch: %d\n", specResponse.Data["SLOTS_PER_EPOCH"].(uint64))
depositContractAddress := bellatrix.ExecutionAddress{}
copy(depositContractAddress[:], specResponse.Data["DEPOSIT_CONTRACT_ADDRESS"].([]byte))
fmt.Printf("Deposit contract address: %s\n", depositContractAddress.String())
fmt.Printf("Seconds per slot: %d\n", int(config["SECONDS_PER_SLOT"].(time.Duration).Seconds()))
fmt.Printf("Slots per epoch: %d\n", config["SLOTS_PER_EPOCH"].(uint64))
os.Exit(_exitSuccess)
},

View File

@@ -29,7 +29,7 @@ var chainQueuesCmd = &cobra.Command{
ethdo chain queues
In quiet mode this will return 0 if the entry and exit queues are 0, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := chainqueues.Run(cmd)
if err != nil {
return err

View File

@@ -18,11 +18,9 @@ import (
"encoding/json"
"fmt"
"sort"
"strconv"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -37,7 +35,7 @@ var chainSpecCmd = &cobra.Command{
ethdo chain spec
In quiet mode this will return 0 if the chain specification can be obtained, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
@@ -48,7 +46,7 @@ In quiet mode this will return 0 if the chain specification can be obtained, oth
})
errCheck(err, "Failed to connect to Ethereum consensus node")
specResponse, err := eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
spec, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
errCheck(err, "Failed to obtain chain specification")
if viper.GetBool("quiet") {
@@ -56,35 +54,35 @@ In quiet mode this will return 0 if the chain specification can be obtained, oth
}
// Tweak the spec for output.
for k, v := range specResponse.Data {
for k, v := range spec {
switch t := v.(type) {
case phase0.Version:
specResponse.Data[k] = fmt.Sprintf("%#x", t)
spec[k] = fmt.Sprintf("%#x", t)
case phase0.DomainType:
specResponse.Data[k] = fmt.Sprintf("%#x", t)
spec[k] = fmt.Sprintf("%#x", t)
case time.Time:
specResponse.Data[k] = strconv.FormatInt(t.Unix(), 10)
spec[k] = fmt.Sprintf("%d", t.Unix())
case time.Duration:
specResponse.Data[k] = strconv.FormatUint(uint64(t.Seconds()), 10)
spec[k] = fmt.Sprintf("%d", uint64(t.Seconds()))
case []byte:
specResponse.Data[k] = fmt.Sprintf("%#x", t)
spec[k] = fmt.Sprintf("%#x", t)
case uint64:
specResponse.Data[k] = strconv.FormatUint(t, 10)
spec[k] = fmt.Sprintf("%d", t)
}
}
if viper.GetBool("json") {
data, err := json.Marshal(specResponse.Data)
data, err := json.Marshal(spec)
errCheck(err, "Failed to marshal JSON")
fmt.Printf("%s\n", string(data))
} else {
keys := make([]string, 0, len(specResponse.Data))
for k := range specResponse.Data {
keys := make([]string, 0, len(spec))
for k := range spec {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("%s: %v\n", key, specResponse.Data[key])
fmt.Printf("%s: %v\n", key, spec[key])
}
}
},

View File

@@ -17,12 +17,10 @@ import (
"context"
"fmt"
"os"
"strconv"
"strings"
"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/phase0"
"github.com/spf13/cobra"
@@ -40,7 +38,7 @@ var chainStatusCmd = &cobra.Command{
ethdo chain status
In quiet mode this will return 0 if the chain status can be obtained, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
@@ -52,24 +50,15 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
)
errCheck(err, "Failed to configure chaintime service")
finalityProvider, isProvider := eth2Client.(eth2client.FinalityProvider)
assert(isProvider, "beacon node does not provide finality; cannot report on chain status")
finalityResponse, err := finalityProvider.Finality(ctx, &api.FinalityOpts{
State: "head",
})
finality, err := finalityProvider.Finality(ctx, "head")
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()
@@ -84,28 +73,12 @@ 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")
@@ -153,13 +126,13 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
if viper.GetBool("verbose") {
validatorsProvider, isProvider := eth2Client.(eth2client.ValidatorsProvider)
if isProvider {
validatorsResponse, err := validatorsProvider.Validators(ctx, &api.ValidatorsOpts{State: "head"})
validators, err := validatorsProvider.Validators(ctx, "head", nil)
errCheck(err, "Failed to obtain validators information")
// Stats of inteest.
totalBalance := phase0.Gwei(0)
activeEffectiveBalance := phase0.Gwei(0)
validatorCount := make(map[apiv1.ValidatorState]int)
for _, validator := range validatorsResponse.Data {
for _, validator := range validators {
validatorCount[validator.Status]++
totalBalance += validator.Balance
if validator.Status.IsActive() {
@@ -189,7 +162,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
nextPeriodTimestamp := chainTime.StartOfEpoch(nextPeriodStartEpoch)
res.WriteString("Sync committee period: ")
res.WriteString(strconv.FormatUint(period, 10))
res.WriteString(fmt.Sprintf("%d", period))
res.WriteString("\n")
if viper.GetBool("verbose") {

View File

@@ -27,7 +27,7 @@ var chainTimeCmd = &cobra.Command{
Long: `Obtain info about the chain at a given time. For example:
ethdo chain time --slot=12345`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := chaintime.Run(cmd)
if err != nil {
return err

View File

@@ -28,7 +28,7 @@ var chainVerifySignedContributionAndProofCmd = &cobra.Command{
ethdo chain verify signedcontributionandproof --data=... --validator=...
validator can be an account, a public key or an index.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := chainverifysignedcontributionandproof.Run(cmd)
if err != nil {
return err

View File

@@ -16,7 +16,6 @@ package cmd
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
@@ -45,12 +44,12 @@ 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 --depositvalue="32 Ether"
ethdo deposit verify --data=depositdata.json --withdrawalaccount=primary/current --value="32 Ether"
The deposit data is compared to the supplied withdrawal account/public key, validator public key, and value to ensure they match.
In quiet mode this will return 0 if the data is verified correctly, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
assert(depositVerifyData != "", "--data is required")
var data []byte
var err error
@@ -76,12 +75,6 @@ In quiet mode this will return 0 if the data is verified correctly, otherwise 1.
deposits, err := util.DepositInfoFromJSON(data)
errCheck(err, "Failed to fetch deposit data")
if viper.GetBool("debug") {
data, err := json.Marshal(deposits)
if err == nil {
fmt.Fprintf(os.Stderr, "Deposit data is %s\n", string(data))
}
}
var withdrawalCredentials []byte
if depositVerifyWithdrawalPubKey != "" {
@@ -270,12 +263,9 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
return false, nil
}
switch {
case len(deposit.DepositMessageRoot) != 32:
if len(deposit.DepositMessageRoot) != 32 {
outputIf(!viper.GetBool("quiet"), "Deposit message root not supplied; not checked")
case len(withdrawalCredentials) != 32:
outputIf(!viper.GetBool("quiet"), "Withdrawal credentials not available; cannot recreate deposit message")
default:
} else {
// We can also verify the deposit message signature.
depositMessage := &phase0.DepositMessage{
PublicKey: pubKey,

View File

@@ -29,8 +29,8 @@ func init() {
RootCmd.AddCommand(epochCmd)
}
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 epochFlags(_ *cobra.Command) {
epochSummaryCmd.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,12 +15,9 @@ 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"
"github.com/spf13/viper"
@@ -38,11 +35,9 @@ type command struct {
allowInsecureConnections bool
// Operation.
epoch string
validatorsStr []string
validators map[phase0.ValidatorIndex]struct{}
stream bool
jsonOutput bool
epoch string
stream bool
jsonOutput bool
// Data access.
eth2Client eth2client.Service
@@ -54,102 +49,49 @@ 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
// Results.
summary *epochSummary
}
type epochSummary struct {
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"`
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"`
}
type epochProposal struct {
ValidatorIndex phase0.ValidatorIndex `json:"validator_index"`
Slot phase0.Slot `json:"slot"`
Block bool `json:"block"`
Slot phase0.Slot `json:"slot"`
Proposer phase0.ValidatorIndex `json:"proposer"`
Block bool `json:"block"`
}
type epochSyncCommittee struct {
ValidatorIndex phase0.ValidatorIndex `json:"validator_index"`
Missed int `json:"missed"`
MissedSlots []phase0.Slot `json:"missed_slots"`
Index phase0.ValidatorIndex `json:"index"`
Missed int `json:"missed"`
}
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"`
type nonParticipatingValidator struct {
Validator phase0.ValidatorIndex `json:"validator_index"`
Slot phase0.Slot `json:"slot"`
Committee phase0.CommitteeIndex `json:"committee_index"`
}
func newCommand(_ context.Context) (*command, error) {
c := &command{
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),
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
summary: &epochSummary{},
}
// Timeout.

View File

@@ -1,4 +1,4 @@
// Copyright © 2022, 2025 Weald Technology Trading.
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -17,7 +17,6 @@ import (
"context"
"encoding/json"
"fmt"
"math/big"
"strings"
)
@@ -51,7 +50,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
missedProposals := make([]string, 0, len(c.summary.Proposals))
for _, proposal := range c.summary.Proposals {
if !proposal.Block {
missedProposals = append(missedProposals, fmt.Sprintf("\n Slot %d (validator %d)", proposal.Slot, proposal.ValidatorIndex))
missedProposals = append(missedProposals, fmt.Sprintf("\n Slot %d (validator %d)", proposal.Slot, proposal.Proposer))
} else {
proposedBlocks++
}
@@ -65,85 +64,17 @@ 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.ValidatorIndex))
builder.WriteString(fmt.Sprintf("%d", proposal.Proposer))
builder.WriteString(" not proposed or not included")
}
}
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),
))
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)))
if c.verbose {
// Sort list by validator index.
for _, validator := range c.summary.NonParticipatingValidators {
@@ -167,7 +98,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
if c.verbose {
for _, syncCommittee := range c.summary.SyncCommittee {
builder.WriteString("\n Validator ")
builder.WriteString(fmt.Sprintf("%d", syncCommittee.ValidatorIndex))
builder.WriteString(fmt.Sprintf("%d", syncCommittee.Index))
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 - 2025 Weald Technology Trading.
// Copyright © 2022, 2023 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,14 +16,12 @@ package epochsummary
import (
"context"
"fmt"
"math/big"
"net/http"
"sort"
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/altair"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
@@ -44,55 +42,33 @@ 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
}
if err := c.processAttesterDuties(ctx); err != nil {
return err
}
if err := c.processSyncCommitteeDuties(ctx); err != nil {
return err
}
return c.processBlobs(ctx)
return c.processSyncCommitteeDuties(ctx)
}
func (c *command) processProposerDuties(ctx context.Context) error {
response, err := c.proposerDutiesProvider.ProposerDuties(ctx, &api.ProposerDutiesOpts{
Epoch: c.summary.Epoch,
})
duties, err := c.proposerDutiesProvider.ProposerDuties(ctx, c.summary.Epoch, nil)
if err != nil {
return errors.Wrap(err, "failed to obtain proposer duties")
}
for _, duty := range response.Data {
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", duty.Slot))
if duties == nil {
return errors.New("empty proposer duties")
}
for _, duty := range duties {
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", duty.Slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", duty.Slot))
}
present := block != nil
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,
ValidatorIndex: duty.ValidatorIndex,
Block: present,
Slot: duty.Slot,
Proposer: duty.ValidatorIndex,
Block: present,
})
}
@@ -100,27 +76,12 @@ 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)),
Indices: validatorIndices,
})
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), nil)
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
}
for _, validator := range validators {
if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch {
activeValidators[validator.Index] = validator
}
@@ -136,10 +97,6 @@ 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.
@@ -149,42 +106,18 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
lastSlot = c.chainTime.CurrentSlot()
}
if err := c.processSlots(ctx, firstSlot, lastSlot); err != nil {
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 {
return err
}
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))
c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0, len(activeValidators)-len(votes))
for activeValidatorIndex := range activeValidators {
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])
if _, exists := votes[activeValidatorIndex]; !exists {
if _, exists := participations[activeValidatorIndex]; exists {
c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, participations[activeValidatorIndex])
}
}
}
@@ -201,20 +134,36 @@ 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,
) error {
) (
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{})
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)
for slot := firstSlot; slot <= lastSlot; slot++ {
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", slot))
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
}
if block == nil {
// No block at this slot; that's fine.
@@ -222,154 +171,81 @@ func (c *command) processSlots(ctx context.Context,
}
slot, err := block.Slot()
if err != nil {
return err
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
attestations, err := block.Attestations()
if err != nil {
return err
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
for _, attestation := range attestations {
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) {
if attestation.Data.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestation.Data.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
// Outside of this epoch's range.
continue
}
slotCommittees, exists := allCommittees[attestationData.Slot]
slotCommittees, exists := allCommittees[attestation.Data.Slot]
if !exists {
response, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, &api.BeaconCommitteesOpts{
State: fmt.Sprintf("%d", attestationData.Slot),
})
beaconCommittees, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", attestation.Data.Slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestationData.Slot))
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
}
for _, beaconCommittee := range response.Data {
for _, beaconCommittee := range beaconCommittees {
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 {
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,
}
participations[index] = &nonParticipatingValidator{
Validator: index,
Slot: beaconCommittee.Slot,
Committee: beaconCommittee.Index,
}
}
}
slotCommittees = allCommittees[attestationData.Slot]
slotCommittees = allCommittees[attestation.Data.Slot]
}
if err := c.extractAttestationData(ctx, attestation, attestationData, slotCommittees, slot, headersCache); err != nil {
return err
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
}
}
}
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
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{}{}
}
}
}
// 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
return len(votes),
len(headCorrects),
len(headTimelys),
len(sourceTimelys),
len(targetCorrects),
len(targetTimelys),
votes,
participations,
nil
}
func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
@@ -378,35 +254,21 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
return nil
}
committeeResponse, err := c.syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{
State: fmt.Sprintf("%d", c.summary.FirstSlot),
})
committee, err := c.syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", c.summary.FirstSlot))
if err != nil {
return errors.Wrap(err, "failed to obtain sync committee")
}
committee := committeeResponse.Data
if len(committee.Validators) == 0 {
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
}
for slot := c.summary.FirstSlot; slot <= c.summary.LastSlot; slot++ {
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", slot))
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
}
@@ -414,26 +276,25 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
// If the block is missed we don't count the sync aggregate miss.
continue
}
if block.Version == spec.DataVersionPhase0 {
var aggregate *altair.SyncAggregate
switch block.Version {
case spec.DataVersionPhase0:
// No sync committees in this fork.
return nil
case spec.DataVersionAltair:
aggregate = block.Altair.Message.Body.SyncAggregate
case spec.DataVersionBellatrix:
aggregate = block.Bellatrix.Message.Body.SyncAggregate
case spec.DataVersionCapella:
aggregate = block.Capella.Message.Body.SyncAggregate
case spec.DataVersionDeneb:
aggregate = block.Deneb.Message.Body.SyncAggregate
default:
return fmt.Errorf("unhandled block version %v", block.Version)
}
aggregate, err := block.SyncAggregate()
if err != nil {
return errors.Wrapf(err, "failed to obtain sync aggregate for slot %d", slot)
}
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
}
}
for i := uint64(0); i < aggregate.SyncCommitteeBits.Len(); i++ {
if !aggregate.SyncCommitteeBits.BitAt(i) {
missed[validatorIndex]++
missedSlots[validatorIndex] = append(missedSlots[validatorIndex], slot)
missed[committee.Validators[int(i)]]++
}
}
}
@@ -442,9 +303,8 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
for index, count := range missed {
if count > 0 {
c.summary.SyncCommittee = append(c.summary.SyncCommittee, &epochSyncCommittee{
ValidatorIndex: index,
Missed: count,
MissedSlots: missedSlots[index],
Index: index,
Missed: count,
})
}
}
@@ -456,7 +316,7 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
return missedDiff > 0
}
// Then order by validator index.
return c.summary.SyncCommittee[i].ValidatorIndex < c.summary.SyncCommittee[j].ValidatorIndex
return c.summary.SyncCommittee[i].Index < c.summary.SyncCommittee[j].Index
})
return nil
@@ -478,7 +338,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
@@ -512,56 +372,3 @@ func (c *command) setup(ctx context.Context) error {
return nil
}
func (c *command) processBlobs(ctx context.Context) error {
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", proposal.Slot))
}
if block == nil {
continue
}
switch block.Version {
case spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix, spec.DataVersionCapella:
// 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)
}
}
return nil
}
func (c *command) fetchBlock(ctx context.Context,
blockID string,
) (
*spec.VersionedSignedBeaconBlock,
error,
) {
block, exists := c.blocksCache[blockID]
if !exists {
var err error
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: blockID,
})
if err != nil {
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
// No block for this slot, that's okay.
return nil, nil
}
return nil, errors.Wrap(err, "failed to fetch block")
}
block = blockResponse.Data
c.blocksCache[blockID] = block
}
return block, nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2022, 2024 Weald Technology Trading.
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package epochsummary
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,19 +27,14 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -29,7 +29,7 @@ var epochSummaryCmd = &cobra.Command{
ethdo epoch summary --epoch=12345
In quiet mode this will return 0 if information for the epoch is found, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := epochsummary.Run(cmd)
if err != nil {
return err
@@ -46,17 +46,9 @@ In quiet mode this will return 0 if information for the epoch is found, otherwis
func init() {
epochCmd.AddCommand(epochSummaryCmd)
epochSummaryFlags(epochSummaryCmd)
}
func epochSummaryFlags(cmd *cobra.Command) {
epochFlags(cmd)
cmd.Flags().StringSlice("validators", nil, "the validators for which to obtain a summary")
epochFlags(epochSummaryCmd)
}
func epochSummaryBindings(cmd *cobra.Command) {
epochBindings(cmd)
if err := viper.BindPFlag("validators", cmd.Flags().Lookup("validators")); err != nil {
panic(err)
}
}

View File

@@ -21,7 +21,6 @@ import (
"strings"
consensusclient "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
v1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
@@ -39,7 +38,7 @@ var exitVerifyCmd = &cobra.Command{
ethdo exit verify --signed-operation=exitdata.json --validator=primary/current
In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
assert(viper.GetString("signed-operation") != "", "signed-operation is required")
@@ -68,39 +67,35 @@ In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.
opRoot, err := signedOp.Message.HashTreeRoot()
errCheck(err, "Failed to obtain exit hash tree root")
genesisResponse, err := eth2Client.(consensusclient.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
genesis, err := eth2Client.(consensusclient.GenesisProvider).Genesis(ctx)
errCheck(err, "Failed to obtain beacon chain genesis")
genesis := genesisResponse.Data
response, err := eth2Client.(consensusclient.SpecProvider).Spec(ctx, &api.SpecOpts{})
errCheck(err, "Failed to obtain spec information")
fork, err := eth2Client.(consensusclient.ForkProvider).Fork(ctx, "head")
errCheck(err, "Failed to obtain fork information")
// Check against Capella fork version (EIP-7044)
// Check against current and prior fork versions.
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{}
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[:])
currentExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork.CurrentVersion[:], genesis.GenesisValidatorsRoot[:])
errCheck(err, "Failed to compute domain")
copy(domain[:], exitDomain)
verified, err := util.VerifyRoot(account, opRoot, domain, sig)
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, fork.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")
}
assert(verified, "Voluntary exit failed to verify against current and previous fork versions")
outputIf(viper.GetBool("verbose"), "Verified")

View File

@@ -19,8 +19,7 @@ import (
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/pkg/errors"
)
@@ -29,10 +28,7 @@ func process(ctx context.Context, data *dataIn) error {
return errors.New("no data")
}
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, &api.EventsOpts{
Topics: data.topics,
Handler: eventHandler,
})
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, data.topics, eventHandler)
if err != nil {
return errors.Wrap(err, "failed to connect for events")
}
@@ -42,7 +38,7 @@ func process(ctx context.Context, data *dataIn) error {
return nil
}
func eventHandler(event *apiv1.Event) {
func eventHandler(event *api.Event) {
if event.Data == nil {
return
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2024 Weald Technology Trading.
// Copyright © 2019, 2020 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
@@ -15,8 +15,8 @@ package nodeevents
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@@ -25,19 +25,14 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := process(ctx, dataIn); 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)
}
return "", errors.Wrap(err, "failed to process")
}
// Process generates all output.

View File

@@ -27,7 +27,7 @@ var nodeEventsCmd = &cobra.Command{
Long: `Report events from a node. For example:
ethdo node events --events=head,chain_reorg.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := nodeevents.Run(cmd)
if err != nil {
return err

View File

@@ -19,7 +19,6 @@ import (
"os"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
@@ -33,7 +32,7 @@ var nodeInfoCmd = &cobra.Command{
ethdo node info
In quiet mode this will return 0 if the node information can be obtained, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
@@ -49,14 +48,14 @@ In quiet mode this will return 0 if the node information can be obtained, otherw
}
if viper.GetBool("verbose") {
versionResponse, err := eth2Client.(eth2client.NodeVersionProvider).NodeVersion(ctx, &api.NodeVersionOpts{})
version, err := eth2Client.(eth2client.NodeVersionProvider).NodeVersion(ctx)
errCheck(err, "Failed to obtain node version")
fmt.Printf("Version: %s\n", versionResponse.Data)
fmt.Printf("Version: %s\n", version)
}
syncStateResponse, err := eth2Client.(eth2client.NodeSyncingProvider).NodeSyncing(ctx, &api.NodeSyncingOpts{})
syncState, err := eth2Client.(eth2client.NodeSyncingProvider).NodeSyncing(ctx)
errCheck(err, "failed to obtain node sync state")
fmt.Printf("Syncing: %t\n", syncStateResponse.Data.SyncDistance != 0)
fmt.Printf("Syncing: %t\n", syncState.SyncDistance != 0)
os.Exit(_exitSuccess)
},

View File

@@ -1,4 +1,4 @@
// Copyright © 2022, 2023 Weald Technology Trading.
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -37,7 +37,6 @@ type command struct {
// Operation.
epoch string
slot string
jsonOutput bool
// Data access.
@@ -56,22 +55,23 @@ type results struct {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
timeout: viper.GetDuration("timeout"),
connection: viper.GetString("connection"),
allowInsecureConnections: viper.GetBool("allow-insecure-connections"),
epoch: viper.GetString("epoch"),
slot: viper.GetString("slot"),
jsonOutput: viper.GetBool("json"),
results: &results{},
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
results: &results{},
}
// Timeout.
if c.timeout == 0 {
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
c.epoch = viper.GetString("epoch")
c.jsonOutput = viper.GetBool("json")
return c, nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2022, 2023 Weald Technology Trading.
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -43,32 +43,19 @@ func (c *command) outputJSON(_ context.Context) (string, error) {
func (c *command) outputTxt(_ context.Context) (string, error) {
builder := strings.Builder{}
if len(c.results.Duties) == 1 {
// Only have a single slot, just print the validator.
duty := c.results.Duties[0]
builder.WriteString("Validator ")
builder.WriteString("Epoch ")
builder.WriteString(fmt.Sprintf("%d:\n", c.results.Epoch))
for _, duty := range c.results.Duties {
builder.WriteString(" Slot ")
builder.WriteString(fmt.Sprintf("%d: ", duty.Slot))
builder.WriteString("validator ")
builder.WriteString(fmt.Sprintf("%d", duty.ValidatorIndex))
if c.verbose {
builder.WriteString(" (pubkey ")
builder.WriteString(fmt.Sprintf("%#x)", duty.PubKey))
}
builder.WriteString("\n")
} else {
// Have multiple slots, print per-slot information.
builder.WriteString("Epoch ")
builder.WriteString(fmt.Sprintf("%d:\n", c.results.Epoch))
for _, duty := range c.results.Duties {
builder.WriteString(" Slot ")
builder.WriteString(fmt.Sprintf("%d: ", duty.Slot))
builder.WriteString("validator ")
builder.WriteString(fmt.Sprintf("%d", duty.ValidatorIndex))
if c.verbose {
builder.WriteString(" (pubkey ")
builder.WriteString(fmt.Sprintf("%#x)", duty.PubKey))
}
builder.WriteString("\n")
}
}
return strings.TrimSuffix(builder.String(), "\n"), nil

View File

@@ -1,4 +1,4 @@
// Copyright © 2022, 2023 Weald Technology Trading.
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -17,8 +17,6 @@ import (
"context"
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/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
@@ -26,59 +24,20 @@ import (
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
err := c.setup(ctx)
if err != nil {
return err
}
if c.slot != "" {
return c.processSlot(ctx)
}
return c.processEpoch(ctx)
}
func (c *command) processSlot(ctx context.Context) error {
var err error
slot, err := util.ParseSlot(ctx, c.chainTime, c.slot)
if err != nil {
return errors.Wrap(err, "failed to parse slot")
}
c.results.Epoch = c.chainTime.SlotToEpoch(slot)
response, err := c.proposerDutiesProvider.ProposerDuties(ctx, &api.ProposerDutiesOpts{
Epoch: c.results.Epoch,
})
if err != nil {
return errors.Wrap(err, "failed to obtain proposer duties")
}
c.results.Duties = make([]*apiv1.ProposerDuty, 0, 1)
for _, duty := range response.Data {
if duty.Slot == slot {
c.results.Duties = append(c.results.Duties, duty)
break
}
}
return nil
}
func (c *command) processEpoch(ctx context.Context) error {
var err error
c.results.Epoch, err = util.ParseEpoch(ctx, c.chainTime, c.epoch)
if err != nil {
return errors.Wrap(err, "failed to parse epoch")
}
dutiesResponse, err := c.proposerDutiesProvider.ProposerDuties(ctx, &api.ProposerDutiesOpts{
Epoch: c.results.Epoch,
})
c.results.Duties, err = c.proposerDutiesProvider.ProposerDuties(ctx, c.results.Epoch, nil)
if err != nil {
return errors.Wrap(err, "failed to obtain proposer duties")
}
c.results.Duties = dutiesResponse.Data
return nil
}
@@ -99,7 +58,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")

View File

@@ -1,4 +1,4 @@
// Copyright © 2022, 2024 Weald Technology Trading.
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package proposerduties
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,19 +27,14 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -29,7 +29,7 @@ var proposerDutiesCmd = &cobra.Command{
ethdo proposer duties --epoch=12345
In quiet mode this will return 0 if duties can be obtained, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := proposerduties.Run(cmd)
if err != nil {
return err
@@ -48,14 +48,10 @@ func init() {
proposerCmd.AddCommand(proposerDutiesCmd)
proposerFlags(proposerDutiesCmd)
proposerDutiesCmd.Flags().String("epoch", "", "the epoch for which to fetch duties")
proposerDutiesCmd.Flags().String("slot", "", "the slot for which to fetch duties")
}
func proposerDutiesBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("slot", cmd.Flags().Lookup("slot")); err != nil {
panic(err)
}
}

View File

@@ -52,7 +52,6 @@ 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,
@@ -77,7 +76,6 @@ var bindings = map[string]func(cmd *cobra.Command){
"validator/yield": validatorYieldBindings,
"validator/expectation": validatorExpectationBindings,
"validator/withdrawal": validatorWithdrawalBindings,
"wallet/batch": walletBatchBindings,
"wallet/create": walletCreateBindings,
"wallet/import": walletImportBindings,
"wallet/sharedexport": walletSharedExportBindings,
@@ -160,7 +158,7 @@ func addPersistentFlags() {
if err := viper.BindPFlag("path", RootCmd.PersistentFlags().Lookup("path")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().String("private-key", "", "Private key to provide access to an account or validator")
RootCmd.PersistentFlags().String("private-key", "", "Private key to provide access to an account or validaotr")
if err := viper.BindPFlag("private-key", RootCmd.PersistentFlags().Lookup("private-key")); err != nil {
panic(err)
}
@@ -225,7 +223,7 @@ func addPersistentFlags() {
if err := viper.BindPFlag("connection", RootCmd.PersistentFlags().Lookup("connection")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().Duration("timeout", 30*time.Second, "the time after which a network request will be considered failed. Increase this if you are running on an error-prone, high-latency or low-bandwidth connection")
RootCmd.PersistentFlags().Duration("timeout", 10*time.Second, "the time after which a network request will be considered failed. Increase this if you are running on an error-prone, high-latency or low-bandwidth connection")
if err := viper.BindPFlag("timeout", RootCmd.PersistentFlags().Lookup("timeout")); err != nil {
panic(err)
}

View File

@@ -40,7 +40,7 @@ var signatureAggregateCmd = &cobra.Command{
Signatures are specified as "signature" for simple aggregation, and as "id:signature" for threshold aggregation.
In quiet mode this will return 0 if the signatures can be aggregated, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
assert(len(signatureAggregateSignatures) > 1, "multiple signatures required to aggregate")
var signature *bls.Sign
var err error

View File

@@ -19,7 +19,6 @@ 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"
@@ -37,7 +36,7 @@ var signatureSignCmd = &cobra.Command{
ethdo signature sign --data=0x5f24e819400c6a8ee2bfc014343cd971b7eb707320025a7bcd83e621e26c35b7 --account="Personal wallet/Operations" --passphrase="my account passphrase"
In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
@@ -60,8 +59,6 @@ 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")
@@ -69,7 +66,7 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
copy(specDomain[:], domain)
var fixedSizeData [32]byte
copy(fixedSizeData[:], data)
outputIf(viper.GetBool("debug"), fmt.Sprintf("Signing %#x with domain %#x by public key %#x", fixedSizeData, specDomain, account.PublicKey().Marshal()))
fmt.Printf("Signing %#x with domain %#x by public key %#x\n", fixedSizeData, specDomain, account.PublicKey().Marshal())
signature, err := util.SignRoot(account, fixedSizeData, specDomain)
errCheck(err, "Failed to sign")

View File

@@ -19,7 +19,6 @@ 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"
@@ -42,7 +41,7 @@ var signatureVerifyCmd = &cobra.Command{
ethdo signature verify --data=0x5f24e819400c6a8ee2bfc014343cd971b7eb707320025a7bcd83e621e26c35b7 --signature=0x8888... --account="Personal wallet/Operations"
In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
@@ -72,10 +71,6 @@ 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

@@ -19,7 +19,6 @@ import (
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/pkg/errors"
)
@@ -42,18 +41,17 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("slot must be a positive integer")
}
genesisResponse, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis information")
}
genesis := genesisResponse.Data
specResponse, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain chain specifications")
}
slotDuration := specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
results.startTime = genesis.GenesisTime.Add((time.Duration(slot*int64(slotDuration.Seconds())) * time.Second))
results.endTime = results.startTime.Add(slotDuration)

View File

@@ -1,4 +1,4 @@
// Copyright © 2021, 2024 Weald Technology Trading.
// 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
@@ -15,8 +15,8 @@ package slottime
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
@@ -34,12 +34,7 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if 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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -29,7 +29,7 @@ var slotTimeCmd = &cobra.Command{
ethdo slot time --slot=12345
In quiet mode this will return 0.`,
RunE: func(cmd *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
res, err := slottime.Run(cmd)
if err != nil {
return err

View File

@@ -16,7 +16,6 @@ package inclusion
import (
"context"
"fmt"
"strconv"
"strings"
)
@@ -54,13 +53,13 @@ func (c *command) output(_ context.Context) (string, error) {
}
}
builder.WriteString("Expected: ")
builder.WriteString(strconv.Itoa(len(c.inclusions)))
builder.WriteString(fmt.Sprintf("%d", len(c.inclusions)))
builder.WriteString("\nIncluded: ")
builder.WriteString(strconv.Itoa(included))
builder.WriteString(fmt.Sprintf("%d", included))
builder.WriteString("\nMissed: ")
builder.WriteString(strconv.Itoa(missed))
builder.WriteString(fmt.Sprintf("%d", missed))
builder.WriteString("\nNo block: ")
builder.WriteString(strconv.Itoa(noBlock))
builder.WriteString(fmt.Sprintf("%d", noBlock))
builder.WriteString("\nPer-slot result: ")
for i, inclusion := range c.inclusions {

View File

@@ -16,10 +16,8 @@ package inclusion
import (
"context"
"fmt"
"net/http"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/pkg/errors"
@@ -43,14 +41,10 @@ func (c *command) process(ctx context.Context) error {
return err
}
syncCommitteeResponse, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommittee(ctx, &api.SyncCommitteeOpts{
State: "head",
Epoch: &c.epoch,
})
syncCommittee, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", c.epoch)
if err != nil {
return errors.Wrap(err, "failed to obtain sync committee information")
}
syncCommittee := syncCommitteeResponse.Data
if syncCommittee == nil {
return errors.New("no sync committee returned")
@@ -72,20 +66,11 @@ func (c *command) process(ctx context.Context) error {
if lastSlot > c.chainTime.CurrentSlot() {
lastSlot = c.chainTime.CurrentSlot()
}
for slot := firstSlot; slot <= lastSlot; slot++ {
blockResponse, err := c.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: fmt.Sprintf("%d", slot),
})
for slot := firstSlot; slot < lastSlot; slot++ {
block, err := c.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
c.inclusions = append(c.inclusions, 0)
continue
}
return errors.Wrap(err, "failed to obtain beacon block")
return err
}
block := blockResponse.Data
if block == nil {
c.inclusions = append(c.inclusions, 0)
continue
@@ -120,20 +105,6 @@ 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)
}
@@ -159,7 +130,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")

View File

@@ -1,4 +1,4 @@
// Copyright © 2022, 2024 Weald Technology Trading.
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package inclusion
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,19 +27,14 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to set up command"), err)
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
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)
}
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
@@ -48,7 +43,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Join(errors.New("failed to obtain output"), err)
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil

View File

@@ -63,7 +63,7 @@ func input(ctx context.Context) (*dataIn, error) {
// Chain time.
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
)
if err != nil {

View File

@@ -50,7 +50,7 @@ func TestOutput(t *testing.T) {
json: true,
validators: []phase0.ValidatorIndex{1, 2, 3},
},
res: `["1","2","3"]`,
res: "[1,2,3]",
},
}

Some files were not shown because too many files have changed in this diff Show More