Compare commits

...

99 Commits

Author SHA1 Message Date
Jim McDonald
bb34776d05 Return on error. 2024-05-27 09:34:06 +01:00
Jim McDonald
e29818340d Additional electra support. 2024-05-15 00:06:52 +01:00
Jim McDonald
083f484907 Support for custom spec. 2024-05-14 23:14:46 +01:00
Jim McDonald
7e09068d30 Merge pull request #134 from lyfsn/max-distance-var
Add maxDistance as an input parameter
2024-04-29 10:09:21 +01:00
lyfsn
2362a1a058 fix default value for maxDistance in process
Signed-off-by: lyfsn <dev.wangyu@proton.me>
2024-04-29 11:15:13 +08:00
Jim McDonald
80373ae237 Merge pull request #135 from wealdtech/dependabot/go_modules/golang.org/x/net-0.23.0
Bump golang.org/x/net from 0.22.0 to 0.23.0
2024-04-19 14:34:32 +01:00
dependabot[bot]
bd2eea3a60 Bump golang.org/x/net from 0.22.0 to 0.23.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-19 13:21:56 +00:00
lyfsn
a23650a5ae Add maxDistance as an input parameter
Signed-off-by: lyfsn <dev.wangyu@proton.me>
2024-04-11 14:59:58 +08:00
Jim McDonald
327df13fba Update workflows. 2024-03-17 22:31:59 +00:00
Jim McDonald
eaa0a19711 Fix tests. 2024-03-17 22:20:48 +00:00
Jim McDonald
8aba766208 Add deposit contract address to "chain info". 2024-03-17 22:18:44 +00:00
Jim McDonald
dd68af4884 More debug info for validator withdrawal calculation. 2024-03-11 14:59:53 +00:00
Jim McDonald
564228ff00 Linting. 2024-03-04 16:34:21 +00:00
Jim McDonald
697b6d4230 Sort validators by index in chain info. 2024-03-04 16:32:06 +00:00
Jim McDonald
c548058190 Fix tests. 2024-01-31 13:17:32 +00:00
Jim McDonald
daa2ac7b6e Update launchpad output for 2.7.0 2024-01-31 13:16:30 +00:00
Jim McDonald
3b8a98bb83 Better error message on timeout. 2024-01-28 16:34:19 +00:00
Jim McDonald
f1021387b3 Update dependencies. 2024-01-22 13:03:09 +00:00
Jim McDonald
cd27401514 Increase efficiency of fetching many validators. 2024-01-18 09:32:47 +00:00
Jim McDonald
faf3c8afa4 Handle empty blocks. 2024-01-17 11:39:09 +00:00
Jim McDonald
6ad8e7afe4 Update version. 2024-01-16 12:35:59 +00:00
Jim McDonald
1cffa7051d Tidy-ups for deneb. 2024-01-16 12:35:18 +00:00
Jim McDonald
0e089bc8ff Include period start and end in eth1 votes output. 2024-01-16 12:29:16 +00:00
Jim McDonald
c6f90a69af Merge branch 'master' of github.com:wealdtech/ethdo 2023-12-25 09:08:26 +00:00
Jim McDonald
58c3a7e279 Bump version. 2023-12-25 09:08:12 +00:00
Jim McDonald
cd1bf4dcfc Fix period parsing for synccommittee members 2023-12-25 09:07:33 +00:00
Jim McDonald
0a24f4ffe6 Merge pull request #124 from wealdtech/dependabot/go_modules/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc-0.46.0
Bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc from 0.45.0 to 0.46.0
2023-12-20 12:22:03 +00:00
Jim McDonald
77dd02a8f1 Merge pull request #126 from wealdtech/dependabot/go_modules/golang.org/x/crypto-0.17.0
Bump golang.org/x/crypto from 0.16.0 to 0.17.0
2023-12-20 12:20:48 +00:00
Jim McDonald
f4c99ac2b1 Update minimum version of go.
Fixes #127
2023-12-20 12:18:59 +00:00
dependabot[bot]
062b968055 Bump golang.org/x/crypto from 0.16.0 to 0.17.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-19 00:06:46 +00:00
dependabot[bot]
8083dd7eeb Bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
Bumps [go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc](https://github.com/open-telemetry/opentelemetry-go-contrib) from 0.45.0 to 0.46.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.45.0...zpages/v0.46.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-07 13:47:00 +00:00
Jim McDonald
841ba8e8f0 Update dependencies for deneb beta 5. 2023-12-07 13:45:01 +00:00
Jim McDonald
32a34fb9c1 Separate current and exit fork versions. 2023-11-29 12:09:15 +00:00
Jim McDonald
e356e9e1b7 Update dependencies. 2023-10-31 13:17:44 +00:00
Jim McDonald
82f5200296 Release 1.33.2 2023-10-19 14:46:13 +01:00
Jim McDonald
36ddb1a2fe Merge pull request #114 from wealdtech/dependabot/go_modules/golang.org/x/net-0.17.0
Bump golang.org/x/net from 0.13.0 to 0.17.0
2023-10-13 16:37:21 +01:00
dependabot[bot]
d4791ac8bd Bump golang.org/x/net from 0.13.0 to 0.17.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.13.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.13.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-11 23:02:40 +00:00
Jim McDonald
fff6469748 Version 1.33.1 2023-09-02 22:24:37 +01:00
Jim McDonald
b11f9cf3a3 Add sepolia support. 2023-08-22 21:56:10 +01:00
Jim McDonald
9d50a72270 Add activation timestamp to validator info. 2023-08-22 09:52:52 +01:00
Jim McDonald
9191cac389 Revert release changes. 2023-08-20 23:34:08 +02:00
Jim McDonald
f1a1d4f64c t 2023-08-20 23:22:05 +02:00
Jim McDonald
1f1ddc1719 Add slot option for proposer duties. 2023-08-16 15:47:06 +02:00
Jim McDonald
92d964f092 Update version. 2023-08-08 07:31:12 +01:00
Jim McDonald
972e35f7d7 Version 1.33.0 2023-08-07 20:54:04 +01:00
Jim McDonald
5f4d7a389d Remove local module directives. 2023-08-07 20:53:02 +01:00
Jim McDonald
696ba64238 Remove local module directives. 2023-08-07 20:52:09 +01:00
Jim McDonald
aeb8142b27 Fix tests with changes to dependency error output. 2023-08-07 20:49:32 +01:00
Jim McDonald
a1ea298983 Add wallet batch command. 2023-08-07 20:47:00 +01:00
Jim McDonald
1c23e7cc54 Update dependencies. 2023-08-04 12:45:54 +01:00
Jim McDonald
b53e42aeb7 Rename fields for latest deneb release. 2023-08-04 12:38:53 +01:00
Jim McDonald
fa325e20f8 Show all epoch slots in synccommittee inclusion. 2023-07-15 22:39:51 +01:00
Jim McDonald
0ce563470f Add blobs to epoch summary. 2023-07-08 22:07:42 +01:00
Jim McDonald
abd3567d05 Merge branch 'master' of github.com:wealdtech/ethdo 2023-07-08 22:04:41 +01:00
Jim McDonald
399d0eaa64 Increase default timeout to 30s. 2023-07-08 22:04:05 +01:00
Jim McDonald
655b9cbbc8 Merge pull request #110 from wealdtech/dependabot/go_modules/google.golang.org/grpc-1.53.0
Bump google.golang.org/grpc from 1.52.0 to 1.53.0
2023-07-06 18:19:49 +02:00
dependabot[bot]
50c22818cb Bump google.golang.org/grpc from 1.52.0 to 1.53.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.52.0 to 1.53.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.52.0...v1.53.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-05 21:41:57 +00:00
Jim McDonald
4991787b9d Merge pull request #109 from barnabasbusa/patch-1
Update usage.md
2023-07-05 11:51:00 +02:00
Barnabas Busa
b652d2e083 Update usage.md 2023-07-05 11:43:06 +02:00
Jim McDonald
ef58db3307 Merge pull request #108 from mksh/patch-1
Fix typo in cmd/root.go
2023-07-05 08:24:32 +02:00
mksh
a95851dc9e Fix typo in cmd/root.go 2023-07-05 03:21:42 +03:00
Jim McDonald
ac7e0985fb "validator summary" requires validators 2023-07-04 21:33:37 +02:00
Jim McDonald
7837165d46 Update block info. 2023-07-04 17:26:52 +02:00
Jim McDonald
46020396e4 Additional debug log for validator exits. 2023-07-02 18:00:08 +02:00
Jim McDonald
105de0a139 Merge branch 'master' of github.com:wealdtech/ethdo 2023-07-02 17:58:42 +02:00
Jim McDonald
a7da10360e Honour --quiet flag in block info command. 2023-07-02 17:58:26 +02:00
Jim McDonald
d5351cbe7f Merge pull request #107 from yorickdowne/patch-1
deposit data format for launchpad
2023-06-27 18:58:29 +02:00
yorickdowne
874839754c Tests use new version
2.5.0 and goerli in tests
2023-06-27 12:31:21 -04:00
yorickdowne
4db0cdae3a deposit data format for launchpad
Goerli launchpad expects "goerli" and version "2.5.0". Make those changes so generated deposit_data works with launchpad out of the box.
2023-06-27 11:37:48 -04:00
Jim McDonald
6f0f3e4c91 Add proposer index to block info output. 2023-06-15 10:54:06 +01:00
Jim McDonald
713bbdd60c Add epoch parameter to validator yield. 2023-06-13 00:22:32 +01:00
Jim McDonald
46ca70a615 Fix sync committee members test. 2023-06-13 00:20:38 +01:00
Jim McDonald
2f24bb7884 Update block info with blob information 2023-06-13 00:18:30 +01:00
Jim McDonald
9136c053b1 Remove print statement. 2023-06-12 12:40:50 +01:00
Jim McDonald
8efab62f8b Do not return error message when creating keystore. 2023-06-06 00:19:11 +01:00
Jim McDonald
7d723148ab linting. 2023-06-05 20:41:04 +01:00
Jim McDonald
2880ec9bdd Allow short form mnemonics. 2023-06-05 20:39:10 +01:00
Jim McDonald
871d1694ef Merge branch 'master' of github.com:wealdtech/ethdo 2023-05-31 11:44:59 +01:00
Jim McDonald
f26c9e9c4a Do not error on deposit verify.
If dpeosit verify was not given credentials it could not recreate the
deposit message, but rather than say this it errored.  Provide a
suitable message instead.

Fixes #102
2023-05-31 11:43:50 +01:00
Jim McDonald
b28d5b2693 Merge pull request #101 from dB2510/rename-exit-offline-image
docs/images: rename exit-offline.png
2023-05-23 06:25:28 +01:00
Dhruv Bodani
695f62bbd5 rename exit-offline.png to show in docs 2023-05-23 10:12:24 +05:30
Jim McDonald
0b8de1f615 Remove 32-bit target. 2023-05-21 09:07:20 +01:00
Jim McDonald
c1e5f1dd23 docker workflow. 2023-05-21 08:54:36 +01:00
Jim McDonald
2e4337fe6d Bump version. 2023-05-19 00:15:16 +01:00
Jim McDonald
dbe45d5c27 JSON output for validator expectation. 2023-05-19 00:04:48 +01:00
Jim McDonald
c963ea8ba5 Merge pull request #98 from wealdtech/mnemonics
Support 12- and 18-word mnemonics with passphrases.
2023-05-18 23:57:58 +01:00
Jim McDonald
484361c034 Support 12- and 18-word mnemonics with passphrases.
Allow single-world (no whitespace) passphrases for 12- and 18-word
mnemonics.

Fixes #87
2023-05-18 23:56:14 +01:00
Jim McDonald
d65f51d5af Merge pull request #97 from wealdtech/exit-validators
Generate multiple validator exits.
2023-05-18 23:38:29 +01:00
Jim McDonald
7857e97057 Generate multiple validator exits.
Fixes #88
2023-05-18 23:34:40 +01:00
Jim McDonald
545665a79f Add generate-keystore to account derive. 2023-05-08 09:21:20 +01:00
Jim McDonald
394fd2bb04 Use String() for execution address output. 2023-05-01 15:44:29 +01:00
Jim McDonald
a7631a6a7f Initial support for deneb. 2023-04-29 12:20:29 +01:00
Jim McDonald
d174219ddc Fix test. 2023-04-16 19:43:02 +01:00
Jim McDonald
854f3061b9 Update docs. 2023-04-16 19:42:07 +01:00
Jim McDonald
6c34d25ebb Update docs to include validator specifier. 2023-04-16 19:25:59 +01:00
Jim McDonald
df34cef2bd Bump version. 2023-04-16 18:59:38 +01:00
Jim McDonald
e8513e60b2 Add validator withdrawal. 2023-04-15 10:17:11 +01:00
Jim McDonald
4aadee3fad Promote --json to global flag. 2023-04-15 09:29:23 +01:00
Jim McDonald
c7adf03aeb Bump version. 2023-04-13 18:15:43 +01:00
198 changed files with 5119 additions and 2290 deletions

58
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Docker
on:
push:
jobs:
# Set variables that will be available to all builds.
env_vars:
runs-on: ubuntu-latest
outputs:
release_version: ${{ steps.release_version.outputs.release_version }}
binary: ${{ steps.binary.outputs.binary }}
steps:
- id: release_version
run: |
RELEASE_VERSION=$(echo ${{ github.ref_name }} | sed -e 's/^[vt]//')
echo "release_version=${RELEASE_VERSION}" >> $GITHUB_OUTPUT
- id: binary
run: |
BINARY=$(basename ${{ github.repository }})
echo "binary=${BINARY}" >> $GITHUB_OUTPUT
# Build.
build:
runs-on: ubuntu-latest
needs: [env_vars]
steps:
- name: Check out repository into the Go module directory
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64/v8
push: true
tags: wealdtech/ethdo:latest
- name: build and push on release
uses: docker/build-push-action@v4
if: ${{ github.event.release.tag_name != '' }}
with:
context: .
platforms: linux/amd64,linux/arm64/v8
push: true
tags: wealdtech/ethdo:${{ github.event.release.tag_name }}

View File

@@ -1,23 +1,23 @@
name: golangci-lint
on:
push:
tags:
- v*
branches:
- master
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: '1.20'
- uses: actions/checkout@v3
go-version: '^1.21'
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v4
with:
args: --timeout=60m
only-new-issues: true

View File

@@ -44,12 +44,12 @@ jobs:
needs: [create_release, env_vars]
steps:
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: '^1.20'
go-version: '^1.21'
- name: Check out repository into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Select correct tag
run: git checkout ${{ github.ref_name }}
@@ -119,12 +119,12 @@ jobs:
needs: [create_release, env_vars]
steps:
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: '^1.20'
go-version: '^1.21'
- name: Check out repository into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Select correct tag
run: git checkout ${{ github.ref_name }}
@@ -164,12 +164,12 @@ jobs:
needs: [create_release, env_vars]
steps:
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: '^1.20'
go-version: '^1.21'
- name: Check out repository into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Select correct tag
run: git checkout ${{ github.ref_name }}

View File

@@ -6,10 +6,10 @@ on:
pull_request:
jobs:
test:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: '1.20'
- uses: actions/checkout@v3
go-version: '^1.21'
- uses: actions/checkout@v4
- uses: n8maninger/action-golang-test@v1

View File

@@ -4,6 +4,16 @@
# This file is not a configuration example,
# it contains the exhaustive configuration with explanations of the options.
issues:
# Which files to exclude: they will be analyzed, but issues from them won't be reported.
# There is no need to include all autogenerated files,
# we confidently recognize autogenerated files.
# If it's not, please let us know.
# "/" will be replaced by current OS file path separator to properly work on Windows.
# Default: []
exclude-files:
- ".*_ssz\\.go$"
# Options for analysis running.
run:
# The default concurrency value is the number of available CPU.
@@ -39,15 +49,6 @@ run:
# Default: true
# skip-dirs-use-default: false
# Which files to skip: they will be analyzed, but issues from them won't be reported.
# Default value is empty list,
# but there is no need to include all autogenerated files,
# we confidently recognize autogenerated files.
# If it's not please let us know.
# "/" will be replaced by current OS file path separator to properly work on Windows.
skip-files:
- ".*_ssz\\.go$"
# If set we pass it to "go list -mod={option}". From "go help modules":
# If invoked with -mod=readonly, the go command is disallowed from the implicit
# automatic updating of go.mod described above. Instead, it fails when any changes
@@ -68,7 +69,7 @@ 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
@@ -128,8 +129,11 @@ linters:
- contextcheck
- cyclop
- deadcode
- depguard
- dupl
- err113
- errorlint
- execinquery
- exhaustive
- exhaustivestruct
- exhaustruct
@@ -141,7 +145,6 @@ linters:
- gochecknoinits
- gocognit
- goconst
- goerr113
- goheader
- golint
- gomnd
@@ -151,12 +154,14 @@ linters:
- lll
- maintidx
- maligned
- mnd
- musttag
- nestif
- nilnil
- nlreturn
- nolintlint
- nosnakecase
- perfsprint
- promlinter
- rowserrcheck
- scopelint

View File

@@ -1,4 +1,62 @@
1.29.1:
dev:
- 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
- add "validator withdrawal" command
1.29.2:
- fix regression where validator index could not be used as an account specifier
1.29.0:

View File

@@ -1,4 +1,4 @@
FROM golang:1.20-bullseye as builder
FROM golang:1.21-bookworm as builder
WORKDIR /app
@@ -10,7 +10,7 @@ COPY . .
RUN go build
FROM debian:bullseye-slim
FROM debian:bookworm-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.13 of go to operate. The version of go can be found with `go version`.
Note that `ethdo` requires at least version 1.20 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.
@@ -171,6 +171,15 @@ If set, the `--debug` argument will output additional information about the oper
Commands will have an exit status of 0 on success and 1 on failure. The specific definition of success is specified in the help for each command.
### Validator specifier
Ethereum validators can be specified in a number of different ways. The options are:
- an `ethdo` account, in the format _wallet_/_account_. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied, with a passphrase (for local accounts) or authority (for remote accounts)
- the validator's 48-byte public key. It is not possible to use the a validator specified in this way to sign validator-related operations
- a keystore, supplied either as direct JSON or as a path to a keystore on the local filesystem. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied
- the validator's numeric index. It is not possible to use a validator specified in this way to sign validator-related operations. Note that this only works with on-chain operations, as the validator's index must be resolved to its public key
## Passphrase strength
`ethdo` will by default not allow creation or export of accounts or wallets with weak passphrases. If a weak pasphrase is used then `ethdo` will refuse to continue.

View File

@@ -18,10 +18,12 @@ 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"
@@ -34,6 +36,7 @@ type ChainInfo struct {
GenesisValidatorsRoot phase0.Root
Epoch phase0.Epoch
GenesisForkVersion phase0.Version
ExitForkVersion phase0.Version
CurrentForkVersion phase0.Version
BLSToExecutionChangeDomainType phase0.DomainType
VoluntaryExitDomainType phase0.DomainType
@@ -45,6 +48,7 @@ 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"`
@@ -57,11 +61,12 @@ type chainInfoVersionJSON struct {
// MarshalJSON implements json.Marshaler.
func (c *ChainInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(&chainInfoJSON{
Version: fmt.Sprintf("%d", c.Version),
Version: strconv.FormatUint(c.Version, 10),
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),
@@ -82,7 +87,7 @@ func (c *ChainInfo) UnmarshalJSON(input []byte) error {
if err != nil {
return errors.Wrap(err, "version invalid")
}
if version < 2 {
if version < 3 {
return errors.New("outdated version; please regenerate your offline data")
}
c.Version = version
@@ -130,6 +135,18 @@ 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")
}
@@ -235,18 +252,18 @@ func ObtainChainInfoFromNode(ctx context.Context,
error,
) {
res := &ChainInfo{
Version: 2,
Version: 3,
Validators: make([]*ValidatorInfo, 0),
Epoch: chainTime.CurrentEpoch(),
}
// Obtain validators.
validators, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, "head", nil)
validatorsResponse, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{State: "head"})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validators")
}
for _, validator := range validators {
for _, validator := range validatorsResponse.Data {
res.Validators = append(res.Validators, &ValidatorInfo{
Index: validator.Index,
Pubkey: validator.Validator.PublicKey,
@@ -254,22 +271,26 @@ 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.
genesis, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx)
genesisResponse, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis information")
}
res.GenesisValidatorsRoot = genesis.GenesisValidatorsRoot
res.GenesisValidatorsRoot = genesisResponse.Data.GenesisValidatorsRoot
// Fetch the genesis fork version from the specification.
spec, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx)
specResponse, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx, &api.SpecOpts{})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["GENESIS_FORK_VERSION"]
tmp, exists := specResponse.Data["GENESIS_FORK_VERSION"]
if !exists {
return nil, errors.New("capella fork version not known by chain")
return nil, errors.New("genesis fork version not known by chain")
}
var isForkVersion bool
res.GenesisForkVersion, isForkVersion = tmp.(phase0.Version)
@@ -277,24 +298,34 @@ 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.
forkSchedule, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx)
forkScheduleResponse, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx, &api.ForkScheduleOpts{})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain fork schedule")
}
for i := range forkSchedule {
if forkSchedule[i].Epoch <= res.Epoch {
res.CurrentForkVersion = forkSchedule[i].CurrentVersion
for i := range forkScheduleResponse.Data {
if forkScheduleResponse.Data[i].Epoch <= res.Epoch {
res.CurrentForkVersion = forkScheduleResponse.Data[i].CurrentVersion
}
}
blsToExecutionChangeDomainType, exists := spec["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
blsToExecutionChangeDomainType, exists := specResponse.Data["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 := spec["DOMAIN_VOLUNTARY_EXIT"].(phase0.DomainType)
voluntaryExitDomainType, exists := specResponse.Data["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.Match("^m/[0-9]+/[0-9]+(/[0-9+])+", []byte(data.path))
match, err := regexp.MatchString("^m/[0-9]+/[0-9]+(/[0-9+])+", 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, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -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.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
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("verbose") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 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
@@ -28,6 +28,7 @@ type dataIn struct {
// Output options.
showPrivateKey bool
showWithdrawalCredentials bool
generateKeystore bool
}
func input(_ context.Context) (*dataIn, error) {
@@ -54,5 +55,8 @@ func input(_ context.Context) (*dataIn, error) {
// Show withdrawal credentials.
data.showWithdrawalCredentials = viper.GetBool("show-withdrawal-credentials")
// Generate keystore.
data.generateKeystore = viper.GetBool("generate-keystore")
return data, nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 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
@@ -15,21 +15,30 @@ package accountderive
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
ethutil "github.com/wealdtech/go-eth2-util"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
type dataOut struct {
showPrivateKey bool
showWithdrawalCredentials bool
generateKeystore bool
key *e2types.BLSPrivateKey
path string
}
func output(_ context.Context, data *dataOut) (string, error) {
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
@@ -37,13 +46,17 @@ func output(_ context.Context, data *dataOut) (string, error) {
return "", errors.New("no key")
}
if data.generateKeystore {
return outputKeystore(ctx, data)
}
builder := strings.Builder{}
if data.showPrivateKey {
builder.WriteString(fmt.Sprintf("Private key: %#x\n", data.key.Marshal()))
}
if data.showWithdrawalCredentials {
withdrawalCredentials := util.SHA256(data.key.PublicKey().Marshal())
withdrawalCredentials := ethutil.SHA256(data.key.PublicKey().Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
builder.WriteString(fmt.Sprintf("Withdrawal credentials: %#x\n", withdrawalCredentials))
}
@@ -53,3 +66,38 @@ func output(_ context.Context, data *dataOut) (string, error) {
return builder.String(), nil
}
func outputKeystore(_ context.Context, data *dataOut) (string, error) {
passphrase, err := util.GetPassphrase()
if err != nil {
return "", errors.New("no passphrase supplied")
}
encryptor := keystorev4.New()
crypto, err := encryptor.Encrypt(data.key.Marshal(), passphrase)
if err != nil {
return "", errors.New("failed to encrypt private key")
}
uuid, err := uuid.NewRandom()
if err != nil {
return "", errors.New("failed to generate UUID")
}
ks := make(map[string]interface{})
ks["uuid"] = uuid.String()
ks["pubkey"] = hex.EncodeToString(data.key.PublicKey().Marshal())
ks["version"] = 4
ks["path"] = data.path
ks["crypto"] = crypto
out, err := json.Marshal(ks)
if err != nil {
return "", errors.Wrap(err, "failed to marshal keystore JSON")
}
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))
}
return "", nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 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
@@ -40,7 +40,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
results := &dataOut{
showPrivateKey: data.showPrivateKey,
showWithdrawalCredentials: data.showWithdrawalCredentials,
generateKeystore: data.generateKeystore,
key: key.(*e2types.BLSPrivateKey),
path: data.path,
}
return results, nil

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -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.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to obtain input"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
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 dataIn.quiet {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return strings.TrimSuffix(results, "\n"), nil

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -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.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to obtain input"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
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("verbose") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -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.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := accountcreate.Run(cmd)
if err != nil {
return err
@@ -51,11 +51,11 @@ func init() {
accountCreateCmd.Flags().Uint32("signing-threshold", 1, "Signing threshold (1 for non-distributed accounts)")
}
func accountCreateBindings() {
if err := viper.BindPFlag("participants", accountCreateCmd.Flags().Lookup("participants")); err != nil {
func accountCreateBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("participants", cmd.Flags().Lookup("participants")); err != nil {
panic(err)
}
if err := viper.BindPFlag("signing-threshold", accountCreateCmd.Flags().Lookup("signing-threshold")); err != nil {
if err := viper.BindPFlag("signing-threshold", cmd.Flags().Lookup("signing-threshold")); err != nil {
panic(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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := accountderive.Run(cmd)
if err != nil {
return err
@@ -49,13 +49,17 @@ func init() {
accountFlags(accountDeriveCmd)
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")
}
func accountDeriveBindings() {
if err := viper.BindPFlag("show-private-key", accountDeriveCmd.Flags().Lookup("show-private-key")); err != nil {
func accountDeriveBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("show-private-key", cmd.Flags().Lookup("show-private-key")); err != nil {
panic(err)
}
if err := viper.BindPFlag("show-withdrawal-credentials", accountDeriveCmd.Flags().Lookup("show-withdrawal-credentials")); err != nil {
if err := viper.BindPFlag("show-withdrawal-credentials", cmd.Flags().Lookup("show-withdrawal-credentials")); err != nil {
panic(err)
}
if err := viper.BindPFlag("generate-keystore", cmd.Flags().Lookup("generate-keystore")); err != nil {
panic(err)
}
}

View File

@@ -24,12 +24,12 @@ import (
var accountImportCmd = &cobra.Command{
Use: "import",
Short: "Import an account",
Long: `Import an account from its private key. For example:
Long: `Import an account from its private key or keystore. For example:
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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := accountimport.Run(cmd)
if err != nil {
return err
@@ -52,14 +52,14 @@ func init() {
accountImportCmd.Flags().String("keystore-passphrase", "", "Passphrase of keystore")
}
func accountImportBindings() {
if err := viper.BindPFlag("key", accountImportCmd.Flags().Lookup("key")); err != nil {
func accountImportBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("key", cmd.Flags().Lookup("key")); err != nil {
panic(err)
}
if err := viper.BindPFlag("keystore", accountImportCmd.Flags().Lookup("keystore")); err != nil {
if err := viper.BindPFlag("keystore", cmd.Flags().Lookup("keystore")); err != nil {
panic(err)
}
if err := viper.BindPFlag("keystore-passphrase", accountImportCmd.Flags().Lookup("keystore-passphrase")); err != nil {
if err := viper.BindPFlag("keystore-passphrase", cmd.Flags().Lookup("keystore-passphrase")); err != nil {
panic(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(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
@@ -44,11 +44,11 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
// Disallow wildcards (for now)
assert(fmt.Sprintf("%s/%s", wallet.Name(), account.Name()) == viper.GetString("account"), "Mismatched account name")
if quiet {
if viper.GetBool("quiet") {
os.Exit(_exitSuccess)
}
outputIf(verbose, fmt.Sprintf("UUID: %v", account.ID()))
outputIf(viper.GetBool("verbose"), fmt.Sprintf("UUID: %v", account.ID()))
var withdrawalPubKey e2types.PublicKey
if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok {
fmt.Printf("Public key: %#x\n", pubKeyProvider.PublicKey().Marshal())
@@ -58,7 +58,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
if distributedAccount, ok := account.(e2wtypes.DistributedAccount); ok {
fmt.Printf("Composite public key: %#x\n", distributedAccount.CompositePublicKey().Marshal())
fmt.Printf("Signing threshold: %d/%d\n", distributedAccount.SigningThreshold(), len(distributedAccount.Participants()))
if verbose {
if viper.GetBool("verbose") {
fmt.Printf("Participants:\n")
for k, v := range distributedAccount.Participants() {
fmt.Printf(" %d: %s\n", k, v)
@@ -67,7 +67,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
withdrawalPubKey = distributedAccount.CompositePublicKey()
}
if verbose {
if viper.GetBool("verbose") {
withdrawalCredentials := util.SHA256(withdrawalPubKey.Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
fmt.Printf("Withdrawal credentials: %#x\n", withdrawalCredentials)

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, args []string) error {
RunE: func(cmd *cobra.Command, _ []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(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []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(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()

View File

@@ -61,10 +61,11 @@ func input(ctx context.Context) (*dataIn, error) {
// Ethereum 2 client.
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !data.quiet,
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
CustomSpecSupport: viper.GetBool("custom-spec"),
LogFallback: !data.quiet,
})
if err != nil {
return nil, err
@@ -72,7 +73,7 @@ func input(ctx context.Context) (*dataIn, error) {
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to set up chaintime service")

View File

@@ -17,7 +17,8 @@ import (
"context"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
@@ -49,12 +50,16 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch) (*api.AttesterDuty, error) {
func duty(ctx context.Context, eth2Client eth2client.Service, validator *apiv1.Validator, epoch spec.Epoch) (*apiv1.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
dutiesResponse, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{
Epoch: epoch,
Indices: []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.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
)
require.NoError(t, err)

View File

@@ -1,4 +1,4 @@
// Copyright © 2021 Weald Technology Trading
// Copyright © 2021, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -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.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

View File

@@ -58,10 +58,11 @@ func input(ctx context.Context) (*dataIn, error) {
// Ethereum 2 client.
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !data.quiet,
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
CustomSpecSupport: viper.GetBool("custom-spec"),
LogFallback: !data.quiet,
})
if err != nil {
return nil, err
@@ -69,7 +70,7 @@ func input(ctx context.Context) (*dataIn, error) {
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to set up chaintime service")

View File

@@ -16,6 +16,7 @@ package attesterinclusion
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/attestantio/go-eth2-client/spec/phase0"
@@ -49,7 +50,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(fmt.Sprintf("%d", data.attestationIndex))
buf.WriteString(strconv.FormatUint(data.attestationIndex, 10))
if data.verbose {
buf.WriteString("\nInclusion delay: ")
buf.WriteString(fmt.Sprintf("%d", data.inclusionDelay))

View File

@@ -17,9 +17,11 @@ import (
"bytes"
"context"
"fmt"
"net/http"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
@@ -38,7 +40,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to set up chaintime service")
@@ -61,14 +63,22 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
startSlot := duty.Slot + 1
endSlot := startSlot + 32
for slot := startSlot; slot < endSlot; slot++ {
signedBlock, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
blockResponse, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: 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")
}
if signedBlock == nil {
block := blockResponse.Data
if block == nil {
continue
}
blockSlot, err := signedBlock.Slot()
blockSlot, err := block.Slot()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block slot")
}
@@ -78,28 +88,38 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data.debug {
fmt.Printf("Fetched block for slot %d\n", slot)
}
attestations, err := signedBlock.Attestations()
attestations, err := block.Attestations()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block attestations")
}
for i, attestation := range attestations {
if attestation.Data.Slot == duty.Slot &&
attestation.Data.Index == duty.CommitteeIndex &&
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
attestationData, err := attestation.Data()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attestation data")
}
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attestation aggregation bits")
}
if attestationData.Slot == duty.Slot &&
attestationData.Index == duty.CommitteeIndex &&
aggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
headCorrect := false
targetCorrect := false
if data.verbose {
headCorrect, err = calcHeadCorrect(ctx, data, attestation)
headCorrect, err = calcHeadCorrect(ctx, data, attestationData)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain head correct result")
}
targetCorrect, err = calcTargetCorrect(ctx, data, attestation)
targetCorrect, err = calcTargetCorrect(ctx, data, attestationData)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain target correct result")
}
}
results.found = true
results.attestation = attestation
// TODO fix.
// results.attestation = attestation
results.slot = slot
results.attestationIndex = uint64(i)
results.inclusionDelay = slot - duty.Slot
@@ -118,55 +138,67 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
func calcHeadCorrect(ctx context.Context, data *dataIn, attestationData *phase0.AttestationData) (bool, error) {
slot := attestationData.Slot
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: 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 header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
return bytes.Equal(response.Data.Root[:], attestationData.BeaconBlockRoot[:]), nil
}
}
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
func calcTargetCorrect(ctx context.Context, data *dataIn, attestationData *phase0.AttestationData) (bool, error) {
// Start with first slot of the target epoch.
slot := data.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
slot := data.chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: 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 header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.Target.Root[:]), nil
return bytes.Equal(response.Data.Root[:], attestationData.Target.Root[:]), nil
}
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch) (*api.AttesterDuty, error) {
func duty(ctx context.Context, eth2Client eth2client.Service, validator *apiv1.Validator, epoch phase0.Epoch) (*apiv1.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []phase0.ValidatorIndex{validator.Index})
dutiesResponse, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{
Epoch: epoch,
Indices: []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.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
)
require.NoError(t, err)

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -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.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := attesterduties.Run(cmd)
if err != nil {
return err
@@ -49,17 +49,13 @@ func init() {
attesterFlags(attesterDutiesCmd)
attesterDutiesCmd.Flags().String("epoch", "head", "the epoch for which to obtain the duties")
attesterDutiesCmd.Flags().String("validator", "", "the index, public key, or acount of the validator")
attesterDutiesCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network")
}
func attesterDutiesBindings() {
if err := viper.BindPFlag("epoch", attesterDutiesCmd.Flags().Lookup("epoch")); err != nil {
func attesterDutiesBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("validator", attesterDutiesCmd.Flags().Lookup("validator")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", attesterDutiesCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
panic(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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := attesterinclusion.Run(cmd)
if err != nil {
return err
@@ -52,14 +52,14 @@ func init() {
attesterInclusionCmd.Flags().String("index", "", "the index of the attester")
}
func attesterInclusionBindings() {
if err := viper.BindPFlag("epoch", attesterInclusionCmd.Flags().Lookup("epoch")); err != nil {
func attesterInclusionBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("validator", attesterInclusionCmd.Flags().Lookup("validator")); err != nil {
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
panic(err)
}
if err := viper.BindPFlag("index", attesterInclusionCmd.Flags().Lookup("index")); err != nil {
if err := viper.BindPFlag("index", cmd.Flags().Lookup("index")); err != nil {
panic(err)
}
}

View File

@@ -34,6 +34,7 @@ type command struct {
timeout time.Duration
connection string
allowInsecureConnections bool
customSpecSupport bool
// Operation.
blockID string
@@ -124,6 +125,7 @@ func newCommand(_ context.Context) (*command, error) {
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
c.customSpecSupport = viper.GetBool("custom-spec")
c.blockID = viper.GetString("blockid")
c.stream = viper.GetBool("stream")

View File

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

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 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
@@ -19,7 +19,9 @@ import (
"fmt"
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/electra"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
@@ -33,13 +35,17 @@ func (c *command) process(ctx context.Context) error {
return err
}
block, err := c.blocksProvider.SignedBeaconBlock(ctx, c.blockID)
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: c.blockID,
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
return errors.New("empty beacon block")
}
return errors.Wrap(err, "failed to obtain beacon block")
}
if block == nil {
return errors.New("empty beacon block")
}
block := blockResponse.Data
slot, err := block.Slot()
if err != nil {
@@ -57,8 +63,12 @@ func (c *command) process(ctx context.Context) error {
// Calculate how many parents we need to fetch.
minSlot := slot
for _, attestation := range attestations {
if attestation.Data.Slot < minSlot {
minSlot = attestation.Data.Slot
attestationData, err := attestation.Data()
if err != nil {
continue
}
if attestationData.Slot < minSlot {
minSlot = attestationData.Slot
}
}
if c.debug {
@@ -97,68 +107,27 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
if c.debug {
fmt.Printf("Processing attestation %d\n", i)
}
analysis := &attestationAnalysis{
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
}
root, err := attestation.HashTreeRoot()
var analysis *attestationAnalysis
switch attestation.Version {
case spec.DataVersionPhase0:
analysis, err = c.phase0Analysis(ctx, slot, attestation.Phase0, blockVotes, spec.DataVersionPhase0)
case spec.DataVersionAltair:
analysis, err = c.phase0Analysis(ctx, slot, attestation.Altair, blockVotes, spec.DataVersionAltair)
case spec.DataVersionBellatrix:
analysis, err = c.phase0Analysis(ctx, slot, attestation.Bellatrix, blockVotes, spec.DataVersionBellatrix)
case spec.DataVersionCapella:
analysis, err = c.phase0Analysis(ctx, slot, attestation.Capella, blockVotes, spec.DataVersionCapella)
case spec.DataVersionDeneb:
analysis, err = c.phase0Analysis(ctx, slot, attestation.Deneb, blockVotes, spec.DataVersionDeneb)
case spec.DataVersionElectra:
analysis, err = c.electraAnalysis(ctx, slot, attestation.Electra, blockVotes, spec.DataVersionElectra)
default:
return fmt.Errorf("unknown version %s", attestation.Version)
}
if err != nil {
return err
}
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[data.Slot][data.Index]
if !exists {
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
// Count new votes.
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[data.Slot][data.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[data.Slot][data.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
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 = attestation.Data.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestation.Data.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
if err != nil {
return err
}
// Calculate target timely.
analysis.TargetTimely = attestation.Data.Slot >= slot-32
}
// Calculate score and value.
if analysis.TargetCorrect && analysis.TargetTimely {
@@ -179,17 +148,194 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
return nil
}
func (c *command) phase0Analysis(ctx context.Context,
slot phase0.Slot,
attestation *phase0.Attestation,
blockVotes map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist,
version spec.DataVersion,
) (
*attestationAnalysis,
error,
) {
analysis := &attestationAnalysis{
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
}
// TODO.
root, err := attestation.HashTreeRoot()
if err != nil {
return nil, err
}
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[data.Slot][data.Index]
if !exists {
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
// Count new votes.
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[data.Slot][data.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[data.Slot][data.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
}
}
// Calculate head correct.
analysis.HeadCorrect, err = c.calcHeadCorrect(ctx, attestation)
if err != nil {
return nil, err
}
// Calculate head timely.
analysis.HeadTimely = analysis.HeadCorrect && attestation.Data.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestation.Data.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
if err != nil {
return nil, err
}
// Calculate target timely.
if version < spec.DataVersionDeneb {
analysis.TargetTimely = attestation.Data.Slot >= slot-32
} else {
analysis.TargetTimely = true
}
}
return analysis, nil
}
func (c *command) electraAnalysis(ctx context.Context,
slot phase0.Slot,
attestation *electra.Attestation,
blockVotes map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist,
version spec.DataVersion,
) (
*attestationAnalysis,
error,
) {
analysis := &attestationAnalysis{
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
}
// TODO.
root, err := attestation.HashTreeRoot()
if err != nil {
return nil, err
}
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[data.Slot][data.Index]
if !exists {
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
// Count new votes.
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[data.Slot][data.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[data.Slot][data.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
}
}
// Calculate head correct.
var err error
analysis.HeadCorrect, err = c.calcElectraHeadCorrect(ctx, attestation)
if err != nil {
return nil, err
}
// Calculate head timely.
analysis.HeadTimely = analysis.HeadCorrect && attestation.Data.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestation.Data.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcElectraTargetCorrect(ctx, attestation)
if err != nil {
return nil, err
}
// Calculate target timely.
if version < spec.DataVersionDeneb {
analysis.TargetTimely = attestation.Data.Slot >= slot-32
} else {
analysis.TargetTimely = true
}
}
return analysis, nil
}
func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedBeaconBlock, minSlot phase0.Slot) error {
parentRoot, err := block.ParentRoot()
if err != nil {
return err
}
root, err := block.Deneb.HashTreeRoot()
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.
parentBlock, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%#x", parentRoot))
parentBlockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: fmt.Sprintf("%#x", parentRoot),
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
return errors.New("empty beacon block")
}
return err
}
parentBlock := parentBlockResponse.Data
if parentBlock == nil {
return fmt.Errorf("unable to obtain parent block %s", parentBlock)
}
@@ -210,10 +356,10 @@ func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedB
}
func (c *command) processParentBlock(_ context.Context, block *spec.VersionedSignedBeaconBlock) error {
attestations, err := block.Attestations()
if err != nil {
return err
}
// attestations, err := block.Attestations()
// if err != nil {
// return err
// }
slot, err := block.Slot()
if err != nil {
return err
@@ -222,31 +368,40 @@ func (c *command) processParentBlock(_ context.Context, block *spec.VersionedSig
fmt.Printf("Processing block %d\n", slot)
}
for i, attestation := range attestations {
root, err := attestation.HashTreeRoot()
if err != nil {
return err
}
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
Block: slot,
Index: i,
}
// TODO reinstate.
// for i, attestation := range attestations {
// root, err := attestation.HashTreeRoot()
// if err != nil {
// return err
// }
// c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
// Block: slot,
// Index: i,
// }
data := attestation.Data
_, exists := c.votes[data.Slot]
if !exists {
c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = c.votes[data.Slot][data.Index]
if !exists {
c.votes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
c.votes[data.Slot][data.Index].SetBitAt(j, true)
}
}
}
// data, err := attestation.Data()
// if err != nil {
// return err
// }
// aggregationBits, err := attestation.AggregationBits()
// if err != nil {
// return err
// }
// _, exists := c.votes[data.Slot]
// if !exists {
// c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
// }
// _, exists = c.votes[data.Slot][data.Index]
// if !exists {
// c.votes[data.Slot][data.Index] = bitfield.NewBitlist(aggregationBits.Len())
// }
// for j := uint64(0); j < aggregationBits.Len(); j++ {
// if aggregationBits.BitAt(j) {
// c.votes[data.Slot][data.Index].SetBitAt(j, true)
// }
// }
//}
return nil
}
@@ -256,10 +411,11 @@ func (c *command) setup(ctx context.Context) error {
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
CustomSpecSupport: c.customSpecSupport,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
@@ -267,7 +423,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
@@ -289,12 +445,12 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("connection does not provide spec information")
}
spec, err := specProvider.Spec(ctx)
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["TIMELY_SOURCE_WEIGHT"]
tmp, exists := specResponse.Data["TIMELY_SOURCE_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
@@ -305,7 +461,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("TIMELY_SOURCE_WEIGHT of unexpected type")
}
tmp, exists = spec["TIMELY_TARGET_WEIGHT"]
tmp, exists = specResponse.Data["TIMELY_TARGET_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(26)
@@ -315,7 +471,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("TIMELY_TARGET_WEIGHT of unexpected type")
}
tmp, exists = spec["TIMELY_HEAD_WEIGHT"]
tmp, exists = specResponse.Data["TIMELY_HEAD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
@@ -325,7 +481,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("TIMELY_HEAD_WEIGHT of unexpected type")
}
tmp, exists = spec["SYNC_REWARD_WEIGHT"]
tmp, exists = specResponse.Data["SYNC_REWARD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(2)
@@ -335,7 +491,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("SYNC_REWARD_WEIGHT of unexpected type")
}
tmp, exists = spec["PROPOSER_WEIGHT"]
tmp, exists = specResponse.Data["PROPOSER_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(8)
@@ -345,7 +501,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("PROPOSER_WEIGHT of unexpected type")
}
tmp, exists = spec["WEIGHT_DENOMINATOR"]
tmp, exists = specResponse.Data["WEIGHT_DENOMINATOR"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(64)
@@ -362,22 +518,31 @@ func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attes
root, exists := c.headRoots[slot]
if !exists {
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
if c.debug {
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
}
return false, nil
}
return false, err
}
if header == nil {
if response.Data == nil {
// No block.
slot--
continue
}
if !header.Canonical {
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
c.headRoots[attestation.Data.Slot] = header.Root
root = header.Root
c.headRoots[attestation.Data.Slot] = response.Data.Root
root = response.Data.Root
break
}
}
@@ -391,22 +556,103 @@ func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Att
// Start with first slot of the target epoch.
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
return false, err
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
if c.debug {
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
}
return false, nil
}
}
if header == nil {
if response.Data == nil {
// No block.
slot--
continue
}
if !header.Canonical {
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
c.targetRoots[attestation.Data.Slot] = header.Root
root = header.Root
c.targetRoots[attestation.Data.Slot] = response.Data.Root
root = response.Data.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
}
func (c *command) calcElectraHeadCorrect(ctx context.Context, attestation *electra.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),
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
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 {
// No block.
slot--
continue
}
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
c.headRoots[attestation.Data.Slot] = response.Data.Root
root = response.Data.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.BeaconBlockRoot[:]), nil
}
func (c *command) calcElectraTargetCorrect(ctx context.Context, attestation *electra.Attestation) (bool, error) {
root, exists := c.targetRoots[attestation.Data.Slot]
if !exists {
// Start with first slot of the target epoch.
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
for {
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
if c.debug {
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
}
return false, nil
}
}
if response.Data == nil {
// No block.
slot--
continue
}
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
c.targetRoots[attestation.Data.Slot] = response.Data.Root
root = response.Data.Root
break
}
}
@@ -439,6 +685,13 @@ func (c *command) analyzeSyncCommittees(_ context.Context, block *spec.Versioned
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
case spec.DataVersionDeneb:
c.analysis.SyncCommitee.Contributions = int(block.Deneb.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Deneb.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 Weald Technology Trading.
// Copyright © 2022, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package blockanalyze
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,14 +27,19 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
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 {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 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
@@ -34,8 +34,9 @@ type dataIn struct {
jsonOutput bool
sszOutput bool
// Chain information.
blockID string
stream bool
blockID string
blockTime string
stream bool
}
func input(ctx context.Context) (*dataIn, error) {
@@ -50,26 +51,21 @@ 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
data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !data.quiet,
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
CustomSpecSupport: viper.GetBool("custom-spec"),
LogFallback: !data.quiet,
})
if err != nil {
return nil, err
}
if viper.GetString("blockid") == "" {
data.blockID = "head"
} else {
// Specific slot.
data.blockID = viper.GetString("blockid")
}
return data, nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020, 2021 Weald Technology Trading
// Copyright © 2019 - 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
@@ -20,14 +20,18 @@ import (
"fmt"
"math/big"
"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"
@@ -54,6 +58,7 @@ func output(_ context.Context, data *dataOut) (string, error) {
func outputBlockGeneral(_ context.Context,
verbose bool,
slot phase0.Slot,
proposerIndex phase0.ValidatorIndex,
blockRoot phase0.Root,
bodyRoot phase0.Root,
parentRoot phase0.Root,
@@ -69,6 +74,7 @@ func outputBlockGeneral(_ 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))
@@ -113,12 +119,14 @@ 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 {
beaconCommittees, err := beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", att.Data.Slot))
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 beaconCommittees {
for _, beaconCommittee := range response.Data {
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
}
@@ -163,11 +171,128 @@ func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Ser
res.WriteString(fmt.Sprintf(" %d:\n", i))
res.WriteString(fmt.Sprintln(" Slashed validators:"))
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", slashedIndices)
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 validators {
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 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]
}
res.WriteString(fmt.Sprintf(" Committee index: %d\n", att.Data.Index))
res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()))
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitlistToString(att.AggregationBits)))
if _, exists := committees[att.Data.Index]; exists {
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index])))
}
res.WriteString(fmt.Sprintf(" 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 outputElectraBlockAttesterSlashings(ctx context.Context,
eth2Client eth2client.Service,
verbose bool,
attesterSlashings []*electra.AttesterSlashing,
) (
string,
error,
) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Attester slashings: %d\n", len(attesterSlashings)))
if verbose {
for i, slashing := range attesterSlashings {
// Say what was slashed.
att1 := slashing.Attestation1
att2 := slashing.Attestation2
slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices)
if len(slashedIndices) == 0 {
continue
}
res.WriteString(fmt.Sprintf(" %d:\n", i))
res.WriteString(fmt.Sprintln(" Slashed validators:"))
response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{
State: "head",
Indices: slashedIndices,
})
if err != nil {
return "", errors.Wrap(err, "failed to obtain beacon committees")
}
for k, v := range response.Data {
res.WriteString(fmt.Sprintf(" %#x (%d)\n", v.Validator.PublicKey[:], k))
}
@@ -220,11 +345,14 @@ func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Servic
if verbose {
for i, voluntaryExit := range voluntaryExits {
res.WriteString(fmt.Sprintf(" %d:\n", i))
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []phase0.ValidatorIndex{voluntaryExit.Message.ValidatorIndex})
response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{
State: "head",
Indices: []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", validators[voluntaryExit.Message.ValidatorIndex].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", response.Data[voluntaryExit.Message.ValidatorIndex].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" Epoch: %d\n", voluntaryExit.Message.Epoch))
}
}
@@ -240,13 +368,16 @@ func outputBlockBLSToExecutionChanges(ctx context.Context, eth2Client eth2client
if verbose {
for i, op := range ops {
res.WriteString(fmt.Sprintf(" %d:\n", i))
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []phase0.ValidatorIndex{op.Message.ValidatorIndex})
response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{
State: "head",
Indices: []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", validators[op.Message.ValidatorIndex].Validator.PublicKey, op.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", response.Data[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: %#x\n", op.Message.ToExecutionAddress))
res.WriteString(fmt.Sprintf(" Execution address: %s\n", op.Message.ToExecutionAddress.String()))
}
}
}
@@ -262,9 +393,9 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
if verbose {
specProvider, isProvider := eth2Client.(eth2client.SpecProvider)
if isProvider {
config, err := specProvider.Spec(ctx)
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
if err == nil {
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
slotsPerEpoch := specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
res.WriteString(" Contributions: ")
res.WriteString(bitvectorToString(syncAggregate.SyncCommitteeBits))
@@ -272,14 +403,16 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
syncCommitteesProvider, isProvider := eth2Client.(eth2client.SyncCommitteesProvider)
if isProvider {
syncCommittee, err := syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", uint64(epoch)*slotsPerEpoch))
syncCommitteeResponse, err := syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{
State: strconv.FormatUint(uint64(epoch)*slotsPerEpoch, 10),
})
if err != nil {
res.WriteString(fmt.Sprintf(" Error: failed to obtain sync committee: %v\n", err))
} else {
res.WriteString(" Contributing validators:")
for i := uint64(0); i < syncAggregate.SyncCommitteeBits.Len(); i++ {
if syncAggregate.SyncCommitteeBits.BitAt(i) {
res.WriteString(fmt.Sprintf(" %d", syncCommittee.Validators[i]))
res.WriteString(fmt.Sprintf(" %d", syncCommitteeResponse.Data.Validators[i]))
}
}
res.WriteString("\n")
@@ -314,6 +447,7 @@ 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,
@@ -388,6 +522,226 @@ 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,
) {
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 = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputBlockAttesterSlashings(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.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// 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 = outputDenebBlobInfo(ctx, data.verbose, signedBlock.Message.Body, 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.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// 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 = outputElectraBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputElectraBlobInfo(ctx, data.verbose, signedBlock.Message.Body, blobs)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
@@ -410,6 +764,7 @@ 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,
@@ -500,6 +855,7 @@ 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,
@@ -583,6 +939,7 @@ 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,
@@ -676,7 +1033,7 @@ func outputCapellaBlockExecutionPayload(_ context.Context,
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
res.WriteString(payload.FeeRecipient.String())
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
@@ -706,6 +1063,194 @@ func outputCapellaBlockExecutionPayload(_ context.Context,
return res.String(), nil
}
func outputDenebBlockExecutionPayload(_ context.Context,
verbose bool,
payload *deneb.ExecutionPayload,
) (
string,
error,
) {
if payload == nil {
return "", nil
}
// If the block number is 0 then we're before the merge.
if payload.BlockNumber == 0 {
return "", nil
}
res := strings.Builder{}
if !verbose {
res.WriteString("Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString("Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
} else {
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString(" Base fee per gas: ")
res.WriteString(string2eth.WeiToString(payload.BaseFeePerGas.ToBig(), true))
res.WriteString("\n Block hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(payload.FeeRecipient.String())
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
res.WriteString(" Timestamp: ")
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
res.WriteString(" Prev RANDAO: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
res.WriteString(" Receipts root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
res.WriteString(" State root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
res.WriteString(" Extra data: ")
if utf8.Valid(payload.ExtraData) {
res.WriteString(fmt.Sprintf("%s\n", string(payload.ExtraData)))
} else {
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
}
res.WriteString(" Logs bloom: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
res.WriteString(" Transactions: ")
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))
}
return res.String(), nil
}
func outputElectraBlockExecutionPayload(_ context.Context,
verbose bool,
payload *electra.ExecutionPayload,
) (
string,
error,
) {
if payload == nil {
return "", nil
}
// If the block number is 0 then we're before the merge.
if payload.BlockNumber == 0 {
return "", nil
}
res := strings.Builder{}
if !verbose {
res.WriteString("Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString("Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
} else {
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString(" Base fee per gas: ")
res.WriteString(string2eth.WeiToString(payload.BaseFeePerGas.ToBig(), true))
res.WriteString("\n Block hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(payload.FeeRecipient.String())
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
res.WriteString(" Timestamp: ")
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
res.WriteString(" Prev RANDAO: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
res.WriteString(" Receipts root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
res.WriteString(" State root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
res.WriteString(" Extra data: ")
if utf8.Valid(payload.ExtraData) {
res.WriteString(fmt.Sprintf("%s\n", string(payload.ExtraData)))
} else {
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
}
res.WriteString(" Logs bloom: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
res.WriteString(" Transactions: ")
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))
}
return res.String(), nil
}
func outputDenebBlobInfo(_ context.Context,
verbose bool,
body *deneb.BeaconBlockBody,
blobs []*deneb.BlobSidecar,
) (
string,
error,
) {
if body == nil {
return "", nil
}
if !verbose {
return fmt.Sprintf("Blobs: %d\n", len(body.BlobKZGCommitments)), nil
}
res := strings.Builder{}
for i, blob := range blobs {
if i == 0 {
res.WriteString("Blobs\n")
}
res.WriteString(fmt.Sprintf(" Index: %d\n", blob.Index))
res.WriteString(fmt.Sprintf(" KZG commitment: %s\n", body.BlobKZGCommitments[i].String()))
}
return res.String(), nil
}
func outputElectraBlobInfo(_ context.Context,
verbose bool,
body *electra.BeaconBlockBody,
blobs []*deneb.BlobSidecar,
) (
string,
error,
) {
if body == nil {
return "", nil
}
if !verbose {
return fmt.Sprintf("Blobs: %d\n", len(body.BlobKZGCommitments)), nil
}
res := strings.Builder{}
for i, blob := range blobs {
if i == 0 {
res.WriteString("Blobs\n")
}
res.WriteString(fmt.Sprintf(" Index: %d\n", blob.Index))
res.WriteString(fmt.Sprintf(" KZG commitment: %s\n", body.BlobKZGCommitments[i].String()))
}
return res.String(), nil
}
func outputBellatrixBlockExecutionPayload(_ context.Context,
verbose bool,
payload *bellatrix.ExecutionPayload,
@@ -744,7 +1289,7 @@ func outputBellatrixBlockExecutionPayload(_ context.Context,
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
res.WriteString(payload.FeeRecipient.String())
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")

View File

@@ -17,16 +17,24 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/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 (
@@ -39,8 +47,8 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.blockID == "" {
return nil, errors.New("no block ID")
if data.blockID == "" && data.blockTime == "" {
return nil, errors.New("no block ID or block time")
}
results = &dataOut{
@@ -49,40 +57,83 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
eth2Client: data.eth2Client,
}
config, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx)
specResponse, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
}
genesis, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
genesisResponse, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
}
genesis := genesisResponse.Data
results.genesisTime = genesis.GenesisTime
results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration)
results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
results.slotDuration = specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
results.slotsPerEpoch = specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, data.blockID)
if data.blockTime != "" {
data.blockID, err = timeToBlockID(ctx, data.eth2Client, data.blockTime)
if err != nil {
return nil, err
}
}
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")
}
if signedBlock == nil {
return nil, errors.New("empty beacon block")
block := blockResponse.Data
if data.quiet {
os.Exit(0)
}
switch signedBlock.Version {
switch block.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(ctx, data.jsonOutput, signedBlock.Phase0); err != nil {
if err := outputPhase0Block(ctx, data.jsonOutput, block.Phase0); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionAltair:
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Altair); err != nil {
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, block.Altair); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Bellatrix); err != nil {
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, block.Bellatrix); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionCapella:
if err := outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Capella); err != nil {
if err := outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, block.Capella); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionDeneb:
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
Block: data.blockID,
})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain blob sidecars")
}
blobSidecars := blobSidecarsResponse.Data
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, block.Deneb, blobSidecars); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionElectra:
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
Block: data.blockID,
})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain blob sidecars")
}
blobSidecars := blobSidecarsResponse.Data
if err := outputElectraBlock(ctx, data.jsonOutput, data.sszOutput, block.Electra, blobSidecars); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
default:
@@ -105,61 +156,65 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return &dataOut{}, nil
}
func headEventHandler(event *api.Event) {
func headEventHandler(event *apiv1.Event) {
ctx := context.Background()
// Only interested in head events.
if event.Topic != "head" {
return
}
blockID := fmt.Sprintf("%#x", event.Data.(*api.HeadEvent).Block[:])
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID)
blockID := fmt.Sprintf("%#x", event.Data.(*apiv1.HeadEvent).Block[:])
blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: blockID,
})
if err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to obtain block: %v\n", err)
}
return
}
if signedBlock == nil {
block := blockResponse.Data
if block == nil {
if !jsonOutput && !sszOutput {
fmt.Println("Empty beacon block")
}
return
}
switch signedBlock.Version {
switch block.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputPhase0Block(ctx, jsonOutput, block.Phase0)
case spec.DataVersionAltair:
if err := outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputAltairBlock(ctx, jsonOutput, sszOutput, block.Altair)
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputBellatrixBlock(ctx, jsonOutput, sszOutput, block.Bellatrix)
case spec.DataVersionCapella:
if err := outputCapellaBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Capella); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
err = outputCapellaBlock(ctx, jsonOutput, sszOutput, block.Capella)
case spec.DataVersionDeneb:
var blobSidecarsResponse *api.Response[[]*deneb.BlobSidecar]
blobSidecarsResponse, err = results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
Block: blockID,
})
if err == nil {
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, block.Deneb, blobSidecarsResponse.Data)
}
case spec.DataVersionElectra:
var blobSidecarsResponse *api.Response[[]*deneb.BlobSidecar]
blobSidecarsResponse, err = results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
Block: blockID,
})
if err == nil {
err = outputElectraBlock(context.Background(), jsonOutput, sszOutput, block.Electra, blobSidecarsResponse.Data)
}
default:
if !jsonOutput && !sszOutput {
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
}
err = errors.New("unknown block version")
}
if err != nil && !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
return
}
if !jsonOutput && !sszOutput {
fmt.Println("")
}
@@ -254,3 +309,99 @@ 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 {
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 := outputDenebBlockText(ctx, results, signedBlock, blobs)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Print(data)
}
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 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, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -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.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := blockanalyze.Run(cmd)
if err != nil {
return err
@@ -49,17 +49,13 @@ func init() {
blockFlags(blockAnalyzeCmd)
blockAnalyzeCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockAnalyzeCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockAnalyzeCmd.Flags().Bool("json", false, "output data in JSON format")
}
func blockAnalyzeBindings() {
if err := viper.BindPFlag("blockid", blockAnalyzeCmd.Flags().Lookup("blockid")); err != nil {
func blockAnalyzeBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stream", blockAnalyzeCmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", blockAnalyzeCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("stream", cmd.Flags().Lookup("stream")); err != nil {
panic(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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := blockinfo.Run(cmd)
if err != nil {
return err
@@ -48,22 +48,22 @@ 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("json", false, "output data in JSON format")
blockInfoCmd.Flags().Bool("ssz", false, "output data in SSZ format")
}
func blockInfoBindings() {
if err := viper.BindPFlag("blockid", blockInfoCmd.Flags().Lookup("blockid")); err != nil {
func blockInfoBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stream", blockInfoCmd.Flags().Lookup("stream")); err != nil {
if err := viper.BindPFlag("block-time", cmd.Flags().Lookup("block-time")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", blockInfoCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("stream", cmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
if err := viper.BindPFlag("ssz", blockInfoCmd.Flags().Lookup("ssz")); err != nil {
if err := viper.BindPFlag("ssz", cmd.Flags().Lookup("ssz")); err != nil {
panic(err)
}
}

View File

@@ -34,6 +34,7 @@ type command struct {
timeout time.Duration
connection string
allowInsecureConnections bool
customSpecSupport bool
// Input.
xepoch string
@@ -50,6 +51,8 @@ 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
@@ -62,10 +65,11 @@ type vote struct {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
json: viper.GetBool("json"),
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
json: viper.GetBool("json"),
customSpecSupport: viper.GetBool("custom-spec"),
}
// Timeout.

View File

@@ -24,11 +24,13 @@ import (
)
type jsonOutput struct {
Period uint64 `json:"period"`
Epoch phase0.Epoch `json:"epoch"`
Slot phase0.Slot `json:"slot"`
Incumbent *phase0.ETH1Data `json:"incumbent"`
Votes []*vote `json:"votes"`
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"`
}
func (c *command) output(ctx context.Context) (string, error) {
@@ -57,11 +59,13 @@ func (c *command) outputJSON(_ context.Context) (string, error) {
})
output := &jsonOutput{
Period: c.period,
Epoch: c.epoch,
Slot: c.slot,
Incumbent: c.incumbent,
Votes: votes,
Period: c.period,
PeriodStart: c.periodStart.Unix(),
PeriodEnd: c.periodEnd.Unix(),
Epoch: c.epoch,
Slot: c.slot,
Incumbent: c.incumbent,
Votes: votes,
}
data, err := json.Marshal(output)
if err != nil {
@@ -78,6 +82,11 @@ 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

@@ -1,4 +1,4 @@
// Copyright © 2022 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
@@ -20,6 +20,7 @@ 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"
@@ -58,10 +59,13 @@ func (c *command) process(ctx context.Context) error {
if fetchSlot > c.chainTime.CurrentSlot() {
fetchSlot = c.chainTime.CurrentSlot()
}
state, err := c.beaconStateProvider.BeaconState(ctx, fmt.Sprintf("%d", fetchSlot))
stateResponse, err := c.beaconStateProvider.BeaconState(ctx, &api.BeaconStateOpts{
State: 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")
}
@@ -73,28 +77,36 @@ 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.incumbent = state.Deneb.ETH1Data
c.eth1DataVotes = state.Deneb.ETH1DataVotes
case spec.DataVersionElectra:
c.incumbent = state.Electra.ETH1Data
c.eth1DataVotes = state.Electra.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 {
@@ -115,10 +127,11 @@ func (c *command) setup(ctx context.Context) error {
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
CustomSpecSupport: c.customSpecSupport,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
@@ -126,7 +139,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
@@ -142,12 +155,12 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("connection does not provide spec information")
}
spec, err := specProvider.Spec(ctx)
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["SLOTS_PER_EPOCH"]
tmp, exists := specResponse.Data["SLOTS_PER_EPOCH"]
if !exists {
return errors.New("spec did not contain SLOTS_PER_EPOCH")
}
@@ -156,7 +169,7 @@ func (c *command) setup(ctx context.Context) error {
if !good {
return errors.New("SLOTS_PER_EPOCH value invalid")
}
tmp, exists = spec["EPOCHS_PER_ETH1_VOTING_PERIOD"]
tmp, exists = specResponse.Data["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 Weald Technology Trading.
// Copyright © 2022, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package chaineth1votes
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,14 +27,19 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
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 {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

View File

@@ -33,6 +33,7 @@ type command struct {
timeout time.Duration
connection string
allowInsecureConnections bool
customSpecSupport bool
// Input.
epoch string
@@ -49,10 +50,11 @@ type command struct {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
json: viper.GetBool("json"),
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
json: viper.GetBool("json"),
customSpecSupport: viper.GetBool("custom-spec"),
}
// Timeout.

View File

@@ -18,6 +18,7 @@ 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"
@@ -34,12 +35,14 @@ func (c *command) process(ctx context.Context) error {
return err
}
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)), nil)
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)),
})
if err != nil {
return errors.Wrap(err, "failed to obtain validators")
}
for _, validator := range validators {
for _, validator := range response.Data {
if validator.Validator == nil {
continue
}
@@ -59,10 +62,11 @@ func (c *command) setup(ctx context.Context) error {
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
CustomSpecSupport: c.customSpecSupport,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
@@ -70,7 +74,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")

View File

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

View File

@@ -31,6 +31,7 @@ type dataIn struct {
// Input
connection string
allowInsecureConnections bool
customSpecSupport bool
timestamp string
slot string
epoch string
@@ -73,6 +74,7 @@ func input(_ context.Context) (*dataIn, error) {
data.connection = viper.GetString("connection")
data.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
data.customSpecSupport = viper.GetBool("custom-spec")
return data, nil
}

View File

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

View File

@@ -31,10 +31,11 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: data.connection,
Timeout: data.timeout,
AllowInsecure: data.allowInsecureConnections,
LogFallback: !data.quiet,
Address: data.connection,
Timeout: data.timeout,
AllowInsecure: data.allowInsecureConnections,
CustomSpecSupport: data.customSpecSupport,
LogFallback: !data.quiet,
})
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
@@ -42,7 +43,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to set up chaintime service")

View File

@@ -1,4 +1,4 @@
// Copyright © 2021 Weald Technology Trading
// Copyright © 2021, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -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.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

View File

@@ -33,6 +33,7 @@ type command struct {
timeout time.Duration
connection string
allowInsecureConnections bool
customSpecSupport bool
// Input.
data string
@@ -60,9 +61,10 @@ type command struct {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
customSpecSupport: viper.GetBool("custom-spec"),
}
// Timeout.

View File

@@ -22,6 +22,7 @@ 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"
@@ -86,10 +87,11 @@ func (c *command) setup(ctx context.Context) error {
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
CustomSpecSupport: c.customSpecSupport,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
@@ -103,29 +105,32 @@ func (c *command) setup(ctx context.Context) error {
}
stateID := fmt.Sprintf("%d", c.item.Message.Contribution.Slot)
validators, err := c.validatorsProvider.Validators(ctx,
stateID,
[]phase0.ValidatorIndex{c.item.Message.AggregatorIndex},
)
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: stateID,
Indices: []phase0.ValidatorIndex{c.item.Message.AggregatorIndex},
})
if err != nil {
return errors.Wrap(err, "failed to obtain validator information")
}
if len(validators) == 0 || validators[c.item.Message.AggregatorIndex] == nil {
if len(response.Data) == 0 || response.Data[c.item.Message.AggregatorIndex] == nil {
return nil
}
c.validatorKnown = true
c.validator = validators[c.item.Message.AggregatorIndex]
c.validator = response.Data[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")
}
c.syncCommittee, err = syncCommitteesProvider.SyncCommittee(ctx, stateID)
syncCommitteeResponse, err := syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{
State: stateID,
})
if err != nil {
return errors.Wrap(err, "failed to obtain sync committee information")
}
c.syncCommittee = syncCommitteeResponse.Data
return nil
}
@@ -137,11 +142,11 @@ func (c *command) isAggregator(ctx context.Context) (bool, error) {
if !isProvider {
return false, errors.New("connection does not provide spec information")
}
var err error
c.spec, err = specProvider.Spec(ctx)
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
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 {
@@ -226,16 +231,19 @@ func (c *command) confirmContributionSignature(ctx context.Context) error {
fmt.Fprintf(os.Stderr, "Contribution validator indices: %v (%d)\n", includedIndices, len(includedIndices))
}
includedValidators, err := c.validatorsProvider.Validators(ctx, "head", includedIndices)
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: "head",
Indices: includedIndices,
})
if err != nil {
return errors.Wrap(err, "failed to obtain subcommittee validators")
}
if len(includedValidators) == 0 {
if len(response.Data) == 0 {
return errors.New("obtained empty subcommittee validator list")
}
var aggregatePubKey *e2types.BLSPublicKey
for _, v := range includedValidators {
for _, v := range response.Data {
pubKeyBytes := make([]byte, 48)
copy(pubKeyBytes, v.Validator.PublicKey[:])
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)

View File

@@ -1,4 +1,4 @@
// Copyright © 2021 Weald Technology Trading.
// Copyright © 2021, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package chainverifysignedcontributionandproof
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,14 +27,19 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
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 {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := chaineth1votes.Run(cmd)
if err != nil {
return err
@@ -51,17 +51,13 @@ func init() {
chainFlags(chainEth1VotesCmd)
chainEth1VotesCmd.Flags().String("epoch", "", "epoch for which to fetch the votes")
chainEth1VotesCmd.Flags().String("period", "", "period for which to fetch the votes")
chainEth1VotesCmd.Flags().Bool("json", false, "output data in JSON format")
}
func chainEth1VotesBindings() {
if err := viper.BindPFlag("epoch", chainEth1VotesCmd.Flags().Lookup("epoch")); err != nil {
func chainEth1VotesBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("period", chainEth1VotesCmd.Flags().Lookup("period")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", chainEth1VotesCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("period", cmd.Flags().Lookup("period")); err != nil {
panic(err)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020, 2022 Weald Technology Trading
// Copyright © 2020 - 2024 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -20,6 +20,8 @@ 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"
@@ -34,48 +36,45 @@ 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(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !viper.GetBool("quiet"),
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
CustomSpecSupport: viper.GetBool("custom-spec"),
LogFallback: !viper.GetBool("quiet"),
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
specResponse, err := eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
errCheck(err, "Failed to obtain beacon chain specification")
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
genesisResponse, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
errCheck(err, "Failed to obtain beacon chain genesis")
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
forkResponse, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, &api.ForkOpts{State: "head"})
errCheck(err, "Failed to obtain current fork")
if quiet {
if viper.GetBool("quiet") {
os.Exit(_exitSuccess)
}
if viper.GetBool("prepare-offline") {
fmt.Printf("Add the following to your command to run it offline:\n --offline --genesis-validators=root=%#x --fork-version=%#x\n", genesis.GenesisValidatorsRoot, fork.CurrentVersion)
os.Exit(_exitSuccess)
}
if genesis.GenesisTime.Unix() == 0 {
if genesisResponse.Data.GenesisTime.Unix() == 0 {
fmt.Println("Genesis time: undefined")
} else {
fmt.Printf("Genesis time: %s\n", genesis.GenesisTime.Format(time.UnixDate))
outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesis.GenesisTime.Unix()))
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 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 verbose {
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)
if viper.GetBool("verbose") {
forkData := &spec.ForkData{
CurrentVersion: fork.CurrentVersion,
GenesisValidatorsRoot: genesis.GenesisValidatorsRoot,
CurrentVersion: forkResponse.Data.CurrentVersion,
GenesisValidatorsRoot: genesisResponse.Data.GenesisValidatorsRoot,
}
forkDataRoot, err := forkData.HashTreeRoot()
if err == nil {
@@ -84,8 +83,12 @@ 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(config["SECONDS_PER_SLOT"].(time.Duration).Seconds()))
fmt.Printf("Slots per epoch: %d\n", config["SLOTS_PER_EPOCH"].(uint64))
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())
os.Exit(_exitSuccess)
},
@@ -94,11 +97,7 @@ In quiet mode this will return 0 if the chain information can be obtained, other
func init() {
chainCmd.AddCommand(chainInfoCmd)
chainFlags(chainInfoCmd)
chainInfoCmd.Flags().Bool("prepare-offline", false, "Provide information useful for offline commands")
}
func chainInfoBindings() {
if err := viper.BindPFlag("prepare-offline", chainInfoCmd.Flags().Lookup("prepare-offline")); err != nil {
panic(err)
}
func chainInfoBindings(_ *cobra.Command) {
}

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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := chainqueues.Run(cmd)
if err != nil {
return err
@@ -48,14 +48,10 @@ func init() {
chainCmd.AddCommand(chainQueuesCmd)
chainFlags(chainQueuesCmd)
chainQueuesCmd.Flags().String("epoch", "", "epoch for which to fetch the queues")
chainQueuesCmd.Flags().Bool("json", false, "output data in JSON format")
}
func chainQueuesBindings() {
if err := viper.BindPFlag("epoch", chainQueuesCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", chainQueuesCmd.Flags().Lookup("json")); err != nil {
func chainQueuesBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
}

100
cmd/chainspec.go Normal file
View File

@@ -0,0 +1,100 @@
// Copyright © 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
//
// 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 (
"context"
"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"
"github.com/wealdtech/ethdo/util"
)
var chainSpecCmd = &cobra.Command{
Use: "spec",
Short: "Obtain specification for a chain",
Long: `Obtain specification for a chain. For example:
ethdo chain spec
In quiet mode this will return 0 if the chain specification can be obtained, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
CustomSpecSupport: viper.GetBool("custom-spec"),
LogFallback: !viper.GetBool("quiet"),
})
errCheck(err, "Failed to connect to Ethereum consensus node")
specResponse, err := eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
errCheck(err, "Failed to obtain chain specification")
if viper.GetBool("quiet") {
return
}
// Tweak the spec for output.
for k, v := range specResponse.Data {
switch t := v.(type) {
case phase0.Version:
specResponse.Data[k] = fmt.Sprintf("%#x", t)
case phase0.DomainType:
specResponse.Data[k] = fmt.Sprintf("%#x", t)
case time.Time:
specResponse.Data[k] = strconv.FormatInt(t.Unix(), 10)
case time.Duration:
specResponse.Data[k] = strconv.FormatUint(uint64(t.Seconds()), 10)
case []byte:
specResponse.Data[k] = fmt.Sprintf("%#x", t)
case uint64:
specResponse.Data[k] = strconv.FormatUint(t, 10)
}
}
if viper.GetBool("json") {
data, err := json.Marshal(specResponse.Data)
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 = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("%s: %v\n", key, specResponse.Data[key])
}
}
},
}
func init() {
chainCmd.AddCommand(chainSpecCmd)
chainFlags(chainSpecCmd)
}
func chainSpecBindings(_ *cobra.Command) {
}

View File

@@ -17,10 +17,12 @@ 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"
@@ -38,27 +40,31 @@ 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(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !viper.GetBool("quiet"),
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
CustomSpecSupport: viper.GetBool("custom-spec"),
LogFallback: !viper.GetBool("quiet"),
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
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")
finality, err := finalityProvider.Finality(ctx, "head")
finalityResponse, err := finalityProvider.Finality(ctx, &api.FinalityOpts{
State: "head",
})
errCheck(err, "Failed to obtain finality information")
finality := finalityResponse.Data
slot := chainTime.CurrentSlot()
@@ -83,7 +89,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
res.WriteString(fmt.Sprintf("%d", epoch))
res.WriteString("\n")
if verbose {
if viper.GetBool("verbose") {
res.WriteString("Epoch slots: ")
res.WriteString(fmt.Sprintf("%d", epochStartSlot))
res.WriteString("-")
@@ -106,7 +112,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
res.WriteString("Justified epoch: ")
res.WriteString(fmt.Sprintf("%d", finality.Justified.Epoch))
res.WriteString("\n")
if verbose {
if viper.GetBool("verbose") {
distance := epoch - finality.Justified.Epoch
res.WriteString("Justified epoch distance: ")
res.WriteString(fmt.Sprintf("%d", distance))
@@ -116,23 +122,23 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
res.WriteString("Finalized epoch: ")
res.WriteString(fmt.Sprintf("%d", finality.Finalized.Epoch))
res.WriteString("\n")
if verbose {
if viper.GetBool("verbose") {
distance := epoch - finality.Finalized.Epoch
res.WriteString("Finalized epoch distance: ")
res.WriteString(fmt.Sprintf("%d", distance))
res.WriteString("\n")
}
if verbose {
if viper.GetBool("verbose") {
validatorsProvider, isProvider := eth2Client.(eth2client.ValidatorsProvider)
if isProvider {
validators, err := validatorsProvider.Validators(ctx, "head", nil)
validatorsResponse, err := validatorsProvider.Validators(ctx, &api.ValidatorsOpts{State: "head"})
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 validators {
for _, validator := range validatorsResponse.Data {
validatorCount[validator.Status]++
totalBalance += validator.Balance
if validator.Status.IsActive() {
@@ -162,10 +168,10 @@ 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(fmt.Sprintf("%d", period))
res.WriteString(strconv.FormatUint(period, 10))
res.WriteString("\n")
if verbose {
if viper.GetBool("verbose") {
res.WriteString("Sync committee epochs: ")
res.WriteString(fmt.Sprintf("%d", periodStartEpoch))
res.WriteString("-")

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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := chaintime.Run(cmd)
if err != nil {
return err
@@ -47,14 +47,14 @@ func init() {
chainTimeCmd.Flags().String("timestamp", "", "The timestamp for which to obtain information (format YYYY-MM-DDTHH:MM:SS+ZZZZ)")
}
func chainTimeBindings() {
if err := viper.BindPFlag("slot", chainTimeCmd.Flags().Lookup("slot")); err != nil {
func chainTimeBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("slot", cmd.Flags().Lookup("slot")); err != nil {
panic(err)
}
if err := viper.BindPFlag("epoch", chainTimeCmd.Flags().Lookup("epoch")); err != nil {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("timestamp", chainTimeCmd.Flags().Lookup("timestamp")); err != nil {
if err := viper.BindPFlag("timestamp", cmd.Flags().Lookup("timestamp")); err != nil {
panic(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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := chainverifysignedcontributionandproof.Run(cmd)
if err != nil {
return err

View File

@@ -16,6 +16,7 @@ package cmd
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
@@ -23,6 +24,7 @@ import (
"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"
e2types "github.com/wealdtech/go-eth2-types/v2"
eth2util "github.com/wealdtech/go-eth2-util"
@@ -48,7 +50,7 @@ var depositVerifyCmd = &cobra.Command{
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(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
assert(depositVerifyData != "", "--data is required")
var data []byte
var err error
@@ -74,6 +76,12 @@ 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 != "" {
@@ -92,7 +100,7 @@ In quiet mode this will return 0 if the data is verified correctly, otherwise 1.
withdrawalCredentials[0] = 0x01 // ETH1_ADDRESS_WITHDRAWAL_PREFIX
copy(withdrawalCredentials[12:], withdrawalAddressBytes)
}
outputIf(debug, fmt.Sprintf("Withdrawal credentials are %#x", withdrawalCredentials))
outputIf(viper.GetBool("debug"), fmt.Sprintf("Withdrawal credentials are %#x", withdrawalCredentials))
depositAmount := uint64(0)
if depositVerifyDepositAmount != "" {
@@ -120,9 +128,9 @@ In quiet mode this will return 0 if the data is verified correctly, otherwise 1.
}
if !verified {
failures = true
outputIf(!quiet, fmt.Sprintf("%s failed verification", depositName))
outputIf(!viper.GetBool("quiet"), fmt.Sprintf("%s failed verification", depositName))
} else {
outputIf(!quiet, fmt.Sprintf("%s verified", depositName))
outputIf(!viper.GetBool("quiet"), fmt.Sprintf("%s verified", depositName))
}
}
@@ -190,34 +198,34 @@ func validatorPubKeysFromInput(input string) (map[[48]byte]bool, error) {
func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, validatorPubKeys map[[48]byte]bool, amount uint64) (bool, error) {
if withdrawalCredentials == nil {
outputIf(!quiet, "Withdrawal public key or address not supplied; withdrawal credentials NOT checked")
outputIf(!viper.GetBool("quiet"), "Withdrawal public key or address not supplied; withdrawal credentials NOT checked")
} else {
if !bytes.Equal(deposit.WithdrawalCredentials, withdrawalCredentials) {
outputIf(!quiet, "Withdrawal credentials incorrect")
outputIf(!viper.GetBool("quiet"), "Withdrawal credentials incorrect")
return false, nil
}
outputIf(!quiet, "Withdrawal credentials verified")
outputIf(!viper.GetBool("quiet"), "Withdrawal credentials verified")
}
if amount == 0 {
outputIf(!quiet, "Amount not supplied; NOT checked")
outputIf(!viper.GetBool("quiet"), "Amount not supplied; NOT checked")
} else {
if deposit.Amount != amount {
outputIf(!quiet, "Amount incorrect")
outputIf(!viper.GetBool("quiet"), "Amount incorrect")
return false, nil
}
outputIf(!quiet, "Amount verified")
outputIf(!viper.GetBool("quiet"), "Amount verified")
}
if len(validatorPubKeys) == 0 {
outputIf(!quiet, "Validator public key not suppled; NOT checked")
outputIf(!viper.GetBool("quiet"), "Validator public key not suppled; NOT checked")
} else {
var key [48]byte
copy(key[:], deposit.PublicKey)
if _, exists := validatorPubKeys[key]; !exists {
outputIf(!quiet, "Validator public key incorrect")
outputIf(!viper.GetBool("quiet"), "Validator public key incorrect")
return false, nil
}
outputIf(!quiet, "Validator public key verified")
outputIf(!viper.GetBool("quiet"), "Validator public key verified")
}
var pubKey phase0.BLSPubKey
@@ -237,34 +245,37 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
}
if bytes.Equal(deposit.DepositDataRoot, depositDataRoot[:]) {
outputIf(!quiet, "Deposit data root verified")
outputIf(!viper.GetBool("quiet"), "Deposit data root verified")
} else {
outputIf(!quiet, "Deposit data root incorrect")
outputIf(!viper.GetBool("quiet"), "Deposit data root incorrect")
return false, nil
}
if len(deposit.ForkVersion) == 0 {
if depositVerifyForkVersion != "" {
outputIf(!quiet, "Data format does not contain fork version for verification; NOT verified")
outputIf(!viper.GetBool("quiet"), "Data format does not contain fork version for verification; NOT verified")
}
} else {
if depositVerifyForkVersion == "" {
outputIf(!quiet, "fork version not supplied; not checked")
outputIf(!viper.GetBool("quiet"), "fork version not supplied; not checked")
} else {
forkVersion, err := hex.DecodeString(strings.TrimPrefix(depositVerifyForkVersion, "0x"))
if err != nil {
return false, errors.Wrap(err, "failed to decode fork version")
}
if bytes.Equal(deposit.ForkVersion, forkVersion) {
outputIf(!quiet, "Fork version verified")
outputIf(!viper.GetBool("quiet"), "Fork version verified")
} else {
outputIf(!quiet, "Fork version incorrect")
outputIf(!viper.GetBool("quiet"), "Fork version incorrect")
return false, nil
}
if len(deposit.DepositMessageRoot) != 32 {
outputIf(!quiet, "Deposit message root not supplied; not checked")
} else {
switch {
case 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:
// We can also verify the deposit message signature.
depositMessage := &phase0.DepositMessage{
PublicKey: pubKey,
@@ -277,9 +288,9 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
}
if bytes.Equal(deposit.DepositMessageRoot, depositMessageRoot[:]) {
outputIf(!quiet, "Deposit message root verified")
outputIf(!viper.GetBool("quiet"), "Deposit message root verified")
} else {
outputIf(!quiet, "Deposit message root incorrect")
outputIf(!viper.GetBool("quiet"), "Deposit message root incorrect")
return false, nil
}
@@ -305,9 +316,9 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
}
signatureVerified := blsSig.Verify(containerRoot[:], validatorPubKey)
if signatureVerified {
outputIf(!quiet, "Deposit message signature verified")
outputIf(!viper.GetBool("quiet"), "Deposit message signature verified")
} else {
outputIf(!quiet, "Deposit message signature NOT verified")
outputIf(!viper.GetBool("quiet"), "Deposit message signature NOT verified")
return false, nil
}
}

View File

@@ -33,8 +33,8 @@ 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() {
if err := viper.BindPFlag("epoch", epochSummaryCmd.Flags().Lookup("epoch")); err != nil {
func epochBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
}

View File

@@ -18,6 +18,7 @@ import (
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
@@ -33,6 +34,7 @@ type command struct {
timeout time.Duration
connection string
allowInsecureConnections bool
customSpecSupport bool
// Operation.
epoch string
@@ -49,6 +51,9 @@ type command struct {
beaconCommitteesProvider eth2client.BeaconCommitteesProvider
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Caches.
blocksCache map[string]*spec.VersionedSignedBeaconBlock
// Results.
summary *epochSummary
}
@@ -67,6 +72,7 @@ type epochSummary struct {
TargetCorrectValidators int `json:"target_correct_validators"`
TargetTimelyValidators int `json:"target_timely_validators"`
NonParticipatingValidators []*nonParticipatingValidator `json:"nonparticipating_validators"`
Blobs int `json:"blobs"`
}
type epochProposal struct {
@@ -88,10 +94,11 @@ type nonParticipatingValidator struct {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
summary: &epochSummary{},
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
summary: &epochSummary{},
blocksCache: make(map[string]*spec.VersionedSignedBeaconBlock),
}
// Timeout.
@@ -102,6 +109,7 @@ func newCommand(_ context.Context) (*command, error) {
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
c.customSpecSupport = viper.GetBool("custom-spec")
c.epoch = viper.GetString("epoch")
c.stream = viper.GetBool("stream")

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 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,12 +16,13 @@ package epochsummary
import (
"context"
"fmt"
"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"
@@ -48,19 +49,22 @@ func (c *command) process(ctx context.Context) error {
if err := c.processAttesterDuties(ctx); err != nil {
return err
}
return c.processSyncCommitteeDuties(ctx)
if err := c.processSyncCommitteeDuties(ctx); err != nil {
return err
}
return c.processBlobs(ctx)
}
func (c *command) processProposerDuties(ctx context.Context) error {
duties, err := c.proposerDutiesProvider.ProposerDuties(ctx, c.summary.Epoch, nil)
response, err := c.proposerDutiesProvider.ProposerDuties(ctx, &api.ProposerDutiesOpts{
Epoch: c.summary.Epoch,
})
if err != nil {
return errors.Wrap(err, "failed to obtain proposer duties")
}
if duties == nil {
return errors.New("empty proposer duties")
}
for _, duty := range duties {
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", duty.Slot))
for _, duty := range response.Data {
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", duty.Slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", duty.Slot))
}
@@ -76,12 +80,14 @@ func (c *command) processProposerDuties(ctx context.Context) error {
}
func (c *command) activeValidators(ctx context.Context) (map[phase0.ValidatorIndex]*apiv1.Validator, error) {
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), nil)
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)),
})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validators for epoch")
}
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
for _, validator := range validators {
for _, validator := range response.Data {
if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch {
activeValidators[validator.Index] = validator
}
@@ -161,7 +167,7 @@ func (c *command) processSlots(ctx context.Context,
headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider)
for slot := firstSlot; slot <= lastSlot; slot++ {
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
}
@@ -178,17 +184,23 @@ func (c *command) processSlots(ctx context.Context,
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
for _, attestation := range attestations {
if attestation.Data.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestation.Data.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
attestationData, err := attestation.Data()
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain attestation data for slot %d", slot))
}
if attestationData.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestationData.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
// Outside of this epoch's range.
continue
}
slotCommittees, exists := allCommittees[attestation.Data.Slot]
slotCommittees, exists := allCommittees[attestationData.Slot]
if !exists {
beaconCommittees, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", attestation.Data.Slot))
response, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, &api.BeaconCommitteesOpts{
State: fmt.Sprintf("%d", attestationData.Slot),
})
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestationData.Slot))
}
for _, beaconCommittee := range beaconCommittees {
for _, beaconCommittee := range response.Data {
if _, exists := allCommittees[beaconCommittee.Slot]; !exists {
allCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
}
@@ -201,22 +213,26 @@ func (c *command) processSlots(ctx context.Context,
}
}
}
slotCommittees = allCommittees[attestation.Data.Slot]
slotCommittees = allCommittees[attestationData.Slot]
}
committee := slotCommittees[attestation.Data.Index]
committee := slotCommittees[attestationData.Index]
inclusionDistance := slot - attestation.Data.Slot
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
inclusionDistance := slot - attestationData.Slot
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestationData)
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestationData)
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
for i := uint64(0); i < attestation.AggregationBits.Len(); i++ {
if attestation.AggregationBits.BitAt(i) {
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
for i := uint64(0); i < aggregationBits.Len(); i++ {
if aggregationBits.BitAt(i) {
votes[committee[int(i)]] = struct{}{}
if _, exists := headCorrects[committee[int(i)]]; !exists && headCorrect {
headCorrects[committee[int(i)]] = struct{}{}
@@ -237,6 +253,7 @@ func (c *command) processSlots(ctx context.Context,
}
}
}
return len(votes),
len(headCorrects),
len(headTimelys),
@@ -254,10 +271,13 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
return nil
}
committee, err := c.syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", c.summary.FirstSlot))
committeeResponse, err := c.syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{
State: 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")
}
@@ -268,7 +288,7 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
}
for slot := c.summary.FirstSlot; slot <= c.summary.LastSlot; slot++ {
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
}
@@ -276,19 +296,14 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
// If the block is missed we don't count the sync aggregate miss.
continue
}
var aggregate *altair.SyncAggregate
switch block.Version {
case spec.DataVersionPhase0:
if block.Version == 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
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 := uint64(0); i < aggregate.SyncCommitteeBits.Len(); i++ {
if !aggregate.SyncCommitteeBits.BitAt(i) {
@@ -325,10 +340,11 @@ func (c *command) setup(ctx context.Context) error {
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
CustomSpecSupport: c.customSpecSupport,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
@@ -336,7 +352,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
@@ -370,3 +386,54 @@ func (c *command) setup(ctx context.Context) error {
return nil
}
func (c *command) processBlobs(ctx context.Context) error {
for slot := c.summary.FirstSlot; slot <= c.summary.LastSlot; slot++ {
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", 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)
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 Weald Technology Trading.
// Copyright © 2022, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package epochsummary
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,14 +27,19 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
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 {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := epochsummary.Run(cmd)
if err != nil {
return err
@@ -47,12 +47,8 @@ In quiet mode this will return 0 if information for the epoch is found, otherwis
func init() {
epochCmd.AddCommand(epochSummaryCmd)
epochFlags(epochSummaryCmd)
epochSummaryCmd.Flags().Bool("json", false, "output data in JSON format")
}
func epochSummaryBindings() {
epochBindings()
if err := viper.BindPFlag("json", epochSummaryCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
func epochSummaryBindings(cmd *cobra.Command) {
epochBindings(cmd)
}

View File

@@ -16,12 +16,14 @@ package cmd
import (
"fmt"
"os"
"github.com/spf13/viper"
)
// errCheck checks for an error and quits if it is present.
func errCheck(err error, msg string) {
if err != nil {
if !quiet {
if !viper.GetBool("quiet") {
if msg == "" {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
} else {
@@ -57,7 +59,7 @@ func assert(condition bool, msg string) {
// die prints an error and quits.
func die(msg string) {
if msg != "" && !quiet {
if msg != "" && !viper.GetBool("quiet") {
fmt.Fprintf(os.Stderr, "%s\n", msg)
}
os.Exit(_exitFailure)

View File

@@ -21,6 +21,7 @@ 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"
@@ -38,7 +39,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(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
ctx := context.Background()
assert(viper.GetString("signed-operation") != "", "signed-operation is required")
@@ -46,10 +47,11 @@ In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.
errCheck(err, "Failed to obtain signed operation")
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !viper.GetBool("quiet"),
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
CustomSpecSupport: viper.GetBool("custom-spec"),
LogFallback: !viper.GetBool("quiet"),
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
@@ -67,10 +69,11 @@ 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")
genesis, err := eth2Client.(consensusclient.GenesisProvider).Genesis(ctx)
genesisResponse, err := eth2Client.(consensusclient.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
errCheck(err, "Failed to obtain beacon chain genesis")
genesis := genesisResponse.Data
fork, err := eth2Client.(consensusclient.ForkProvider).Fork(ctx, "head")
response, err := eth2Client.(consensusclient.ForkProvider).Fork(ctx, &api.ForkOpts{State: "head"})
errCheck(err, "Failed to obtain fork information")
// Check against current and prior fork versions.
@@ -83,14 +86,14 @@ In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.
// Try with the current fork.
domain := phase0.Domain{}
currentExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork.CurrentVersion[:], genesis.GenesisValidatorsRoot[:])
currentExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, response.Data.CurrentVersion[:], genesis.GenesisValidatorsRoot[:])
errCheck(err, "Failed to compute domain")
copy(domain[:], currentExitDomain)
verified, err = util.VerifyRoot(account, opRoot, domain, sig)
errCheck(err, "Failed to verify voluntary exit")
if !verified {
// Try again with the previous fork.
previousExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork.PreviousVersion[:], genesis.GenesisValidatorsRoot[:])
previousExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, response.Data.PreviousVersion[:], genesis.GenesisValidatorsRoot[:])
copy(domain[:], previousExitDomain)
errCheck(err, "Failed to compute domain")
verified, err = util.VerifyRoot(account, opRoot, domain, sig)
@@ -98,7 +101,7 @@ In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.
}
assert(verified, "Voluntary exit failed to verify against current and previous fork versions")
outputIf(verbose, "Verified")
outputIf(viper.GetBool("verbose"), "Verified")
os.Exit(_exitSuccess)
},
}
@@ -133,8 +136,8 @@ func init() {
exitVerifyCmd.Flags().String("signed-operation", "", "JSON data, or path to JSON data")
}
func exitVerifyBindings() {
if err := viper.BindPFlag("signed-operation", exitVerifyCmd.Flags().Lookup("signed-operation")); err != nil {
func exitVerifyBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("signed-operation", cmd.Flags().Lookup("signed-operation")); err != nil {
panic(err)
}
}

View File

@@ -51,10 +51,11 @@ func input(ctx context.Context) (*dataIn, error) {
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !data.quiet,
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
CustomSpecSupport: viper.GetBool("custom-spec"),
LogFallback: !data.quiet,
})
if err != nil {
return nil, err

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package nodeevents
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@@ -25,14 +25,19 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := process(ctx, dataIn); err != nil {
return "", errors.Wrap(err, "failed to process")
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)
}
}
// 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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := nodeevents.Run(cmd)
if err != nil {
return err
@@ -45,8 +45,8 @@ func init() {
nodeEventsCmd.Flags().StringSlice("topics", nil, "The topics of events for which to listen (attestation,block,chain_reorg,finalized_checkpoint,head,voluntary_exit)")
}
func nodeEventsBindings() {
if err := viper.BindPFlag("topics", nodeEventsCmd.Flags().Lookup("topics")); err != nil {
func nodeEventsBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("topics", cmd.Flags().Lookup("topics")); err != nil {
panic(err)
}
}

View File

@@ -19,6 +19,7 @@ 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"
@@ -32,30 +33,31 @@ 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(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !viper.GetBool("quiet"),
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
CustomSpecSupport: viper.GetBool("custom-spec"),
LogFallback: !viper.GetBool("quiet"),
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
if quiet {
if viper.GetBool("quiet") {
os.Exit(_exitSuccess)
}
if verbose {
version, err := eth2Client.(eth2client.NodeVersionProvider).NodeVersion(ctx)
if viper.GetBool("verbose") {
versionResponse, err := eth2Client.(eth2client.NodeVersionProvider).NodeVersion(ctx, &api.NodeVersionOpts{})
errCheck(err, "Failed to obtain node version")
fmt.Printf("Version: %s\n", version)
fmt.Printf("Version: %s\n", versionResponse.Data)
}
syncState, err := eth2Client.(eth2client.NodeSyncingProvider).NodeSyncing(ctx)
syncStateResponse, err := eth2Client.(eth2client.NodeSyncingProvider).NodeSyncing(ctx, &api.NodeSyncingOpts{})
errCheck(err, "failed to obtain node sync state")
fmt.Printf("Syncing: %t\n", syncState.SyncDistance != 0)
fmt.Printf("Syncing: %t\n", syncStateResponse.Data.SyncDistance != 0)
os.Exit(_exitSuccess)
},

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 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
@@ -34,9 +34,11 @@ type command struct {
timeout time.Duration
connection string
allowInsecureConnections bool
customSpecSupport bool
// Operation.
epoch string
slot string
jsonOutput bool
// Data access.
@@ -55,23 +57,23 @@ type results struct {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
results: &results{},
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"),
customSpecSupport: viper.GetBool("custom-spec"),
epoch: viper.GetString("epoch"),
slot: viper.GetString("slot"),
jsonOutput: viper.GetBool("json"),
results: &results{},
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
if c.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 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
@@ -43,19 +43,32 @@ func (c *command) outputJSON(_ context.Context) (string, error) {
func (c *command) outputTxt(_ context.Context) (string, error) {
builder := strings.Builder{}
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 ")
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(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 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
@@ -17,6 +17,8 @@ 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"
@@ -24,20 +26,59 @@ import (
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
err := c.setup(ctx)
if err != nil {
if err := c.setup(ctx); 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")
}
c.results.Duties, err = c.proposerDutiesProvider.ProposerDuties(ctx, c.results.Epoch, nil)
dutiesResponse, 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 = dutiesResponse.Data
return nil
}
@@ -47,10 +88,11 @@ func (c *command) setup(ctx context.Context) error {
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
CustomSpecSupport: c.customSpecSupport,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
@@ -58,7 +100,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package proposerduties
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,14 +27,19 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
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 {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := proposerduties.Run(cmd)
if err != nil {
return err
@@ -48,14 +48,14 @@ func init() {
proposerCmd.AddCommand(proposerDutiesCmd)
proposerFlags(proposerDutiesCmd)
proposerDutiesCmd.Flags().String("epoch", "", "the epoch for which to fetch duties")
proposerDutiesCmd.Flags().Bool("json", false, "output data in JSON format")
proposerDutiesCmd.Flags().String("slot", "", "the slot for which to fetch duties")
}
func proposerDutiesBindings() {
if err := viper.BindPFlag("epoch", proposerDutiesCmd.Flags().Lookup("epoch")); err != nil {
func proposerDutiesBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", proposerDutiesCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("slot", cmd.Flags().Lookup("slot")); err != nil {
panic(err)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2021 Weald Technology Trading.
// Copyright © 2019 - 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
@@ -33,21 +33,56 @@ import (
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
var (
cfgFile string
quiet bool
verbose bool
debug bool
)
var cfgFile string
// RootCmd represents the base command when called without any subcommands.
var RootCmd = &cobra.Command{
Use: "ethdo",
Short: "Ethereum 2 CLI",
Long: `Manage common Ethereum 2 tasks from the command line.`,
Short: "Ethereum consensus layer CLI",
Long: `Manage common Ethereum consensus layer tasks from the command line.`,
PersistentPreRunE: persistentPreRunE,
}
// bindings are the command-specific bindings.
var bindings = map[string]func(cmd *cobra.Command){
"account/create": accountCreateBindings,
"account/derive": accountDeriveBindings,
"account/import": accountImportBindings,
"attester/duties": attesterDutiesBindings,
"attester/inclusion": attesterInclusionBindings,
"block/analyze": blockAnalyzeBindings,
"block/info": blockInfoBindings,
"chain/eth1votes": chainEth1VotesBindings,
"chain/info": chainInfoBindings,
"chain/queues": chainQueuesBindings,
"chain/spec": chainSpecBindings,
"chain/time": chainTimeBindings,
"chain/verify/signedcontributionandproof": chainVerifySignedContributionAndProofBindings,
"epoch/summary": epochSummaryBindings,
"exit/verify": exitVerifyBindings,
"node/events": nodeEventsBindings,
"proposer/duties": proposerDutiesBindings,
"slot/time": slotTimeBindings,
"synccommittee/inclusion": synccommitteeInclusionBindings,
"synccommittee/members": synccommitteeMembersBindings,
"validator/credentials/get": validatorCredentialsGetBindings,
"validator/credentials/set": validatorCredentialsSetBindings,
"validator/depositdata": validatorDepositdataBindings,
"validator/duties": validatorDutiesBindings,
"validator/exit": validatorExitBindings,
"validator/info": validatorInfoBindings,
"validator/keycheck": validatorKeycheckBindings,
"validator/summary": validatorSummaryBindings,
"validator/yield": validatorYieldBindings,
"validator/expectation": validatorExpectationBindings,
"validator/withdrawal": validatorWithdrawalBindings,
"wallet/batch": walletBatchBindings,
"wallet/create": walletCreateBindings,
"wallet/import": walletImportBindings,
"wallet/sharedexport": walletSharedExportBindings,
"wallet/sharedimport": walletSharedImportBindings,
}
func persistentPreRunE(cmd *cobra.Command, _ []string) error {
if cmd.Name() == "help" {
// User just wants help
@@ -63,11 +98,14 @@ func persistentPreRunE(cmd *cobra.Command, _ []string) error {
zerolog.SetGlobalLevel(zerolog.Disabled)
// We bind viper here so that we bind to the correct command.
quiet = viper.GetBool("quiet")
verbose = viper.GetBool("verbose")
debug = viper.GetBool("debug")
quiet := viper.GetBool("quiet")
verbose := viper.GetBool("verbose")
debug := viper.GetBool("debug")
includeCommandBindings(cmd)
// Command-specific bindings.
if bindingsFunc, exists := bindings[commandPath(cmd)]; exists {
bindingsFunc(cmd)
}
if quiet && verbose {
fmt.Println("Cannot supply both quiet and verbose flags")
@@ -79,78 +117,6 @@ func persistentPreRunE(cmd *cobra.Command, _ []string) error {
return util.SetupStore()
}
// nolint:gocyclo
func includeCommandBindings(cmd *cobra.Command) {
switch commandPath(cmd) {
case "account/create":
accountCreateBindings()
case "account/derive":
accountDeriveBindings()
case "account/import":
accountImportBindings()
case "attester/duties":
attesterDutiesBindings()
case "attester/inclusion":
attesterInclusionBindings()
case "block/analyze":
blockAnalyzeBindings()
case "block/info":
blockInfoBindings()
case "chain/eth1votes":
chainEth1VotesBindings()
case "chain/info":
chainInfoBindings()
case "chain/queues":
chainQueuesBindings()
case "chain/time":
chainTimeBindings()
case "chain/verify/signedcontributionandproof":
chainVerifySignedContributionAndProofBindings(cmd)
case "epoch/summary":
epochSummaryBindings()
case "exit/verify":
exitVerifyBindings()
case "node/events":
nodeEventsBindings()
case "proposer/duties":
proposerDutiesBindings()
case "slot/time":
slotTimeBindings()
case "synccommittee/inclusion":
synccommitteeInclusionBindings()
case "synccommittee/members":
synccommitteeMembersBindings()
case "validator/credentials/get":
validatorCredentialsGetBindings()
case "validator/credentials/set":
validatorCredentialsSetBindings()
case "validator/depositdata":
validatorDepositdataBindings()
case "validator/duties":
validatorDutiesBindings()
case "validator/exit":
validatorExitBindings()
case "validator/info":
validatorInfoBindings()
case "validator/keycheck":
validatorKeycheckBindings()
case "validator/summary":
validatorSummaryBindings()
case "validator/yield":
validatorYieldBindings()
case "validator/expectation":
validatorExpectationBindings()
case "wallet/create":
walletCreateBindings()
case "wallet/import":
walletImportBindings()
case "wallet/sharedexport":
walletSharedExportBindings()
case "wallet/sharedimport":
walletSharedImportBindings()
}
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
@@ -167,8 +133,13 @@ func init() {
}
cobra.OnInitialize(initConfig)
addPersistentFlags()
}
//nolint:gocyclo
func addPersistentFlags() {
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.ethdo.yaml)")
RootCmd.PersistentFlags().String("log", "", "log activity to the named file (default $HOME/ethdo.log). Logs are written for every action that generates a transaction")
if err := viper.BindPFlag("log", RootCmd.PersistentFlags().Lookup("log")); err != nil {
panic(err)
@@ -189,7 +160,7 @@ func init() {
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")
RootCmd.PersistentFlags().String("private-key", "", "Private key to provide access to an account or validator")
if err := viper.BindPFlag("private-key", RootCmd.PersistentFlags().Lookup("private-key")); err != nil {
panic(err)
}
@@ -242,6 +213,10 @@ func init() {
if err := viper.BindPFlag("verbose", RootCmd.PersistentFlags().Lookup("verbose")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().Bool("json", false, "generate JSON output where available")
if err := viper.BindPFlag("json", RootCmd.PersistentFlags().Lookup("json")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().Bool("debug", false, "generate debug output")
if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil {
panic(err)
@@ -250,7 +225,7 @@ func init() {
if err := viper.BindPFlag("connection", RootCmd.PersistentFlags().Lookup("connection")); err != nil {
panic(err)
}
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")
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")
if err := viper.BindPFlag("timeout", RootCmd.PersistentFlags().Lookup("timeout")); err != nil {
panic(err)
}
@@ -278,6 +253,10 @@ func init() {
if err := viper.BindPFlag("allow-insecure-connections", RootCmd.PersistentFlags().Lookup("allow-insecure-connections")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().Bool("custom-spec", false, "allow custom (non-mainnet) beacon node specifications")
if err := viper.BindPFlag("custom-spec", RootCmd.PersistentFlags().Lookup("custom-spec")); err != nil {
panic(err)
}
}
// initConfig reads in config file and ENV variables if set.

View File

@@ -23,6 +23,7 @@ import (
"github.com/herumi/bls-eth-go-binary/bls"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
)
@@ -39,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(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
assert(len(signatureAggregateSignatures) > 1, "multiple signatures required to aggregate")
var signature *bls.Sign
var err error
@@ -50,7 +51,7 @@ In quiet mode this will return 0 if the signatures can be aggregated, otherwise
}
errCheck(err, "Failed to aggregate signature")
outputIf(!quiet, fmt.Sprintf("%#x", signature.Serialize()))
outputIf(!viper.GetBool("quiet"), fmt.Sprintf("%#x", signature.Serialize()))
os.Exit(_exitSuccess)
},
}

View File

@@ -36,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(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
@@ -51,7 +51,7 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
errCheck(err, "Failed to parse domain")
assert(len(domain) == 32, "Domain data invalid")
}
outputIf(debug, fmt.Sprintf("Domain is %#x", domain))
outputIf(viper.GetBool("debug"), fmt.Sprintf("Domain is %#x", domain))
var account e2wtypes.Account
switch {
@@ -66,11 +66,11 @@ 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)
fmt.Printf("Signing %#x with domain %#x by public key %#x\n", fixedSizeData, specDomain, account.PublicKey().Marshal())
outputIf(viper.GetBool("debug"), fmt.Sprintf("Signing %#x with domain %#x by public key %#x", fixedSizeData, specDomain, account.PublicKey().Marshal()))
signature, err := util.SignRoot(account, fixedSizeData, specDomain)
errCheck(err, "Failed to sign")
outputIf(!quiet, fmt.Sprintf("%#x", signature.Marshal()))
outputIf(!viper.GetBool("quiet"), fmt.Sprintf("%#x", signature.Marshal()))
os.Exit(_exitSuccess)
},
}

View File

@@ -41,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(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
@@ -73,7 +73,7 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
account, err = util.ParseAccount(ctx, viper.GetString("public-key"), nil, false)
}
errCheck(err, "Failed to obtain account")
outputIf(debug, fmt.Sprintf("Public key is %#x", account.PublicKey().Marshal()))
outputIf(viper.GetBool("debug"), fmt.Sprintf("Public key is %#x", account.PublicKey().Marshal()))
var specDomain spec.Domain
copy(specDomain[:], domain)
@@ -83,7 +83,7 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
errCheck(err, "Failed to verify data")
assert(verified, "Failed to verify")
outputIf(verbose, "Verified")
outputIf(viper.GetBool("verbose"), "Verified")
os.Exit(_exitSuccess)
},
}

View File

@@ -53,10 +53,11 @@ func input(ctx context.Context) (*dataIn, error) {
// Ethereum 2 client.
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !data.quiet,
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
CustomSpecSupport: viper.GetBool("custom-spec"),
LogFallback: !data.quiet,
})
if err != nil {
return nil, err

View File

@@ -19,6 +19,7 @@ import (
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/pkg/errors"
)
@@ -41,17 +42,18 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("slot must be a positive integer")
}
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
genesisResponse, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis information")
}
genesis := genesisResponse.Data
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
specResponse, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain chain specifications")
}
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
slotDuration := specResponse.Data["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 Weald Technology Trading
// Copyright © 2021, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -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.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
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, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := slottime.Run(cmd)
if err != nil {
return err
@@ -50,8 +50,8 @@ func init() {
slotTimeCmd.Flags().String("slot", "", "the ID of the slot to fetch")
}
func slotTimeBindings() {
if err := viper.BindPFlag("slot", slotTimeCmd.Flags().Lookup("slot")); err != nil {
func slotTimeBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("slot", cmd.Flags().Lookup("slot")); err != nil {
panic(err)
}
}

View File

@@ -33,6 +33,7 @@ type command struct {
timeout time.Duration
connection string
allowInsecureConnections bool
customSpecSupport bool
// Input.
validator string
@@ -51,9 +52,10 @@ type command struct {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
customSpecSupport: viper.GetBool("custom-spec"),
}
// Timeout.

View File

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

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 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,8 +16,10 @@ 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"
@@ -41,10 +43,14 @@ func (c *command) process(ctx context.Context) error {
return err
}
syncCommittee, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", c.epoch)
syncCommitteeResponse, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommittee(ctx, &api.SyncCommitteeOpts{
State: "head",
Epoch: &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")
@@ -66,11 +72,20 @@ func (c *command) process(ctx context.Context) error {
if lastSlot > c.chainTime.CurrentSlot() {
lastSlot = c.chainTime.CurrentSlot()
}
for slot := firstSlot; slot < lastSlot; slot++ {
block, err := c.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
for slot := firstSlot; slot <= lastSlot; slot++ {
blockResponse, err := c.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
return err
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")
}
block := blockResponse.Data
if block == nil {
c.inclusions = append(c.inclusions, 0)
continue
@@ -98,6 +113,20 @@ func (c *command) process(ctx context.Context) error {
} else {
c.inclusions = append(c.inclusions, 2)
}
case spec.DataVersionDeneb:
aggregate = block.Deneb.Message.Body.SyncAggregate
if aggregate.SyncCommitteeBits.BitAt(c.committeeIndex) {
c.inclusions = append(c.inclusions, 1)
} 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)
}
default:
return fmt.Errorf("unhandled block version %v", block.Version)
}
@@ -112,10 +141,11 @@ func (c *command) setup(ctx context.Context) error {
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
CustomSpecSupport: c.customSpecSupport,
LogFallback: !c.quiet,
})
if err != nil {
return err
@@ -123,7 +153,7 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")

View File

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

View File

@@ -52,10 +52,11 @@ func input(ctx context.Context) (*dataIn, error) {
// Ethereum 2 client.
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !data.quiet,
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
CustomSpecSupport: viper.GetBool("custom-spec"),
LogFallback: !data.quiet,
})
if err != nil {
return nil, err
@@ -63,7 +64,7 @@ func input(ctx context.Context) (*dataIn, error) {
// Chain time.
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
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"]`,
},
}

View File

@@ -19,6 +19,7 @@ import (
"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"
"github.com/wealdtech/ethdo/util"
@@ -34,10 +35,14 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, err
}
syncCommittee, err := data.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", epoch)
syncCommitteeResponse, err := data.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommittee(ctx, &api.SyncCommitteeOpts{
State: "head",
Epoch: &epoch,
})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain sync committee information")
}
syncCommittee := syncCommitteeResponse.Data
if syncCommittee == nil {
return nil, errors.New("no sync committee returned")

View File

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

View File

@@ -1,4 +1,4 @@
// Copyright © 2021 Weald Technology Trading
// Copyright © 2021, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package members
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.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
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") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

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