mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-11 06:58:02 -05:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ad8e7afe4 | ||
|
|
1cffa7051d | ||
|
|
0e089bc8ff | ||
|
|
c6f90a69af | ||
|
|
58c3a7e279 | ||
|
|
cd1bf4dcfc | ||
|
|
0a24f4ffe6 | ||
|
|
77dd02a8f1 | ||
|
|
f4c99ac2b1 | ||
|
|
062b968055 | ||
|
|
8083dd7eeb | ||
|
|
841ba8e8f0 | ||
|
|
32a34fb9c1 | ||
|
|
e356e9e1b7 | ||
|
|
82f5200296 | ||
|
|
36ddb1a2fe | ||
|
|
d4791ac8bd | ||
|
|
fff6469748 | ||
|
|
b11f9cf3a3 | ||
|
|
9d50a72270 | ||
|
|
9191cac389 | ||
|
|
f1a1d4f64c | ||
|
|
1f1ddc1719 | ||
|
|
92d964f092 | ||
|
|
972e35f7d7 | ||
|
|
5f4d7a389d | ||
|
|
696ba64238 | ||
|
|
aeb8142b27 | ||
|
|
a1ea298983 | ||
|
|
1c23e7cc54 | ||
|
|
b53e42aeb7 | ||
|
|
fa325e20f8 | ||
|
|
0ce563470f | ||
|
|
abd3567d05 | ||
|
|
399d0eaa64 | ||
|
|
655b9cbbc8 | ||
|
|
50c22818cb | ||
|
|
4991787b9d | ||
|
|
b652d2e083 | ||
|
|
ef58db3307 | ||
|
|
a95851dc9e | ||
|
|
ac7e0985fb | ||
|
|
7837165d46 | ||
|
|
46020396e4 | ||
|
|
105de0a139 | ||
|
|
a7da10360e | ||
|
|
d5351cbe7f | ||
|
|
874839754c | ||
|
|
4db0cdae3a | ||
|
|
6f0f3e4c91 | ||
|
|
713bbdd60c | ||
|
|
46ca70a615 | ||
|
|
2f24bb7884 | ||
|
|
9136c053b1 | ||
|
|
8efab62f8b | ||
|
|
7d723148ab | ||
|
|
2880ec9bdd | ||
|
|
871d1694ef | ||
|
|
f26c9e9c4a | ||
|
|
b28d5b2693 | ||
|
|
695f62bbd5 | ||
|
|
0b8de1f615 | ||
|
|
c1e5f1dd23 | ||
|
|
2e4337fe6d | ||
|
|
dbe45d5c27 | ||
|
|
c963ea8ba5 | ||
|
|
484361c034 | ||
|
|
d65f51d5af | ||
|
|
7857e97057 | ||
|
|
545665a79f | ||
|
|
394fd2bb04 | ||
|
|
a7631a6a7f | ||
|
|
d174219ddc | ||
|
|
854f3061b9 | ||
|
|
6c34d25ebb | ||
|
|
df34cef2bd | ||
|
|
e8513e60b2 | ||
|
|
4aadee3fad |
58
.github/workflows/docker.yml
vendored
Normal file
58
.github/workflows/docker.yml
vendored
Normal 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 }}
|
||||
@@ -128,6 +128,7 @@ linters:
|
||||
- contextcheck
|
||||
- cyclop
|
||||
- deadcode
|
||||
- depguard
|
||||
- dupl
|
||||
- errorlint
|
||||
- exhaustive
|
||||
|
||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,3 +1,50 @@
|
||||
1.35.0:
|
||||
- support Deneb
|
||||
- add start and end dates for eth1votes period
|
||||
|
||||
1.34.1:
|
||||
- fix period parsing for "synccommittee members" command
|
||||
|
||||
1.34.0:
|
||||
- update dependencies
|
||||
- use Capella fork for all exits
|
||||
- support Deneb beta 5
|
||||
|
||||
1.33.2:
|
||||
- fix windows build
|
||||
|
||||
1.33.1:
|
||||
- add "slot" to "proposer duties" command
|
||||
- add activation epoch and time to "validator info" command where applicable
|
||||
- add "holesky" to the list of supported networks
|
||||
- avoid crash when requesting validators from beacon node without debug enabled
|
||||
|
||||
1.33.0:
|
||||
- show all slots with 'synccommittee inclusion'
|
||||
- add "wallet batch" command
|
||||
|
||||
1.32.0:
|
||||
- fix incorrect error when "deposit verify" is not given a withdrawal address
|
||||
- allow truncated mnemonics (first four characters of each word)
|
||||
- add deneb information to "block info"
|
||||
- add epoch parameter to "validator yield"
|
||||
- add proposer index to "block info"
|
||||
- "block info" honours "--quiet" flag
|
||||
- "block info" accepts "--block-time" option
|
||||
- increase default operation timeout from 10s to 30s
|
||||
- "epoch summary" JSON lists number of blobs
|
||||
|
||||
1.31.0:
|
||||
- initial support for deneb
|
||||
- add "--generate-keystore" option for "account derive"
|
||||
- update "validator exit" command to be able to generate multiple exits
|
||||
- support for 12-word and 18-word mnemonics with single-word (no whitespace) passphrases
|
||||
- add JSON output for "validator expectation"
|
||||
|
||||
1.30.0:
|
||||
- add "chain spec" command
|
||||
- add "validator withdrawal" command
|
||||
|
||||
1.29.2:
|
||||
- fix regression where validator index could not be used as an account specifier
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.20-bullseye as builder
|
||||
FROM golang:1.20-bookworm as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -10,7 +10,7 @@ COPY . .
|
||||
|
||||
RUN go build
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
11
README.md
11
README.md
@@ -38,7 +38,7 @@ docker pull wealdtech/ethdo
|
||||
go install github.com/wealdtech/ethdo@latest
|
||||
```
|
||||
|
||||
Note that `ethdo` requires at least version 1.13 of go to operate. The version of go can be found with `go version`.
|
||||
Note that `ethdo` requires at least version 1.20 of go to operate. The version of go can be found with `go version`.
|
||||
|
||||
If this does not work please see the [troubleshooting](https://github.com/wealdtech/ethdo/blob/master/docs/troubleshooting.md) page.
|
||||
|
||||
@@ -171,6 +171,15 @@ If set, the `--debug` argument will output additional information about the oper
|
||||
|
||||
Commands will have an exit status of 0 on success and 1 on failure. The specific definition of success is specified in the help for each command.
|
||||
|
||||
### Validator specifier
|
||||
|
||||
Ethereum validators can be specified in a number of different ways. The options are:
|
||||
|
||||
- an `ethdo` account, in the format _wallet_/_account_. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied, with a passphrase (for local accounts) or authority (for remote accounts)
|
||||
- the validator's 48-byte public key. It is not possible to use the a validator specified in this way to sign validator-related operations
|
||||
- a keystore, supplied either as direct JSON or as a path to a keystore on the local filesystem. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied
|
||||
- the validator's numeric index. It is not possible to use a validator specified in this way to sign validator-related operations. Note that this only works with on-chain operations, as the validator's index must be resolved to its public key
|
||||
|
||||
## Passphrase strength
|
||||
|
||||
`ethdo` will by default not allow creation or export of accounts or wallets with weak passphrases. If a weak pasphrase is used then `ethdo` will refuse to continue.
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"strings"
|
||||
|
||||
consensusclient "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
@@ -34,6 +35,7 @@ type ChainInfo struct {
|
||||
GenesisValidatorsRoot phase0.Root
|
||||
Epoch phase0.Epoch
|
||||
GenesisForkVersion phase0.Version
|
||||
ExitForkVersion phase0.Version
|
||||
CurrentForkVersion phase0.Version
|
||||
BLSToExecutionChangeDomainType phase0.DomainType
|
||||
VoluntaryExitDomainType phase0.DomainType
|
||||
@@ -45,6 +47,7 @@ type chainInfoJSON struct {
|
||||
GenesisValidatorsRoot string `json:"genesis_validators_root"`
|
||||
Epoch string `json:"epoch"`
|
||||
GenesisForkVersion string `json:"genesis_fork_version"`
|
||||
ExitForkVersion string `json:"exit_fork_version"`
|
||||
CurrentForkVersion string `json:"current_fork_version"`
|
||||
BLSToExecutionChangeDomainType string `json:"bls_to_execution_change_domain_type"`
|
||||
VoluntaryExitDomainType string `json:"voluntary_exit_domain_type"`
|
||||
@@ -57,11 +60,12 @@ type chainInfoVersionJSON struct {
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (c *ChainInfo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&chainInfoJSON{
|
||||
Version: fmt.Sprintf("%d", c.Version),
|
||||
Version: strconv.FormatUint(c.Version, 10),
|
||||
Validators: c.Validators,
|
||||
GenesisValidatorsRoot: fmt.Sprintf("%#x", c.GenesisValidatorsRoot),
|
||||
Epoch: fmt.Sprintf("%d", c.Epoch),
|
||||
GenesisForkVersion: fmt.Sprintf("%#x", c.GenesisForkVersion),
|
||||
ExitForkVersion: fmt.Sprintf("%#x", c.ExitForkVersion),
|
||||
CurrentForkVersion: fmt.Sprintf("%#x", c.CurrentForkVersion),
|
||||
BLSToExecutionChangeDomainType: fmt.Sprintf("%#x", c.BLSToExecutionChangeDomainType),
|
||||
VoluntaryExitDomainType: fmt.Sprintf("%#x", c.VoluntaryExitDomainType),
|
||||
@@ -82,7 +86,7 @@ func (c *ChainInfo) UnmarshalJSON(input []byte) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "version invalid")
|
||||
}
|
||||
if version < 2 {
|
||||
if version < 3 {
|
||||
return errors.New("outdated version; please regenerate your offline data")
|
||||
}
|
||||
c.Version = version
|
||||
@@ -130,6 +134,18 @@ func (c *ChainInfo) UnmarshalJSON(input []byte) error {
|
||||
}
|
||||
copy(c.GenesisForkVersion[:], genesisForkVersionBytes)
|
||||
|
||||
if data.ExitForkVersion == "" {
|
||||
return errors.New("exit fork version missing")
|
||||
}
|
||||
exitForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.ExitForkVersion, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "exit fork version invalid")
|
||||
}
|
||||
if len(exitForkVersionBytes) != phase0.ForkVersionLength {
|
||||
return errors.New("exit fork version incorrect length")
|
||||
}
|
||||
copy(c.ExitForkVersion[:], exitForkVersionBytes)
|
||||
|
||||
if data.CurrentForkVersion == "" {
|
||||
return errors.New("current fork version missing")
|
||||
}
|
||||
@@ -235,18 +251,18 @@ func ObtainChainInfoFromNode(ctx context.Context,
|
||||
error,
|
||||
) {
|
||||
res := &ChainInfo{
|
||||
Version: 2,
|
||||
Version: 3,
|
||||
Validators: make([]*ValidatorInfo, 0),
|
||||
Epoch: chainTime.CurrentEpoch(),
|
||||
}
|
||||
|
||||
// Obtain validators.
|
||||
validators, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, "head", nil)
|
||||
validatorsResponse, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{State: "head"})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain validators")
|
||||
}
|
||||
|
||||
for _, validator := range validators {
|
||||
for _, validator := range validatorsResponse.Data {
|
||||
res.Validators = append(res.Validators, &ValidatorInfo{
|
||||
Index: validator.Index,
|
||||
Pubkey: validator.Validator.PublicKey,
|
||||
@@ -256,20 +272,20 @@ func ObtainChainInfoFromNode(ctx context.Context,
|
||||
}
|
||||
|
||||
// Genesis validators root obtained from beacon node.
|
||||
genesis, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx)
|
||||
genesisResponse, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain genesis information")
|
||||
}
|
||||
res.GenesisValidatorsRoot = genesis.GenesisValidatorsRoot
|
||||
res.GenesisValidatorsRoot = genesisResponse.Data.GenesisValidatorsRoot
|
||||
|
||||
// Fetch the genesis fork version from the specification.
|
||||
spec, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx)
|
||||
specResponse, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain spec")
|
||||
}
|
||||
tmp, exists := spec["GENESIS_FORK_VERSION"]
|
||||
tmp, exists := specResponse.Data["GENESIS_FORK_VERSION"]
|
||||
if !exists {
|
||||
return nil, errors.New("capella fork version not known by chain")
|
||||
return nil, errors.New("genesis fork version not known by chain")
|
||||
}
|
||||
var isForkVersion bool
|
||||
res.GenesisForkVersion, isForkVersion = tmp.(phase0.Version)
|
||||
@@ -277,24 +293,34 @@ func ObtainChainInfoFromNode(ctx context.Context,
|
||||
return nil, errors.New("could not obtain GENESIS_FORK_VERSION")
|
||||
}
|
||||
|
||||
// Fetch the exit fork version (Capella) from the specification.
|
||||
tmp, exists = specResponse.Data["CAPELLA_FORK_VERSION"]
|
||||
if !exists {
|
||||
return nil, errors.New("capella fork version not known by chain")
|
||||
}
|
||||
res.ExitForkVersion, isForkVersion = tmp.(phase0.Version)
|
||||
if !isForkVersion {
|
||||
return nil, errors.New("could not obtain CAPELLA_FORK_VERSION")
|
||||
}
|
||||
|
||||
// Fetch the current fork version from the fork schedule.
|
||||
forkSchedule, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx)
|
||||
forkScheduleResponse, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx, &api.ForkScheduleOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain fork schedule")
|
||||
}
|
||||
for i := range forkSchedule {
|
||||
if forkSchedule[i].Epoch <= res.Epoch {
|
||||
res.CurrentForkVersion = forkSchedule[i].CurrentVersion
|
||||
for i := range forkScheduleResponse.Data {
|
||||
if forkScheduleResponse.Data[i].Epoch <= res.Epoch {
|
||||
res.CurrentForkVersion = forkScheduleResponse.Data[i].CurrentVersion
|
||||
}
|
||||
}
|
||||
|
||||
blsToExecutionChangeDomainType, exists := spec["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
|
||||
blsToExecutionChangeDomainType, exists := specResponse.Data["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
|
||||
if !exists {
|
||||
return nil, errors.New("failed to obtain DOMAIN_BLS_TO_EXECUTION_CHANGE")
|
||||
}
|
||||
copy(res.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType[:])
|
||||
|
||||
voluntaryExitDomainType, exists := spec["DOMAIN_VOLUNTARY_EXIT"].(phase0.DomainType)
|
||||
voluntaryExitDomainType, exists := specResponse.Data["DOMAIN_VOLUNTARY_EXIT"].(phase0.DomainType)
|
||||
if !exists {
|
||||
return nil, errors.New("failed to obtain DOMAIN_VOLUNTARY_EXIT")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// Copyright © 2020, 2023 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -28,6 +28,7 @@ type dataIn struct {
|
||||
// Output options.
|
||||
showPrivateKey bool
|
||||
showWithdrawalCredentials bool
|
||||
generateKeystore bool
|
||||
}
|
||||
|
||||
func input(_ context.Context) (*dataIn, error) {
|
||||
@@ -54,5 +55,8 @@ func input(_ context.Context) (*dataIn, error) {
|
||||
// Show withdrawal credentials.
|
||||
data.showWithdrawalCredentials = viper.GetBool("show-withdrawal-credentials")
|
||||
|
||||
// Generate keystore.
|
||||
data.generateKeystore = viper.GetBool("generate-keystore")
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// Copyright © 2020, 2023 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -15,21 +15,30 @@ package accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
util "github.com/wealdtech/go-eth2-util"
|
||||
ethutil "github.com/wealdtech/go-eth2-util"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
showPrivateKey bool
|
||||
showWithdrawalCredentials bool
|
||||
generateKeystore bool
|
||||
key *e2types.BLSPrivateKey
|
||||
path string
|
||||
}
|
||||
|
||||
func output(_ context.Context, data *dataOut) (string, error) {
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
@@ -37,13 +46,17 @@ func output(_ context.Context, data *dataOut) (string, error) {
|
||||
return "", errors.New("no key")
|
||||
}
|
||||
|
||||
if data.generateKeystore {
|
||||
return outputKeystore(ctx, data)
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
|
||||
if data.showPrivateKey {
|
||||
builder.WriteString(fmt.Sprintf("Private key: %#x\n", data.key.Marshal()))
|
||||
}
|
||||
if data.showWithdrawalCredentials {
|
||||
withdrawalCredentials := util.SHA256(data.key.PublicKey().Marshal())
|
||||
withdrawalCredentials := ethutil.SHA256(data.key.PublicKey().Marshal())
|
||||
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
|
||||
builder.WriteString(fmt.Sprintf("Withdrawal credentials: %#x\n", withdrawalCredentials))
|
||||
}
|
||||
@@ -53,3 +66,38 @@ func output(_ context.Context, data *dataOut) (string, error) {
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func outputKeystore(_ context.Context, data *dataOut) (string, error) {
|
||||
passphrase, err := util.GetPassphrase()
|
||||
if err != nil {
|
||||
return "", errors.New("no passphrase supplied")
|
||||
}
|
||||
|
||||
encryptor := keystorev4.New()
|
||||
crypto, err := encryptor.Encrypt(data.key.Marshal(), passphrase)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to encrypt private key")
|
||||
}
|
||||
|
||||
uuid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return "", errors.New("failed to generate UUID")
|
||||
}
|
||||
ks := make(map[string]interface{})
|
||||
ks["uuid"] = uuid.String()
|
||||
ks["pubkey"] = hex.EncodeToString(data.key.PublicKey().Marshal())
|
||||
ks["version"] = 4
|
||||
ks["path"] = data.path
|
||||
ks["crypto"] = crypto
|
||||
out, err := json.Marshal(ks)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal keystore JSON")
|
||||
}
|
||||
|
||||
keystoreFilename := fmt.Sprintf("keystore-%s-%d.json", strings.ReplaceAll(data.path, "/", "_"), time.Now().Unix())
|
||||
|
||||
if err := os.WriteFile(keystoreFilename, out, 0o600); err != nil {
|
||||
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", keystoreFilename))
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -72,7 +72,7 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
|
||||
data.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set up chaintime service")
|
||||
|
||||
@@ -17,7 +17,8 @@ import (
|
||||
"context"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
@@ -49,12 +50,16 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch) (*api.AttesterDuty, error) {
|
||||
func duty(ctx context.Context, eth2Client eth2client.Service, validator *apiv1.Validator, epoch spec.Epoch) (*apiv1.AttesterDuty, error) {
|
||||
// Find the attesting slot for the given epoch.
|
||||
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
|
||||
dutiesResponse, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{
|
||||
Epoch: epoch,
|
||||
Indices: []spec.ValidatorIndex{validator.Index},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain attester duties")
|
||||
}
|
||||
duties := dutiesResponse.Data
|
||||
|
||||
if len(duties) == 0 {
|
||||
return nil, errors.New("validator does not have duty for that epoch")
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestProcess(t *testing.T) {
|
||||
|
||||
chainTime, err := standardchaintime.New(context.Background(),
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
|
||||
data.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set up chaintime service")
|
||||
|
||||
@@ -16,6 +16,7 @@ package attesterinclusion
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
@@ -49,7 +50,7 @@ func output(_ context.Context, data *dataOut) (string, error) {
|
||||
buf.WriteString("Attestation included in block ")
|
||||
buf.WriteString(fmt.Sprintf("%d", data.slot))
|
||||
buf.WriteString(", index ")
|
||||
buf.WriteString(fmt.Sprintf("%d", data.attestationIndex))
|
||||
buf.WriteString(strconv.FormatUint(data.attestationIndex, 10))
|
||||
if data.verbose {
|
||||
buf.WriteString("\nInclusion delay: ")
|
||||
buf.WriteString(fmt.Sprintf("%d", data.inclusionDelay))
|
||||
|
||||
@@ -19,7 +19,8 @@ import (
|
||||
"fmt"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
@@ -38,7 +39,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
|
||||
data.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set up chaintime service")
|
||||
@@ -61,14 +62,17 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
startSlot := duty.Slot + 1
|
||||
endSlot := startSlot + 32
|
||||
for slot := startSlot; slot < endSlot; slot++ {
|
||||
signedBlock, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
blockResponse, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain block")
|
||||
}
|
||||
if signedBlock == nil {
|
||||
block := blockResponse.Data
|
||||
if block == nil {
|
||||
continue
|
||||
}
|
||||
blockSlot, err := signedBlock.Slot()
|
||||
blockSlot, err := block.Slot()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain block slot")
|
||||
}
|
||||
@@ -78,7 +82,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data.debug {
|
||||
fmt.Printf("Fetched block for slot %d\n", slot)
|
||||
}
|
||||
attestations, err := signedBlock.Attestations()
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain block attestations")
|
||||
}
|
||||
@@ -121,21 +125,23 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
|
||||
slot := attestation.Data.Slot
|
||||
for {
|
||||
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if header == nil {
|
||||
if response.Data == nil {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
if !header.Canonical {
|
||||
if !response.Data.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
return bytes.Equal(header.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
|
||||
return bytes.Equal(response.Data.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,30 +149,36 @@ func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.At
|
||||
// Start with first slot of the target epoch.
|
||||
slot := data.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
|
||||
for {
|
||||
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if header == nil {
|
||||
if response.Data == nil {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
if !header.Canonical {
|
||||
if !response.Data.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
return bytes.Equal(header.Root[:], attestation.Data.Target.Root[:]), nil
|
||||
return bytes.Equal(response.Data.Root[:], attestation.Data.Target.Root[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch) (*api.AttesterDuty, error) {
|
||||
func duty(ctx context.Context, eth2Client eth2client.Service, validator *apiv1.Validator, epoch phase0.Epoch) (*apiv1.AttesterDuty, error) {
|
||||
// Find the attesting slot for the given epoch.
|
||||
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []phase0.ValidatorIndex{validator.Index})
|
||||
dutiesResponse, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{
|
||||
Epoch: epoch,
|
||||
Indices: []phase0.ValidatorIndex{validator.Index},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain attester duties")
|
||||
}
|
||||
duties := dutiesResponse.Data
|
||||
|
||||
if len(duties) == 0 {
|
||||
return nil, errors.New("validator does not have duty for that epoch")
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestProcess(t *testing.T) {
|
||||
|
||||
chainTime, err := standardchaintime.New(context.Background(),
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -49,17 +49,13 @@ func init() {
|
||||
attesterFlags(attesterDutiesCmd)
|
||||
attesterDutiesCmd.Flags().String("epoch", "head", "the epoch for which to obtain the duties")
|
||||
attesterDutiesCmd.Flags().String("validator", "", "the index, public key, or acount of the validator")
|
||||
attesterDutiesCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network")
|
||||
}
|
||||
|
||||
func attesterDutiesBindings() {
|
||||
if err := viper.BindPFlag("epoch", attesterDutiesCmd.Flags().Lookup("epoch")); err != nil {
|
||||
func attesterDutiesBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("validator", attesterDutiesCmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", attesterDutiesCmd.Flags().Lookup("json")); err != nil {
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +52,14 @@ func init() {
|
||||
attesterInclusionCmd.Flags().String("index", "", "the index of the attester")
|
||||
}
|
||||
|
||||
func attesterInclusionBindings() {
|
||||
if err := viper.BindPFlag("epoch", attesterInclusionCmd.Flags().Lookup("epoch")); err != nil {
|
||||
func attesterInclusionBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("validator", attesterInclusionCmd.Flags().Lookup("validator")); err != nil {
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("index", attesterInclusionCmd.Flags().Lookup("index")); err != nil {
|
||||
if err := viper.BindPFlag("index", cmd.Flags().Lookup("index")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -82,26 +83,26 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
|
||||
for i, attestation := range c.analysis.Attestations {
|
||||
if c.verbose {
|
||||
builder.WriteString("Attestation ")
|
||||
builder.WriteString(fmt.Sprintf("%d", i))
|
||||
builder.WriteString(strconv.Itoa(i))
|
||||
builder.WriteString(": ")
|
||||
builder.WriteString("distance ")
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.Distance))
|
||||
builder.WriteString(strconv.Itoa(attestation.Distance))
|
||||
builder.WriteString(", ")
|
||||
|
||||
if attestation.Duplicate != nil {
|
||||
builder.WriteString("duplicate of attestation ")
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Index))
|
||||
builder.WriteString(strconv.Itoa(attestation.Duplicate.Index))
|
||||
builder.WriteString(" in block ")
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Block))
|
||||
builder.WriteString("\n")
|
||||
continue
|
||||
}
|
||||
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.NewVotes))
|
||||
builder.WriteString(strconv.Itoa(attestation.NewVotes))
|
||||
builder.WriteString("/")
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.Votes))
|
||||
builder.WriteString(strconv.Itoa(attestation.Votes))
|
||||
builder.WriteString("/")
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.PossibleVotes))
|
||||
builder.WriteString(strconv.Itoa(attestation.PossibleVotes))
|
||||
builder.WriteString(" new/total/possible votes")
|
||||
if attestation.NewVotes == 0 {
|
||||
builder.WriteString("\n")
|
||||
@@ -137,7 +138,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
|
||||
if c.analysis.SyncCommitee.Contributions > 0 {
|
||||
if c.verbose {
|
||||
builder.WriteString("Sync committee contributions: ")
|
||||
builder.WriteString(fmt.Sprintf("%d", c.analysis.SyncCommitee.Contributions))
|
||||
builder.WriteString(strconv.Itoa(c.analysis.SyncCommitee.Contributions))
|
||||
builder.WriteString(" contributions, score ")
|
||||
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.SyncCommitee.Score))
|
||||
builder.WriteString(", value ")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2022 Weald Technology Trading.
|
||||
// Copyright © 2022, 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
@@ -33,13 +34,17 @@ func (c *command) process(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
block, err := c.blocksProvider.SignedBeaconBlock(ctx, c.blockID)
|
||||
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: c.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
var apiError *api.Error
|
||||
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
|
||||
return errors.New("empty beacon block")
|
||||
}
|
||||
return errors.Wrap(err, "failed to obtain beacon block")
|
||||
}
|
||||
if block == nil {
|
||||
return errors.New("empty beacon block")
|
||||
}
|
||||
block := blockResponse.Data
|
||||
|
||||
slot, err := block.Slot()
|
||||
if err != nil {
|
||||
@@ -145,7 +150,7 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
|
||||
}
|
||||
|
||||
// Calculate head timely.
|
||||
analysis.HeadTimely = attestation.Data.Slot == slot-1
|
||||
analysis.HeadTimely = analysis.HeadCorrect && attestation.Data.Slot == slot-1
|
||||
|
||||
// Calculate source timely.
|
||||
analysis.SourceTimely = attestation.Data.Slot >= slot-5
|
||||
@@ -157,7 +162,11 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
|
||||
}
|
||||
|
||||
// Calculate target timely.
|
||||
analysis.TargetTimely = attestation.Data.Slot >= slot-32
|
||||
if block.Version < spec.DataVersionDeneb {
|
||||
analysis.TargetTimely = attestation.Data.Slot >= slot-32
|
||||
} else {
|
||||
analysis.TargetTimely = true
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate score and value.
|
||||
@@ -184,12 +193,30 @@ func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedB
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := block.Deneb.HashTreeRoot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
slot, err := block.Slot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Printf("Parent root of %#x@%d is %#x\n", root, slot, parentRoot)
|
||||
}
|
||||
|
||||
// Obtain the parent block.
|
||||
parentBlock, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%#x", parentRoot))
|
||||
parentBlockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: fmt.Sprintf("%#x", parentRoot),
|
||||
})
|
||||
if err != nil {
|
||||
var apiError *api.Error
|
||||
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
|
||||
return errors.New("empty beacon block")
|
||||
}
|
||||
return err
|
||||
}
|
||||
parentBlock := parentBlockResponse.Data
|
||||
if parentBlock == nil {
|
||||
return fmt.Errorf("unable to obtain parent block %s", parentBlock)
|
||||
}
|
||||
@@ -267,7 +294,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
@@ -289,12 +316,12 @@ func (c *command) setup(ctx context.Context) error {
|
||||
return errors.New("connection does not provide spec information")
|
||||
}
|
||||
|
||||
spec, err := specProvider.Spec(ctx)
|
||||
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain spec")
|
||||
}
|
||||
|
||||
tmp, exists := spec["TIMELY_SOURCE_WEIGHT"]
|
||||
tmp, exists := specResponse.Data["TIMELY_SOURCE_WEIGHT"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(14)
|
||||
@@ -305,7 +332,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
return errors.New("TIMELY_SOURCE_WEIGHT of unexpected type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["TIMELY_TARGET_WEIGHT"]
|
||||
tmp, exists = specResponse.Data["TIMELY_TARGET_WEIGHT"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(26)
|
||||
@@ -315,7 +342,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
return errors.New("TIMELY_TARGET_WEIGHT of unexpected type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["TIMELY_HEAD_WEIGHT"]
|
||||
tmp, exists = specResponse.Data["TIMELY_HEAD_WEIGHT"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(14)
|
||||
@@ -325,7 +352,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
return errors.New("TIMELY_HEAD_WEIGHT of unexpected type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["SYNC_REWARD_WEIGHT"]
|
||||
tmp, exists = specResponse.Data["SYNC_REWARD_WEIGHT"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(2)
|
||||
@@ -335,7 +362,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
return errors.New("SYNC_REWARD_WEIGHT of unexpected type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["PROPOSER_WEIGHT"]
|
||||
tmp, exists = specResponse.Data["PROPOSER_WEIGHT"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(8)
|
||||
@@ -345,7 +372,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
return errors.New("PROPOSER_WEIGHT of unexpected type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["WEIGHT_DENOMINATOR"]
|
||||
tmp, exists = specResponse.Data["WEIGHT_DENOMINATOR"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(64)
|
||||
@@ -362,22 +389,31 @@ func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attes
|
||||
root, exists := c.headRoots[slot]
|
||||
if !exists {
|
||||
for {
|
||||
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
var apiError *api.Error
|
||||
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
|
||||
if c.debug {
|
||||
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if header == nil {
|
||||
if response.Data == nil {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
if !header.Canonical {
|
||||
if !response.Data.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
c.headRoots[attestation.Data.Slot] = header.Root
|
||||
root = header.Root
|
||||
c.headRoots[attestation.Data.Slot] = response.Data.Root
|
||||
root = response.Data.Root
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -391,22 +427,30 @@ func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Att
|
||||
// Start with first slot of the target epoch.
|
||||
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
|
||||
for {
|
||||
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
var apiError *api.Error
|
||||
if errors.As(err, &apiError) && apiError.StatusCode == 404 {
|
||||
if c.debug {
|
||||
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if header == nil {
|
||||
if response.Data == nil {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
if !header.Canonical {
|
||||
if !response.Data.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
c.targetRoots[attestation.Data.Slot] = header.Root
|
||||
root = header.Root
|
||||
c.targetRoots[attestation.Data.Slot] = response.Data.Root
|
||||
root = response.Data.Root
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -439,6 +483,13 @@ func (c *command) analyzeSyncCommittees(_ context.Context, block *spec.Versioned
|
||||
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
|
||||
c.analysis.Value += c.analysis.SyncCommitee.Value
|
||||
return nil
|
||||
case spec.DataVersionDeneb:
|
||||
c.analysis.SyncCommitee.Contributions = int(block.Deneb.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
|
||||
c.analysis.SyncCommitee.PossibleContributions = int(block.Deneb.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
|
||||
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
|
||||
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
|
||||
c.analysis.Value += c.analysis.SyncCommitee.Value
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported block version %d", block.Version)
|
||||
}
|
||||
|
||||
@@ -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,7 +51,8 @@ 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
|
||||
@@ -64,12 +66,5 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if viper.GetString("blockid") == "" {
|
||||
data.blockID = "head"
|
||||
} else {
|
||||
// Specific slot.
|
||||
data.blockID = viper.GetString("blockid")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020, 2021 Weald Technology Trading
|
||||
// Copyright © 2019 - 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -20,14 +20,17 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
"github.com/attestantio/go-eth2-client/spec/capella"
|
||||
"github.com/attestantio/go-eth2-client/spec/deneb"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prysmaticlabs/go-bitfield"
|
||||
@@ -54,6 +57,7 @@ func output(_ context.Context, data *dataOut) (string, error) {
|
||||
func outputBlockGeneral(_ context.Context,
|
||||
verbose bool,
|
||||
slot phase0.Slot,
|
||||
proposerIndex phase0.ValidatorIndex,
|
||||
blockRoot phase0.Root,
|
||||
bodyRoot phase0.Root,
|
||||
parentRoot phase0.Root,
|
||||
@@ -69,6 +73,7 @@ func outputBlockGeneral(_ context.Context,
|
||||
res := strings.Builder{}
|
||||
|
||||
res.WriteString(fmt.Sprintf("Slot: %d\n", slot))
|
||||
res.WriteString(fmt.Sprintf("Proposing validator index: %d\n", proposerIndex))
|
||||
res.WriteString(fmt.Sprintf("Epoch: %d\n", phase0.Epoch(uint64(slot)/slotsPerEpoch)))
|
||||
res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(slot)*int64(slotDuration.Seconds()), 0)))
|
||||
res.WriteString(fmt.Sprintf("Block root: %#x\n", blockRoot))
|
||||
@@ -113,12 +118,14 @@ func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service,
|
||||
// Fetch committees for this epoch if not already obtained.
|
||||
committees, exists := validatorCommittees[att.Data.Slot]
|
||||
if !exists {
|
||||
beaconCommittees, err := beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", att.Data.Slot))
|
||||
response, err := beaconCommitteesProvider.BeaconCommittees(ctx, &api.BeaconCommitteesOpts{
|
||||
State: fmt.Sprintf("%d", att.Data.Slot),
|
||||
})
|
||||
if err != nil {
|
||||
// Failed to get it; create an empty committee to stop us continually attempting to re-fetch.
|
||||
validatorCommittees[att.Data.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
} else {
|
||||
for _, beaconCommittee := range beaconCommittees {
|
||||
for _, beaconCommittee := range response.Data {
|
||||
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
|
||||
validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
}
|
||||
@@ -163,11 +170,14 @@ func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Ser
|
||||
|
||||
res.WriteString(fmt.Sprintf(" %d:\n", i))
|
||||
res.WriteString(fmt.Sprintln(" Slashed validators:"))
|
||||
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", slashedIndices)
|
||||
response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{
|
||||
State: "head",
|
||||
Indices: slashedIndices,
|
||||
})
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain beacon committees")
|
||||
}
|
||||
for k, v := range validators {
|
||||
for k, v := range response.Data {
|
||||
res.WriteString(fmt.Sprintf(" %#x (%d)\n", v.Validator.PublicKey[:], k))
|
||||
}
|
||||
|
||||
@@ -220,11 +230,14 @@ func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Servic
|
||||
if verbose {
|
||||
for i, voluntaryExit := range voluntaryExits {
|
||||
res.WriteString(fmt.Sprintf(" %d:\n", i))
|
||||
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []phase0.ValidatorIndex{voluntaryExit.Message.ValidatorIndex})
|
||||
response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{
|
||||
State: "head",
|
||||
Indices: []phase0.ValidatorIndex{voluntaryExit.Message.ValidatorIndex},
|
||||
})
|
||||
if err != nil {
|
||||
res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err))
|
||||
} else {
|
||||
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[voluntaryExit.Message.ValidatorIndex].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
|
||||
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", response.Data[voluntaryExit.Message.ValidatorIndex].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
|
||||
res.WriteString(fmt.Sprintf(" Epoch: %d\n", voluntaryExit.Message.Epoch))
|
||||
}
|
||||
}
|
||||
@@ -240,13 +253,16 @@ func outputBlockBLSToExecutionChanges(ctx context.Context, eth2Client eth2client
|
||||
if verbose {
|
||||
for i, op := range ops {
|
||||
res.WriteString(fmt.Sprintf(" %d:\n", i))
|
||||
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []phase0.ValidatorIndex{op.Message.ValidatorIndex})
|
||||
response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{
|
||||
State: "head",
|
||||
Indices: []phase0.ValidatorIndex{op.Message.ValidatorIndex},
|
||||
})
|
||||
if err != nil {
|
||||
res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err))
|
||||
} else {
|
||||
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[op.Message.ValidatorIndex].Validator.PublicKey, op.Message.ValidatorIndex))
|
||||
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", response.Data[op.Message.ValidatorIndex].Validator.PublicKey, op.Message.ValidatorIndex))
|
||||
res.WriteString(fmt.Sprintf(" BLS public key: %#x\n", op.Message.FromBLSPubkey))
|
||||
res.WriteString(fmt.Sprintf(" Execution address: %#x\n", op.Message.ToExecutionAddress))
|
||||
res.WriteString(fmt.Sprintf(" Execution address: %s\n", op.Message.ToExecutionAddress.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,9 +278,9 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
|
||||
if verbose {
|
||||
specProvider, isProvider := eth2Client.(eth2client.SpecProvider)
|
||||
if isProvider {
|
||||
config, err := specProvider.Spec(ctx)
|
||||
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
|
||||
if err == nil {
|
||||
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
|
||||
slotsPerEpoch := specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
|
||||
|
||||
res.WriteString(" Contributions: ")
|
||||
res.WriteString(bitvectorToString(syncAggregate.SyncCommitteeBits))
|
||||
@@ -272,14 +288,16 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
|
||||
|
||||
syncCommitteesProvider, isProvider := eth2Client.(eth2client.SyncCommitteesProvider)
|
||||
if isProvider {
|
||||
syncCommittee, err := syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", uint64(epoch)*slotsPerEpoch))
|
||||
syncCommitteeResponse, err := syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{
|
||||
State: strconv.FormatUint(uint64(epoch)*slotsPerEpoch, 10),
|
||||
})
|
||||
if err != nil {
|
||||
res.WriteString(fmt.Sprintf(" Error: failed to obtain sync committee: %v\n", err))
|
||||
} else {
|
||||
res.WriteString(" Contributing validators:")
|
||||
for i := uint64(0); i < syncAggregate.SyncCommitteeBits.Len(); i++ {
|
||||
if syncAggregate.SyncCommitteeBits.BitAt(i) {
|
||||
res.WriteString(fmt.Sprintf(" %d", syncCommittee.Validators[i]))
|
||||
res.WriteString(fmt.Sprintf(" %d", syncCommitteeResponse.Data.Validators[i]))
|
||||
}
|
||||
}
|
||||
res.WriteString("\n")
|
||||
@@ -314,6 +332,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 +407,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 +539,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 +630,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 +714,7 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
|
||||
tmp, err := outputBlockGeneral(ctx,
|
||||
data.verbose,
|
||||
signedBlock.Message.Slot,
|
||||
signedBlock.Message.ProposerIndex,
|
||||
blockRoot,
|
||||
bodyRoot,
|
||||
signedBlock.Message.ParentRoot,
|
||||
@@ -676,7 +808,7 @@ func outputCapellaBlockExecutionPayload(_ context.Context,
|
||||
res.WriteString(" Parent hash: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
|
||||
res.WriteString(" Fee recipient: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
|
||||
res.WriteString(payload.FeeRecipient.String())
|
||||
res.WriteString(" Gas limit: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
|
||||
res.WriteString(" Gas used: ")
|
||||
@@ -706,6 +838,100 @@ func outputCapellaBlockExecutionPayload(_ context.Context,
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputDenebBlockExecutionPayload(_ context.Context,
|
||||
verbose bool,
|
||||
payload *deneb.ExecutionPayload,
|
||||
) (
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
if payload == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// If the block number is 0 then we're before the merge.
|
||||
if payload.BlockNumber == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
res := strings.Builder{}
|
||||
if !verbose {
|
||||
res.WriteString("Execution block number: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
|
||||
res.WriteString("Transactions: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
|
||||
} else {
|
||||
res.WriteString("Execution payload:\n")
|
||||
res.WriteString(" Execution block number: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
|
||||
res.WriteString(" Base fee per gas: ")
|
||||
res.WriteString(string2eth.WeiToString(payload.BaseFeePerGas.ToBig(), true))
|
||||
res.WriteString("\n Block hash: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
|
||||
res.WriteString(" Parent hash: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
|
||||
res.WriteString(" Fee recipient: ")
|
||||
res.WriteString(payload.FeeRecipient.String())
|
||||
res.WriteString(" Gas limit: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
|
||||
res.WriteString(" Gas used: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
|
||||
res.WriteString(" Timestamp: ")
|
||||
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
|
||||
res.WriteString(" Prev RANDAO: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
|
||||
res.WriteString(" Receipts root: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
|
||||
res.WriteString(" State root: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
|
||||
res.WriteString(" Extra data: ")
|
||||
if utf8.Valid(payload.ExtraData) {
|
||||
res.WriteString(fmt.Sprintf("%s\n", string(payload.ExtraData)))
|
||||
} else {
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
|
||||
}
|
||||
res.WriteString(" Logs bloom: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
|
||||
res.WriteString(" Transactions: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
|
||||
res.WriteString(" Withdrawals: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(payload.Withdrawals)))
|
||||
res.WriteString(" Excess blob gas: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.ExcessBlobGas))
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputDenebBlobInfo(_ context.Context,
|
||||
verbose bool,
|
||||
body *deneb.BeaconBlockBody,
|
||||
blobs []*deneb.BlobSidecar,
|
||||
) (
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
if body == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !verbose {
|
||||
return fmt.Sprintf("Blobs: %d\n", len(body.BlobKZGCommitments)), nil
|
||||
}
|
||||
|
||||
res := strings.Builder{}
|
||||
|
||||
for i, blob := range blobs {
|
||||
if i == 0 {
|
||||
res.WriteString("Blobs\n")
|
||||
}
|
||||
res.WriteString(fmt.Sprintf(" Index: %d\n", blob.Index))
|
||||
res.WriteString(fmt.Sprintf(" KZG commitment: %s\n", body.BlobKZGCommitments[i].String()))
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBellatrixBlockExecutionPayload(_ context.Context,
|
||||
verbose bool,
|
||||
payload *bellatrix.ExecutionPayload,
|
||||
@@ -744,7 +970,7 @@ func outputBellatrixBlockExecutionPayload(_ context.Context,
|
||||
res.WriteString(" Parent hash: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
|
||||
res.WriteString(" Fee recipient: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
|
||||
res.WriteString(payload.FeeRecipient.String())
|
||||
res.WriteString(" Gas limit: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
|
||||
res.WriteString(" Gas used: ")
|
||||
|
||||
@@ -17,16 +17,22 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
"github.com/attestantio/go-eth2-client/spec/capella"
|
||||
"github.com/attestantio/go-eth2-client/spec/deneb"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -39,8 +45,8 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
if data.blockID == "" {
|
||||
return nil, errors.New("no block ID")
|
||||
if data.blockID == "" && data.blockTime == "" {
|
||||
return nil, errors.New("no block ID or block time")
|
||||
}
|
||||
|
||||
results = &dataOut{
|
||||
@@ -49,40 +55,70 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
eth2Client: data.eth2Client,
|
||||
}
|
||||
|
||||
config, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
specResponse, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
|
||||
}
|
||||
genesis, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
genesisResponse, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
|
||||
}
|
||||
genesis := genesisResponse.Data
|
||||
results.genesisTime = genesis.GenesisTime
|
||||
results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration)
|
||||
results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
|
||||
results.slotDuration = specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
|
||||
results.slotsPerEpoch = specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
|
||||
|
||||
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, data.blockID)
|
||||
if data.blockTime != "" {
|
||||
data.blockID, err = timeToBlockID(ctx, data.eth2Client, data.blockTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
var apiErr *api.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
|
||||
if data.quiet {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil, errors.New("empty beacon block")
|
||||
}
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon block")
|
||||
}
|
||||
if signedBlock == nil {
|
||||
return nil, errors.New("empty beacon block")
|
||||
block := blockResponse.Data
|
||||
if data.quiet {
|
||||
os.Exit(0)
|
||||
}
|
||||
switch signedBlock.Version {
|
||||
|
||||
switch block.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
if err := outputPhase0Block(ctx, data.jsonOutput, signedBlock.Phase0); err != nil {
|
||||
if err := outputPhase0Block(ctx, data.jsonOutput, block.Phase0); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
case spec.DataVersionAltair:
|
||||
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Altair); err != nil {
|
||||
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, block.Altair); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
case spec.DataVersionBellatrix:
|
||||
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Bellatrix); err != nil {
|
||||
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, block.Bellatrix); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
case spec.DataVersionCapella:
|
||||
if err := outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Capella); err != nil {
|
||||
if err := outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, block.Capella); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
case spec.DataVersionDeneb:
|
||||
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain blob sidecars")
|
||||
}
|
||||
blobSidecars := blobSidecarsResponse.Data
|
||||
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, block.Deneb, blobSidecars); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
default:
|
||||
@@ -105,61 +141,57 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return &dataOut{}, nil
|
||||
}
|
||||
|
||||
func headEventHandler(event *api.Event) {
|
||||
func headEventHandler(event *apiv1.Event) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Only interested in head events.
|
||||
if event.Topic != "head" {
|
||||
return
|
||||
}
|
||||
|
||||
blockID := fmt.Sprintf("%#x", event.Data.(*api.HeadEvent).Block[:])
|
||||
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID)
|
||||
blockID := fmt.Sprintf("%#x", event.Data.(*apiv1.HeadEvent).Block[:])
|
||||
blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: blockID,
|
||||
})
|
||||
if err != nil {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to obtain block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if signedBlock == nil {
|
||||
block := blockResponse.Data
|
||||
if block == nil {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Println("Empty beacon block")
|
||||
}
|
||||
return
|
||||
}
|
||||
switch signedBlock.Version {
|
||||
|
||||
switch block.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err = outputPhase0Block(ctx, jsonOutput, block.Phase0)
|
||||
case spec.DataVersionAltair:
|
||||
if err := outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair); err != nil {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err = outputAltairBlock(ctx, jsonOutput, sszOutput, block.Altair)
|
||||
case spec.DataVersionBellatrix:
|
||||
if err := outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix); err != nil {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err = outputBellatrixBlock(ctx, jsonOutput, sszOutput, block.Bellatrix)
|
||||
case spec.DataVersionCapella:
|
||||
if err := outputCapellaBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Capella); err != nil {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
}
|
||||
return
|
||||
err = outputCapellaBlock(ctx, jsonOutput, sszOutput, block.Capella)
|
||||
case spec.DataVersionDeneb:
|
||||
var blobSidecarsResponse *api.Response[[]*deneb.BlobSidecar]
|
||||
blobSidecarsResponse, err = results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: blockID,
|
||||
})
|
||||
if err == nil {
|
||||
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, block.Deneb, blobSidecarsResponse.Data)
|
||||
}
|
||||
default:
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
|
||||
}
|
||||
err = errors.New("unknown block version")
|
||||
}
|
||||
if err != nil && !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Println("")
|
||||
}
|
||||
@@ -254,3 +286,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.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d", chainTime.TimestampToSlot(timestamp)), nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ type command struct {
|
||||
slot phase0.Slot
|
||||
epoch phase0.Epoch
|
||||
period uint64
|
||||
periodStart time.Time
|
||||
periodEnd time.Time
|
||||
incumbent *phase0.ETH1Data
|
||||
eth1DataVotes []*phase0.ETH1Data
|
||||
votes map[string]*vote
|
||||
|
||||
@@ -24,11 +24,13 @@ import (
|
||||
)
|
||||
|
||||
type jsonOutput struct {
|
||||
Period uint64 `json:"period"`
|
||||
Epoch phase0.Epoch `json:"epoch"`
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
Incumbent *phase0.ETH1Data `json:"incumbent"`
|
||||
Votes []*vote `json:"votes"`
|
||||
Period uint64 `json:"period"`
|
||||
PeriodStart int64 `json:"period_start"`
|
||||
PeriodEnd int64 `json:"period_end"`
|
||||
Epoch phase0.Epoch `json:"epoch"`
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
Incumbent *phase0.ETH1Data `json:"incumbent"`
|
||||
Votes []*vote `json:"votes"`
|
||||
}
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
@@ -57,11 +59,13 @@ func (c *command) outputJSON(_ context.Context) (string, error) {
|
||||
})
|
||||
|
||||
output := &jsonOutput{
|
||||
Period: c.period,
|
||||
Epoch: c.epoch,
|
||||
Slot: c.slot,
|
||||
Incumbent: c.incumbent,
|
||||
Votes: votes,
|
||||
Period: c.period,
|
||||
PeriodStart: c.periodStart.Unix(),
|
||||
PeriodEnd: c.periodEnd.Unix(),
|
||||
Epoch: c.epoch,
|
||||
Slot: c.slot,
|
||||
Incumbent: c.incumbent,
|
||||
Votes: votes,
|
||||
}
|
||||
data, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
@@ -78,6 +82,11 @@ func (c *command) outputText(_ context.Context) (string, error) {
|
||||
builder.WriteString(fmt.Sprintf("%d\n", c.period))
|
||||
|
||||
if c.verbose {
|
||||
builder.WriteString("Period start: ")
|
||||
builder.WriteString(fmt.Sprintf("%s\n", c.periodStart))
|
||||
builder.WriteString("Period end: ")
|
||||
builder.WriteString(fmt.Sprintf("%s\n", c.periodEnd))
|
||||
|
||||
builder.WriteString("Incumbent: ")
|
||||
builder.WriteString(fmt.Sprintf("block %#x, deposit count %d\n", c.incumbent.BlockHash, c.incumbent.DepositCount))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2022 Weald Technology Trading.
|
||||
// Copyright © 2022, 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
@@ -58,10 +59,13 @@ func (c *command) process(ctx context.Context) error {
|
||||
if fetchSlot > c.chainTime.CurrentSlot() {
|
||||
fetchSlot = c.chainTime.CurrentSlot()
|
||||
}
|
||||
state, err := c.beaconStateProvider.BeaconState(ctx, fmt.Sprintf("%d", fetchSlot))
|
||||
stateResponse, err := c.beaconStateProvider.BeaconState(ctx, &api.BeaconStateOpts{
|
||||
State: fmt.Sprintf("%d", fetchSlot),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain state")
|
||||
}
|
||||
state := stateResponse.Data
|
||||
if state == nil {
|
||||
return errors.New("state not returned by beacon node")
|
||||
}
|
||||
@@ -73,28 +77,33 @@ func (c *command) process(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
c.slot, err = state.Slot()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain slot")
|
||||
}
|
||||
switch state.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
c.slot = state.Phase0.Slot
|
||||
c.incumbent = state.Phase0.ETH1Data
|
||||
c.eth1DataVotes = state.Phase0.ETH1DataVotes
|
||||
case spec.DataVersionAltair:
|
||||
c.slot = state.Altair.Slot
|
||||
c.incumbent = state.Altair.ETH1Data
|
||||
c.eth1DataVotes = state.Altair.ETH1DataVotes
|
||||
case spec.DataVersionBellatrix:
|
||||
c.slot = state.Bellatrix.Slot
|
||||
c.incumbent = state.Bellatrix.ETH1Data
|
||||
c.eth1DataVotes = state.Bellatrix.ETH1DataVotes
|
||||
case spec.DataVersionCapella:
|
||||
c.slot = state.Capella.Slot
|
||||
c.incumbent = state.Capella.ETH1Data
|
||||
c.eth1DataVotes = state.Capella.ETH1DataVotes
|
||||
case spec.DataVersionDeneb:
|
||||
c.incumbent = state.Deneb.ETH1Data
|
||||
c.eth1DataVotes = state.Deneb.ETH1DataVotes
|
||||
default:
|
||||
return fmt.Errorf("unhandled beacon state version %v", state.Version)
|
||||
}
|
||||
|
||||
c.period = uint64(c.epoch) / c.epochsPerEth1VotingPeriod
|
||||
c.periodStart = c.chainTime.StartOfEpoch(phase0.Epoch(c.period * c.epochsPerEth1VotingPeriod))
|
||||
c.periodEnd = c.chainTime.StartOfEpoch(phase0.Epoch((c.period + 1) * c.epochsPerEth1VotingPeriod))
|
||||
|
||||
c.votes = make(map[string]*vote)
|
||||
for _, eth1Vote := range c.eth1DataVotes {
|
||||
@@ -126,7 +135,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
@@ -142,12 +151,12 @@ func (c *command) setup(ctx context.Context) error {
|
||||
return errors.New("connection does not provide spec information")
|
||||
}
|
||||
|
||||
spec, err := specProvider.Spec(ctx)
|
||||
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain spec")
|
||||
}
|
||||
|
||||
tmp, exists := spec["SLOTS_PER_EPOCH"]
|
||||
tmp, exists := specResponse.Data["SLOTS_PER_EPOCH"]
|
||||
if !exists {
|
||||
return errors.New("spec did not contain SLOTS_PER_EPOCH")
|
||||
}
|
||||
@@ -156,7 +165,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
if !good {
|
||||
return errors.New("SLOTS_PER_EPOCH value invalid")
|
||||
}
|
||||
tmp, exists = spec["EPOCHS_PER_ETH1_VOTING_PERIOD"]
|
||||
tmp, exists = specResponse.Data["EPOCHS_PER_ETH1_VOTING_PERIOD"]
|
||||
if !exists {
|
||||
return errors.New("spec did not contain EPOCHS_PER_ETH1_VOTING_PERIOD")
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
@@ -34,12 +35,14 @@ func (c *command) process(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)), nil)
|
||||
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
|
||||
State: fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validators")
|
||||
}
|
||||
|
||||
for _, validator := range validators {
|
||||
for _, validator := range response.Data {
|
||||
if validator.Validator == nil {
|
||||
continue
|
||||
}
|
||||
@@ -70,7 +73,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
|
||||
@@ -16,6 +16,7 @@ package chaintime
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -59,14 +60,14 @@ func output(_ context.Context, data *dataOut) (string, error) {
|
||||
builder.WriteString(data.epochStart.Format("2006-01-02 15:04:05"))
|
||||
if data.verbose {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.epochStart.Unix()))
|
||||
builder.WriteString(strconv.FormatInt(data.epochStart.Unix(), 10))
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n Epoch end ")
|
||||
builder.WriteString(data.epochEnd.Format("2006-01-02 15:04:05"))
|
||||
if data.verbose {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.epochEnd.Unix()))
|
||||
builder.WriteString(strconv.FormatInt(data.epochEnd.Unix(), 10))
|
||||
builder.WriteString(")")
|
||||
}
|
||||
|
||||
@@ -76,27 +77,27 @@ func output(_ context.Context, data *dataOut) (string, error) {
|
||||
builder.WriteString(data.slotStart.Format("2006-01-02 15:04:05"))
|
||||
if data.verbose {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.slotStart.Unix()))
|
||||
builder.WriteString(strconv.FormatInt(data.slotStart.Unix(), 10))
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n Slot end ")
|
||||
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
|
||||
if data.verbose {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.slotEnd.Unix()))
|
||||
builder.WriteString(strconv.FormatInt(data.slotEnd.Unix(), 10))
|
||||
builder.WriteString(")")
|
||||
}
|
||||
|
||||
if data.hasSyncCommittees {
|
||||
builder.WriteString("\nSync committee period ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriod))
|
||||
builder.WriteString(strconv.FormatUint(data.syncCommitteePeriod, 10))
|
||||
builder.WriteString("\n Sync committee period start ")
|
||||
builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05"))
|
||||
builder.WriteString(" (epoch ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart))
|
||||
if data.verbose {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodStart.Unix()))
|
||||
builder.WriteString(strconv.FormatInt(data.syncCommitteePeriodStart.Unix(), 10))
|
||||
}
|
||||
builder.WriteString(")\n Sync committee period end ")
|
||||
builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05"))
|
||||
@@ -104,7 +105,7 @@ func output(_ context.Context, data *dataOut) (string, error) {
|
||||
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd))
|
||||
if data.verbose {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEnd.Unix()))
|
||||
builder.WriteString(strconv.FormatInt(data.syncCommitteePeriodEnd.Unix(), 10))
|
||||
}
|
||||
builder.WriteString(")")
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
|
||||
chainTime, err := standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set up chaintime service")
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"os"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
@@ -103,29 +104,32 @@ func (c *command) setup(ctx context.Context) error {
|
||||
}
|
||||
|
||||
stateID := fmt.Sprintf("%d", c.item.Message.Contribution.Slot)
|
||||
validators, err := c.validatorsProvider.Validators(ctx,
|
||||
stateID,
|
||||
[]phase0.ValidatorIndex{c.item.Message.AggregatorIndex},
|
||||
)
|
||||
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
|
||||
State: stateID,
|
||||
Indices: []phase0.ValidatorIndex{c.item.Message.AggregatorIndex},
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validator information")
|
||||
}
|
||||
|
||||
if len(validators) == 0 || validators[c.item.Message.AggregatorIndex] == nil {
|
||||
if len(response.Data) == 0 || response.Data[c.item.Message.AggregatorIndex] == nil {
|
||||
return nil
|
||||
}
|
||||
c.validatorKnown = true
|
||||
c.validator = validators[c.item.Message.AggregatorIndex]
|
||||
c.validator = response.Data[c.item.Message.AggregatorIndex]
|
||||
|
||||
// Obtain the sync committee
|
||||
syncCommitteesProvider, isProvider := c.eth2Client.(eth2client.SyncCommitteesProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide sync committee information")
|
||||
}
|
||||
c.syncCommittee, err = syncCommitteesProvider.SyncCommittee(ctx, stateID)
|
||||
syncCommitteeResponse, err := syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{
|
||||
State: stateID,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain sync committee information")
|
||||
}
|
||||
c.syncCommittee = syncCommitteeResponse.Data
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -137,11 +141,11 @@ func (c *command) isAggregator(ctx context.Context) (bool, error) {
|
||||
if !isProvider {
|
||||
return false, errors.New("connection does not provide spec information")
|
||||
}
|
||||
var err error
|
||||
c.spec, err = specProvider.Spec(ctx)
|
||||
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain spec information")
|
||||
}
|
||||
c.spec = specResponse.Data
|
||||
|
||||
tmp, exists := c.spec["SYNC_COMMITTEE_SIZE"]
|
||||
if !exists {
|
||||
@@ -226,16 +230,19 @@ func (c *command) confirmContributionSignature(ctx context.Context) error {
|
||||
fmt.Fprintf(os.Stderr, "Contribution validator indices: %v (%d)\n", includedIndices, len(includedIndices))
|
||||
}
|
||||
|
||||
includedValidators, err := c.validatorsProvider.Validators(ctx, "head", includedIndices)
|
||||
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
|
||||
State: "head",
|
||||
Indices: includedIndices,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain subcommittee validators")
|
||||
}
|
||||
if len(includedValidators) == 0 {
|
||||
if len(response.Data) == 0 {
|
||||
return errors.New("obtained empty subcommittee validator list")
|
||||
}
|
||||
|
||||
var aggregatePubKey *e2types.BLSPublicKey
|
||||
for _, v := range includedValidators {
|
||||
for _, v := range response.Data {
|
||||
pubKeyBytes := make([]byte, 48)
|
||||
copy(pubKeyBytes, v.Validator.PublicKey[:])
|
||||
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
@@ -45,37 +46,32 @@ In quiet mode this will return 0 if the chain information can be obtained, other
|
||||
})
|
||||
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
|
||||
|
||||
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
specResponse, err := eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
errCheck(err, "Failed to obtain beacon chain specification")
|
||||
|
||||
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
genesisResponse, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
|
||||
errCheck(err, "Failed to obtain beacon chain genesis")
|
||||
|
||||
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
|
||||
forkResponse, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, &api.ForkOpts{State: "head"})
|
||||
errCheck(err, "Failed to obtain current fork")
|
||||
|
||||
if quiet {
|
||||
if viper.GetBool("quiet") {
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
if viper.GetBool("prepare-offline") {
|
||||
fmt.Printf("Add the following to your command to run it offline:\n --offline --genesis-validators=root=%#x --fork-version=%#x\n", genesis.GenesisValidatorsRoot, fork.CurrentVersion)
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
if genesis.GenesisTime.Unix() == 0 {
|
||||
if genesisResponse.Data.GenesisTime.Unix() == 0 {
|
||||
fmt.Println("Genesis time: undefined")
|
||||
} else {
|
||||
fmt.Printf("Genesis time: %s\n", genesis.GenesisTime.Format(time.UnixDate))
|
||||
outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesis.GenesisTime.Unix()))
|
||||
fmt.Printf("Genesis time: %s\n", genesisResponse.Data.GenesisTime.Format(time.UnixDate))
|
||||
outputIf(viper.GetBool("verbose"), fmt.Sprintf("Genesis timestamp: %v", genesisResponse.Data.GenesisTime.Unix()))
|
||||
}
|
||||
fmt.Printf("Genesis validators root: %#x\n", genesis.GenesisValidatorsRoot)
|
||||
fmt.Printf("Genesis fork version: %#x\n", config["GENESIS_FORK_VERSION"].(spec.Version))
|
||||
fmt.Printf("Current fork version: %#x\n", fork.CurrentVersion)
|
||||
if verbose {
|
||||
fmt.Printf("Genesis validators root: %#x\n", genesisResponse.Data.GenesisValidatorsRoot)
|
||||
fmt.Printf("Genesis fork version: %#x\n", specResponse.Data["GENESIS_FORK_VERSION"].(spec.Version))
|
||||
fmt.Printf("Current fork version: %#x\n", forkResponse.Data.CurrentVersion)
|
||||
if viper.GetBool("verbose") {
|
||||
forkData := &spec.ForkData{
|
||||
CurrentVersion: fork.CurrentVersion,
|
||||
GenesisValidatorsRoot: genesis.GenesisValidatorsRoot,
|
||||
CurrentVersion: forkResponse.Data.CurrentVersion,
|
||||
GenesisValidatorsRoot: genesisResponse.Data.GenesisValidatorsRoot,
|
||||
}
|
||||
forkDataRoot, err := forkData.HashTreeRoot()
|
||||
if err == nil {
|
||||
@@ -84,8 +80,8 @@ In quiet mode this will return 0 if the chain information can be obtained, other
|
||||
fmt.Printf("Fork digest: %#x\n", forkDigest)
|
||||
}
|
||||
}
|
||||
fmt.Printf("Seconds per slot: %d\n", int(config["SECONDS_PER_SLOT"].(time.Duration).Seconds()))
|
||||
fmt.Printf("Slots per epoch: %d\n", config["SLOTS_PER_EPOCH"].(uint64))
|
||||
fmt.Printf("Seconds per slot: %d\n", int(specResponse.Data["SECONDS_PER_SLOT"].(time.Duration).Seconds()))
|
||||
fmt.Printf("Slots per epoch: %d\n", specResponse.Data["SLOTS_PER_EPOCH"].(uint64))
|
||||
|
||||
os.Exit(_exitSuccess)
|
||||
},
|
||||
@@ -94,11 +90,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) {
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
99
cmd/chainspec.go
Normal file
99
cmd/chainspec.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright © 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
var chainSpecCmd = &cobra.Command{
|
||||
Use: "spec",
|
||||
Short: "Obtain specification for a chain",
|
||||
Long: `Obtain specification for a chain. For example:
|
||||
|
||||
ethdo chain spec
|
||||
|
||||
In quiet mode this will return 0 if the chain specification can be obtained, otherwise 1.`,
|
||||
Run: func(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")
|
||||
|
||||
specResponse, err := eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
errCheck(err, "Failed to obtain chain specification")
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return
|
||||
}
|
||||
|
||||
// Tweak the spec for output.
|
||||
for k, v := range specResponse.Data {
|
||||
switch t := v.(type) {
|
||||
case phase0.Version:
|
||||
specResponse.Data[k] = fmt.Sprintf("%#x", t)
|
||||
case phase0.DomainType:
|
||||
specResponse.Data[k] = fmt.Sprintf("%#x", t)
|
||||
case time.Time:
|
||||
specResponse.Data[k] = strconv.FormatInt(t.Unix(), 10)
|
||||
case time.Duration:
|
||||
specResponse.Data[k] = strconv.FormatUint(uint64(t.Seconds()), 10)
|
||||
case []byte:
|
||||
specResponse.Data[k] = fmt.Sprintf("%#x", t)
|
||||
case uint64:
|
||||
specResponse.Data[k] = strconv.FormatUint(t, 10)
|
||||
}
|
||||
}
|
||||
|
||||
if viper.GetBool("json") {
|
||||
data, err := json.Marshal(specResponse.Data)
|
||||
errCheck(err, "Failed to marshal JSON")
|
||||
fmt.Printf("%s\n", string(data))
|
||||
} else {
|
||||
keys := make([]string, 0, len(specResponse.Data))
|
||||
for k := range specResponse.Data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
fmt.Printf("%s: %v\n", key, specResponse.Data[key])
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
chainCmd.AddCommand(chainSpecCmd)
|
||||
chainFlags(chainSpecCmd)
|
||||
}
|
||||
|
||||
func chainSpecBindings(_ *cobra.Command) {
|
||||
}
|
||||
@@ -17,10 +17,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -50,15 +52,18 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
|
||||
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
|
||||
|
||||
chainTime, err := standardchaintime.New(ctx,
|
||||
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
)
|
||||
errCheck(err, "Failed to configure chaintime service")
|
||||
|
||||
finalityProvider, isProvider := eth2Client.(eth2client.FinalityProvider)
|
||||
assert(isProvider, "beacon node does not provide finality; cannot report on chain status")
|
||||
finality, err := finalityProvider.Finality(ctx, "head")
|
||||
finalityResponse, err := finalityProvider.Finality(ctx, &api.FinalityOpts{
|
||||
State: "head",
|
||||
})
|
||||
errCheck(err, "Failed to obtain finality information")
|
||||
finality := finalityResponse.Data
|
||||
|
||||
slot := chainTime.CurrentSlot()
|
||||
|
||||
@@ -83,7 +88,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
|
||||
res.WriteString(fmt.Sprintf("%d", epoch))
|
||||
res.WriteString("\n")
|
||||
|
||||
if verbose {
|
||||
if viper.GetBool("verbose") {
|
||||
res.WriteString("Epoch slots: ")
|
||||
res.WriteString(fmt.Sprintf("%d", epochStartSlot))
|
||||
res.WriteString("-")
|
||||
@@ -106,7 +111,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
|
||||
res.WriteString("Justified epoch: ")
|
||||
res.WriteString(fmt.Sprintf("%d", finality.Justified.Epoch))
|
||||
res.WriteString("\n")
|
||||
if verbose {
|
||||
if viper.GetBool("verbose") {
|
||||
distance := epoch - finality.Justified.Epoch
|
||||
res.WriteString("Justified epoch distance: ")
|
||||
res.WriteString(fmt.Sprintf("%d", distance))
|
||||
@@ -116,23 +121,23 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
|
||||
res.WriteString("Finalized epoch: ")
|
||||
res.WriteString(fmt.Sprintf("%d", finality.Finalized.Epoch))
|
||||
res.WriteString("\n")
|
||||
if verbose {
|
||||
if viper.GetBool("verbose") {
|
||||
distance := epoch - finality.Finalized.Epoch
|
||||
res.WriteString("Finalized epoch distance: ")
|
||||
res.WriteString(fmt.Sprintf("%d", distance))
|
||||
res.WriteString("\n")
|
||||
}
|
||||
|
||||
if verbose {
|
||||
if viper.GetBool("verbose") {
|
||||
validatorsProvider, isProvider := eth2Client.(eth2client.ValidatorsProvider)
|
||||
if isProvider {
|
||||
validators, err := validatorsProvider.Validators(ctx, "head", nil)
|
||||
validatorsResponse, err := validatorsProvider.Validators(ctx, &api.ValidatorsOpts{State: "head"})
|
||||
errCheck(err, "Failed to obtain validators information")
|
||||
// Stats of inteest.
|
||||
totalBalance := phase0.Gwei(0)
|
||||
activeEffectiveBalance := phase0.Gwei(0)
|
||||
validatorCount := make(map[apiv1.ValidatorState]int)
|
||||
for _, validator := range validators {
|
||||
for _, validator := range validatorsResponse.Data {
|
||||
validatorCount[validator.Status]++
|
||||
totalBalance += validator.Balance
|
||||
if validator.Status.IsActive() {
|
||||
@@ -162,10 +167,10 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
|
||||
nextPeriodTimestamp := chainTime.StartOfEpoch(nextPeriodStartEpoch)
|
||||
|
||||
res.WriteString("Sync committee period: ")
|
||||
res.WriteString(fmt.Sprintf("%d", period))
|
||||
res.WriteString(strconv.FormatUint(period, 10))
|
||||
res.WriteString("\n")
|
||||
|
||||
if verbose {
|
||||
if viper.GetBool("verbose") {
|
||||
res.WriteString("Sync committee epochs: ")
|
||||
res.WriteString(fmt.Sprintf("%d", periodStartEpoch))
|
||||
res.WriteString("-")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ func epochFlags(_ *cobra.Command) {
|
||||
epochSummaryCmd.Flags().String("epoch", "", "the epoch for which to obtain information (default current, can be 'current', 'last' or a number)")
|
||||
}
|
||||
|
||||
func epochBindings() {
|
||||
if err := viper.BindPFlag("epoch", epochSummaryCmd.Flags().Lookup("epoch")); err != nil {
|
||||
func epochBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -88,10 +93,11 @@ type nonParticipatingValidator struct {
|
||||
|
||||
func newCommand(_ context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
summary: &epochSummary{},
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
summary: &epochSummary{},
|
||||
blocksCache: make(map[string]*spec.VersionedSignedBeaconBlock),
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2022 Weald Technology Trading.
|
||||
// Copyright © 2022, 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -19,9 +19,9 @@ import (
|
||||
"sort"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
@@ -48,19 +48,22 @@ func (c *command) process(ctx context.Context) error {
|
||||
if err := c.processAttesterDuties(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.processSyncCommitteeDuties(ctx)
|
||||
if err := c.processSyncCommitteeDuties(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.processBlobs(ctx)
|
||||
}
|
||||
|
||||
func (c *command) processProposerDuties(ctx context.Context) error {
|
||||
duties, err := c.proposerDutiesProvider.ProposerDuties(ctx, c.summary.Epoch, nil)
|
||||
response, err := c.proposerDutiesProvider.ProposerDuties(ctx, &api.ProposerDutiesOpts{
|
||||
Epoch: c.summary.Epoch,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain proposer duties")
|
||||
}
|
||||
if duties == nil {
|
||||
return errors.New("empty proposer duties")
|
||||
}
|
||||
for _, duty := range duties {
|
||||
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", duty.Slot))
|
||||
|
||||
for _, duty := range response.Data {
|
||||
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", duty.Slot))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", duty.Slot))
|
||||
}
|
||||
@@ -76,12 +79,14 @@ func (c *command) processProposerDuties(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (c *command) activeValidators(ctx context.Context) (map[phase0.ValidatorIndex]*apiv1.Validator, error) {
|
||||
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), nil)
|
||||
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
|
||||
State: fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain validators for epoch")
|
||||
}
|
||||
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
|
||||
for _, validator := range validators {
|
||||
for _, validator := range response.Data {
|
||||
if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch {
|
||||
activeValidators[validator.Index] = validator
|
||||
}
|
||||
@@ -161,7 +166,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))
|
||||
}
|
||||
@@ -184,11 +189,13 @@ func (c *command) processSlots(ctx context.Context,
|
||||
}
|
||||
slotCommittees, exists := allCommittees[attestation.Data.Slot]
|
||||
if !exists {
|
||||
beaconCommittees, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", attestation.Data.Slot))
|
||||
response, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, &api.BeaconCommitteesOpts{
|
||||
State: fmt.Sprintf("%d", attestation.Data.Slot),
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
|
||||
}
|
||||
for _, beaconCommittee := range beaconCommittees {
|
||||
for _, beaconCommittee := range response.Data {
|
||||
if _, exists := allCommittees[beaconCommittee.Slot]; !exists {
|
||||
allCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
}
|
||||
@@ -254,10 +261,13 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
committee, err := c.syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", c.summary.FirstSlot))
|
||||
committeeResponse, err := c.syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{
|
||||
State: fmt.Sprintf("%d", c.summary.FirstSlot),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain sync committee")
|
||||
}
|
||||
committee := committeeResponse.Data
|
||||
if len(committee.Validators) == 0 {
|
||||
return errors.Wrap(err, "empty sync committee")
|
||||
}
|
||||
@@ -268,7 +278,7 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
|
||||
}
|
||||
|
||||
for slot := c.summary.FirstSlot; slot <= c.summary.LastSlot; slot++ {
|
||||
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
block, err := c.fetchBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
|
||||
}
|
||||
@@ -276,19 +286,14 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
|
||||
// If the block is missed we don't count the sync aggregate miss.
|
||||
continue
|
||||
}
|
||||
var aggregate *altair.SyncAggregate
|
||||
switch block.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
if block.Version == spec.DataVersionPhase0 {
|
||||
// No sync committees in this fork.
|
||||
return nil
|
||||
case spec.DataVersionAltair:
|
||||
aggregate = block.Altair.Message.Body.SyncAggregate
|
||||
case spec.DataVersionBellatrix:
|
||||
aggregate = block.Bellatrix.Message.Body.SyncAggregate
|
||||
case spec.DataVersionCapella:
|
||||
aggregate = block.Capella.Message.Body.SyncAggregate
|
||||
default:
|
||||
return fmt.Errorf("unhandled block version %v", block.Version)
|
||||
}
|
||||
|
||||
aggregate, err := block.SyncAggregate()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to obtain sync aggregate for slot %d", slot)
|
||||
}
|
||||
for i := uint64(0); i < aggregate.SyncCommitteeBits.Len(); i++ {
|
||||
if !aggregate.SyncCommitteeBits.BitAt(i) {
|
||||
@@ -336,7 +341,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
@@ -370,3 +375,46 @@ 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
|
||||
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: blockID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to fetch block")
|
||||
}
|
||||
block = blockResponse.Data
|
||||
c.blocksCache[blockID] = block
|
||||
}
|
||||
return block, nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"strings"
|
||||
|
||||
consensusclient "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
v1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
@@ -67,10 +68,11 @@ In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.
|
||||
opRoot, err := signedOp.Message.HashTreeRoot()
|
||||
errCheck(err, "Failed to obtain exit hash tree root")
|
||||
|
||||
genesis, err := eth2Client.(consensusclient.GenesisProvider).Genesis(ctx)
|
||||
genesisResponse, err := eth2Client.(consensusclient.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
|
||||
errCheck(err, "Failed to obtain beacon chain genesis")
|
||||
genesis := genesisResponse.Data
|
||||
|
||||
fork, err := eth2Client.(consensusclient.ForkProvider).Fork(ctx, "head")
|
||||
response, err := eth2Client.(consensusclient.ForkProvider).Fork(ctx, &api.ForkOpts{State: "head"})
|
||||
errCheck(err, "Failed to obtain fork information")
|
||||
|
||||
// Check against current and prior fork versions.
|
||||
@@ -83,14 +85,14 @@ In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.
|
||||
|
||||
// Try with the current fork.
|
||||
domain := phase0.Domain{}
|
||||
currentExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork.CurrentVersion[:], genesis.GenesisValidatorsRoot[:])
|
||||
currentExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, response.Data.CurrentVersion[:], genesis.GenesisValidatorsRoot[:])
|
||||
errCheck(err, "Failed to compute domain")
|
||||
copy(domain[:], currentExitDomain)
|
||||
verified, err = util.VerifyRoot(account, opRoot, domain, sig)
|
||||
errCheck(err, "Failed to verify voluntary exit")
|
||||
if !verified {
|
||||
// Try again with the previous fork.
|
||||
previousExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork.PreviousVersion[:], genesis.GenesisValidatorsRoot[:])
|
||||
previousExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, response.Data.PreviousVersion[:], genesis.GenesisValidatorsRoot[:])
|
||||
copy(domain[:], previousExitDomain)
|
||||
errCheck(err, "Failed to compute domain")
|
||||
verified, err = util.VerifyRoot(account, opRoot, domain, sig)
|
||||
@@ -98,7 +100,7 @@ In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.
|
||||
}
|
||||
assert(verified, "Voluntary exit failed to verify against current and previous fork versions")
|
||||
|
||||
outputIf(verbose, "Verified")
|
||||
outputIf(viper.GetBool("verbose"), "Verified")
|
||||
os.Exit(_exitSuccess)
|
||||
},
|
||||
}
|
||||
@@ -133,8 +135,8 @@ func init() {
|
||||
exitVerifyCmd.Flags().String("signed-operation", "", "JSON data, or path to JSON data")
|
||||
}
|
||||
|
||||
func exitVerifyBindings() {
|
||||
if err := viper.BindPFlag("signed-operation", exitVerifyCmd.Flags().Lookup("signed-operation")); err != nil {
|
||||
func exitVerifyBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("signed-operation", cmd.Flags().Lookup("signed-operation")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"os"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
@@ -43,19 +44,19 @@ In quiet mode this will return 0 if the node information can be obtained, otherw
|
||||
})
|
||||
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
|
||||
|
||||
if quiet {
|
||||
if viper.GetBool("quiet") {
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
version, err := eth2Client.(eth2client.NodeVersionProvider).NodeVersion(ctx)
|
||||
if viper.GetBool("verbose") {
|
||||
versionResponse, err := eth2Client.(eth2client.NodeVersionProvider).NodeVersion(ctx, &api.NodeVersionOpts{})
|
||||
errCheck(err, "Failed to obtain node version")
|
||||
fmt.Printf("Version: %s\n", version)
|
||||
fmt.Printf("Version: %s\n", versionResponse.Data)
|
||||
}
|
||||
|
||||
syncState, err := eth2Client.(eth2client.NodeSyncingProvider).NodeSyncing(ctx)
|
||||
syncStateResponse, err := eth2Client.(eth2client.NodeSyncingProvider).NodeSyncing(ctx, &api.NodeSyncingOpts{})
|
||||
errCheck(err, "failed to obtain node sync state")
|
||||
fmt.Printf("Syncing: %t\n", syncState.SyncDistance != 0)
|
||||
fmt.Printf("Syncing: %t\n", syncStateResponse.Data.SyncDistance != 0)
|
||||
|
||||
os.Exit(_exitSuccess)
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
@@ -55,23 +56,22 @@ type results struct {
|
||||
|
||||
func newCommand(_ context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
results: &results{},
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
timeout: viper.GetDuration("timeout"),
|
||||
connection: viper.GetString("connection"),
|
||||
allowInsecureConnections: viper.GetBool("allow-insecure-connections"),
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2022 Weald Technology Trading.
|
||||
// Copyright © 2022, 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"context"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
@@ -24,20 +26,59 @@ import (
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
// Obtain information we need to process.
|
||||
err := c.setup(ctx)
|
||||
if err != nil {
|
||||
if err := c.setup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.slot != "" {
|
||||
return c.processSlot(ctx)
|
||||
}
|
||||
|
||||
return c.processEpoch(ctx)
|
||||
}
|
||||
|
||||
func (c *command) processSlot(ctx context.Context) error {
|
||||
var err error
|
||||
slot, err := util.ParseSlot(ctx, c.chainTime, c.slot)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse slot")
|
||||
}
|
||||
|
||||
c.results.Epoch = c.chainTime.SlotToEpoch(slot)
|
||||
|
||||
response, err := c.proposerDutiesProvider.ProposerDuties(ctx, &api.ProposerDutiesOpts{
|
||||
Epoch: c.results.Epoch,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain proposer duties")
|
||||
}
|
||||
|
||||
c.results.Duties = make([]*apiv1.ProposerDuty, 0, 1)
|
||||
|
||||
for _, duty := range response.Data {
|
||||
if duty.Slot == slot {
|
||||
c.results.Duties = append(c.results.Duties, duty)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) processEpoch(ctx context.Context) error {
|
||||
var err error
|
||||
c.results.Epoch, err = util.ParseEpoch(ctx, c.chainTime, c.epoch)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse epoch")
|
||||
}
|
||||
|
||||
c.results.Duties, err = c.proposerDutiesProvider.ProposerDuties(ctx, c.results.Epoch, nil)
|
||||
dutiesResponse, err := c.proposerDutiesProvider.ProposerDuties(ctx, &api.ProposerDutiesOpts{
|
||||
Epoch: c.results.Epoch,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain proposer duties")
|
||||
}
|
||||
c.results.Duties = dutiesResponse.Data
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -58,7 +99,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
148
cmd/root.go
148
cmd/root.go
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019 - 2021 Weald Technology Trading.
|
||||
// Copyright © 2019 - 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -33,21 +33,56 @@ import (
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
)
|
||||
var cfgFile string
|
||||
|
||||
// RootCmd represents the base command when called without any subcommands.
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "ethdo",
|
||||
Short: "Ethereum 2 CLI",
|
||||
Long: `Manage common Ethereum 2 tasks from the command line.`,
|
||||
Short: "Ethereum consensus layer CLI",
|
||||
Long: `Manage common Ethereum consensus layer tasks from the command line.`,
|
||||
PersistentPreRunE: persistentPreRunE,
|
||||
}
|
||||
|
||||
// bindings are the command-specific bindings.
|
||||
var bindings = map[string]func(cmd *cobra.Command){
|
||||
"account/create": accountCreateBindings,
|
||||
"account/derive": accountDeriveBindings,
|
||||
"account/import": accountImportBindings,
|
||||
"attester/duties": attesterDutiesBindings,
|
||||
"attester/inclusion": attesterInclusionBindings,
|
||||
"block/analyze": blockAnalyzeBindings,
|
||||
"block/info": blockInfoBindings,
|
||||
"chain/eth1votes": chainEth1VotesBindings,
|
||||
"chain/info": chainInfoBindings,
|
||||
"chain/queues": chainQueuesBindings,
|
||||
"chain/spec": chainSpecBindings,
|
||||
"chain/time": chainTimeBindings,
|
||||
"chain/verify/signedcontributionandproof": chainVerifySignedContributionAndProofBindings,
|
||||
"epoch/summary": epochSummaryBindings,
|
||||
"exit/verify": exitVerifyBindings,
|
||||
"node/events": nodeEventsBindings,
|
||||
"proposer/duties": proposerDutiesBindings,
|
||||
"slot/time": slotTimeBindings,
|
||||
"synccommittee/inclusion": synccommitteeInclusionBindings,
|
||||
"synccommittee/members": synccommitteeMembersBindings,
|
||||
"validator/credentials/get": validatorCredentialsGetBindings,
|
||||
"validator/credentials/set": validatorCredentialsSetBindings,
|
||||
"validator/depositdata": validatorDepositdataBindings,
|
||||
"validator/duties": validatorDutiesBindings,
|
||||
"validator/exit": validatorExitBindings,
|
||||
"validator/info": validatorInfoBindings,
|
||||
"validator/keycheck": validatorKeycheckBindings,
|
||||
"validator/summary": validatorSummaryBindings,
|
||||
"validator/yield": validatorYieldBindings,
|
||||
"validator/expectation": validatorExpectationBindings,
|
||||
"validator/withdrawal": validatorWithdrawalBindings,
|
||||
"wallet/batch": walletBatchBindings,
|
||||
"wallet/create": walletCreateBindings,
|
||||
"wallet/import": walletImportBindings,
|
||||
"wallet/sharedexport": walletSharedExportBindings,
|
||||
"wallet/sharedimport": walletSharedImportBindings,
|
||||
}
|
||||
|
||||
func persistentPreRunE(cmd *cobra.Command, _ []string) error {
|
||||
if cmd.Name() == "help" {
|
||||
// User just wants help
|
||||
@@ -63,11 +98,14 @@ func persistentPreRunE(cmd *cobra.Command, _ []string) error {
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
|
||||
// We bind viper here so that we bind to the correct command.
|
||||
quiet = viper.GetBool("quiet")
|
||||
verbose = viper.GetBool("verbose")
|
||||
debug = viper.GetBool("debug")
|
||||
quiet := viper.GetBool("quiet")
|
||||
verbose := viper.GetBool("verbose")
|
||||
debug := viper.GetBool("debug")
|
||||
|
||||
includeCommandBindings(cmd)
|
||||
// Command-specific bindings.
|
||||
if bindingsFunc, exists := bindings[commandPath(cmd)]; exists {
|
||||
bindingsFunc(cmd)
|
||||
}
|
||||
|
||||
if quiet && verbose {
|
||||
fmt.Println("Cannot supply both quiet and verbose flags")
|
||||
@@ -79,78 +117,6 @@ func persistentPreRunE(cmd *cobra.Command, _ []string) error {
|
||||
return util.SetupStore()
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func includeCommandBindings(cmd *cobra.Command) {
|
||||
switch commandPath(cmd) {
|
||||
case "account/create":
|
||||
accountCreateBindings()
|
||||
case "account/derive":
|
||||
accountDeriveBindings()
|
||||
case "account/import":
|
||||
accountImportBindings()
|
||||
case "attester/duties":
|
||||
attesterDutiesBindings()
|
||||
case "attester/inclusion":
|
||||
attesterInclusionBindings()
|
||||
case "block/analyze":
|
||||
blockAnalyzeBindings()
|
||||
case "block/info":
|
||||
blockInfoBindings()
|
||||
case "chain/eth1votes":
|
||||
chainEth1VotesBindings()
|
||||
case "chain/info":
|
||||
chainInfoBindings()
|
||||
case "chain/queues":
|
||||
chainQueuesBindings()
|
||||
case "chain/time":
|
||||
chainTimeBindings()
|
||||
case "chain/verify/signedcontributionandproof":
|
||||
chainVerifySignedContributionAndProofBindings(cmd)
|
||||
case "epoch/summary":
|
||||
epochSummaryBindings()
|
||||
case "exit/verify":
|
||||
exitVerifyBindings()
|
||||
case "node/events":
|
||||
nodeEventsBindings()
|
||||
case "proposer/duties":
|
||||
proposerDutiesBindings()
|
||||
case "slot/time":
|
||||
slotTimeBindings()
|
||||
case "synccommittee/inclusion":
|
||||
synccommitteeInclusionBindings()
|
||||
case "synccommittee/members":
|
||||
synccommitteeMembersBindings()
|
||||
case "validator/credentials/get":
|
||||
validatorCredentialsGetBindings()
|
||||
case "validator/credentials/set":
|
||||
validatorCredentialsSetBindings()
|
||||
case "validator/depositdata":
|
||||
validatorDepositdataBindings()
|
||||
case "validator/duties":
|
||||
validatorDutiesBindings()
|
||||
case "validator/exit":
|
||||
validatorExitBindings()
|
||||
case "validator/info":
|
||||
validatorInfoBindings()
|
||||
case "validator/keycheck":
|
||||
validatorKeycheckBindings()
|
||||
case "validator/summary":
|
||||
validatorSummaryBindings()
|
||||
case "validator/yield":
|
||||
validatorYieldBindings()
|
||||
case "validator/expectation":
|
||||
validatorExpectationBindings()
|
||||
case "wallet/create":
|
||||
walletCreateBindings()
|
||||
case "wallet/import":
|
||||
walletImportBindings()
|
||||
case "wallet/sharedexport":
|
||||
walletSharedExportBindings()
|
||||
case "wallet/sharedimport":
|
||||
walletSharedImportBindings()
|
||||
}
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
@@ -167,8 +133,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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -41,17 +42,18 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return nil, errors.New("slot must be a positive integer")
|
||||
}
|
||||
|
||||
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
genesisResponse, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain genesis information")
|
||||
}
|
||||
genesis := genesisResponse.Data
|
||||
|
||||
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
specResponse, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain chain specifications")
|
||||
}
|
||||
|
||||
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
|
||||
slotDuration := specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
|
||||
|
||||
results.startTime = genesis.GenesisTime.Add((time.Duration(slot*int64(slotDuration.Seconds())) * time.Second))
|
||||
results.endTime = results.startTime.Add(slotDuration)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package inclusion
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -53,13 +54,13 @@ func (c *command) output(_ context.Context) (string, error) {
|
||||
}
|
||||
}
|
||||
builder.WriteString("Expected: ")
|
||||
builder.WriteString(fmt.Sprintf("%d", len(c.inclusions)))
|
||||
builder.WriteString(strconv.Itoa(len(c.inclusions)))
|
||||
builder.WriteString("\nIncluded: ")
|
||||
builder.WriteString(fmt.Sprintf("%d", included))
|
||||
builder.WriteString(strconv.Itoa(included))
|
||||
builder.WriteString("\nMissed: ")
|
||||
builder.WriteString(fmt.Sprintf("%d", missed))
|
||||
builder.WriteString(strconv.Itoa(missed))
|
||||
builder.WriteString("\nNo block: ")
|
||||
builder.WriteString(fmt.Sprintf("%d", noBlock))
|
||||
builder.WriteString(strconv.Itoa(noBlock))
|
||||
|
||||
builder.WriteString("\nPer-slot result: ")
|
||||
for i, inclusion := range c.inclusions {
|
||||
|
||||
@@ -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
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/pkg/errors"
|
||||
@@ -41,10 +42,14 @@ func (c *command) process(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
syncCommittee, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", c.epoch)
|
||||
syncCommitteeResponse, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommittee(ctx, &api.SyncCommitteeOpts{
|
||||
State: "head",
|
||||
Epoch: &c.epoch,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain sync committee information")
|
||||
}
|
||||
syncCommittee := syncCommitteeResponse.Data
|
||||
|
||||
if syncCommittee == nil {
|
||||
return errors.New("no sync committee returned")
|
||||
@@ -66,11 +71,14 @@ func (c *command) process(ctx context.Context) error {
|
||||
if lastSlot > c.chainTime.CurrentSlot() {
|
||||
lastSlot = c.chainTime.CurrentSlot()
|
||||
}
|
||||
for slot := firstSlot; slot < lastSlot; slot++ {
|
||||
block, err := c.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
for slot := firstSlot; slot <= lastSlot; slot++ {
|
||||
blockResponse, err := c.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
block := blockResponse.Data
|
||||
if block == nil {
|
||||
c.inclusions = append(c.inclusions, 0)
|
||||
continue
|
||||
@@ -98,6 +106,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)
|
||||
}
|
||||
@@ -123,7 +138,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
|
||||
@@ -63,7 +63,7 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
|
||||
// Chain time.
|
||||
data.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
|
||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestOutput(t *testing.T) {
|
||||
json: true,
|
||||
validators: []phase0.ValidatorIndex{1, 2, 3},
|
||||
},
|
||||
res: "[1,2,3]",
|
||||
res: `["1","2","3"]`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"strings"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
@@ -34,10 +35,14 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
syncCommittee, err := data.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", epoch)
|
||||
syncCommitteeResponse, err := data.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommittee(ctx, &api.SyncCommitteeOpts{
|
||||
State: "head",
|
||||
Epoch: &epoch,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain sync committee information")
|
||||
}
|
||||
syncCommittee := syncCommitteeResponse.Data
|
||||
|
||||
if syncCommittee == nil {
|
||||
return nil, errors.New("no sync committee returned")
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestProcess(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
chainTime, err := standardchaintime.New(context.Background(),
|
||||
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -53,11 +53,11 @@ func init() {
|
||||
synccommitteeInclusionCmd.Flags().String("validator", "", "the index, public key, or acount of the validator")
|
||||
}
|
||||
|
||||
func synccommitteeInclusionBindings() {
|
||||
if err := viper.BindPFlag("epoch", synccommitteeInclusionCmd.Flags().Lookup("epoch")); err != nil {
|
||||
func synccommitteeInclusionBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("validator", synccommitteeInclusionCmd.Flags().Lookup("validator")); err != nil {
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,15 +49,15 @@ epoch can be a specific epoch. period can be 'current' for the current sync per
|
||||
func init() {
|
||||
synccommitteeCmd.AddCommand(synccommitteeMembersCmd)
|
||||
synccommitteeFlags(synccommitteeMembersCmd)
|
||||
synccommitteeMembersCmd.Flags().Int64("epoch", -1, "the epoch for which to fetch sync committees")
|
||||
synccommitteeMembersCmd.Flags().String("epoch", "", "the epoch for which to fetch sync committees")
|
||||
synccommitteeMembersCmd.Flags().String("period", "", "the sync committee period for which to fetch sync committees ('current', 'next')")
|
||||
}
|
||||
|
||||
func synccommitteeMembersBindings() {
|
||||
if err := viper.BindPFlag("epoch", synccommitteeMembersCmd.Flags().Lookup("epoch")); err != nil {
|
||||
func synccommitteeMembersBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("period", synccommitteeMembersCmd.Flags().Lookup("period")); err != nil {
|
||||
if err := viper.BindPFlag("period", cmd.Flags().Lookup("period")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ package validatorcredentialsget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -47,7 +48,7 @@ func (c *command) output(_ context.Context) (string, error) {
|
||||
|
||||
// addressBytesToEIP55 converts a byte array in to an EIP-55 string format.
|
||||
func addressBytesToEIP55(address []byte) string {
|
||||
bytes := []byte(fmt.Sprintf("%x", address))
|
||||
bytes := []byte(hex.EncodeToString(address))
|
||||
hash := ethutil.Keccak256(bytes)
|
||||
for i := 0; i < len(bytes); i++ {
|
||||
hashByte := hash[i/2]
|
||||
|
||||
@@ -163,7 +163,7 @@ func (c *command) generateOperationFromMnemonicAndPath(ctx context.Context) erro
|
||||
}
|
||||
|
||||
validatorKeyPath := c.path
|
||||
match := validatorPath.Match([]byte(c.path))
|
||||
match := validatorPath.MatchString(c.path)
|
||||
if !match {
|
||||
return fmt.Errorf("path %s does not match EIP-2334 format for a validator", c.path)
|
||||
}
|
||||
@@ -732,7 +732,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
// Set up chaintime.
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithGenesisTimeProvider(c.consensusClient.(consensusclient.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.consensusClient.(consensusclient.GenesisProvider)),
|
||||
standardchaintime.WithSpecProvider(c.consensusClient.(consensusclient.SpecProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -828,7 +828,7 @@ func (c *command) obtainForkVersion(_ context.Context) (phase0.Version, error) {
|
||||
|
||||
// addressBytesToEIP55 converts a byte array in to an EIP-55 string format.
|
||||
func addressBytesToEIP55(address []byte) string {
|
||||
bytes := []byte(fmt.Sprintf("%x", address))
|
||||
bytes := []byte(hex.EncodeToString(address))
|
||||
hash := ethutil.Keccak256(bytes)
|
||||
for i := 0; i < len(bytes); i++ {
|
||||
hashByte := hash[i/2]
|
||||
|
||||
@@ -109,9 +109,10 @@ func validatorDepositDataOutputLaunchpad(datum *dataOut) (string, error) {
|
||||
forkVersionMap := map[spec.Version]string{
|
||||
[4]byte{0x00, 0x00, 0x00, 0x00}: "mainnet",
|
||||
[4]byte{0x00, 0x00, 0x20, 0x09}: "pyrmont",
|
||||
[4]byte{0x00, 0x00, 0x10, 0x20}: "prater",
|
||||
[4]byte{0x00, 0x00, 0x10, 0x20}: "goerli",
|
||||
[4]byte{0x80, 0x00, 0x00, 0x69}: "ropsten",
|
||||
[4]byte{0x90, 0x00, 0x00, 0x69}: "sepolia",
|
||||
[4]byte{0x00, 0x01, 0x70, 0x00}: "holesky",
|
||||
}
|
||||
|
||||
if datum.validatorPubKey == nil {
|
||||
@@ -137,7 +138,7 @@ func validatorDepositDataOutputLaunchpad(datum *dataOut) (string, error) {
|
||||
if network, exists := forkVersionMap[*datum.forkVersion]; exists {
|
||||
networkName = network
|
||||
}
|
||||
output := fmt.Sprintf(`{"pubkey":"%x","withdrawal_credentials":"%x","amount":%d,"signature":"%x","deposit_message_root":"%x","deposit_data_root":"%x","fork_version":"%x","eth2_network_name":"%s","deposit_cli_version":"1.1.0"}`,
|
||||
output := fmt.Sprintf(`{"pubkey":"%x","withdrawal_credentials":"%x","amount":%d,"signature":"%x","deposit_message_root":"%x","deposit_data_root":"%x","fork_version":"%x","eth2_network_name":"%s","deposit_cli_version":"2.5.0"}`,
|
||||
*datum.validatorPubKey,
|
||||
datum.withdrawalCredentials,
|
||||
datum.amount,
|
||||
|
||||
@@ -423,7 +423,7 @@ func TestOutputLaunchpad(t *testing.T) {
|
||||
depositMessageRoot: depositMessageRoot,
|
||||
},
|
||||
},
|
||||
res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"00002009","eth2_network_name":"pyrmont","deposit_cli_version":"1.1.0"}]`,
|
||||
res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"00002009","eth2_network_name":"pyrmont","deposit_cli_version":"2.5.0"}]`,
|
||||
},
|
||||
{
|
||||
name: "SinglePrater",
|
||||
@@ -440,7 +440,7 @@ func TestOutputLaunchpad(t *testing.T) {
|
||||
depositMessageRoot: depositMessageRoot,
|
||||
},
|
||||
},
|
||||
res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"00001020","eth2_network_name":"prater","deposit_cli_version":"1.1.0"}]`,
|
||||
res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"00001020","eth2_network_name":"goerli","deposit_cli_version":"2.5.0"}]`,
|
||||
},
|
||||
{
|
||||
name: "Double",
|
||||
@@ -468,7 +468,7 @@ func TestOutputLaunchpad(t *testing.T) {
|
||||
depositMessageRoot: depositMessageRoot2,
|
||||
},
|
||||
},
|
||||
res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"00002009","eth2_network_name":"pyrmont","deposit_cli_version":"1.1.0"},{"pubkey":"b89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","withdrawal_credentials":"00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594","amount":32000000000,"signature":"911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e","deposit_message_root":"bb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52","deposit_data_root":"3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab","fork_version":"00002009","eth2_network_name":"pyrmont","deposit_cli_version":"1.1.0"}]`,
|
||||
res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"00002009","eth2_network_name":"pyrmont","deposit_cli_version":"2.5.0"},{"pubkey":"b89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","withdrawal_credentials":"00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594","amount":32000000000,"signature":"911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e","deposit_message_root":"bb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52","deposit_data_root":"3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab","fork_version":"00002009","eth2_network_name":"pyrmont","deposit_cli_version":"2.5.0"}]`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ func createWithdrawalCredentials(data *dataIn) ([]byte, error) {
|
||||
|
||||
// addressBytesToEIP55 converts a byte array in to an EIP-55 string format.
|
||||
func addressBytesToEIP55(address []byte) string {
|
||||
bytes := []byte(fmt.Sprintf("%x", address))
|
||||
bytes := []byte(hex.EncodeToString(address))
|
||||
hash := util.Keccak256(bytes)
|
||||
for i := 0; i < len(bytes); i++ {
|
||||
hashByte := hash[i/2]
|
||||
|
||||
@@ -18,7 +18,8 @@ import (
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
@@ -75,28 +76,32 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
}
|
||||
results.nextEpochAttesterDuty = nextEpochAttesterDuty
|
||||
|
||||
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
genesisResponse, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain genesis data")
|
||||
}
|
||||
results.genesisTime = genesis.GenesisTime
|
||||
results.genesisTime = genesisResponse.Data.GenesisTime
|
||||
|
||||
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
specResponse, err := eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
|
||||
}
|
||||
results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
|
||||
results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration)
|
||||
results.slotsPerEpoch = specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
|
||||
results.slotDuration = specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func attesterDuty(ctx context.Context, eth2Client eth2client.Service, validatorIndex spec.ValidatorIndex, epoch spec.Epoch) (*api.AttesterDuty, error) {
|
||||
func attesterDuty(ctx context.Context, eth2Client eth2client.Service, validatorIndex spec.ValidatorIndex, epoch spec.Epoch) (*apiv1.AttesterDuty, error) {
|
||||
// Find the attesting slot for the given epoch.
|
||||
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validatorIndex})
|
||||
dutiesResponse, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{
|
||||
Epoch: epoch,
|
||||
Indices: []spec.ValidatorIndex{validatorIndex},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain attester duties")
|
||||
}
|
||||
duties := dutiesResponse.Data
|
||||
|
||||
if len(duties) == 0 {
|
||||
return nil, errors.New("validator does not have duty for that epoch")
|
||||
@@ -105,30 +110,33 @@ func attesterDuty(ctx context.Context, eth2Client eth2client.Service, validatorI
|
||||
return duties[0], nil
|
||||
}
|
||||
|
||||
func proposerDuties(ctx context.Context, eth2Client eth2client.Service, validatorIndex spec.ValidatorIndex, epoch spec.Epoch) ([]*api.ProposerDuty, error) {
|
||||
func proposerDuties(ctx context.Context, eth2Client eth2client.Service, validatorIndex spec.ValidatorIndex, epoch spec.Epoch) ([]*apiv1.ProposerDuty, error) {
|
||||
// Fetch the proposer duties for this epoch.
|
||||
proposerDuties, err := eth2Client.(eth2client.ProposerDutiesProvider).ProposerDuties(ctx, epoch, []spec.ValidatorIndex{validatorIndex})
|
||||
proposerDuties, err := eth2Client.(eth2client.ProposerDutiesProvider).ProposerDuties(ctx, &api.ProposerDutiesOpts{
|
||||
Epoch: epoch,
|
||||
Indices: []spec.ValidatorIndex{validatorIndex},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain proposer duties")
|
||||
}
|
||||
|
||||
return proposerDuties, nil
|
||||
return proposerDuties.Data, nil
|
||||
}
|
||||
|
||||
func currentEpoch(ctx context.Context, eth2Client eth2client.Service) (spec.Epoch, error) {
|
||||
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
specResponse, err := eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to obtain beacon chain configuration")
|
||||
}
|
||||
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
|
||||
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
|
||||
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
slotsPerEpoch := specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
|
||||
slotDuration := specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
|
||||
|
||||
genesisResponse, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to obtain genesis data")
|
||||
}
|
||||
|
||||
if genesis.GenesisTime.After(time.Now()) {
|
||||
if genesisResponse.Data.GenesisTime.After(time.Now()) {
|
||||
return spec.Epoch(0), nil
|
||||
}
|
||||
return spec.Epoch(uint64(time.Since(genesis.GenesisTime).Seconds()) / (uint64(slotDuration.Seconds()) * slotsPerEpoch)), nil
|
||||
return spec.Epoch(uint64(time.Since(genesisResponse.Data.GenesisTime).Seconds()) / (uint64(slotDuration.Seconds()) * slotsPerEpoch)), nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ type command struct {
|
||||
forkVersion string
|
||||
genesisValidatorsRoot string
|
||||
prepareOffline bool
|
||||
signedOperationInput string
|
||||
signedOperationsInput string
|
||||
epoch string
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
@@ -58,7 +59,7 @@ type command struct {
|
||||
chainTime chaintime.Service
|
||||
|
||||
// Output.
|
||||
signedOperation *phase0.SignedVoluntaryExit
|
||||
signedOperations []*phase0.SignedVoluntaryExit
|
||||
}
|
||||
|
||||
func newCommand(_ context.Context) (*command, error) {
|
||||
@@ -76,10 +77,12 @@ func newCommand(_ context.Context) (*command, error) {
|
||||
mnemonic: viper.GetString("mnemonic"),
|
||||
path: viper.GetString("path"),
|
||||
privateKey: viper.GetString("private-key"),
|
||||
signedOperationInput: viper.GetString("signed-operation"),
|
||||
signedOperationsInput: viper.GetString("signed-operations"),
|
||||
validator: viper.GetString("validator"),
|
||||
forkVersion: viper.GetString("fork-version"),
|
||||
genesisValidatorsRoot: viper.GetString("genesis-validators-root"),
|
||||
epoch: viper.GetString("epoch"),
|
||||
signedOperations: make([]*phase0.SignedVoluntaryExit, 0),
|
||||
}
|
||||
|
||||
// Account and validator are synonymous.
|
||||
|
||||
@@ -33,15 +33,21 @@ func (c *command) output(_ context.Context) (string, error) {
|
||||
}
|
||||
|
||||
if c.json || c.offline {
|
||||
data, err := json.Marshal(c.signedOperation)
|
||||
var data []byte
|
||||
var err error
|
||||
if len(c.signedOperations) == 1 {
|
||||
data, err = json.Marshal(c.signedOperations[0])
|
||||
} else {
|
||||
data, err = json.Marshal(c.signedOperations)
|
||||
}
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal signed operation")
|
||||
return "", errors.Wrap(err, "failed to marshal signed operations")
|
||||
}
|
||||
if c.json {
|
||||
return string(data), nil
|
||||
}
|
||||
if err := os.WriteFile(exitOperationFilename, data, 0o600); err != nil {
|
||||
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", exitOperationFilename))
|
||||
if err := os.WriteFile(exitOperationsFilename, data, 0o600); err != nil {
|
||||
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", exitOperationsFilename))
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -48,7 +49,7 @@ var validatorPath = regexp.MustCompile("^m/12381/3600/[0-9]+/0/0$")
|
||||
|
||||
var (
|
||||
offlinePreparationFilename = "offline-preparation.json"
|
||||
exitOperationFilename = "exit-operation.json"
|
||||
exitOperationsFilename = "exit-operations.json"
|
||||
)
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
@@ -68,37 +69,41 @@ func (c *command) process(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.obtainOperation(ctx); err != nil {
|
||||
if err := c.obtainOperations(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if validated, reason := c.validateOperation(ctx); !validated {
|
||||
return fmt.Errorf("operation failed validation: %s", reason)
|
||||
if len(c.signedOperations) == 0 {
|
||||
return errors.New("no suitable validators found; no operations generated")
|
||||
}
|
||||
|
||||
if validated, reason := c.validateOperations(ctx); !validated {
|
||||
return fmt.Errorf("operations failed validation: %s", reason)
|
||||
}
|
||||
|
||||
if c.json || c.offline {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Not broadcasting exit operation\n")
|
||||
fmt.Fprintf(os.Stderr, "Not broadcasting exit operations\n")
|
||||
}
|
||||
// Want JSON output, or cannot broadcast.
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.broadcastOperation(ctx)
|
||||
return c.broadcastOperations(ctx)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperation(ctx context.Context) error {
|
||||
if (c.mnemonic == "" || c.path == "") && c.privateKey == "" && c.validator == "" {
|
||||
func (c *command) obtainOperations(ctx context.Context) error {
|
||||
if c.mnemonic == "" && c.privateKey == "" && c.validator == "" {
|
||||
// No input information; fetch the operation from a file.
|
||||
err := c.obtainOperationFromFileOrInput(ctx)
|
||||
err := c.obtainOperationsFromFileOrInput(ctx)
|
||||
if err == nil {
|
||||
// Success.
|
||||
return nil
|
||||
}
|
||||
if c.signedOperationInput != "" {
|
||||
if c.signedOperationsInput != "" {
|
||||
return errors.Wrap(err, "failed to obtain supplied signed operation")
|
||||
}
|
||||
return errors.Wrap(err, fmt.Sprintf("no account, mnemonic or private key specified, and no %s file loaded", exitOperationFilename))
|
||||
return errors.Wrap(err, fmt.Sprintf("no account, mnemonic or private key specified, and no %s file loaded", exitOperationsFilename))
|
||||
}
|
||||
|
||||
if c.mnemonic != "" {
|
||||
@@ -110,7 +115,8 @@ func (c *command) obtainOperation(ctx context.Context) error {
|
||||
// Have a mnemonic and validator.
|
||||
return c.generateOperationFromMnemonicAndValidator(ctx)
|
||||
default:
|
||||
return errors.New("mnemonic must be supplied with either a path or validator")
|
||||
// Have a mnemonic only.
|
||||
return c.generateOperationsFromMnemonic(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,20 +132,30 @@ func (c *command) obtainOperation(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromMnemonicAndPath(ctx context.Context) error {
|
||||
// Turn the validators in to a map for easy lookup.
|
||||
validators := make(map[string]*beacon.ValidatorInfo, 0)
|
||||
for _, validator := range c.chainInfo.Validators {
|
||||
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
|
||||
}
|
||||
|
||||
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validatorKeyPath := c.path
|
||||
match := validatorPath.Match([]byte(c.path))
|
||||
match := validatorPath.MatchString(c.path)
|
||||
if !match {
|
||||
return fmt.Errorf("path %s does not match EIP-2334 format for a validator", c.path)
|
||||
}
|
||||
|
||||
if err := c.generateOperationFromSeedAndPath(ctx, seed, validatorKeyPath); err != nil {
|
||||
found, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate operation from seed and path")
|
||||
}
|
||||
if !found {
|
||||
return errors.New("no validator found with the provided path and mnemonic, please run with --debug to see more information")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -155,6 +171,10 @@ func (c *command) generateOperationFromMnemonicAndValidator(ctx context.Context)
|
||||
return err
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Searching for validator with index %d and public key %s\n", validatorInfo.Index, validatorInfo.Pubkey.String())
|
||||
}
|
||||
|
||||
// Scan the keys from the seed to find the path.
|
||||
maxDistance := 1024
|
||||
// Start scanning the validator keys.
|
||||
@@ -187,6 +207,49 @@ func (c *command) generateOperationFromMnemonicAndValidator(ctx context.Context)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
|
||||
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Turn the validators in to a map for easy lookup.
|
||||
validators := make(map[string]*beacon.ValidatorInfo, 0)
|
||||
for _, validator := range c.chainInfo.Validators {
|
||||
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
|
||||
}
|
||||
|
||||
maxDistance := 1024
|
||||
// Start scanning the validator keys.
|
||||
lastFoundIndex := 0
|
||||
foundValidatorCount := 0
|
||||
for i := 0; ; i++ {
|
||||
// If no validators have been found in the last maxDistance indices, stop scanning.
|
||||
if i-lastFoundIndex > maxDistance {
|
||||
// If no validators were found at all, return an error.
|
||||
if foundValidatorCount == 0 {
|
||||
return fmt.Errorf("failed to find validators using the provided mnemonic: searched %d indices without finding a validator", maxDistance)
|
||||
}
|
||||
break
|
||||
}
|
||||
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
|
||||
|
||||
found, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath)
|
||||
if err != nil {
|
||||
// We log errors but keep going.
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Failed to generate for path %s: %v\n", validatorKeyPath, err.Error())
|
||||
}
|
||||
}
|
||||
if found {
|
||||
lastFoundIndex = i
|
||||
foundValidatorCount++
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromPrivateKey(ctx context.Context) error {
|
||||
validatorAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
|
||||
if err != nil {
|
||||
@@ -205,62 +268,104 @@ func (c *command) generateOperationFromValidator(ctx context.Context) error {
|
||||
return c.generateOperationFromAccount(ctx, validatorAccount)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationFromFileOrInput(ctx context.Context) error {
|
||||
// Start off by attempting to use the provided signed operation.
|
||||
if c.signedOperationInput != "" {
|
||||
return c.obtainOperationFromInput(ctx)
|
||||
func (c *command) obtainOperationsFromFileOrInput(ctx context.Context) error {
|
||||
// Start off by attempting to use the provided signed operations.
|
||||
if c.signedOperationsInput != "" {
|
||||
return c.obtainOperationsFromInput(ctx)
|
||||
}
|
||||
// If not, read it from the file with the standard name.
|
||||
return c.obtainOperationFromFile(ctx)
|
||||
return c.obtainOperationsFromFile(ctx)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationFromFile(ctx context.Context) error {
|
||||
_, err := os.Stat(exitOperationFilename)
|
||||
func (c *command) obtainOperationsFromFile(ctx context.Context) error {
|
||||
_, err := os.Stat(exitOperationsFilename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read exit operation file")
|
||||
return errors.Wrap(err, "failed to read exit operations file")
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "%s found; loading operation\n", exitOperationFilename)
|
||||
fmt.Fprintf(os.Stderr, "%s found; loading operations\n", exitOperationsFilename)
|
||||
}
|
||||
data, err := os.ReadFile(exitOperationFilename)
|
||||
data, err := os.ReadFile(exitOperationsFilename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read exit operation file")
|
||||
return errors.Wrap(err, "failed to read exit operations file")
|
||||
}
|
||||
if err := json.Unmarshal(data, &c.signedOperation); err != nil {
|
||||
return errors.Wrap(err, "failed to parse exit operation file")
|
||||
if err := json.Unmarshal(data, &c.signedOperations); err != nil {
|
||||
return errors.Wrap(err, "failed to parse exit operations file")
|
||||
}
|
||||
|
||||
return c.verifySignedOperation(ctx, c.signedOperation)
|
||||
return c.verifySignedOperations(ctx)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationFromInput(ctx context.Context) error {
|
||||
if !strings.HasPrefix(c.signedOperationInput, "{") {
|
||||
func (c *command) obtainOperationsFromInput(ctx context.Context) error {
|
||||
if !strings.HasPrefix(c.signedOperationsInput, "{") &&
|
||||
!strings.HasPrefix(c.signedOperationsInput, "[") {
|
||||
// This looks like a file; read it in.
|
||||
data, err := os.ReadFile(c.signedOperationInput)
|
||||
data, err := os.ReadFile(c.signedOperationsInput)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read input file")
|
||||
}
|
||||
c.signedOperationInput = string(data)
|
||||
c.signedOperationsInput = string(data)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(c.signedOperationInput), &c.signedOperation); err != nil {
|
||||
if strings.HasPrefix(c.signedOperationsInput, "{") {
|
||||
// Single operation; put it in an array.
|
||||
c.signedOperationsInput = fmt.Sprintf("[%s]", c.signedOperationsInput)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(c.signedOperationsInput), &c.signedOperations); err != nil {
|
||||
return errors.Wrap(err, "failed to parse exit operation input")
|
||||
}
|
||||
|
||||
return c.verifySignedOperation(ctx, c.signedOperation)
|
||||
return c.verifySignedOperations(ctx)
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
|
||||
validators map[string]*beacon.ValidatorInfo,
|
||||
seed []byte,
|
||||
path string,
|
||||
) error {
|
||||
) (
|
||||
bool,
|
||||
error,
|
||||
) {
|
||||
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate validator private key")
|
||||
return false, errors.Wrap(err, "failed to generate validator private key")
|
||||
}
|
||||
|
||||
c.privateKey = fmt.Sprintf("%#x", validatorPrivkey.Marshal())
|
||||
return c.generateOperationFromPrivateKey(ctx)
|
||||
privateKey := fmt.Sprintf("%#x", validatorPrivkey.Marshal())
|
||||
|
||||
validatorInfo, exists := validators[fmt.Sprintf("%#x", validatorPrivkey.PublicKey().Marshal())]
|
||||
if !exists {
|
||||
return false, errors.New("unknown validator")
|
||||
}
|
||||
if validatorInfo.State == apiv1.ValidatorStateActiveExiting ||
|
||||
validatorInfo.State == apiv1.ValidatorStateActiveSlashed ||
|
||||
validatorInfo.State == apiv1.ValidatorStateExitedUnslashed ||
|
||||
validatorInfo.State == apiv1.ValidatorStateExitedSlashed ||
|
||||
validatorInfo.State == apiv1.ValidatorStateWithdrawalPossible ||
|
||||
validatorInfo.State == apiv1.ValidatorStateWithdrawalDone {
|
||||
return false, fmt.Errorf("validator is in state %v, not suitable to generate an exit", validatorInfo.State)
|
||||
}
|
||||
|
||||
validatorAccount, err := util.ParseAccount(ctx, privateKey, nil, true)
|
||||
if err != nil {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "no validator found at path %s: %v\n", path, err)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
if validatorAccount == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := c.generateOperationFromAccount(ctx, validatorAccount); err != nil {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "failed to generate operation at path %s: %v\n", path, err)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromAccount(ctx context.Context,
|
||||
@@ -276,8 +381,40 @@ func (c *command) generateOperationFromAccount(ctx context.Context,
|
||||
return err
|
||||
}
|
||||
|
||||
c.signedOperation, err = c.createSignedOperation(ctx, info, account, c.chainInfo.Epoch)
|
||||
return err
|
||||
epoch, err := c.selectEpoch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Using %d for epoch\n", epoch)
|
||||
}
|
||||
|
||||
signedOperation, err := c.createSignedOperation(ctx, info, account, epoch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.signedOperations = append(c.signedOperations, signedOperation)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) selectEpoch() (phase0.Epoch, error) {
|
||||
if c.epoch == "" {
|
||||
// No user-supplied epoch; use the one from chain info.
|
||||
return c.chainInfo.Epoch, nil
|
||||
}
|
||||
|
||||
epoch, err := strconv.ParseInt(c.epoch, 10, 64)
|
||||
if err != nil {
|
||||
return 0, errors.New("epoch invalid")
|
||||
}
|
||||
|
||||
if epoch < 0 {
|
||||
// Relative epoch.
|
||||
return c.chainInfo.Epoch - phase0.Epoch(-epoch), nil
|
||||
}
|
||||
|
||||
return phase0.Epoch(epoch), nil
|
||||
}
|
||||
|
||||
func (c *command) createSignedOperation(ctx context.Context,
|
||||
@@ -322,6 +459,15 @@ func (c *command) createSignedOperation(ctx context.Context,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *command) verifySignedOperations(ctx context.Context) error {
|
||||
for _, op := range c.signedOperations {
|
||||
if err := c.verifySignedOperation(ctx, op); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) verifySignedOperation(ctx context.Context, op *phase0.SignedVoluntaryExit) error {
|
||||
root, err := op.Message.HashTreeRoot()
|
||||
if err != nil {
|
||||
@@ -366,14 +512,26 @@ func (c *command) verifySignedOperation(ctx context.Context, op *phase0.SignedVo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) validateOperations(ctx context.Context) (bool, string) {
|
||||
for _, op := range c.signedOperations {
|
||||
valid, issue := c.validateOperation(ctx, op)
|
||||
if !valid {
|
||||
return valid, issue
|
||||
}
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (c *command) validateOperation(_ context.Context,
|
||||
op *phase0.SignedVoluntaryExit,
|
||||
) (
|
||||
bool,
|
||||
string,
|
||||
) {
|
||||
var validatorInfo *beacon.ValidatorInfo
|
||||
for _, chainValidatorInfo := range c.chainInfo.Validators {
|
||||
if chainValidatorInfo.Index == c.signedOperation.Message.ValidatorIndex {
|
||||
if chainValidatorInfo.Index == op.Message.ValidatorIndex {
|
||||
validatorInfo = chainValidatorInfo
|
||||
break
|
||||
}
|
||||
@@ -382,7 +540,7 @@ func (c *command) validateOperation(_ context.Context,
|
||||
return false, "validator not known on chain"
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Validator exit operation: %v", c.signedOperation)
|
||||
fmt.Fprintf(os.Stderr, "Validator exit operation: %v", op)
|
||||
fmt.Fprintf(os.Stderr, "On-chain validator info: %v\n", validatorInfo)
|
||||
}
|
||||
|
||||
@@ -398,8 +556,20 @@ func (c *command) validateOperation(_ context.Context,
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (c *command) broadcastOperation(ctx context.Context) error {
|
||||
return c.consensusClient.(consensusclient.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, c.signedOperation)
|
||||
func (c *command) broadcastOperations(ctx context.Context) error {
|
||||
for _, op := range c.signedOperations {
|
||||
if c.debug {
|
||||
data, err := json.Marshal(op)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Broadcasting %s\n", string(data))
|
||||
}
|
||||
}
|
||||
if err := c.consensusClient.(consensusclient.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, op); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) setup(ctx context.Context) error {
|
||||
@@ -429,7 +599,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
// Set up chaintime.
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithGenesisTimeProvider(c.consensusClient.(consensusclient.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.consensusClient.(consensusclient.GenesisProvider)),
|
||||
standardchaintime.WithSpecProvider(c.consensusClient.(consensusclient.SpecProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -444,7 +614,7 @@ func (c *command) generateDomain(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
forkVersion, err := c.obtainForkVersion(ctx)
|
||||
forkVersion, err := c.obtainExitForkVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -491,10 +661,11 @@ func (c *command) obtainGenesisValidatorsRoot(_ context.Context) (phase0.Root, e
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Using genesis validators root %#x\n", genesisValidatorsRoot)
|
||||
}
|
||||
|
||||
return genesisValidatorsRoot, nil
|
||||
}
|
||||
|
||||
func (c *command) obtainForkVersion(_ context.Context) (phase0.Version, error) {
|
||||
func (c *command) obtainExitForkVersion(_ context.Context) (phase0.Version, error) {
|
||||
forkVersion := phase0.Version{}
|
||||
|
||||
if c.forkVersion != "" {
|
||||
@@ -513,12 +684,13 @@ func (c *command) obtainForkVersion(_ context.Context) (phase0.Version, error) {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Fork version obtained from chain info\n")
|
||||
}
|
||||
// Use the current fork version for generating an exit as per the spec.
|
||||
copy(forkVersion[:], c.chainInfo.CurrentForkVersion[:])
|
||||
// Use the Capella fork version for generating an exit as per the spec.
|
||||
copy(forkVersion[:], c.chainInfo.ExitForkVersion[:])
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Using fork version %#x\n", forkVersion)
|
||||
}
|
||||
|
||||
return forkVersion, nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/beacon"
|
||||
@@ -95,7 +96,7 @@ func TestGenerateOperationFromMnemonicAndPath(t *testing.T) {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, test.command.signedOperation)
|
||||
require.Equal(t, test.expected, test.command.signedOperations[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -187,7 +188,7 @@ func TestGenerateOperationFromMnemonicAndValidator(t *testing.T) {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, test.command.signedOperation)
|
||||
require.Equal(t, test.expected, test.command.signedOperations[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -198,29 +199,44 @@ func TestGenerateOperationFromSeedAndPath(t *testing.T) {
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
validator0 := &beacon.ValidatorInfo{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
State: apiv1.ValidatorStateActiveOngoing,
|
||||
}
|
||||
validator1 := &beacon.ValidatorInfo{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
State: apiv1.ValidatorStateActiveOngoing,
|
||||
}
|
||||
validator2 := &beacon.ValidatorInfo{
|
||||
Index: 2,
|
||||
Pubkey: phase0.BLSPubKey{0xaf, 0x9c, 0xe4, 0x4f, 0x50, 0x14, 0x8d, 0xb4, 0x12, 0x19, 0x4a, 0xf0, 0xba, 0xf0, 0xba, 0xb3, 0x6b, 0xd5, 0xc3, 0xe0, 0xc4, 0x93, 0x89, 0x11, 0xa4, 0xe5, 0x02, 0xe3, 0x98, 0xb5, 0x9e, 0x5c, 0xca, 0x7c, 0x78, 0xe3, 0xfe, 0x03, 0x41, 0x95, 0x47, 0x88, 0x79, 0xee, 0xb2, 0x3d, 0xb0, 0xa6},
|
||||
WithdrawalCredentials: []byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
State: apiv1.ValidatorStateActiveOngoing,
|
||||
}
|
||||
validator3 := &beacon.ValidatorInfo{
|
||||
Index: 3,
|
||||
Pubkey: phase0.BLSPubKey{0x86, 0xd3, 0x30, 0xaf, 0x51, 0xfa, 0x59, 0x3f, 0xa9, 0xf9, 0x3e, 0xdb, 0x9d, 0x16, 0x64, 0x01, 0x86, 0xbe, 0x2e, 0x93, 0xea, 0x94, 0xd2, 0x59, 0x78, 0x1e, 0x1e, 0xb3, 0x4d, 0xeb, 0x84, 0x4c, 0x39, 0x68, 0xd7, 0x5e, 0xa9, 0x1d, 0x19, 0xf1, 0x59, 0xdb, 0xd0, 0x52, 0x3c, 0x6c, 0x5b, 0xa5},
|
||||
WithdrawalCredentials: []byte{0x00, 0x81, 0x68, 0x45, 0x6b, 0x6d, 0x9a, 0x32, 0x83, 0x93, 0x1f, 0xea, 0x52, 0x10, 0xda, 0x12, 0x2d, 0x1e, 0x65, 0xe8, 0xed, 0x50, 0xb8, 0xe8, 0xf5, 0x91, 0x11, 0x83, 0xb0, 0x2f, 0xd1, 0x25},
|
||||
State: apiv1.ValidatorStateActiveOngoing,
|
||||
}
|
||||
|
||||
validators := map[string]*beacon.ValidatorInfo{
|
||||
"0xb384f767d964e100c8a9b21018d08c25ffebae268b3ab6d610353897541971726dbfc3c7463884c68a531515aab94c87": validator0,
|
||||
"0xb3d89e2f29c712c6a9f8e5a269b97617c4a94dd6f6662ab3b07ce9e5434573f15b5c988cd14bbd5804f77156a8af1cfa": validator1,
|
||||
"0xaf9ce44f50148db412194af0baf0bab36bd5c3e0c4938911a4e502e398b59e5cca7c78e3fe034195478879eeb23db0a6": validator2,
|
||||
"0x86d330af51fa593fa9f93edb9d16640186be2e93ea94d259781e1eb34deb844c3968d75ea91d19f159dbd0523c6c5ba5": validator3,
|
||||
}
|
||||
chainInfo := &beacon.ChainInfo{
|
||||
Version: 1,
|
||||
Validators: []*beacon.ValidatorInfo{
|
||||
{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
{
|
||||
Index: 2,
|
||||
Pubkey: phase0.BLSPubKey{0xaf, 0x9c, 0xe4, 0x4f, 0x50, 0x14, 0x8d, 0xb4, 0x12, 0x19, 0x4a, 0xf0, 0xba, 0xf0, 0xba, 0xb3, 0x6b, 0xd5, 0xc3, 0xe0, 0xc4, 0x93, 0x89, 0x11, 0xa4, 0xe5, 0x02, 0xe3, 0x98, 0xb5, 0x9e, 0x5c, 0xca, 0x7c, 0x78, 0xe3, 0xfe, 0x03, 0x41, 0x95, 0x47, 0x88, 0x79, 0xee, 0xb2, 0x3d, 0xb0, 0xa6},
|
||||
WithdrawalCredentials: []byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
{
|
||||
Index: 3,
|
||||
Pubkey: phase0.BLSPubKey{0x86, 0xd3, 0x30, 0xaf, 0x51, 0xfa, 0x59, 0x3f, 0xa9, 0xf9, 0x3e, 0xdb, 0x9d, 0x16, 0x64, 0x01, 0x86, 0xbe, 0x2e, 0x93, 0xea, 0x94, 0xd2, 0x59, 0x78, 0x1e, 0x1e, 0xb3, 0x4d, 0xeb, 0x84, 0x4c, 0x39, 0x68, 0xd7, 0x5e, 0xa9, 0x1d, 0x19, 0xf1, 0x59, 0xdb, 0xd0, 0x52, 0x3c, 0x6c, 0x5b, 0xa5},
|
||||
WithdrawalCredentials: []byte{0x00, 0x81, 0x68, 0x45, 0x6b, 0x6d, 0x9a, 0x32, 0x83, 0x93, 0x1f, 0xea, 0x52, 0x10, 0xda, 0x12, 0x2d, 0x1e, 0x65, 0xe8, 0xed, 0x50, 0xb8, 0xe8, 0xf5, 0x91, 0x11, 0x83, 0xb0, 0x2f, 0xd1, 0x25},
|
||||
},
|
||||
validator0,
|
||||
validator1,
|
||||
validator2,
|
||||
validator3,
|
||||
},
|
||||
GenesisValidatorsRoot: phase0.Root{},
|
||||
Epoch: 1,
|
||||
@@ -296,12 +312,14 @@ func TestGenerateOperationFromSeedAndPath(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.generateOperationFromSeedAndPath(ctx, test.seed, test.path)
|
||||
found, err := test.command.generateOperationFromSeedAndPath(ctx, validators, test.seed, test.path)
|
||||
if test.err != "" {
|
||||
require.False(t, found)
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, test.command.signedOperation)
|
||||
require.True(t, found)
|
||||
require.Equal(t, test.expected, test.command.signedOperations[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -340,10 +358,12 @@ func TestVerifyOperation(t *testing.T) {
|
||||
name: "SignatureMissing",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
signedOperations: []*phase0.SignedVoluntaryExit{
|
||||
{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -353,12 +373,14 @@ func TestVerifyOperation(t *testing.T) {
|
||||
name: "SignatureShort",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
signedOperations: []*phase0.SignedVoluntaryExit{
|
||||
{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
Signature: phase0.BLSSignature{0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
},
|
||||
err: "invalid signature",
|
||||
@@ -367,12 +389,14 @@ func TestVerifyOperation(t *testing.T) {
|
||||
name: "SignatureIncorrect",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
signedOperations: []*phase0.SignedVoluntaryExit{
|
||||
{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x99, 0x78, 0xb4, 0x9c, 0x21, 0x60, 0x3f, 0x04, 0xa3, 0x04, 0x4e, 0x4c, 0x49, 0x0c, 0xb4, 0x68, 0x7c, 0x6e, 0x14, 0xc2, 0xda, 0xed, 0x25, 0x92, 0xe0, 0x02, 0x2d, 0xcd, 0x63, 0xeb, 0xe7, 0x4a, 0xf1, 0x1a, 0xca, 0xba, 0xae, 0x50, 0xe1, 0x8a, 0x1d, 0xae, 0x96, 0xd9, 0xd2, 0x56, 0xbf, 0x9f, 0x02, 0x48, 0x85, 0x05, 0xc1, 0xfb, 0xb3, 0x4a, 0x0b, 0x68, 0xec, 0xc5, 0xb5, 0xf5, 0xea, 0x53, 0xdb, 0xd0, 0x09, 0x08, 0xe3, 0x1e, 0xa8, 0xca, 0x9d, 0x02, 0x08, 0x3b, 0x9e, 0xf1, 0xc7, 0xd2, 0x32, 0xf4, 0xba, 0xd9, 0xea, 0x56, 0x4b, 0xc5, 0x87, 0xd5, 0x27, 0xb7, 0x74, 0x97, 0x8a, 0xee},
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x99, 0x78, 0xb4, 0x9c, 0x21, 0x60, 0x3f, 0x04, 0xa3, 0x04, 0x4e, 0x4c, 0x49, 0x0c, 0xb4, 0x68, 0x7c, 0x6e, 0x14, 0xc2, 0xda, 0xed, 0x25, 0x92, 0xe0, 0x02, 0x2d, 0xcd, 0x63, 0xeb, 0xe7, 0x4a, 0xf1, 0x1a, 0xca, 0xba, 0xae, 0x50, 0xe1, 0x8a, 0x1d, 0xae, 0x96, 0xd9, 0xd2, 0x56, 0xbf, 0x9f, 0x02, 0x48, 0x85, 0x05, 0xc1, 0xfb, 0xb3, 0x4a, 0x0b, 0x68, 0xec, 0xc5, 0xb5, 0xf5, 0xea, 0x53, 0xdb, 0xd0, 0x09, 0x08, 0xe3, 0x1e, 0xa8, 0xca, 0x9d, 0x02, 0x08, 0x3b, 0x9e, 0xf1, 0xc7, 0xd2, 0x32, 0xf4, 0xba, 0xd9, 0xea, 0x56, 0x4b, 0xc5, 0x87, 0xd5, 0x27, 0xb7, 0x74, 0x97, 0x8a, 0xee},
|
||||
},
|
||||
},
|
||||
err: "signature does not verify",
|
||||
@@ -383,12 +407,14 @@ func TestVerifyOperation(t *testing.T) {
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
validator: "0",
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
signedOperations: []*phase0.SignedVoluntaryExit{
|
||||
{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x89, 0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x89, 0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -396,7 +422,7 @@ func TestVerifyOperation(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.verifySignedOperation(ctx, test.command.signedOperation)
|
||||
err := test.command.verifySignedOperation(ctx, test.command.signedOperations[0])
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
@@ -438,39 +464,39 @@ func TestObtainOperationFromInput(t *testing.T) {
|
||||
{
|
||||
name: "InvalidFilename",
|
||||
command: &command{
|
||||
signedOperationInput: `[]`,
|
||||
chainInfo: chainInfo,
|
||||
signedOperationsInput: `missing.json`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "failed to read input file: open []: no such file or directory",
|
||||
err: "failed to read input file: open missing.json: no such file or directory",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON",
|
||||
command: &command{
|
||||
signedOperationInput: `{invalid}`,
|
||||
chainInfo: chainInfo,
|
||||
signedOperationsInput: `{invalid}`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "failed to parse exit operation input: invalid character 'i' looking for beginning of object key string",
|
||||
},
|
||||
{
|
||||
name: "Unverifable",
|
||||
command: &command{
|
||||
signedOperationInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x9978b49c21603f04a3044e4c490cb4687c6e14c2daed2592e0022dcd63ebe74af11acabaae50e18a1dae96d9d256bf9f02488505c1fbb34a0b68ecc5b5f5ea53dbd00908e31ea8ca9d02083b9ef1c7d232f4bad9ea564bc587d527b774978aee"}`,
|
||||
chainInfo: chainInfo,
|
||||
signedOperationsInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x9978b49c21603f04a3044e4c490cb4687c6e14c2daed2592e0022dcd63ebe74af11acabaae50e18a1dae96d9d256bf9f02488505c1fbb34a0b68ecc5b5f5ea53dbd00908e31ea8ca9d02083b9ef1c7d232f4bad9ea564bc587d527b774978aee"}`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "signature does not verify",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
command: &command{
|
||||
signedOperationInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x89f5c44288f95e19b6c139f2623005665b983462a228120977d81f2ef547560be22446de21a8a937d9dda4e2d2ec4175196496cdd1306dec4a125f8c861f806171504a9d6a610ec4e135047e4fb67052ecc4561360d0c3de04b6fbc4474223ff"}`,
|
||||
chainInfo: chainInfo,
|
||||
signedOperationsInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x89f5c44288f95e19b6c139f2623005665b983462a228120977d81f2ef547560be22446de21a8a937d9dda4e2d2ec4175196496cdd1306dec4a125f8c861f806171504a9d6a610ec4e135047e4fb67052ecc4561360d0c3de04b6fbc4474223ff"}`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.obtainOperationFromInput(ctx)
|
||||
err := test.command.obtainOperationsFromFileOrInput(ctx)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
|
||||
@@ -15,9 +15,12 @@ package validatorexpectation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/hako/durafmt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@@ -26,6 +29,7 @@ type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
json bool
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
@@ -38,18 +42,43 @@ type command struct {
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
validatorsProvider eth2client.ValidatorsProvider
|
||||
activeValidators int
|
||||
|
||||
// Output.
|
||||
// Results.
|
||||
res *results
|
||||
}
|
||||
|
||||
type results struct {
|
||||
activeValidators uint64
|
||||
timeBetweenProposals time.Duration
|
||||
timeBetweenSyncCommittees time.Duration
|
||||
}
|
||||
|
||||
type resultsJSON struct {
|
||||
ActiveValidators string `json:"active_validators"`
|
||||
TimeBetweenProposals string `json:"time_between_proposals"`
|
||||
SecsBetweenProposals string `json:"secs_between_proposals"`
|
||||
TimeBetweenSyncCommittees string `json:"time_between_sync_committees"`
|
||||
SecsBetweenSyncCommittees string `json:"secs_between_sync_committees"`
|
||||
}
|
||||
|
||||
func (r *results) MarshalJSON() ([]byte, error) {
|
||||
data := &resultsJSON{
|
||||
ActiveValidators: strconv.FormatUint(r.activeValidators, 10),
|
||||
TimeBetweenProposals: durafmt.Parse(r.timeBetweenProposals).LimitFirstN(2).String(),
|
||||
SecsBetweenProposals: strconv.FormatInt(int64(r.timeBetweenProposals.Seconds()), 10),
|
||||
TimeBetweenSyncCommittees: durafmt.Parse(r.timeBetweenSyncCommittees).LimitFirstN(2).String(),
|
||||
SecsBetweenSyncCommittees: strconv.FormatInt(int64(r.timeBetweenSyncCommittees.Seconds()), 10),
|
||||
}
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func newCommand(_ context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
json: viper.GetBool("json"),
|
||||
res: &results{},
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
|
||||
@@ -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,24 +15,42 @@ package validatorexpectation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hako/durafmt"
|
||||
)
|
||||
|
||||
func (c *command) output(_ context.Context) (string, error) {
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.json {
|
||||
return c.outputJSON(ctx)
|
||||
}
|
||||
return c.outputTxt(ctx)
|
||||
}
|
||||
|
||||
func (c *command) outputJSON(_ context.Context) (string, error) {
|
||||
data, err := json.Marshal(c.res)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s\n", string(data)), nil
|
||||
}
|
||||
|
||||
func (c *command) outputTxt(_ context.Context) (string, error) {
|
||||
builder := strings.Builder{}
|
||||
|
||||
builder.WriteString("Expected time between block proposals: ")
|
||||
builder.WriteString(durafmt.Parse(c.timeBetweenProposals).LimitFirstN(2).String())
|
||||
builder.WriteString(durafmt.Parse(c.res.timeBetweenProposals).LimitFirstN(2).String())
|
||||
builder.WriteString("\n")
|
||||
|
||||
builder.WriteString("Expected time between sync committees: ")
|
||||
builder.WriteString(durafmt.Parse(c.timeBetweenSyncCommittees).LimitFirstN(2).String())
|
||||
builder.WriteString(durafmt.Parse(c.res.timeBetweenSyncCommittees).LimitFirstN(2).String())
|
||||
builder.WriteString("\n")
|
||||
|
||||
return builder.String(), nil
|
||||
|
||||
@@ -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
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
@@ -31,7 +32,7 @@ func (c *command) process(ctx context.Context) error {
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Printf("Active validators: %d\n", c.activeValidators)
|
||||
fmt.Printf("Active validators: %d\n", c.res.activeValidators)
|
||||
}
|
||||
|
||||
if err := c.calculateProposalChance(ctx); err != nil {
|
||||
@@ -45,12 +46,12 @@ func (c *command) calculateProposalChance(ctx context.Context) error {
|
||||
// Chance of proposing a block is 1/activeValidators.
|
||||
// Expectation of number of slots before proposing a block is 1/p, == activeValidators slots.
|
||||
|
||||
spec, err := c.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
specResponse, err := c.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp, exists := spec["SECONDS_PER_SLOT"]
|
||||
tmp, exists := specResponse.Data["SECONDS_PER_SLOT"]
|
||||
if !exists {
|
||||
return errors.New("spec missing SECONDS_PER_SLOT")
|
||||
}
|
||||
@@ -59,7 +60,7 @@ func (c *command) calculateProposalChance(ctx context.Context) error {
|
||||
return errors.New("SECONDS_PER_SLOT of incorrect type")
|
||||
}
|
||||
|
||||
c.timeBetweenProposals = slotDuration * time.Duration(c.activeValidators) / time.Duration(c.validators)
|
||||
c.res.timeBetweenProposals = slotDuration * time.Duration(c.res.activeValidators) / time.Duration(c.validators)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -68,12 +69,12 @@ func (c *command) calculateSyncCommitteeChance(ctx context.Context) error {
|
||||
// Chance of being in a sync committee is SYNC_COMMITTEE_SIZE/activeValidators.
|
||||
// Expectation of number of periods before being in a sync committee is 1/p, activeValidators/SYNC_COMMITTEE_SIZE periods.
|
||||
|
||||
spec, err := c.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
specResponse, err := c.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp, exists := spec["SECONDS_PER_SLOT"]
|
||||
tmp, exists := specResponse.Data["SECONDS_PER_SLOT"]
|
||||
if !exists {
|
||||
return errors.New("spec missing SECONDS_PER_SLOT")
|
||||
}
|
||||
@@ -82,7 +83,7 @@ func (c *command) calculateSyncCommitteeChance(ctx context.Context) error {
|
||||
return errors.New("SECONDS_PER_SLOT of incorrect type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["SYNC_COMMITTEE_SIZE"]
|
||||
tmp, exists = specResponse.Data["SYNC_COMMITTEE_SIZE"]
|
||||
if !exists {
|
||||
return errors.New("spec missing SYNC_COMMITTEE_SIZE")
|
||||
}
|
||||
@@ -91,7 +92,7 @@ func (c *command) calculateSyncCommitteeChance(ctx context.Context) error {
|
||||
return errors.New("SYNC_COMMITTEE_SIZE of incorrect type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["SLOTS_PER_EPOCH"]
|
||||
tmp, exists = specResponse.Data["SLOTS_PER_EPOCH"]
|
||||
if !exists {
|
||||
return errors.New("spec missing SLOTS_PER_EPOCH")
|
||||
}
|
||||
@@ -100,7 +101,7 @@ func (c *command) calculateSyncCommitteeChance(ctx context.Context) error {
|
||||
return errors.New("SLOTS_PER_EPOCH of incorrect type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["EPOCHS_PER_SYNC_COMMITTEE_PERIOD"]
|
||||
tmp, exists = specResponse.Data["EPOCHS_PER_SYNC_COMMITTEE_PERIOD"]
|
||||
if !exists {
|
||||
return errors.New("spec missing EPOCHS_PER_SYNC_COMMITTEE_PERIOD")
|
||||
}
|
||||
@@ -109,12 +110,12 @@ func (c *command) calculateSyncCommitteeChance(ctx context.Context) error {
|
||||
return errors.New("EPOCHS_PER_SYNC_COMMITTEE_PERIOD of incorrect type")
|
||||
}
|
||||
|
||||
periodsBetweenSyncCommittees := uint64(c.activeValidators) / syncCommitteeSize
|
||||
periodsBetweenSyncCommittees := c.res.activeValidators / syncCommitteeSize
|
||||
if c.debug {
|
||||
fmt.Printf("Sync committee periods between inclusion: %d\n", periodsBetweenSyncCommittees)
|
||||
}
|
||||
|
||||
c.timeBetweenSyncCommittees = slotDuration * time.Duration(slotsPerEpoch*epochsPerPeriod) * time.Duration(periodsBetweenSyncCommittees) / time.Duration(c.validators)
|
||||
c.res.timeBetweenSyncCommittees = slotDuration * time.Duration(slotsPerEpoch*epochsPerPeriod) * time.Duration(periodsBetweenSyncCommittees) / time.Duration(c.validators)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -135,7 +136,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
chainTime, err := standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
@@ -148,16 +149,18 @@ func (c *command) setup(ctx context.Context) error {
|
||||
return errors.New("connection does not provide validator information")
|
||||
}
|
||||
|
||||
validators, err := c.validatorsProvider.Validators(ctx, "head", nil)
|
||||
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
|
||||
State: "head",
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validators")
|
||||
}
|
||||
|
||||
currentEpoch := chainTime.CurrentEpoch()
|
||||
for _, validator := range validators {
|
||||
for _, validator := range response.Data {
|
||||
if validator.Validator.ActivationEpoch <= currentEpoch &&
|
||||
validator.Validator.ExitEpoch > currentEpoch {
|
||||
c.activeValidators++
|
||||
c.res.activeValidators++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@ func input(_ context.Context) (*dataIn, error) {
|
||||
}
|
||||
|
||||
data.mnemonic = viper.GetString("mnemonic")
|
||||
data.privKey = viper.GetString("privkey")
|
||||
data.privKey = viper.GetString("private-key")
|
||||
if data.mnemonic == "" && data.privKey == "" {
|
||||
return nil, errors.New("mnemonic or privkey is required")
|
||||
return nil, errors.New("mnemonic or private key is required")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{
|
||||
"withdrawal-credentials": "0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02",
|
||||
},
|
||||
err: "mnemonic or privkey is required",
|
||||
err: "mnemonic or private key is required",
|
||||
},
|
||||
{
|
||||
name: "GoodWithMnemonic",
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"sort"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
@@ -27,6 +28,10 @@ import (
|
||||
)
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
if len(c.validators) == 0 {
|
||||
return errors.New("no validators supplied")
|
||||
}
|
||||
|
||||
// Obtain information we need to process.
|
||||
err := c.setup(ctx)
|
||||
if err != nil {
|
||||
@@ -77,21 +82,23 @@ func (c *command) process(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (c *command) processProposerDuties(ctx context.Context) error {
|
||||
duties, err := c.proposerDutiesProvider.ProposerDuties(ctx, c.summary.Epoch, nil)
|
||||
response, err := c.proposerDutiesProvider.ProposerDuties(ctx, &api.ProposerDutiesOpts{
|
||||
Epoch: c.summary.Epoch,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain proposer duties")
|
||||
}
|
||||
if duties == nil {
|
||||
return errors.New("empty proposer duties")
|
||||
}
|
||||
for _, duty := range duties {
|
||||
for _, duty := range response.Data {
|
||||
if _, exists := c.validatorsByIndex[duty.ValidatorIndex]; !exists {
|
||||
continue
|
||||
}
|
||||
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", duty.Slot))
|
||||
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: fmt.Sprintf("%d", duty.Slot),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", duty.Slot))
|
||||
}
|
||||
block := blockResponse.Data
|
||||
present := block != nil
|
||||
c.summary.Proposals = append(c.summary.Proposals, &epochProposal{
|
||||
Slot: duty.Slot,
|
||||
@@ -129,10 +136,14 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Obtain the duties for the validators to know where they should be attesting.
|
||||
duties, err := c.attesterDutiesProvider.AttesterDuties(ctx, c.summary.Epoch, activeValidatorIndices)
|
||||
dutiesResponse, err := c.attesterDutiesProvider.AttesterDuties(ctx, &api.AttesterDutiesOpts{
|
||||
Epoch: c.summary.Epoch,
|
||||
Indices: activeValidatorIndices,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain attester duties")
|
||||
}
|
||||
duties := dutiesResponse.Data
|
||||
for slot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch); slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1); slot++ {
|
||||
index := int(slot - c.chainTime.FirstSlotOfEpoch(c.summary.Epoch))
|
||||
c.summary.Slots[index].Attestations = &slotAttestations{}
|
||||
@@ -209,14 +220,13 @@ func (c *command) processAttesterDutiesSlot(ctx context.Context,
|
||||
headersCache *util.BeaconBlockHeaderCache,
|
||||
activeValidatorIndices []phase0.ValidatorIndex,
|
||||
) error {
|
||||
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
|
||||
}
|
||||
if block == nil {
|
||||
// No block at this slot; that's fine.
|
||||
return nil
|
||||
}
|
||||
block := blockResponse.Data
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -389,7 +399,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
|
||||
108
cmd/validator/withdrawal/command.go
Normal file
108
cmd/validator/withdrawal/command.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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 validatorwithdrawl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
consensusclient "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
offline bool
|
||||
json bool
|
||||
|
||||
// Input.
|
||||
validator string
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Processing.
|
||||
consensusClient consensusclient.Service
|
||||
chainTime chaintime.Service
|
||||
maxWithdrawalsPerPayload uint64
|
||||
maxEffectiveBalance phase0.Gwei
|
||||
|
||||
// Output.
|
||||
res *res
|
||||
}
|
||||
|
||||
func newCommand(_ context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
offline: viper.GetBool("offline"),
|
||||
json: viper.GetBool("json"),
|
||||
timeout: viper.GetDuration("timeout"),
|
||||
connection: viper.GetString("connection"),
|
||||
allowInsecureConnections: viper.GetBool("allow-insecure-connections"),
|
||||
validator: viper.GetString("validator"),
|
||||
res: &res{},
|
||||
}
|
||||
|
||||
// Timeout is required.
|
||||
if c.timeout == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
|
||||
if c.validator == "" {
|
||||
return nil, errors.New("validator is required")
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type res struct {
|
||||
WithdrawalsToGo uint64
|
||||
BlocksToGo uint64
|
||||
Block uint64
|
||||
Wait time.Duration
|
||||
Expected time.Time
|
||||
}
|
||||
|
||||
type resJSON struct {
|
||||
WithdrawalsToGo uint64 `json:"withdrawals_to_go"`
|
||||
BlocksToGo uint64 `json:"blocks_to_go"`
|
||||
Block uint64 `json:"block"`
|
||||
Wait string `json:"wait"`
|
||||
WaitSecs uint64 `json:"wait_secs"`
|
||||
Expected string `json:"expected"`
|
||||
ExpectedTimestamp int64 `json:"expected_timestamp"`
|
||||
}
|
||||
|
||||
func (r *res) MarshalJSON() ([]byte, error) {
|
||||
data := resJSON{
|
||||
WithdrawalsToGo: r.WithdrawalsToGo,
|
||||
BlocksToGo: r.BlocksToGo,
|
||||
Block: r.Block,
|
||||
Wait: r.Wait.Round(time.Second).String(),
|
||||
WaitSecs: uint64(r.Wait.Round(time.Second).Seconds()),
|
||||
Expected: r.Expected.Format("2006-01-02T15:04:05"),
|
||||
ExpectedTimestamp: r.Expected.Unix(),
|
||||
}
|
||||
return json.Marshal(data)
|
||||
}
|
||||
39
cmd/validator/withdrawal/output.go
Normal file
39
cmd/validator/withdrawal/output.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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 validatorwithdrawl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
//nolint:unparam
|
||||
func (c *command) output(_ context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.json {
|
||||
data, err := json.Marshal(c.res)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal results")
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Withdrawal expected at %s in block %d", c.res.Expected.Format("2006-01-02T15:04:05"), c.res.Block), nil
|
||||
}
|
||||
157
cmd/validator/withdrawal/process.go
Normal file
157
cmd/validator/withdrawal/process.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// 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 validatorwithdrawl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
consensusclient "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
const (
|
||||
ethWithdrawalPrefix = 0x01
|
||||
)
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
if err := c.setup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validator, err := util.ParseValidator(ctx, c.consensusClient.(consensusclient.ValidatorsProvider), c.validator, "head")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse validator")
|
||||
}
|
||||
|
||||
if validator.Validator.WithdrawalCredentials[0] != ethWithdrawalPrefix {
|
||||
return errors.New("validator does not have suitable withdrawal credentials")
|
||||
}
|
||||
if validator.Balance == 0 {
|
||||
return errors.New("validator has nothing to withdraw")
|
||||
}
|
||||
|
||||
blockResponse, err := c.consensusClient.(consensusclient.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: "head",
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain block")
|
||||
}
|
||||
block := blockResponse.Data
|
||||
slot, err := block.Slot()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain block slot")
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Slot is %d\n", slot)
|
||||
}
|
||||
|
||||
response, err := c.consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{
|
||||
State: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validators")
|
||||
}
|
||||
validators := make([]*apiv1.Validator, len(response.Data))
|
||||
for _, validator := range response.Data {
|
||||
validators[validator.Index] = validator
|
||||
}
|
||||
|
||||
withdrawals, err := block.Withdrawals()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain withdrawals from block")
|
||||
}
|
||||
if len(withdrawals) == 0 {
|
||||
return errors.New("block without withdrawals; cannot obtain next withdrawal validator index")
|
||||
}
|
||||
nextWithdrawalValidatorIndex := phase0.ValidatorIndex((int(withdrawals[len(withdrawals)-1].ValidatorIndex) + 1) % len(validators))
|
||||
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Next withdrawal validator index is %d\n", nextWithdrawalValidatorIndex)
|
||||
}
|
||||
|
||||
index := int(nextWithdrawalValidatorIndex)
|
||||
for {
|
||||
if index == len(validators) {
|
||||
index = 0
|
||||
}
|
||||
if index == int(validator.Index) {
|
||||
break
|
||||
}
|
||||
if validators[index].Validator.WithdrawalCredentials[0] == ethWithdrawalPrefix &&
|
||||
validators[index].Validator.EffectiveBalance == c.maxEffectiveBalance {
|
||||
c.res.WithdrawalsToGo++
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
c.res.BlocksToGo = c.res.WithdrawalsToGo / c.maxWithdrawalsPerPayload
|
||||
if c.res.WithdrawalsToGo%c.maxWithdrawalsPerPayload != 0 {
|
||||
c.res.BlocksToGo++
|
||||
}
|
||||
c.res.Block = uint64(slot) + c.res.BlocksToGo
|
||||
c.res.Expected = c.chainTime.StartOfSlot(phase0.Slot(c.res.Block))
|
||||
c.res.Wait = time.Until(c.res.Expected)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) setup(ctx context.Context) error {
|
||||
// Connect to the consensus node.
|
||||
var err error
|
||||
c.consensusClient, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
|
||||
Address: c.connection,
|
||||
Timeout: c.timeout,
|
||||
AllowInsecure: c.allowInsecureConnections,
|
||||
LogFallback: !c.quiet,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set up chaintime.
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithGenesisProvider(c.consensusClient.(consensusclient.GenesisProvider)),
|
||||
standardchaintime.WithSpecProvider(c.consensusClient.(consensusclient.SpecProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create chaintime service")
|
||||
}
|
||||
|
||||
specResponse, err := c.consensusClient.(consensusclient.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain spec")
|
||||
}
|
||||
|
||||
if val, exists := specResponse.Data["MAX_WITHDRAWALS_PER_PAYLOAD"]; !exists {
|
||||
c.maxWithdrawalsPerPayload = 16
|
||||
} else {
|
||||
c.maxWithdrawalsPerPayload = val.(uint64)
|
||||
}
|
||||
|
||||
if val, exists := specResponse.Data["MAX_EFFECTIVE_BALANCE"]; !exists {
|
||||
c.maxEffectiveBalance = 32000000000
|
||||
} else {
|
||||
c.maxEffectiveBalance = phase0.Gwei(val.(uint64))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
50
cmd/validator/withdrawal/run.go
Normal file
50
cmd/validator/withdrawal/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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 validatorwithdrawl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
c, err := newCommand(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to set up command")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
if err := c.process(ctx); err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := c.output(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -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
|
||||
@@ -36,6 +36,7 @@ type command struct {
|
||||
|
||||
// Input.
|
||||
validators string
|
||||
epoch string
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
@@ -63,6 +64,7 @@ func newCommand(_ context.Context) (*command, error) {
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
json: viper.GetBool("json"),
|
||||
epoch: viper.GetString("epoch"),
|
||||
results: &output{},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2022 Weald Technology Trading.
|
||||
// Copyright © 2022, 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/shopspring/decimal"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
@@ -48,12 +49,12 @@ var (
|
||||
|
||||
// calculateYield calculates yield from the number of active validators.
|
||||
func (c *command) calculateYield(ctx context.Context) error {
|
||||
spec, err := c.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
specResponse, err := c.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp, exists := spec["BASE_REWARD_FACTOR"]
|
||||
tmp, exists := specResponse.Data["BASE_REWARD_FACTOR"]
|
||||
if !exists {
|
||||
return errors.New("spec missing BASE_REWARD_FACTOR")
|
||||
}
|
||||
@@ -125,7 +126,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
if c.validators == "" {
|
||||
chainTime, err := standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
@@ -138,17 +139,23 @@ func (c *command) setup(ctx context.Context) error {
|
||||
return errors.New("connection does not provide validator information")
|
||||
}
|
||||
|
||||
validators, err := validatorsProvider.Validators(ctx, "head", nil)
|
||||
epoch, err := util.ParseEpoch(ctx, chainTime, c.epoch)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validators")
|
||||
return errors.Wrap(err, "failed to parse epoch")
|
||||
}
|
||||
|
||||
response, err := validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
|
||||
State: fmt.Sprintf("%d", chainTime.FirstSlotOfEpoch(epoch)),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentEpoch := chainTime.CurrentEpoch()
|
||||
activeValidators := decimal.Zero
|
||||
activeValidatorBalance := decimal.Zero
|
||||
for _, validator := range validators {
|
||||
if validator.Validator.ActivationEpoch <= currentEpoch &&
|
||||
validator.Validator.ExitEpoch > currentEpoch {
|
||||
for _, validator := range response.Data {
|
||||
if validator.Validator.ActivationEpoch <= epoch &&
|
||||
validator.Validator.ExitEpoch > epoch {
|
||||
activeValidators = activeValidators.Add(one)
|
||||
activeValidatorBalance = activeValidatorBalance.Add(decimal.NewFromInt(int64(validator.Validator.EffectiveBalance)))
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ func init() {
|
||||
validatorCredentialsGetCmd.Flags().String("validator", "", "Validator for which to get validator credentials")
|
||||
}
|
||||
|
||||
func validatorCredentialsGetBindings() {
|
||||
if err := viper.BindPFlag("validator", validatorCredentialsGetCmd.Flags().Lookup("validator")); err != nil {
|
||||
func validatorCredentialsGetBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,38 +61,34 @@ func init() {
|
||||
validatorCredentialsSetCmd.Flags().String("withdrawal-account", "", "Account with which the validator's withdrawal credentials were set")
|
||||
validatorCredentialsSetCmd.Flags().String("withdrawal-address", "", "Execution address to which to direct withdrawals")
|
||||
validatorCredentialsSetCmd.Flags().String("signed-operations", "", "Use pre-defined JSON signed operation as created by --json to transmit the credentials change operation (reads from change-operations.json if not present)")
|
||||
validatorCredentialsSetCmd.Flags().Bool("json", false, "Generate JSON data containing a signed operation rather than broadcast it to the network (implied when offline)")
|
||||
validatorCredentialsSetCmd.Flags().Bool("offline", false, "Do not attempt to connect to a beacon node to obtain information for the operation")
|
||||
validatorCredentialsSetCmd.Flags().String("fork-version", "", "Fork version to use for signing (overrides fetching from beacon node)")
|
||||
validatorCredentialsSetCmd.Flags().String("genesis-validators-root", "", "Genesis validators root to use for signing (overrides fetching from beacon node)")
|
||||
}
|
||||
|
||||
func validatorCredentialsSetBindings() {
|
||||
if err := viper.BindPFlag("prepare-offline", validatorCredentialsSetCmd.Flags().Lookup("prepare-offline")); err != nil {
|
||||
func validatorCredentialsSetBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("prepare-offline", cmd.Flags().Lookup("prepare-offline")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("validator", validatorCredentialsSetCmd.Flags().Lookup("validator")); err != nil {
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("signed-operations", validatorCredentialsSetCmd.Flags().Lookup("signed-operations")); err != nil {
|
||||
if err := viper.BindPFlag("signed-operations", cmd.Flags().Lookup("signed-operations")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("withdrawal-account", validatorCredentialsSetCmd.Flags().Lookup("withdrawal-account")); err != nil {
|
||||
if err := viper.BindPFlag("withdrawal-account", cmd.Flags().Lookup("withdrawal-account")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("withdrawal-address", validatorCredentialsSetCmd.Flags().Lookup("withdrawal-address")); err != nil {
|
||||
if err := viper.BindPFlag("withdrawal-address", cmd.Flags().Lookup("withdrawal-address")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", validatorCredentialsSetCmd.Flags().Lookup("json")); err != nil {
|
||||
if err := viper.BindPFlag("offline", cmd.Flags().Lookup("offline")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("offline", validatorCredentialsSetCmd.Flags().Lookup("offline")); err != nil {
|
||||
if err := viper.BindPFlag("fork-version", cmd.Flags().Lookup("fork-version")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("fork-version", validatorCredentialsSetCmd.Flags().Lookup("fork-version")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("genesis-validators-root", validatorCredentialsSetCmd.Flags().Lookup("genesis-validators-root")); err != nil {
|
||||
if err := viper.BindPFlag("genesis-validators-root", cmd.Flags().Lookup("genesis-validators-root")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,29 +59,29 @@ func init() {
|
||||
validatorDepositDataCmd.Flags().Bool("launchpad", false, "Print launchpad-compatible JSON")
|
||||
}
|
||||
|
||||
func validatorDepositdataBindings() {
|
||||
if err := viper.BindPFlag("validatoraccount", validatorDepositDataCmd.Flags().Lookup("validatoraccount")); err != nil {
|
||||
func validatorDepositdataBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("validatoraccount", cmd.Flags().Lookup("validatoraccount")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("withdrawalaccount", validatorDepositDataCmd.Flags().Lookup("withdrawalaccount")); err != nil {
|
||||
if err := viper.BindPFlag("withdrawalaccount", cmd.Flags().Lookup("withdrawalaccount")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("withdrawalpubkey", validatorDepositDataCmd.Flags().Lookup("withdrawalpubkey")); err != nil {
|
||||
if err := viper.BindPFlag("withdrawalpubkey", cmd.Flags().Lookup("withdrawalpubkey")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("withdrawaladdress", validatorDepositDataCmd.Flags().Lookup("withdrawaladdress")); err != nil {
|
||||
if err := viper.BindPFlag("withdrawaladdress", cmd.Flags().Lookup("withdrawaladdress")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("depositvalue", validatorDepositDataCmd.Flags().Lookup("depositvalue")); err != nil {
|
||||
if err := viper.BindPFlag("depositvalue", cmd.Flags().Lookup("depositvalue")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("raw", validatorDepositDataCmd.Flags().Lookup("raw")); err != nil {
|
||||
if err := viper.BindPFlag("raw", cmd.Flags().Lookup("raw")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("forkversion", validatorDepositDataCmd.Flags().Lookup("forkversion")); err != nil {
|
||||
if err := viper.BindPFlag("forkversion", cmd.Flags().Lookup("forkversion")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("launchpad", validatorDepositDataCmd.Flags().Lookup("launchpad")); err != nil {
|
||||
if err := viper.BindPFlag("launchpad", cmd.Flags().Lookup("launchpad")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +51,11 @@ func init() {
|
||||
validatorDutiesCmd.Flags().String("index", "", "validator index for duties")
|
||||
}
|
||||
|
||||
func validatorDutiesBindings() {
|
||||
if err := viper.BindPFlag("pubkey", validatorDutiesCmd.Flags().Lookup("pubkey")); err != nil {
|
||||
func validatorDutiesBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("pubkey", cmd.Flags().Lookup("pubkey")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("index", validatorDutiesCmd.Flags().Lookup("index")); err != nil {
|
||||
if err := viper.BindPFlag("index", cmd.Flags().Lookup("index")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -23,13 +23,16 @@ import (
|
||||
|
||||
var validatorExitCmd = &cobra.Command{
|
||||
Use: "exit",
|
||||
Short: "Send an exit request for a validator",
|
||||
Long: `Send an exit request for a validator. For example:
|
||||
Short: "Send an exit request for one or more validators",
|
||||
Long: `Send an exit request for one or more validators. For example:
|
||||
|
||||
ethdo validator exit --validator=12345
|
||||
|
||||
The validator and key can be specified in one of a number of ways:
|
||||
|
||||
- mnemonic using --mnemonic; this will scan the mnemonic and generate all applicable operations
|
||||
- mnemonic and path to the validator key using --mnemonic and --path; this will generate a single operation
|
||||
- mnemonic and validator index or public key --mnemonic and --validator; this will generate a single operation
|
||||
- mnemonic and path to the validator using --mnemonic and --path
|
||||
- mnemonic and validator index or public key using --mnemonic and --validator
|
||||
- validator private key using --private-key
|
||||
@@ -54,39 +57,35 @@ In quiet mode this will return 0 if the exit operation has been generated (and s
|
||||
func init() {
|
||||
validatorCmd.AddCommand(validatorExitCmd)
|
||||
validatorFlags(validatorExitCmd)
|
||||
validatorExitCmd.Flags().Int64("epoch", -1, "Epoch at which to exit (defaults to current epoch)")
|
||||
validatorExitCmd.Flags().String("epoch", "", "Epoch at which to exit (defaults to current epoch)")
|
||||
validatorExitCmd.Flags().Bool("prepare-offline", false, "Create files for offline use")
|
||||
validatorExitCmd.Flags().String("validator", "", "Validator to exit")
|
||||
validatorExitCmd.Flags().String("signed-operation", "", "Use pre-defined JSON signed operation as created by --json to transmit the exit operation (reads from exit-operation.json if not present)")
|
||||
validatorExitCmd.Flags().Bool("json", false, "Generate JSON data containing a signed operation rather than broadcast it to the network (implied when offline)")
|
||||
validatorExitCmd.Flags().String("signed-operations", "", "Use pre-defined JSON signed operation as created by --json to transmit the exit operations (reads from exit-operations.json if not present)")
|
||||
validatorExitCmd.Flags().Bool("offline", false, "Do not attempt to connect to a beacon node to obtain information for the operation")
|
||||
validatorExitCmd.Flags().String("fork-version", "", "Fork version to use for signing (overrides fetching from beacon node)")
|
||||
validatorExitCmd.Flags().String("genesis-validators-root", "", "Genesis validators root to use for signing (overrides fetching from beacon node)")
|
||||
}
|
||||
|
||||
func validatorExitBindings() {
|
||||
if err := viper.BindPFlag("epoch", validatorExitCmd.Flags().Lookup("epoch")); err != nil {
|
||||
func validatorExitBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("prepare-offline", validatorExitCmd.Flags().Lookup("prepare-offline")); err != nil {
|
||||
if err := viper.BindPFlag("prepare-offline", cmd.Flags().Lookup("prepare-offline")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("validator", validatorExitCmd.Flags().Lookup("validator")); err != nil {
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("signed-operation", validatorExitCmd.Flags().Lookup("signed-operation")); err != nil {
|
||||
if err := viper.BindPFlag("signed-operations", cmd.Flags().Lookup("signed-operations")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", validatorExitCmd.Flags().Lookup("json")); err != nil {
|
||||
if err := viper.BindPFlag("offline", cmd.Flags().Lookup("offline")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("offline", validatorExitCmd.Flags().Lookup("offline")); err != nil {
|
||||
if err := viper.BindPFlag("fork-version", cmd.Flags().Lookup("fork-version")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("fork-version", validatorExitCmd.Flags().Lookup("fork-version")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("genesis-validators-root", validatorExitCmd.Flags().Lookup("genesis-validators-root")); err != nil {
|
||||
if err := viper.BindPFlag("genesis-validators-root", cmd.Flags().Lookup("genesis-validators-root")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ func init() {
|
||||
validatorExpectationCmd.Flags().Int64("validators", 1, "Number of validators")
|
||||
}
|
||||
|
||||
func validatorExpectationBindings() {
|
||||
if err := viper.BindPFlag("validators", validatorExpectationCmd.Flags().Lookup("validators")); err != nil {
|
||||
func validatorExpectationBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("validators", cmd.Flags().Lookup("validators")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
string2eth "github.com/wealdtech/go-string2eth"
|
||||
)
|
||||
@@ -53,6 +54,14 @@ In quiet mode this will return 0 if the validator information can be obtained, o
|
||||
})
|
||||
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
|
||||
|
||||
chainTime, err := standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
errCheck(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
if viper.GetString("validator") == "" {
|
||||
fmt.Println("validator is required")
|
||||
os.Exit(_exitFailure)
|
||||
@@ -61,10 +70,10 @@ In quiet mode this will return 0 if the validator information can be obtained, o
|
||||
validator, err := util.ParseValidator(ctx, eth2Client.(eth2client.ValidatorsProvider), viper.GetString("validator"), "head")
|
||||
errCheck(err, "Failed to obtain validator")
|
||||
|
||||
if verbose {
|
||||
if viper.GetBool("verbose") {
|
||||
network, err := util.Network(ctx, eth2Client)
|
||||
errCheck(err, "Failed to obtain network")
|
||||
outputIf(debug, fmt.Sprintf("Network is %s", network))
|
||||
outputIf(viper.GetBool("debug"), fmt.Sprintf("Network is %s", network))
|
||||
pubKey, err := validator.PubKey(ctx)
|
||||
if err == nil {
|
||||
deposits, totalDeposited, err := graphData(network, pubKey[:])
|
||||
@@ -75,16 +84,22 @@ In quiet mode this will return 0 if the validator information can be obtained, o
|
||||
}
|
||||
}
|
||||
|
||||
if quiet {
|
||||
if viper.GetBool("quiet") {
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
if validator.Status.IsPending() || validator.Status.HasActivated() {
|
||||
fmt.Printf("Index: %d\n", validator.Index)
|
||||
}
|
||||
if verbose {
|
||||
if viper.GetBool("verbose") {
|
||||
if validator.Status.IsPending() {
|
||||
fmt.Printf("Activation eligibility epoch: %d\n", validator.Validator.ActivationEligibilityEpoch)
|
||||
if validator.Validator.ActivationEpoch == 0xffffffffffffffff {
|
||||
fmt.Printf("Activation eligibility epoch: %d\n", validator.Validator.ActivationEligibilityEpoch)
|
||||
fmt.Printf("Activation eligibility timestamp: %v\n", chainTime.StartOfEpoch(validator.Validator.ActivationEligibilityEpoch))
|
||||
} else {
|
||||
fmt.Printf("Activation epoch: %d\n", validator.Validator.ActivationEpoch)
|
||||
fmt.Printf("Activation timestamp: %v\n", chainTime.StartOfEpoch(validator.Validator.ActivationEpoch))
|
||||
}
|
||||
}
|
||||
if validator.Status.HasActivated() {
|
||||
fmt.Printf("Activation epoch: %d\n", validator.Validator.ActivationEpoch)
|
||||
@@ -102,7 +117,7 @@ In quiet mode this will return 0 if the validator information can be obtained, o
|
||||
if validator.Status.IsActive() {
|
||||
fmt.Printf("Effective balance: %s\n", string2eth.GWeiToString(uint64(validator.Validator.EffectiveBalance), true))
|
||||
}
|
||||
if verbose {
|
||||
if viper.GetBool("verbose") {
|
||||
fmt.Printf("Withdrawal credentials: %#x\n", validator.Validator.WithdrawalCredentials)
|
||||
}
|
||||
|
||||
@@ -174,8 +189,8 @@ func init() {
|
||||
validatorFlags(validatorInfoCmd)
|
||||
}
|
||||
|
||||
func validatorInfoBindings() {
|
||||
if err := viper.BindPFlag("validator", validatorInfoCmd.Flags().Lookup("validator")); err != nil {
|
||||
func validatorInfoBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ var validatorKeycheckCmd = &cobra.Command{
|
||||
Short: "Check that the withdrawal credentials for a validator matches the given key.",
|
||||
Long: `Check that the withdrawal credentials for a validator matches the given key. For example:
|
||||
|
||||
ethdo validator keycheck --withdrawal-credentials=0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02 --privkey=0x1b46e61babc7a6a0fbfe8e416de3c71f85e367f24e0bfcb12e57adb11117662c
|
||||
ethdo validator keycheck --withdrawal-credentials=0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02 --private-key=0x1b46e61babc7a6a0fbfe8e416de3c71f85e367f24e0bfcb12e57adb11117662c
|
||||
|
||||
A mnemonic can be used in place of a private key, in which case the first 1,024 indices of the standard withdrawal key path will be scanned for a matching key.
|
||||
|
||||
@@ -50,14 +50,10 @@ func init() {
|
||||
validatorCmd.AddCommand(validatorKeycheckCmd)
|
||||
validatorFlags(validatorKeycheckCmd)
|
||||
validatorKeycheckCmd.Flags().String("withdrawal-credentials", "", "Withdrawal credentials to check (can run offline)")
|
||||
validatorKeycheckCmd.Flags().String("privkey", "", "Private key from which to generate withdrawal credentials")
|
||||
}
|
||||
|
||||
func validatorKeycheckBindings() {
|
||||
if err := viper.BindPFlag("withdrawal-credentials", validatorKeycheckCmd.Flags().Lookup("withdrawal-credentials")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("privkey", validatorKeycheckCmd.Flags().Lookup("privkey")); err != nil {
|
||||
func validatorKeycheckBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("withdrawal-credentials", cmd.Flags().Lookup("withdrawal-credentials")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user