Compare commits

...

85 Commits

Author SHA1 Message Date
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
Jim McDonald
35944ca8c5 Update changelog. 2023-04-13 18:12:45 +01:00
Jim McDonald
083d625813 Allow use of validator index in account specifier.
Fixes #83
2023-04-13 18:11:46 +01:00
Jim McDonald
263db812b3 Update dependencies. 2023-04-09 23:24:48 +01:00
Jim McDonald
2c01d18195 Update parameter names; add fallback connection.
Update parameter names to meet newer standards.

Provide a fallback beacon node if none other supplied.
2023-04-09 23:19:19 +01:00
Jim McDonald
df99d43415 Merge branch 'master' of github.com:wealdtech/ethdo 2023-04-09 19:23:08 +01:00
Jim McDonald
d81546f2de Update docs to match new validator info options. 2023-04-09 19:20:02 +01:00
Jim McDonald
3d87a917af Merge pull request #75 from nflaig/patch-1
Document Lodestar settings in README
2023-03-31 19:06:41 +01:00
Nico Flaig
9240ed4857 Document Lodestar settings in README 2023-03-31 17:54:06 +02:00
Jim McDonald
18f9e8dca2 Fix test. 2023-03-21 08:42:01 +00:00
Jim McDonald
fbc24b81d2 Linting. 2023-03-21 08:36:39 +00:00
Jim McDonald
f898466395 Increase timeout for fetching offline preparation. 2023-03-21 08:04:33 +00:00
Jim McDonald
e496fa1977 Do not mask account errors. 2023-03-07 13:50:50 +00:00
Jim McDonald
d016326779 Merge pull request #71 from lastperson/patch-1
Fix the 'credentials set' example.
2023-03-03 11:16:45 +00:00
Oleksii Matiiasevych
ee14b5ee8e Fix the 'credentials set' example. 2023-03-03 14:03:05 +07:00
Jim McDonald
9ae927feab Handle keystore in validator credentials set. 2023-02-27 22:07:45 +00:00
Jim McDonald
7087a0a55c Allow keystore without path. 2023-02-27 11:16:36 +00:00
Jim McDonald
793a8d6d79 Allow keystore as source of validator. 2023-02-26 22:43:45 +00:00
Jim McDonald
e15b22dc3c Updates for go 1.20. 2023-02-26 19:45:47 +00:00
Jim McDonald
6ddd453900 Update dependencies. 2023-02-26 12:58:49 +00:00
Jim McDonald
24755099c0 Update workflow. 2023-02-26 12:57:22 +00:00
203 changed files with 3958 additions and 1986 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-22.04
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-22.04
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

@@ -46,7 +46,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ^1.19
go-version: '^1.20'
- name: Check out repository into the Go module directory
uses: actions/checkout@v3
@@ -59,6 +59,10 @@ jobs:
- name: Compile
run: |
# Do not attempt to upgrade grub, as it errors on github (24th Feb 2023)
sudo apt-mark hold grub-efi-amd64-signed grub-efi-amd64-bin
sudo apt-get update
sudo apt-get upgrade
go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz ${{ needs.env_vars.outputs.binary }}
sha256sum ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz | sed -e 's/ .*//' >${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz.sha256
@@ -84,8 +88,6 @@ jobs:
- name: Cross compile (ARM64)
run: |
sudo apt-get update
sudo apt-get upgrade
sudo apt install -y gcc-aarch64-linux-gnu libstdc++-11-pic-arm64-cross
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz ${{ needs.env_vars.outputs.binary }}
@@ -119,7 +121,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ^1.19
go-version: '^1.20'
- name: Check out repository into the Go module directory
uses: actions/checkout@v3
@@ -164,7 +166,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ^1.19
go-version: '^1.20'
- name: Check out repository into the Go module directory
uses: actions/checkout@v3

View File

@@ -128,6 +128,7 @@ linters:
- contextcheck
- cyclop
- deadcode
- depguard
- dupl
- errorlint
- exhaustive

View File

@@ -1,3 +1,49 @@
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:
- allow use of keystores with validator credentials set
- tidy up various command options to provide more standard usage
- add mainnet fallback beacon node
1.28.4:
- allow validator exit to use a keystore as its validator parameter
1.28.2:
- fix bix stopping validator exit creation by direct validator specification

View File

@@ -1,4 +1,4 @@
FROM golang:1.18-bullseye as builder
FROM golang:1.20-bullseye as builder
WORKDIR /app

View File

@@ -86,6 +86,11 @@ Teku disables the REST API by default. To enable it, the beacon node must be st
The default port for the REST API is 5051, which can be changed with the `--rest-api-port` parameter.
### Lodestar
Lodestar enables the REST API by default and should just work locally. If you want to access the REST API from a remote server then you should also look to change the `--rest.address` to `0.0.0.0` as per the Lodestar documentation.
The default port for the REST API is 9596, which can be changed with the `--rest.port` parameter.
## Usage
`ethdo` contains a large number of features that are useful for day-to-day interactions with the different consensus clients.
@@ -166,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

@@ -269,7 +269,7 @@ func ObtainChainInfoFromNode(ctx context.Context,
}
tmp, exists := spec["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)

View File

@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(accountCmd)
}
func accountFlags(cmd *cobra.Command) {
func accountFlags(_ *cobra.Command) {
}

View File

@@ -25,7 +25,7 @@ type dataOut struct {
account e2wtypes.Account
}
func output(ctx context.Context, data *dataOut) (string, error) {
func output(_ context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}

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 © 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,9 +28,10 @@ type dataIn struct {
// Output options.
showPrivateKey bool
showWithdrawalCredentials bool
generateKeystore bool
}
func input(ctx context.Context) (*dataIn, error) {
func input(_ context.Context) (*dataIn, error) {
data := &dataIn{}
// Quiet.
@@ -54,5 +55,8 @@ func input(ctx 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,18 +15,26 @@ package accountderive
import (
"context"
"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(ctx context.Context, data *dataOut) (string, error) {
@@ -37,13 +45,17 @@ func output(ctx 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 +65,38 @@ func output(ctx 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"] = fmt.Sprintf("%x", 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

@@ -25,7 +25,7 @@ type dataOut struct {
account e2wtypes.Account
}
func output(ctx context.Context, data *dataOut) (string, error) {
func output(_ context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}

View File

@@ -24,7 +24,7 @@ type dataOut struct {
key []byte
}
func output(ctx context.Context, data *dataOut) (string, error) {
func output(_ context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}

View File

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

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

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

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

@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(attesterCmd)
}
func attesterFlags(cmd *cobra.Command) {
func attesterFlags(_ *cobra.Command) {
}

View File

@@ -18,9 +18,11 @@ import (
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
@@ -31,13 +33,11 @@ type dataIn struct {
verbose bool
debug bool
json bool
// Chain information.
slotsPerEpoch uint64
// Operation.
account string
pubKey string
validator string
eth2Client eth2client.Service
epoch spec.Epoch
chainTime chaintime.Service
epoch phase0.Epoch
}
func input(ctx context.Context) (*dataIn, error) {
@@ -52,38 +52,37 @@ func input(ctx context.Context) (*dataIn, error) {
data.debug = viper.GetBool("debug")
data.json = viper.GetBool("json")
// Account or pubkey.
if viper.GetString("account") == "" && viper.GetString("pubkey") == "" {
return nil, errors.New("account or pubkey is required")
// Validator.
data.validator = viper.GetString("validator")
if data.validator == "" {
return nil, errors.New("validator is required")
}
data.account = viper.GetString("account")
data.pubKey = viper.GetString("pubkey")
// Ethereum 2 client.
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
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,
})
if err != nil {
return nil, err
}
// Required data.
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
return nil, errors.Wrap(err, "failed to set up chaintime service")
}
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
// Epoch
epoch := viper.GetInt64("epoch")
if epoch == -1 {
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis data")
}
epoch = int64(time.Since(genesis.GenesisTime).Seconds()) / (int64(slotDuration.Seconds()) * int64(data.slotsPerEpoch))
// Epoch.
data.epoch, err = util.ParseEpoch(ctx, data.chainTime, viper.GetString("epoch"))
if err != nil {
return nil, err
}
data.epoch = spec.Epoch(epoch)
return data, nil
}

View File

@@ -17,6 +17,7 @@ import (
"context"
"os"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
@@ -61,19 +62,21 @@ func TestInput(t *testing.T) {
err: "timeout is required",
},
{
name: "AccountMissing",
name: "ValidatorMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "account or pubkey is required",
err: "validator is required",
},
{
name: "ConnectionMissing",
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"timeout": "5s",
"validator": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
res: &dataIn{
timeout: 5 * time.Second,
},
err: "failed to connect to any beacon node",
},
}

View File

@@ -30,7 +30,7 @@ type dataOut struct {
duty *api.AttesterDuty
}
func output(ctx context.Context, data *dataOut) (string, error) {
func output(_ context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}

View File

@@ -15,16 +15,12 @@ package attesterduties
import (
"context"
"encoding/hex"
"fmt"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
@@ -32,43 +28,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("no data")
}
var account e2wtypes.Account
var err error
if data.account != "" {
ctx, cancel := context.WithTimeout(ctx, data.timeout)
defer cancel()
_, account, err = util.WalletAndAccountFromPath(ctx, data.account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.pubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", data.pubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", data.pubKey))
}
}
// Fetch validator
pubKeys := make([]spec.BLSPubKey, 1)
pubKey, err := util.BestPublicKey(account)
validator, err := util.ParseValidator(ctx, data.eth2Client.(eth2client.ValidatorsProvider), data.validator, "head")
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for account")
}
copy(pubKeys[0][:], pubKey.Marshal())
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), pubKeys)
if err != nil {
return nil, errors.New("failed to obtain validator information")
}
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
var validator *api.Validator
for _, v := range validators {
validator = v
return nil, err
}
results := &dataOut{
@@ -77,7 +39,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
verbose: data.verbose,
}
duty, err := duty(ctx, data.eth2Client, validator, data.epoch, data.slotsPerEpoch)
duty, err := duty(ctx, data.eth2Client, validator, data.epoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain duty for validator")
}
@@ -87,7 +49,7 @@ 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, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
if err != nil {

View File

@@ -18,9 +18,11 @@ import (
"os"
"testing"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/auto"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
)
func TestProcess(t *testing.T) {
@@ -33,6 +35,12 @@ func TestProcess(t *testing.T) {
)
require.NoError(t, err)
chainTime, err := standardchaintime.New(context.Background(),
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
)
require.NoError(t, err)
tests := []struct {
name string
dataIn *dataIn
@@ -45,10 +53,9 @@ func TestProcess(t *testing.T) {
{
name: "Client",
dataIn: &dataIn{
eth2Client: eth2Client,
slotsPerEpoch: 32,
pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
epoch: 100,
eth2Client: eth2Client,
chainTime: chainTime,
validator: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
},
},
}

View File

@@ -15,7 +15,6 @@ package attesterinclusion
import (
"context"
"fmt"
"time"
eth2client "github.com/attestantio/go-eth2-client"
@@ -23,6 +22,7 @@ import (
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
@@ -32,15 +32,11 @@ type dataIn struct {
quiet bool
verbose bool
debug bool
// Chain information.
slotsPerEpoch uint64
// Operation.
eth2Client eth2client.Service
chainTime chaintime.Service
epoch spec.Epoch
account string
pubKey string
index string
validator string
}
func input(ctx context.Context) (*dataIn, error) {
@@ -54,48 +50,35 @@ func input(ctx context.Context) (*dataIn, error) {
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Account.
data.account = viper.GetString("account")
// PubKey.
data.pubKey = viper.GetString("pubkey")
// ID.
data.index = viper.GetString("index")
if viper.GetString("account") == "" && viper.GetString("index") == "" && viper.GetString("pubkey") == "" {
return nil, errors.New("account, index or pubkey is required")
data.validator = viper.GetString("validator")
if data.validator == "" {
return nil, errors.New("validator is required")
}
// Ethereum 2 client.
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
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,
})
if err != nil {
return nil, err
}
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
return nil, errors.Wrap(err, "failed to set up chaintime service")
}
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
// Epoch.
epoch := viper.GetInt64("epoch")
if epoch == -1 {
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis data")
}
epoch = int64(time.Since(genesis.GenesisTime).Seconds()) / (int64(slotDuration.Seconds()) * int64(data.slotsPerEpoch))
if epoch > 0 {
epoch--
}
}
data.epoch = spec.Epoch(epoch)
if data.debug {
fmt.Printf("Epoch is %d\n", data.epoch)
data.epoch, err = util.ParseEpoch(ctx, data.chainTime, viper.GetString("epoch"))
if err != nil {
return nil, err
}
return data, nil

View File

@@ -17,6 +17,7 @@ import (
"context"
"os"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
@@ -61,19 +62,21 @@ func TestInput(t *testing.T) {
err: "timeout is required",
},
{
name: "IndexMissing",
name: "ValidatorMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "account, index or pubkey is required",
err: "validator is required",
},
{
name: "ConnectionMissing",
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"timeout": "5s",
"validator": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
res: &dataIn{
timeout: 5 * time.Second,
},
err: "failed to connect to any beacon node",
},
}

View File

@@ -38,7 +38,7 @@ type dataOut struct {
targetTimely bool
}
func output(ctx context.Context, data *dataOut) (string, error) {
func output(_ context.Context, data *dataOut) (string, error) {
buf := strings.Builder{}
if data == nil {
return buf.String(), errors.New("no data")

View File

@@ -31,7 +31,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("no data")
}
var err error
validator, err := util.ParseValidator(ctx, data.eth2Client.(eth2client.ValidatorsProvider), data.validator, "head")
if err != nil {
return nil, err
}
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
@@ -41,30 +44,13 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.Wrap(err, "failed to set up chaintime service")
}
validatorIndex, err := util.ValidatorIndex(ctx, data.eth2Client, data.account, data.pubKey, data.index)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator index")
}
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), []phase0.ValidatorIndex{validatorIndex})
if err != nil {
return nil, errors.New("failed to obtain validator information")
}
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
var validator *api.Validator
for _, v := range validators {
validator = v
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
duty, err := duty(ctx, data.eth2Client, validator, data.epoch, data.slotsPerEpoch)
duty, err := duty(ctx, data.eth2Client, validator, data.epoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain duty for validator")
}
@@ -175,7 +161,7 @@ func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.At
}
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []phase0.ValidatorIndex{validator.Index})
if err != nil {

View File

@@ -18,18 +18,26 @@ import (
"os"
"testing"
"github.com/attestantio/go-eth2-client/auto"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/http"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
eth2Client, err := auto.New(context.Background(),
auto.WithLogLevel(zerolog.Disabled),
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
eth2Client, err := http.New(context.Background(),
http.WithLogLevel(zerolog.Disabled),
http.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
)
require.NoError(t, err)
chainTime, err := standardchaintime.New(context.Background(),
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
)
require.NoError(t, err)
@@ -45,10 +53,9 @@ func TestProcess(t *testing.T) {
{
name: "Client",
dataIn: &dataIn{
eth2Client: eth2Client,
slotsPerEpoch: 32,
pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
epoch: 100,
eth2Client: eth2Client,
chainTime: chainTime,
validator: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
},
},
}

View File

@@ -26,7 +26,7 @@ var attesterDutiesCmd = &cobra.Command{
Short: "Obtain information about duties of an attester",
Long: `Obtain information about dutes of an attester. For example:
ethdo attester duties --account=Validators/00001 --epoch=12345
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 {
@@ -47,19 +47,15 @@ In quiet mode this will return 0 if a duty from the attester is found, otherwise
func init() {
attesterCmd.AddCommand(attesterDutiesCmd)
attesterFlags(attesterDutiesCmd)
attesterDutiesCmd.Flags().Int64("epoch", -1, "the last complete epoch")
attesterDutiesCmd.Flags().String("pubkey", "", "the public key of the attester")
attesterDutiesCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network")
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")
}
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("pubkey", attesterDutiesCmd.Flags().Lookup("pubkey")); 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

@@ -26,7 +26,7 @@ var attesterInclusionCmd = &cobra.Command{
Short: "Obtain information about attester inclusion",
Long: `Obtain information about attester inclusion. For example:
ethdo attester inclusion --account=Validators/00001 --epoch=12345
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 {
@@ -47,19 +47,19 @@ In quiet mode this will return 0 if an attestation from the attester is found on
func init() {
attesterCmd.AddCommand(attesterInclusionCmd)
attesterFlags(attesterInclusionCmd)
attesterInclusionCmd.Flags().Int64("epoch", -1, "the last complete epoch")
attesterInclusionCmd.Flags().String("pubkey", "", "the public key of the attester")
attesterInclusionCmd.Flags().String("epoch", "-1", "the epoch for which to obtain the inclusion")
attesterInclusionCmd.Flags().String("validator", "", "the index, public key, or account of the validator")
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("pubkey", attesterInclusionCmd.Flags().Lookup("pubkey")); 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

@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(blockCmd)
}
func blockFlags(cmd *cobra.Command) {
func blockFlags(_ *cobra.Command) {
}

View File

@@ -105,7 +105,7 @@ type attestationData struct {
Index int `json:"index"`
}
func newCommand(ctx context.Context) (*command, error) {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),

View File

@@ -106,9 +106,8 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
if attestation.NewVotes == 0 {
builder.WriteString("\n")
continue
} else {
builder.WriteString(", ")
}
builder.WriteString(", ")
switch {
case !attestation.HeadCorrect:
builder.WriteString("head vote incorrect, ")

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
@@ -77,11 +77,7 @@ func (c *command) analyze(ctx context.Context, block *spec.VersionedSignedBeacon
return err
}
if err := c.analyzeSyncCommittees(ctx, block); err != nil {
return err
}
return nil
return c.analyzeSyncCommittees(ctx, block)
}
func (c *command) analyzeAttestations(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
@@ -213,7 +209,7 @@ func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedB
return c.fetchParents(ctx, parentBlock, minSlot)
}
func (c *command) processParentBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
func (c *command) processParentBlock(_ context.Context, block *spec.VersionedSignedBeaconBlock) error {
attestations, err := block.Attestations()
if err != nil {
return err
@@ -259,7 +255,12 @@ func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
@@ -412,7 +413,7 @@ func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Att
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
}
func (c *command) analyzeSyncCommittees(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
func (c *command) analyzeSyncCommittees(_ context.Context, block *spec.VersionedSignedBeaconBlock) error {
c.analysis.SyncCommitee = &syncCommitteeAnalysis{}
switch block.Version {
case spec.DataVersionPhase0:
@@ -438,6 +439,13 @@ func (c *command) analyzeSyncCommittees(ctx context.Context, block *spec.Version
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

@@ -39,7 +39,7 @@ func TestProcess(t *testing.T) {
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"blockid": "invalid",
},
err: "failed to obtain beacon block: failed to request signed beacon block: GET failed with status 400: {\"code\":400,\"message\":\"Invalid block: invalid\"}",
err: "failed to obtain beacon block: failed to request signed beacon block: GET failed with status 400: {\"code\":400,\"message\":\"BAD_REQUEST: Unsupported endpoint version: v2\",\"stacktraces\":[]}",
},
}

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,21 +51,20 @@ 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, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
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,
})
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

@@ -61,13 +61,6 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to connect to any beacon node",
},
{
name: "ConnectionBad",
vars: map[string]interface{}{

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
@@ -28,6 +28,7 @@ import (
"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/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
@@ -43,7 +44,7 @@ type dataOut struct {
slotsPerEpoch uint64
}
func output(ctx context.Context, data *dataOut) (string, error) {
func output(_ context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
@@ -51,9 +52,10 @@ func output(ctx context.Context, data *dataOut) (string, error) {
return "", nil
}
func outputBlockGeneral(ctx context.Context,
func outputBlockGeneral(_ context.Context,
verbose bool,
slot phase0.Slot,
proposerIndex phase0.ValidatorIndex,
blockRoot phase0.Root,
bodyRoot phase0.Root,
parentRoot phase0.Root,
@@ -69,6 +71,7 @@ func outputBlockGeneral(ctx context.Context,
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Slot: %d\n", slot))
res.WriteString(fmt.Sprintf("Proposing validator index: %d\n", proposerIndex))
res.WriteString(fmt.Sprintf("Epoch: %d\n", phase0.Epoch(uint64(slot)/slotsPerEpoch)))
res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(slot)*int64(slotDuration.Seconds()), 0)))
res.WriteString(fmt.Sprintf("Block root: %#x\n", blockRoot))
@@ -89,7 +92,7 @@ func outputBlockGeneral(ctx context.Context,
return res.String(), nil
}
func outputBlockETH1Data(ctx context.Context, eth1Data *phase0.ETH1Data) (string, error) {
func outputBlockETH1Data(_ context.Context, eth1Data *phase0.ETH1Data) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Ethereum 1 deposit count: %d\n", eth1Data.DepositCount))
@@ -194,7 +197,7 @@ func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Ser
return res.String(), nil
}
func outputBlockDeposits(ctx context.Context, verbose bool, deposits []*phase0.Deposit) (string, error) {
func outputBlockDeposits(_ context.Context, verbose bool, deposits []*phase0.Deposit) (string, error) {
res := strings.Builder{}
// Deposits.
@@ -246,7 +249,7 @@ func outputBlockBLSToExecutionChanges(ctx context.Context, eth2Client eth2client
} else {
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[op.Message.ValidatorIndex].Validator.PublicKey, op.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" BLS public key: %#x\n", op.Message.FromBLSPubkey))
res.WriteString(fmt.Sprintf(" Execution address: %#x\n", op.Message.ToExecutionAddress))
res.WriteString(fmt.Sprintf(" Execution address: %s\n", op.Message.ToExecutionAddress.String()))
}
}
}
@@ -314,6 +317,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 +392,116 @@ 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 outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
@@ -410,6 +524,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 +615,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 +699,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,
@@ -638,7 +755,7 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
return res.String(), nil
}
func outputCapellaBlockExecutionPayload(ctx context.Context,
func outputCapellaBlockExecutionPayload(_ context.Context,
verbose bool,
payload *capella.ExecutionPayload,
) (
@@ -676,7 +793,7 @@ func outputCapellaBlockExecutionPayload(ctx 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,7 +823,102 @@ func outputCapellaBlockExecutionPayload(ctx context.Context,
return res.String(), nil
}
func outputBellatrixBlockExecutionPayload(ctx context.Context,
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 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", blob.KzgCommitment.String()))
res.WriteString(fmt.Sprintf(" KZG proof: %s\n", blob.KzgProof.String()))
}
return res.String(), nil
}
func outputBellatrixBlockExecutionPayload(_ context.Context,
verbose bool,
payload *bellatrix.ExecutionPayload,
) (
@@ -744,7 +956,7 @@ func outputBellatrixBlockExecutionPayload(ctx 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

@@ -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
@@ -17,6 +17,9 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
@@ -25,8 +28,10 @@ import (
"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/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
)
var (
@@ -39,6 +44,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.blockID == "" && data.blockTime == "" {
return nil, errors.New("no block ID or block time")
}
results = &dataOut{
debug: data.debug,
@@ -58,13 +66,27 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration)
results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
if data.blockTime != "" {
data.blockID, err = timeToBlockID(ctx, data.eth2Client, data.blockTime)
if err != nil {
return nil, err
}
}
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, data.blockID)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon block")
}
if signedBlock == nil {
if data.quiet {
os.Exit(1)
}
return nil, errors.New("empty beacon block")
}
if data.quiet {
os.Exit(0)
}
switch signedBlock.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(ctx, data.jsonOutput, signedBlock.Phase0); err != nil {
@@ -82,6 +104,14 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if err := outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Capella); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionDeneb:
blobs, err := results.eth2Client.(eth2client.BeaconBlockBlobsProvider).BeaconBlockBlobs(ctx, data.blockID)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain blobs")
}
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Deneb, blobs); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
default:
return nil, errors.New("unknown block version")
}
@@ -103,13 +133,15 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
func headEventHandler(event *api.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)
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, blockID)
if err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to obtain block: %v\n", err)
@@ -122,41 +154,30 @@ func headEventHandler(event *api.Event) {
}
return
}
switch signedBlock.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, signedBlock.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, signedBlock.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, signedBlock.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, signedBlock.Capella)
case spec.DataVersionDeneb:
var blobs []*deneb.BlobSidecar
blobs, err = results.eth2Client.(eth2client.BeaconBlockBlobsProvider).BeaconBlockBlobs(ctx, blockID)
if err == nil {
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Deneb, blobs)
}
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("")
}
@@ -251,3 +272,70 @@ 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 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.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
)
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 - 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,11 +43,11 @@ func TestProcess(t *testing.T) {
err: "no data",
},
{
name: "Client",
name: "NoBlockID",
dataIn: &dataIn{
eth2Client: eth2Client,
},
err: "empty beacon block",
err: "no block ID",
},
}

View File

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

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

@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(chainCmd)
}
func chainFlags(cmd *cobra.Command) {
func chainFlags(_ *cobra.Command) {
}

View File

@@ -60,7 +60,7 @@ type vote struct {
Count int `json:"count"`
}
func newCommand(ctx context.Context) (*command, error) {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),

View File

@@ -42,7 +42,7 @@ func (c *command) output(ctx context.Context) (string, error) {
return c.outputText(ctx)
}
func (c *command) outputJSON(ctx context.Context) (string, error) {
func (c *command) outputJSON(_ context.Context) (string, error) {
votes := make([]*vote, 0, len(c.votes))
totalVotes := 0
for _, vote := range c.votes {
@@ -71,7 +71,7 @@ func (c *command) outputJSON(ctx context.Context) (string, error) {
return string(data), nil
}
func (c *command) outputText(ctx context.Context) (string, error) {
func (c *command) outputText(_ context.Context) (string, error) {
builder := strings.Builder{}
builder.WriteString("Voting period: ")

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
@@ -90,6 +90,10 @@ func (c *command) process(ctx context.Context) error {
c.slot = state.Capella.Slot
c.incumbent = state.Capella.ETH1Data
c.eth1DataVotes = state.Capella.ETH1DataVotes
case spec.DataVersionDeneb:
c.slot = state.Deneb.Slot
c.incumbent = state.Deneb.ETH1Data
c.eth1DataVotes = state.Deneb.ETH1DataVotes
default:
return fmt.Errorf("unhandled beacon state version %v", state.Version)
}
@@ -114,7 +118,12 @@ func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}

View File

@@ -47,7 +47,7 @@ type command struct {
exitQueue int
}
func newCommand(ctx context.Context) (*command, error) {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),

View File

@@ -36,7 +36,7 @@ func (c *command) output(ctx context.Context) (string, error) {
return c.outputText(ctx)
}
func (c *command) outputJSON(ctx context.Context) (string, error) {
func (c *command) outputJSON(_ context.Context) (string, error) {
output := &jsonOutput{
ActivationQueue: c.activationQueue,
ExitQueue: c.exitQueue,
@@ -49,7 +49,7 @@ func (c *command) outputJSON(ctx context.Context) (string, error) {
return string(data), nil
}
func (c *command) outputText(ctx context.Context) (string, error) {
func (c *command) outputText(_ context.Context) (string, error) {
builder := strings.Builder{}
if c.activationQueue > 0 {

View File

@@ -58,7 +58,12 @@ func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}

View File

@@ -36,7 +36,7 @@ type dataIn struct {
epoch string
}
func input(ctx context.Context) (*dataIn, error) {
func input(_ context.Context) (*dataIn, error) {
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {

View File

@@ -34,6 +34,7 @@ type dataOut struct {
slot spec.Slot
slotStart time.Time
slotEnd time.Time
hasSyncCommittees bool
syncCommitteePeriod uint64
syncCommitteePeriodStart time.Time
syncCommitteePeriodEpochStart spec.Epoch
@@ -41,7 +42,7 @@ type dataOut struct {
syncCommitteePeriodEpochEnd spec.Epoch
}
func output(ctx context.Context, data *dataOut) (string, error) {
func output(_ context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
@@ -56,27 +57,59 @@ func output(ctx context.Context, data *dataOut) (string, error) {
builder.WriteString(fmt.Sprintf("%d", data.epoch))
builder.WriteString("\n Epoch start ")
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(")")
}
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(")")
}
builder.WriteString("\nSlot ")
builder.WriteString(fmt.Sprintf("%d", data.slot))
builder.WriteString("\n Slot start ")
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(")")
}
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(")")
}
builder.WriteString("\nSync committee period ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriod))
builder.WriteString("\n Sync committee period start ")
builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart))
builder.WriteString(")\n Sync committee period end ")
builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd))
builder.WriteString(")\n")
if data.hasSyncCommittees {
builder.WriteString("\nSync committee period ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriod))
builder.WriteString("\n Sync committee period start ")
builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart))
if data.verbose {
builder.WriteString(", ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodStart.Unix()))
}
builder.WriteString(")\n Sync committee period end ")
builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd))
if data.verbose {
builder.WriteString(", ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEnd.Unix()))
}
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}

View File

@@ -21,6 +21,7 @@ import (
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
@@ -29,22 +30,22 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("no data")
}
eth2Client, err := util.ConnectToBeaconNode(ctx, data.connection, data.timeout, data.allowInsecureConnections)
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: data.connection,
Timeout: data.timeout,
AllowInsecure: data.allowInsecureConnections,
LogFallback: !data.quiet,
})
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
}
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
epochsPerSyncCommitteePeriod := config["EPOCHS_PER_SYNC_COMMITTEE_PERIOD"].(uint64)
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis data")
return nil, errors.Wrap(err, "failed to set up chaintime service")
}
results := &dataOut{
@@ -62,34 +63,33 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
results.slot = phase0.Slot(slot)
case data.epoch != "":
epoch, err := strconv.ParseUint(data.epoch, 10, 64)
epoch, err := util.ParseEpoch(ctx, chainTime, data.epoch)
if err != nil {
return nil, errors.Wrap(err, "failed to parse epoch")
}
results.slot = phase0.Slot(epoch * slotsPerEpoch)
results.slot = chainTime.FirstSlotOfEpoch(epoch)
case data.timestamp != "":
timestamp, err := time.Parse("2006-01-02T15:04:05-0700", data.timestamp)
if err != nil {
return nil, errors.Wrap(err, "failed to parse timestamp")
}
secs := timestamp.Sub(genesis.GenesisTime)
if secs < 0 {
return nil, errors.New("timestamp prior to genesis")
}
results.slot = phase0.Slot(secs / slotDuration)
results.slot = chainTime.TimestampToSlot(timestamp)
}
// Fill in the info given the slot.
results.slotStart = genesis.GenesisTime.Add(time.Duration(results.slot) * slotDuration)
results.slotEnd = genesis.GenesisTime.Add(time.Duration(results.slot+1) * slotDuration)
results.epoch = phase0.Epoch(uint64(results.slot) / slotsPerEpoch)
results.epochStart = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch)*slotsPerEpoch) * slotDuration)
results.epochEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch+1)*slotsPerEpoch) * slotDuration)
results.syncCommitteePeriod = uint64(results.epoch) / epochsPerSyncCommitteePeriod
results.syncCommitteePeriodEpochStart = phase0.Epoch(results.syncCommitteePeriod * epochsPerSyncCommitteePeriod)
results.syncCommitteePeriodEpochEnd = phase0.Epoch((results.syncCommitteePeriod+1)*epochsPerSyncCommitteePeriod) - 1
results.syncCommitteePeriodStart = genesis.GenesisTime.Add(time.Duration(uint64(results.syncCommitteePeriodEpochStart)*slotsPerEpoch) * slotDuration)
results.syncCommitteePeriodEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.syncCommitteePeriodEpochEnd)*slotsPerEpoch) * slotDuration)
results.slotStart = chainTime.StartOfSlot(results.slot)
results.slotEnd = chainTime.StartOfSlot(results.slot + 1)
results.epoch = chainTime.SlotToEpoch(results.slot)
results.epochStart = chainTime.StartOfEpoch(results.epoch)
results.epochEnd = chainTime.StartOfEpoch(results.epoch + 1)
if results.epoch >= chainTime.FirstEpochOfSyncPeriod(chainTime.AltairInitialSyncCommitteePeriod()) {
results.hasSyncCommittees = true
results.syncCommitteePeriod = chainTime.SlotToSyncCommitteePeriod(results.slot)
results.syncCommitteePeriodEpochStart = chainTime.FirstEpochOfSyncPeriod(results.syncCommitteePeriod)
results.syncCommitteePeriodEpochEnd = chainTime.FirstEpochOfSyncPeriod(results.syncCommitteePeriod + 1)
results.syncCommitteePeriodStart = chainTime.StartOfEpoch(results.syncCommitteePeriodEpochStart)
results.syncCommitteePeriodEnd = chainTime.StartOfEpoch(results.syncCommitteePeriodEpochEnd)
}
return results, nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2021 Weald Technology Trading
// Copyright © 2021 - 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,7 +15,6 @@ package chaintime
import (
"context"
"fmt"
"os"
"testing"
"time"
@@ -47,16 +46,11 @@ func TestProcess(t *testing.T) {
slot: "1",
},
expected: &dataOut{
epochStart: time.Unix(1606824023, 0),
epochEnd: time.Unix(1606824407, 0),
slot: 1,
slotStart: time.Unix(1606824035, 0),
slotEnd: time.Unix(1606824047, 0),
syncCommitteePeriod: 0,
syncCommitteePeriodStart: time.Unix(1606824023, 0),
syncCommitteePeriodEnd: time.Unix(1606921943, 0),
syncCommitteePeriodEpochStart: 0,
syncCommitteePeriodEpochEnd: 255,
epochStart: time.Unix(1606824023, 0),
epochEnd: time.Unix(1606824407, 0),
slot: 1,
slotStart: time.Unix(1606824035, 0),
slotEnd: time.Unix(1606824047, 0),
},
},
{
@@ -68,17 +62,12 @@ func TestProcess(t *testing.T) {
epoch: "2",
},
expected: &dataOut{
epoch: 2,
epochStart: time.Unix(1606824791, 0),
epochEnd: time.Unix(1606825175, 0),
slot: 64,
slotStart: time.Unix(1606824791, 0),
slotEnd: time.Unix(1606824803, 0),
syncCommitteePeriod: 0,
syncCommitteePeriodStart: time.Unix(1606824023, 0),
syncCommitteePeriodEnd: time.Unix(1606921943, 0),
syncCommitteePeriodEpochStart: 0,
syncCommitteePeriodEpochEnd: 255,
epoch: 2,
epochStart: time.Unix(1606824791, 0),
epochEnd: time.Unix(1606825175, 0),
slot: 64,
slotStart: time.Unix(1606824791, 0),
slotEnd: time.Unix(1606824803, 0),
},
},
{
@@ -87,20 +76,21 @@ func TestProcess(t *testing.T) {
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
timestamp: "2021-01-01T00:00:00+0000",
timestamp: "2023-01-01T00:00:00+0000",
},
expected: &dataOut{
epoch: 6862,
epochStart: time.Unix(1609459031, 0),
epochEnd: time.Unix(1609459415, 0),
slot: 219598,
slotStart: time.Unix(1609459199, 0),
slotEnd: time.Unix(1609459211, 0),
syncCommitteePeriod: 26,
syncCommitteePeriodStart: time.Unix(1609379927, 0),
syncCommitteePeriodEnd: time.Unix(1609477847, 0),
syncCommitteePeriodEpochStart: 6656,
syncCommitteePeriodEpochEnd: 6911,
epoch: 171112,
epochStart: time.Unix(1672531031, 0),
epochEnd: time.Unix(1672531415, 0),
slot: 5475598,
slotStart: time.Unix(1672531199, 0),
slotEnd: time.Unix(1672531211, 0),
hasSyncCommittees: true,
syncCommitteePeriod: 668,
syncCommitteePeriodStart: time.Unix(1672491095, 0),
syncCommitteePeriodEnd: time.Unix(1672589399, 0),
syncCommitteePeriodEpochStart: 171008,
syncCommitteePeriodEpochEnd: 171264,
},
},
}
@@ -112,7 +102,6 @@ func TestProcess(t *testing.T) {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
fmt.Printf("****** %d %d\n", res.syncCommitteePeriodStart.Unix(), res.syncCommitteePeriodEnd.Unix())
require.Equal(t, test.expected, res)
}
})

View File

@@ -58,7 +58,7 @@ type command struct {
additionalInfo string
}
func newCommand(ctx context.Context) (*command, error) {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),

View File

@@ -18,7 +18,7 @@ import (
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
func (c *command) output(_ context.Context) (string, error) {
if c.quiet {
return "", nil
}

View File

@@ -85,7 +85,12 @@ func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}

View File

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

@@ -37,7 +37,12 @@ In quiet mode this will return 0 if the chain information can be obtained, other
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
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"),
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
@@ -49,12 +54,7 @@ In quiet mode this will return 0 if the chain information can be obtained, other
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
errCheck(err, "Failed to obtain current fork")
if 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)
if viper.GetBool("quiet") {
os.Exit(_exitSuccess)
}
@@ -62,12 +62,12 @@ In quiet mode this will return 0 if the chain information can be obtained, other
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()))
outputIf(viper.GetBool("verbose"), fmt.Sprintf("Genesis timestamp: %v", genesis.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 {
if viper.GetBool("verbose") {
forkData := &spec.ForkData{
CurrentVersion: fork.CurrentVersion,
GenesisValidatorsRoot: genesis.GenesisValidatorsRoot,
@@ -89,11 +89,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

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

97
cmd/chainspec.go Normal file
View File

@@ -0,0 +1,97 @@
// 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"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"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(cmd *cobra.Command, args []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"),
})
errCheck(err, "Failed to connect to Ethereum consensus node")
spec, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
errCheck(err, "Failed to obtain chain specification")
if viper.GetBool("quiet") {
return
}
// Tweak the spec for output.
for k, v := range spec {
switch t := v.(type) {
case phase0.Version:
spec[k] = fmt.Sprintf("%#x", t)
case phase0.DomainType:
spec[k] = fmt.Sprintf("%#x", t)
case time.Time:
spec[k] = fmt.Sprintf("%d", t.Unix())
case time.Duration:
spec[k] = fmt.Sprintf("%d", uint64(t.Seconds()))
case []byte:
spec[k] = fmt.Sprintf("%#x", t)
case uint64:
spec[k] = fmt.Sprintf("%d", t)
}
}
if viper.GetBool("json") {
data, err := json.Marshal(spec)
errCheck(err, "Failed to marshal JSON")
fmt.Printf("%s\n", string(data))
} else {
keys := make([]string, 0, len(spec))
for k := range spec {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("%s: %v\n", key, spec[key])
}
}
},
}
func init() {
chainCmd.AddCommand(chainSpecCmd)
chainFlags(chainSpecCmd)
}
func chainSpecBindings(_ *cobra.Command) {
}

View File

@@ -41,7 +41,12 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
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"),
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
chainTime, err := standardchaintime.New(ctx,
@@ -78,7 +83,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("-")
@@ -101,7 +106,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))
@@ -111,14 +116,14 @@ 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)
@@ -160,7 +165,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
res.WriteString(fmt.Sprintf("%d", period))
res.WriteString("\n")
if verbose {
if viper.GetBool("verbose") {
res.WriteString("Sync committee epochs: ")
res.WriteString(fmt.Sprintf("%d", periodStartEpoch))
res.WriteString("-")

View File

@@ -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,5 +28,5 @@ func init() {
RootCmd.AddCommand(depositCmd)
}
func depositFlags(cmd *cobra.Command) {
func depositFlags(_ *cobra.Command) {
}

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

@@ -29,12 +29,12 @@ func init() {
RootCmd.AddCommand(epochCmd)
}
func epochFlags(cmd *cobra.Command) {
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"
@@ -49,6 +50,9 @@ type command struct {
beaconCommitteesProvider eth2client.BeaconCommitteesProvider
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Caches.
blocksCache map[string]*spec.VersionedSignedBeaconBlock
// Results.
summary *epochSummary
}
@@ -67,6 +71,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 {
@@ -86,12 +91,13 @@ type nonParticipatingValidator struct {
Committee phase0.CommitteeIndex `json:"committee_index"`
}
func newCommand(ctx context.Context) (*command, error) {
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.

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
@@ -51,8 +51,7 @@ func (c *command) process(ctx context.Context) error {
if err := c.processSyncCommitteeDuties(ctx); err != nil {
return err
}
return nil
return c.processBlobs(ctx)
}
func (c *command) processProposerDuties(ctx context.Context) error {
@@ -64,7 +63,7 @@ func (c *command) processProposerDuties(ctx context.Context) error {
return errors.New("empty proposer duties")
}
for _, duty := range duties {
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", duty.Slot))
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))
}
@@ -165,7 +164,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))
}
@@ -272,7 +271,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))
}
@@ -291,6 +290,8 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
aggregate = block.Bellatrix.Message.Body.SyncAggregate
case spec.DataVersionCapella:
aggregate = block.Capella.Message.Body.SyncAggregate
case spec.DataVersionDeneb:
aggregate = block.Deneb.Message.Body.SyncAggregate
default:
return fmt.Errorf("unhandled block version %v", block.Version)
}
@@ -328,7 +329,12 @@ func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
@@ -369,3 +375,43 @@ 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)
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
block, err = c.blocksProvider.SignedBeaconBlock(ctx, blockID)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch block")
}
c.blocksCache[blockID] = block
}
return block, nil
}

View File

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

@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(exitCmd)
}
func exitFlags(cmd *cobra.Command) {
func exitFlags(_ *cobra.Command) {
}

View File

@@ -14,80 +14,97 @@
package cmd
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
consensusclient "github.com/attestantio/go-eth2-client"
v1 "github.com/attestantio/go-eth2-client/api/v1"
"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"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
var exitVerifyPubKey string
var exitVerifyCmd = &cobra.Command{
Use: "verify",
Short: "Verify exit data is valid",
Long: `Verify that exit data generated by "ethdo validator exit" is correct for a given account. For example:
ethdo exit verify --data=exitdata.json --account=primary/current
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) {
ctx := context.Background()
assert(viper.GetString("account") != "" || exitVerifyPubKey != "", "account or public key is required")
account, err := exitVerifyAccount(ctx)
errCheck(err, "Failed to obtain account")
assert(viper.GetString("signed-operation") != "", "signed-operation is required")
signedOp, err := obtainSignedOperation(viper.GetString("signed-operation"))
errCheck(err, "Failed to obtain signed operation")
assert(viper.GetString("exit") != "", "exit is required")
data, err := obtainExitData(viper.GetString("exit"))
errCheck(err, "Failed to obtain exit data")
// Confirm signature is good.
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
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"),
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
validator, err := util.ParseValidator(ctx, eth2Client.(consensusclient.ValidatorsProvider), fmt.Sprintf("%d", signedOp.Message.ValidatorIndex), "head")
errCheck(err, "failed to obtain validator")
pubkey, err := validator.PubKey(ctx)
errCheck(err, "failed to obtain validator public key")
account, err := util.ParseAccount(ctx, pubkey.String(), nil, false)
errCheck(err, "failed to obtain account")
// Ensure the validator is in a suitable state.
assert(validator.Status == v1.ValidatorStateActiveOngoing, "validator not in a suitable state to exit")
// Obtain the hash tree root of the message to check the signature.
opRoot, err := signedOp.Message.HashTreeRoot()
errCheck(err, "Failed to obtain exit hash tree root")
genesis, err := eth2Client.(consensusclient.GenesisProvider).Genesis(ctx)
errCheck(err, "Failed to obtain beacon chain genesis")
domain := e2types.Domain(e2types.DomainVoluntaryExit, data.ForkVersion[:], genesis.GenesisValidatorsRoot[:])
var exitDomain spec.Domain
copy(exitDomain[:], domain)
exit := &spec.VoluntaryExit{
Epoch: data.Exit.Message.Epoch,
ValidatorIndex: data.Exit.Message.ValidatorIndex,
}
exitRoot, err := exit.HashTreeRoot()
errCheck(err, "Failed to obtain exit hash tree root")
fork, err := eth2Client.(consensusclient.ForkProvider).Fork(ctx, "head")
errCheck(err, "Failed to obtain fork information")
// Check against current and prior fork versions.
signatureBytes := make([]byte, 96)
copy(signatureBytes, data.Exit.Signature[:])
copy(signatureBytes, signedOp.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(signatureBytes)
errCheck(err, "Invalid signature")
verified, err := util.VerifyRoot(account, exitRoot, exitDomain, sig)
verified := false
// Try with the current fork.
domain := phase0.Domain{}
currentExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork.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")
assert(verified, "Voluntary exit failed to verify")
if !verified {
// Try again with the previous fork.
previousExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork.PreviousVersion[:], genesis.GenesisValidatorsRoot[:])
copy(domain[:], previousExitDomain)
errCheck(err, "Failed to compute domain")
verified, err = util.VerifyRoot(account, opRoot, domain, sig)
errCheck(err, "Failed to verify voluntary exit")
}
assert(verified, "Voluntary exit failed to verify against current and previous fork versions")
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
errCheck(err, "Failed to obtain current fork")
assert(bytes.Equal(data.ForkVersion[:], fork.CurrentVersion[:]) || bytes.Equal(data.ForkVersion[:], fork.PreviousVersion[:]), "Exit is for an old fork version and is no longer valid")
outputIf(verbose, "Verified")
outputIf(viper.GetBool("verbose"), "Verified")
os.Exit(_exitSuccess)
},
}
// obtainExitData obtains exit data from an input, could be JSON itself or a path to JSON.
func obtainExitData(input string) (*util.ValidatorExitData, error) {
// obtainSignedOperation obtains exit data from an input, could be JSON itself or a path to JSON.
func obtainSignedOperation(input string) (*phase0.SignedVoluntaryExit, error) {
var err error
var data []byte
// Input could be JSON or a path to JSON
@@ -101,46 +118,23 @@ func obtainExitData(input string) (*util.ValidatorExitData, error) {
return nil, errors.Wrap(err, "failed to find deposit data file")
}
}
exitData := &util.ValidatorExitData{}
err = json.Unmarshal(data, exitData)
signedOp := &phase0.SignedVoluntaryExit{}
err = json.Unmarshal(data, signedOp)
if err != nil {
return nil, errors.Wrap(err, "data is not valid JSON")
}
return exitData, nil
}
// exitVerifyAccount obtains the account for the exitVerify command.
func exitVerifyAccount(ctx context.Context) (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
if viper.GetString("account") != "" {
_, account, err = walletAndAccountFromPath(ctx, viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(exitVerifyPubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", exitVerifyPubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", exitVerifyPubKey))
}
}
return account, nil
return signedOp, nil
}
func init() {
exitCmd.AddCommand(exitVerifyCmd)
exitFlags(exitVerifyCmd)
exitVerifyCmd.Flags().String("exit", "", "JSON data, or path to JSON data")
exitVerifyCmd.Flags().StringVar(&exitVerifyPubKey, "pubkey", "", "Public key for which to verify exit")
exitVerifyCmd.Flags().String("signed-operation", "", "JSON data, or path to JSON data")
}
func exitVerifyBindings() {
if err := viper.BindPFlag("exit", exitVerifyCmd.Flags().Lookup("exit")); err != nil {
func exitVerifyBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("signed-operation", cmd.Flags().Lookup("signed-operation")); err != nil {
panic(err)
}
}

View File

@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(nodeCmd)
}
func nodeFlags(cmd *cobra.Command) {
func nodeFlags(_ *cobra.Command) {
}

View File

@@ -50,7 +50,12 @@ func input(ctx context.Context) (*dataIn, error) {
data.topics = viper.GetStringSlice("topics")
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
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,
})
if err != nil {
return nil, err
}

View File

@@ -61,13 +61,6 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to connect to any beacon node",
},
{
name: "ConnectionBad",
vars: map[string]interface{}{

View File

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

@@ -24,9 +24,6 @@ import (
"github.com/wealdtech/ethdo/util"
)
// defaultBeaconNode is used if no other connection is supplied.
var defaultBeaconNode = "http://mainnet-consensus.attestant.io/"
var nodeInfoCmd = &cobra.Command{
Use: "info",
Short: "Obtain information about a node",
@@ -38,33 +35,19 @@ In quiet mode this will return 0 if the node information can be obtained, otherw
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
if viper.GetString("connection") != "" {
// The user provided a connection, so don't second-guess them by using a different node.
fmt.Fprintln(os.Stderr, err.Error())
return
}
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"),
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
// The user did not provide a connection, so attempt to use the default node.
if viper.GetBool("debug") {
fmt.Fprintf(os.Stderr, "No node connection, attempting to use %s\n", defaultBeaconNode)
}
eth2Client, err = util.ConnectToBeaconNode(ctx, defaultBeaconNode, viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
return
}
if !viper.GetBool("quiet") {
fmt.Fprintf(os.Stderr, "No connection supplied; using mainnet public access endpoint\n")
}
}
if quiet {
if viper.GetBool("quiet") {
os.Exit(_exitSuccess)
}
if verbose {
if viper.GetBool("verbose") {
version, err := eth2Client.(eth2client.NodeVersionProvider).NodeVersion(ctx)
errCheck(err, "Failed to obtain node version")
fmt.Printf("Version: %s\n", version)

View File

@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(proposerCmd)
}
func proposerFlags(cmd *cobra.Command) {
func proposerFlags(_ *cobra.Command) {
}

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
@@ -37,6 +37,7 @@ type command struct {
// Operation.
epoch string
slot string
jsonOutput bool
// Data access.
@@ -53,25 +54,24 @@ type results struct {
Duties []*apiv1.ProposerDuty `json:"duties"`
}
func newCommand(ctx context.Context) (*command, error) {
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"),
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,7 @@ import (
"context"
eth2client "github.com/attestantio/go-eth2-client"
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,11 +25,45 @@ 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)
duties, err := c.proposerDutiesProvider.ProposerDuties(ctx, c.results.Epoch, nil)
if err != nil {
return errors.Wrap(err, "failed to obtain proposer duties")
}
c.results.Duties = make([]*apiv1.ProposerDuty, 0, 1)
for _, duty := range duties {
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")
@@ -46,7 +81,12 @@ func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}

View File

@@ -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,22 +33,57 @@ 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,
}
func persistentPreRunE(cmd *cobra.Command, args []string) error {
// 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
return nil
@@ -63,11 +98,14 @@ func persistentPreRunE(cmd *cobra.Command, args []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, args []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,12 @@ func init() {
}
cobra.OnInitialize(initConfig)
addPersistentFlags()
}
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 +159,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 +212,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 +224,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)
}

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

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

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

@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(slotCmd)
}
func slotFlags(cmd *cobra.Command) {
func slotFlags(_ *cobra.Command) {
}

View File

@@ -52,7 +52,12 @@ func input(ctx context.Context) (*dataIn, error) {
// Ethereum 2 client.
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
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,
})
if err != nil {
return nil, err
}

View File

@@ -67,14 +67,6 @@ func TestInput(t *testing.T) {
},
err: "slot is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
"slot": "1",
},
err: "failed to connect to any beacon node",
},
}
for _, test := range tests {

View File

@@ -29,7 +29,7 @@ type dataOut struct {
endTime time.Time
}
func output(ctx context.Context, data *dataOut) (string, error) {
func output(_ context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}

View File

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

@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(synccommitteeCmd)
}
func synccommitteeFlags(cmd *cobra.Command) {
func synccommitteeFlags(_ *cobra.Command) {
}

View File

@@ -18,6 +18,7 @@ import (
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
@@ -34,22 +35,21 @@ type command struct {
allowInsecureConnections bool
// Input.
account string
pubKey string
index string
epoch int64
validator string
epochStr string
// Data access.
eth2Client eth2client.Service
chainTime chaintime.Service
// Output.
epoch phase0.Epoch
inCommittee bool
committeeIndex uint64
inclusions []int
}
func newCommand(ctx context.Context) (*command, error) {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
@@ -67,15 +67,13 @@ func newCommand(ctx context.Context) (*command, error) {
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
// Validator.
c.account = viper.GetString("account")
c.pubKey = viper.GetString("pubkey")
c.index = viper.GetString("index")
if c.account == "" && c.pubKey == "" && c.index == "" {
return nil, errors.New("account, pubkey or index required")
c.validator = viper.GetString("validator")
if c.validator == "" {
return nil, errors.New("validator is required")
}
// Epoch.
c.epoch = viper.GetInt64("epoch")
c.epochStr = viper.GetString("epoch")
return c, nil
}

View File

@@ -43,14 +43,14 @@ func TestInput(t *testing.T) {
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "account, pubkey or index required",
err: "validator is required",
},
{
name: "Good",
vars: map[string]interface{}{
"validators": "1",
"timeout": "5s",
"index": "1",
"validator": "1",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},

View File

@@ -19,7 +19,7 @@ import (
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
func (c *command) output(_ context.Context) (string, error) {
if c.quiet {
return "", 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
@@ -20,7 +20,6 @@ import (
eth2client "github.com/attestantio/go-eth2-client"
"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"
"github.com/wealdtech/ethdo/util"
@@ -32,14 +31,17 @@ func (c *command) process(ctx context.Context) error {
return err
}
firstSlot, lastSlot := c.calculateSlots(ctx)
validatorIndex, err := util.ValidatorIndex(ctx, c.eth2Client, c.account, c.pubKey, c.index)
validator, err := util.ParseValidator(ctx, c.eth2Client.(eth2client.ValidatorsProvider), c.validator, "head")
if err != nil {
return err
}
syncCommittee, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", phase0.Epoch(c.epoch))
c.epoch, err = util.ParseEpoch(ctx, c.chainTime, c.epochStr)
if err != nil {
return err
}
syncCommittee, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", c.epoch)
if err != nil {
return errors.Wrap(err, "failed to obtain sync committee information")
}
@@ -49,7 +51,7 @@ func (c *command) process(ctx context.Context) error {
}
for i := range syncCommittee.Validators {
if syncCommittee.Validators[i] == validatorIndex {
if syncCommittee.Validators[i] == validator.Index {
c.inCommittee = true
c.committeeIndex = uint64(i)
break
@@ -57,12 +59,14 @@ func (c *command) process(ctx context.Context) error {
}
if c.inCommittee {
firstSlot := c.chainTime.FirstSlotOfEpoch(c.epoch)
lastSlot := c.chainTime.LastSlotOfEpoch(c.epoch)
// This validator is in the sync committee. Check blocks to see where it has been included.
c.inclusions = make([]int, 0)
if lastSlot > c.chainTime.CurrentSlot() {
lastSlot = c.chainTime.CurrentSlot()
}
for slot := firstSlot; slot < lastSlot; slot++ {
for slot := firstSlot; slot <= lastSlot; slot++ {
block, err := c.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return err
@@ -94,6 +98,13 @@ 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)
}
default:
return fmt.Errorf("unhandled block version %v", block.Version)
}
@@ -107,7 +118,12 @@ func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
})
if err != nil {
return err
}
@@ -122,15 +138,3 @@ func (c *command) setup(ctx context.Context) error {
return nil
}
func (c *command) calculateSlots(ctx context.Context) (phase0.Slot, phase0.Slot) {
var firstSlot phase0.Slot
var lastSlot phase0.Slot
if c.epoch == -1 {
c.epoch = int64(c.chainTime.CurrentEpoch()) - 1
}
firstSlot = c.chainTime.FirstSlotOfEpoch(phase0.Epoch(c.epoch))
lastSlot = c.chainTime.FirstSlotOfEpoch(phase0.Epoch(c.epoch) + 1)
return firstSlot, lastSlot
}

View File

@@ -32,28 +32,20 @@ func TestProcess(t *testing.T) {
vars map[string]interface{}
err string
}{
{
name: "MissingConnection",
vars: map[string]interface{}{
"timeout": "5s",
"index": "1",
},
err: "failed to connect to any beacon node",
},
{
name: "InvalidConnection",
vars: map[string]interface{}{
"timeout": "5s",
"index": "1",
"validator": "1",
"connection": "invalid",
},
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://invalid/eth/v1/beacon/genesis\": dial tcp: lookup invalid: no such host",
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://invalid/eth/v1/beacon/genesis\": dial tcp: lookup invalid on 127.0.0.53:53: no such host",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"index": "1",
"validator": "1",
"epoch": "-1",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},

View File

@@ -34,7 +34,7 @@ type dataIn struct {
// Operation.
eth2Client eth2client.Service
chainTime chaintime.Service
epoch int64
epoch string
period string
}
@@ -51,7 +51,12 @@ func input(ctx context.Context) (*dataIn, error) {
// Ethereum 2 client.
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
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,
})
if err != nil {
return nil, err
}
@@ -66,7 +71,7 @@ func input(ctx context.Context) (*dataIn, error) {
}
// Epoch
data.epoch = viper.GetInt64("epoch")
data.epoch = viper.GetString("epoch")
data.period = viper.GetString("period")
return data, nil

View File

@@ -60,13 +60,6 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to connect to any beacon node",
},
{
name: "ConnectionInvalid",
vars: map[string]interface{}{

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