Compare commits

...

43 Commits

Author SHA1 Message Date
Jim McDonald
1b0f4e2803 Bump version. 2022-08-18 14:42:57 +01:00
Jim McDonald
301224748c Update workflow. 2022-08-18 14:28:23 +01:00
Jim McDonald
1e15b836c2 Bump version. 2022-08-11 08:16:55 +01:00
Jim McDonald
1e709b7592 Remove mandatory connection parameter.
The connection parameter is no longer mandatory, in that ethdo will
attempt to obtain a connection using well-known ports if no override is
supplied.  As such the `--connection` parameter can be omitted and so is
not force-required as part of the command initialisation.
2022-08-11 08:11:55 +01:00
Jim McDonald
8744a85cb7 Merge pull request #45 from tcrossland/master
feat: support block analyze on bellatrix
2022-08-06 08:16:58 +01:00
Tom Crossland
92ad77d8f5 feat: support block analyze on bellatrix 2022-08-04 17:25:20 +02:00
Jim McDonald
2298640e4c Merge pull request #44 from aaron-alderman/fix/deposit-message-root-verification
Add deposit message root match verification
2022-07-16 16:27:24 +01:00
Jim McDonald
5baef59672 Tidy up streaming output. 2022-07-13 11:21:09 +01:00
Aaron Alderman
e54e8affa7 Add deposit message root match verification 2022-07-12 10:47:27 +08:00
Jim McDonald
97fa04a7b2 Bump version. 2022-07-10 18:28:54 +01:00
Jim McDonald
4977ee82e5 Add dpeosit signature verirication to "deposit verify". 2022-07-10 12:33:19 +01:00
Jim McDonald
090680366c Do not print 0-value deposit validator information. 2022-06-23 09:52:45 +01:00
Jim McDonald
531c86847f Tidy up tests. 2022-06-22 07:51:22 +01:00
Jim McDonald
446e437531 Update docs. 2022-06-22 07:51:14 +01:00
Jim McDonald
63d8ccf1a0 Add "proposer duties". 2022-06-22 07:50:53 +01:00
Jim McDonald
77abe0e158 Add sepolia support. 2022-06-21 10:05:29 +01:00
Jim McDonald
547f8d9e71 Fix potential crash when new validator is activated. 2022-06-20 16:18:28 +01:00
Jim McDonald
e144217f25 Add "validator yield". 2022-06-12 11:17:59 +01:00
Jim McDonald
d919810ce1 Tidy up eth1 votes output. 2022-06-10 18:13:49 +01:00
Jim McDonald
0bdf68edf6 Do not fetch future states. 2022-06-01 12:42:40 +01:00
Jim McDonald
b24341b7da Do not fetch future states. 2022-06-01 12:42:12 +01:00
Jim McDonald
384ee3dcaa Bump version. 2022-06-01 12:22:23 +01:00
Jim McDonald
3e8b1a6dad Add "chain eth1votes". 2022-06-01 12:21:40 +01:00
Jim McDonald
d2dec4a444 Tidy up formatting. 2022-05-31 13:39:36 +01:00
Jim McDonald
7e171bdb1e Provide more epoch summary information. 2022-05-30 21:52:30 +01:00
Jim McDonald
0cedf79a89 Bump version. 2022-05-30 16:08:58 +01:00
Jim McDonald
65ad1248ce epoch summary no sync committee information pre-Altair. 2022-05-30 16:08:31 +01:00
Jim McDonald
e1180f97ce error on attester inclusion without required params. 2022-05-26 22:29:21 +01:00
Jim McDonald
394b4a7cd2 Bump version. 2022-05-19 13:00:38 +01:00
Jim McDonald
fd574aae34 Tidy up tests. 2022-05-19 13:00:05 +01:00
Jim McDonald
7fe503f51d Add ropsten support. 2022-05-19 12:59:48 +01:00
Jim McDonald
6bfb0ef098 Add validator credentials get command. 2022-05-10 15:02:41 +01:00
Jim McDonald
46c667d387 Add "chain queues". 2022-04-23 08:51:30 +01:00
Jim McDonald
50f4a9cace Update workflow. 2022-04-15 08:09:06 +01:00
Jim McDonald
cd20875744 Provide clearer error if attempting to import to an HD wallet. 2022-03-24 17:28:46 +00:00
Jim McDonald
84f682a0da Guess connection if none supplied. 2022-03-24 09:46:11 +00:00
Jim McDonald
6389b7dfbd Test coverage. 2022-03-23 22:14:34 +00:00
Jim McDonald
0ef65b8bda Allow account import from keystores. 2022-03-23 22:08:10 +00:00
Jim McDonald
4426c3279d Allow account import from keystores. 2022-03-23 22:06:48 +00:00
Jim McDonald
883f9f834e Update changelog. 2022-03-17 20:21:30 +00:00
Jim McDonald
e0fd3df9dd Add "epoch summary" command. 2022-03-17 20:20:52 +00:00
Jim McDonald
e6d3c67e39 Bump version. 2022-03-16 11:19:52 +00:00
Jim McDonald
6d0a0225c2 Tidy up graffiti and execution output for block info. 2022-03-16 08:52:28 +00:00
92 changed files with 3996 additions and 200 deletions

View File

@@ -1,23 +1,21 @@
name: golangci-lint
on: [ push, pull_request ]
on:
push:
branches:
- master
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
- uses: actions/setup-go@v3
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.29
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
args: --timeout=10m
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
go-version: 1.17
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest

View File

@@ -14,7 +14,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.16
go-version: ^1.17
id: go
- name: Check out code into the Go module directory
@@ -50,11 +50,11 @@ jobs:
- name: Fetch xgo
run: |
go install github.com/wealdtech/xgo@latest
go install github.com/crazy-max/xgo@v0.14.0
- name: Cross-compile linux
run: |
xgo -v -x -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION}" --targets="linux/amd64" github.com/wealdtech/ethdo
xgo -v -x -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION}" --targets="linux/amd64,linux/arm64" github.com/wealdtech/ethdo
- name: Cross-compile windows
run: |
@@ -72,11 +72,11 @@ jobs:
tar zcf ethdo-${RELEASE_VERSION}-linux-amd64.tar.gz ethdo
sha256sum ethdo-${RELEASE_VERSION}-linux-amd64.tar.gz >ethdo-${RELEASE_VERSION}-linux-amd64.sha256
# - name: Create linux ARM64 tgz file
# run: |
# mv ethdo-linux-arm64 ethdo
# tar zcf ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz ethdo
# sha256sum ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz >ethdo-${RELEASE_VERSION}-linux-arm64.sha256
- name: Create linux ARM64 tgz file
run: |
mv ethdo-linux-arm64 ethdo
tar zcf ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz ethdo
sha256sum ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz >ethdo-${RELEASE_VERSION}-linux-arm64.sha256
- name: Create release
id: create_release
@@ -133,24 +133,24 @@ jobs:
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz
asset_content_type: application/gzip
# - name: Upload linux ARM64 checksum file
# id: upload-release-asset-linux-arm64-checksum
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
# asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
# asset_content_type: text/plain
- name: Upload linux ARM64 checksum file
id: upload-release-asset-linux-arm64-checksum
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
asset_content_type: text/plain
# - name: Upload linux ARM64 tgz file
# id: upload-release-asset-linux-arm64
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz
# asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz
# asset_content_type: application/gzip
- name: Upload linux ARM64 tgz file
id: upload-release-asset-linux-arm64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz
asset_content_type: application/gzip

View File

@@ -1,3 +1,47 @@
1.25.2:
- no longer require connection parameter
- support "block analyze" on bellatrix (thanks @tcrossland)
- check deposit message root match for verifying deposits (thanks @aaron-alderman)
1.25.0:
- add "proposer duties"
- add deposit signature verification to "deposit verify"
1.24.1:
- fix potential crash when new validators are activated
- add "sepolia" to the list of supported networks
1.24.0:
- add "validator yield"
1.23.1:
- do not fetch future state for chain eth1votes
1.23.0:
- do not fetch sync committee information for epoch summaries prior to Altair
- ensure that "attester inclusion" without validator returns appropriate error
- provide more information in "epoch summary" with verbose flag
- add "chain eth1votes"
1.22.0:
- add "ropsten" to the list of supported networks
1.21.0:
- add "validator credentials get"
1.20.0:
- add "chain queues"
1.19.1:
- add the ability to import keystores to ethdo wallets
- use defaults to connect to beacon nodes if no explicit connection defined
1.19.0:
- add "epoch summary"
1.18.2:
- tidy up output of "block info"
1.18.1:
- do not show execution payload if empty

View File

@@ -11,6 +11,7 @@ A command-line tool for managing common tasks in Ethereum 2.
- [Binaries](#binaries)
- [Docker](#docker)
- [Source](#source)
- [Setting up](#setting-up)
- [Usage](#usage)
- [Maintainers](#maintainers)
- [Contribute](#contribute)
@@ -61,13 +62,37 @@ docker run --network=host ethdo chain status
Alternatively, if the beacon node is running in a separate docker container a shared network can be created with `docker network create eth2` and accessed by adding `--network=eth2` added to both the beacon node and `ethdo` containers.
## Setting up
`ethdo` needs a connection to a beacon node for many of its features. `ethdo` can connect to any beacon node that fully supports the [standard REST API](https://ethereum.github.io/beacon-APIs/). The following changes are required to beacon nodes to make this available.
### Lighthouse
Lighthouse disables the REST API by default. To enable it, the beacon node must be started with the `--http` parameter. If you want to access the REST API from a remote server then you should also look to change the `--http-address` and `--http-allow-origin` options as per the Lighthouse documentation.
The default port for the REST API is 5052, which can be changed with the `--http-port` parameter.
### Nimbus
Nimbus disables the REST API by default. To enable it, the beacon node must be started with the `--rest` parameter. If you want to access the REST API from a remote server then you should also look to change the `--rest-address` and `--rest-allow-origin` options as per the Nimbus documentation.
The default port for the REST API is 5052, which can be changed with the `--rest-port` parameter.
### Prysm
Prysm enables the REST API by default. You will need to add the parameter `--grpc-max-msg-size 268435456` to be obtain to obtain large sets of information such as the list of current validators. If you want to access the REST API from a remote server then you should also look to change the `--grpc-gateway-host` and `--grpc-gateway-corsdomain` options as per the Prysm documentation.
The default port for the REST API is 3500, which can be changed with the `--grpc-gateway-port` parameter.
### Teku
Teku disables the REST API by default. To enable it, the beacon node must be started with the `--rest-api-enabled` parameter. If you want to access the REST API from a remote server then you should also look to change the `--rest-api-interface`, `--rest-api-host-allowlist` and `--rest-api-cors-origins` options as per the Teku documentation.
The default port for the REST API is 5051, which can be changed with the `--rest-api-port` parameter.
## Usage
ethdo contains a large number of features that are useful for day-to-day interactions with the Ethereum 2 blockchain.
`ethdo` contains a large number of features that are useful for day-to-day interactions with the Ethereum 2 blockchain.
### Wallets and accounts
ethdo uses the [go-eth2-wallet](https://github.com/wealdtech/go-eth2-wallet) system to provide unified access to different wallet types. When on the filesystem the locations of the created wallets and accounts are:
`ethdo` uses the [go-eth2-wallet](https://github.com/wealdtech/go-eth2-wallet) system to provide unified access to different wallet types. When on the filesystem the locations of the created wallets and accounts are:
- for Linux: $HOME/.config/ethereum2/wallets
- for OSX: $HOME/Library/Application Support/ethereum2/wallets

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -16,6 +16,7 @@ package accountimport
import (
"context"
"encoding/hex"
"io/ioutil"
"strings"
"time"
@@ -27,12 +28,14 @@ import (
)
type dataIn struct {
timeout time.Duration
wallet e2wtypes.Wallet
key []byte
accountName string
passphrase string
walletPassphrase string
timeout time.Duration
wallet e2wtypes.Wallet
key []byte
accountName string
passphrase string
walletPassphrase string
keystore []byte
keystorePassphrase []byte
}
func input(ctx context.Context) (*dataIn, error) {
@@ -74,14 +77,55 @@ func input(ctx context.Context) (*dataIn, error) {
// Wallet passphrase.
data.walletPassphrase = util.GetWalletPassphrase()
// Key.
if viper.GetString("key") == "" {
return nil, errors.New("key is required")
if viper.GetString("key") == "" && viper.GetString("keystore") == "" {
return nil, errors.New("key or keystore is required")
}
data.key, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("key"), "0x"))
if err != nil {
return nil, errors.Wrap(err, "key is malformed")
if viper.GetString("key") != "" && viper.GetString("keystore") != "" {
return nil, errors.New("only one of key and keystore is required")
}
if viper.GetString("key") != "" {
data.key, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("key"), "0x"))
if err != nil {
return nil, errors.Wrap(err, "key is malformed")
}
}
if viper.GetString("keystore") != "" {
data.keystorePassphrase = []byte(viper.GetString("keystore-passphrase"))
if len(data.keystorePassphrase) == 0 {
return nil, errors.New("must supply keystore passphrase with keystore-passphrase when supplying keystore")
}
data.keystore, err = obtainKeystore(viper.GetString("keystore"))
if err != nil {
return nil, errors.Wrap(err, "invalid keystore")
}
}
return data, nil
}
// obtainKeystore obtains keystore from an input, could be JSON itself or a path to JSON.
func obtainKeystore(input string) ([]byte, error) {
var err error
var data []byte
// Input could be JSON or a path to JSON
if strings.HasPrefix(input, "{") {
// Looks like JSON
data = []byte(input)
} else {
// Assume it's a path to JSON
data, err = ioutil.ReadFile(input)
if err != nil {
return nil, errors.Wrap(err, "failed to find deposit data file")
}
}
return data, nil
// exitData := &util.ValidatorExitData{}
// err = json.Unmarshal(data, exitData)
// if err != nil {
// return nil, errors.Wrap(err, "data is not valid JSON")
// }
// return exitData, nil
}

View File

@@ -102,7 +102,7 @@ func TestInput(t *testing.T) {
"account": "Test wallet/Test account",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "key is required",
err: "key or keystore is required",
},
{
name: "KeyMalformed",
@@ -114,6 +114,26 @@ func TestInput(t *testing.T) {
},
err: "key is malformed: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "KeyandKeystore",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"passphrase": "ce%NohGhah4ye5ra",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
"keystore": "{}",
},
err: "only one of key and keystore is required",
},
{
name: "KeystoreNoKeystorePassphrase",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"keystore": "{}",
},
err: "must supply keystore passphrase with keystore-passphrase when supplying keystore",
},
{
name: "Good",
vars: map[string]interface{}{

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 -2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,9 +15,14 @@ package accountimport
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
"github.com/wealdtech/go-ecodec"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -43,9 +48,23 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}()
}
if len(data.key) > 0 {
return processFromKey(ctx, data)
}
if len(data.keystore) > 0 {
return processFromKeystore(ctx, data)
}
return nil, errors.New("unsupported import mechanism")
}
func processFromKey(ctx context.Context, data *dataIn) (*dataOut, error) {
results := &dataOut{}
account, err := data.wallet.(e2wtypes.WalletAccountImporter).ImportAccount(ctx, data.accountName, data.key, []byte(data.passphrase))
importer, isImporter := data.wallet.(e2wtypes.WalletAccountImporter)
if !isImporter {
return nil, fmt.Errorf("%s wallets do not support importing accounts", data.wallet.Type())
}
account, err := importer.ImportAccount(ctx, data.accountName, data.key, []byte(data.passphrase))
if err != nil {
return nil, errors.Wrap(err, "failed to import account")
}
@@ -53,3 +72,39 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
func processFromKeystore(ctx context.Context, data *dataIn) (*dataOut, error) {
// Need to import the keystore in to a temporary wallet to fetch the private key.
store := scratch.New()
encryptor := keystorev4.New()
// Need to add a couple of fields to the keystore to make it compliant.
keystoreData := fmt.Sprintf(`{"name":"Import","encryptor":"keystore",%s`, string(data.keystore[1:]))
walletData := fmt.Sprintf(`{"wallet":{"name":"ImportTest","type":"non-deterministic","uuid":"e1526407-1dc7-4f3f-9d05-ab696f40707c","version":1},"accounts":[%s]}`, keystoreData)
encryptedData, err := ecodec.Encrypt([]byte(walletData), data.keystorePassphrase)
if err != nil {
return nil, err
}
wallet, err := nd.Import(ctx, encryptedData, data.keystorePassphrase, store, encryptor)
if err != nil {
return nil, errors.Wrap(err, "failed to import wallet")
}
account := <-wallet.Accounts(ctx)
privateKeyProvider, isPrivateKeyProvider := account.(e2wtypes.AccountPrivateKeyProvider)
if !isPrivateKeyProvider {
return nil, errors.New("account does not provide its private key")
}
if locker, isLocker := account.(e2wtypes.AccountLocker); isLocker {
if err = locker.Unlock(ctx, data.keystorePassphrase); err != nil {
return nil, errors.Wrap(err, "failed to unlock account")
}
}
key, err := privateKeyProvider.PrivateKey(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain private key")
}
data.key = key.Marshal()
// We have the key from the keystore; import it.
return processFromKey(ctx, data)
}

View File

@@ -48,10 +48,18 @@ func init() {
accountCmd.AddCommand(accountImportCmd)
accountFlags(accountImportCmd)
accountImportCmd.Flags().String("key", "", "Private key of the account to import (0x...)")
accountImportCmd.Flags().String("keystore", "", "Keystore, or path to keystore ")
accountImportCmd.Flags().String("keystore-passphrase", "", "Passphrase of keystore")
}
func accountImportBindings() {
if err := viper.BindPFlag("key", accountImportCmd.Flags().Lookup("key")); err != nil {
panic(err)
}
if err := viper.BindPFlag("keystore", accountImportCmd.Flags().Lookup("keystore")); err != nil {
panic(err)
}
if err := viper.BindPFlag("keystore-passphrase", accountImportCmd.Flags().Lookup("keystore-passphrase")); err != nil {
panic(err)
}
}

View File

@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
"timeout": "5s",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to any beacon node",
},
}

View File

@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
"timeout": "5s",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to any beacon node",
},
}

View File

@@ -49,7 +49,7 @@ func init() {
attesterFlags(attesterInclusionCmd)
attesterInclusionCmd.Flags().Int64("epoch", -1, "the last complete epoch")
attesterInclusionCmd.Flags().String("pubkey", "", "the public key of the attester")
attesterInclusionCmd.Flags().Int64("index", -1, "the index of the attester")
attesterInclusionCmd.Flags().String("index", "", "the index of the attester")
}
func attesterInclusionBindings() {

View File

@@ -122,9 +122,6 @@ func newCommand(ctx context.Context) (*command, error) {
}
c.timeout = viper.GetDuration("timeout")
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")

View File

@@ -37,27 +37,10 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"validators": "1",
"timeout": "5s",
},
err: "connection is required",
},
{
name: "ValidatorsZero",
vars: map[string]interface{}{
"timeout": "5s",
"validators": "0",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "validators must be at least 1",
},
{
name: "Good",
vars: map[string]interface{}{
"validators": "1",
"blockid": "1",
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},

View File

@@ -425,6 +425,13 @@ func (c *command) analyzeSyncCommittees(ctx context.Context, block *spec.Version
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
case spec.DataVersionBellatrix:
c.analysis.SyncCommitee.Contributions = int(block.Bellatrix.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Bellatrix.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
default:
return fmt.Errorf("unsupported block version %d", block.Version)
}

View File

@@ -33,13 +33,13 @@ func TestProcess(t *testing.T) {
err string
}{
{
name: "InvalidData",
name: "NoBlock",
vars: map[string]interface{}{
"timeout": "60s",
"validators": "1",
"data": "[[",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"blockid": "invalid",
},
err: "failed to obtain beacon block: failed to request signed beacon block: GET failed with status 400: {\"code\":400,\"message\":\"Invalid block: invalid\"}",
},
}

View File

@@ -66,7 +66,7 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to any beacon node",
},
{
name: "ConnectionBad",
@@ -79,7 +79,7 @@ func TestInput(t *testing.T) {
timeout: 5 * time.Second,
blockID: "justified",
},
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
},
{
name: "BlockIDNil",

View File

@@ -77,8 +77,9 @@ func outputBlockGeneral(ctx context.Context,
res.WriteString(fmt.Sprintf("State root: %#x\n", stateRoot))
}
if len(graffiti) > 0 && hex.EncodeToString(graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
graffiti = bytes.TrimRight(graffiti, "\u0000")
if utf8.Valid(graffiti) {
res.WriteString(fmt.Sprintf("Graffiti: %s\n", strings.TrimRight(string(graffiti), "\u0000")))
res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(graffiti)))
} else {
res.WriteString(fmt.Sprintf("Graffiti: %#x\n", graffiti))
}
@@ -530,14 +531,21 @@ func outputBlockExecutionPayload(ctx context.Context,
return "", nil
}
// If the block number is 0 then we're before the merge.
if payload.BlockNumber == 0 {
return "", nil
}
res := strings.Builder{}
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
if verbose {
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))
baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas))
for i := 0; i < 32; i++ {
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
@@ -567,6 +575,8 @@ func outputBlockExecutionPayload(ctx context.Context,
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)))
}
return res.String(), nil

View File

@@ -82,6 +82,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data.stream {
jsonOutput = data.jsonOutput
sszOutput = data.sszOutput
if !jsonOutput && !sszOutput {
fmt.Println("")
}
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
if err != nil {
return nil, errors.Wrap(err, "failed to start block stream")
@@ -101,13 +104,13 @@ func headEventHandler(event *api.Event) {
blockID := fmt.Sprintf("%#x", event.Data.(*api.HeadEvent).Block[:])
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID)
if err != nil {
if !jsonOutput {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to obtain block: %v\n", err)
}
return
}
if signedBlock == nil {
if !jsonOutput {
if !jsonOutput && !sszOutput {
fmt.Println("Empty beacon block")
}
return
@@ -115,31 +118,34 @@ func headEventHandler(event *api.Event) {
switch signedBlock.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
if !jsonOutput {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
case spec.DataVersionAltair:
if err := outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair); err != nil {
if !jsonOutput {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix); err != nil {
if !jsonOutput {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
default:
if !jsonOutput {
if !jsonOutput && !sszOutput {
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
}
return
}
if !jsonOutput && !sszOutput {
fmt.Println("")
}
}
func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0.SignedBeaconBlock) error {
@@ -155,7 +161,7 @@ func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Printf("%s\n", data)
fmt.Print(data)
}
return nil
}
@@ -179,7 +185,7 @@ func outputAltairBlock(ctx context.Context, jsonOutput bool, sszOutput bool, sig
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Printf("%s\n", data)
fmt.Print(data)
}
return nil
}
@@ -203,7 +209,7 @@ func outputBellatrixBlock(ctx context.Context, jsonOutput bool, sszOutput bool,
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Printf("%s\n", data)
fmt.Print(data)
}
return nil
}

View File

@@ -0,0 +1,84 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaineth1votes
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
json bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Input.
xepoch string
xperiod string
// Data access.
eth2Client eth2client.Service
chainTime chaintime.Service
beaconStateProvider eth2client.BeaconStateProvider
slotsPerEpoch uint64
epochsPerEth1VotingPeriod uint64
// Output.
slot phase0.Slot
epoch phase0.Epoch
period uint64
incumbent *phase0.ETH1Data
eth1DataVotes []*phase0.ETH1Data
votes map[string]*vote
}
type vote struct {
Vote *phase0.ETH1Data `json:"vote"`
Count int `json:"count"`
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
json: viper.GetBool("json"),
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
c.xepoch = viper.GetString("epoch")
c.xperiod = viper.GetString("period")
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
return c, nil
}

View File

@@ -0,0 +1,64 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaineth1votes
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
_, err := newCommand(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,125 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaineth1votes
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/attestantio/go-eth2-client/spec/phase0"
)
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"`
}
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.json {
return c.outputJSON(ctx)
}
return c.outputText(ctx)
}
func (c *command) outputJSON(ctx context.Context) (string, error) {
votes := make([]*vote, 0, len(c.votes))
totalVotes := 0
for _, vote := range c.votes {
votes = append(votes, vote)
totalVotes += vote.Count
}
sort.Slice(votes, func(i int, j int) bool {
if votes[i].Count != votes[j].Count {
return votes[i].Count > votes[j].Count
}
return votes[i].Vote.DepositCount < votes[j].Vote.DepositCount
})
output := &jsonOutput{
Period: c.period,
Epoch: c.epoch,
Slot: c.slot,
Incumbent: c.incumbent,
Votes: votes,
}
data, err := json.Marshal(output)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputText(ctx context.Context) (string, error) {
builder := strings.Builder{}
builder.WriteString("Voting period: ")
builder.WriteString(fmt.Sprintf("%d\n", c.period))
if c.verbose {
builder.WriteString("Incumbent: ")
builder.WriteString(fmt.Sprintf("block %#x, deposit count %d\n", c.incumbent.BlockHash, c.incumbent.DepositCount))
}
votes := make([]*vote, 0, len(c.votes))
totalVotes := 0
for _, vote := range c.votes {
votes = append(votes, vote)
totalVotes += vote.Count
}
sort.Slice(votes, func(i int, j int) bool {
if votes[i].Count != votes[j].Count {
return votes[i].Count > votes[j].Count
}
return votes[i].Vote.DepositCount < votes[j].Vote.DepositCount
})
slot := c.chainTime.CurrentSlot()
if slot > c.slot {
slot = c.slot
}
slotsThroughPeriod := slot + 1 - phase0.Slot(c.period*(c.slotsPerEpoch*c.epochsPerEth1VotingPeriod))
builder.WriteString("Slots through period: ")
builder.WriteString(fmt.Sprintf("%d (%d)\n", slotsThroughPeriod, c.slot))
builder.WriteString("Votes this period: ")
builder.WriteString(fmt.Sprintf("%d\n", totalVotes))
if len(votes) > 0 {
if c.verbose {
for _, vote := range votes {
builder.WriteString(fmt.Sprintf(" block %#x, deposit count %d: %d vote", vote.Vote.BlockHash, vote.Vote.DepositCount, vote.Count))
if vote.Count != 1 {
builder.WriteString("s")
}
builder.WriteString(fmt.Sprintf(" (%0.2f%%)\n", 100.0*float64(vote.Count)/float64(slotsThroughPeriod)))
}
} else {
builder.WriteString(fmt.Sprintf("Leading vote is for block %#x with %d votes (%0.2f%%)\n", votes[0].Vote.BlockHash, votes[0].Count, 100.0*float64(votes[0].Count)/float64(slotsThroughPeriod)))
}
}
return strings.TrimSuffix(builder.String(), "\n"), nil
}

View File

@@ -0,0 +1,161 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaineth1votes
import (
"context"
"encoding/json"
"fmt"
"strconv"
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"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
var err error
if c.xperiod != "" {
period, err := strconv.ParseUint(c.xperiod, 10, 64)
if err != nil {
return err
}
c.epoch = phase0.Epoch(c.epochsPerEth1VotingPeriod*(period+1)) - 1
} else {
c.epoch, err = util.ParseEpoch(ctx, c.chainTime, c.xepoch)
if err != nil {
return err
}
}
// Do not fetch from the future.
if c.epoch > c.chainTime.CurrentEpoch() {
c.epoch = c.chainTime.CurrentEpoch()
}
// Need to fetch the state from the last slot of the epoch.
fetchSlot := c.chainTime.FirstSlotOfEpoch(c.epoch+1) - 1
// Do not fetch from the future.
if fetchSlot > c.chainTime.CurrentSlot() {
fetchSlot = c.chainTime.CurrentSlot()
}
state, err := c.beaconStateProvider.BeaconState(ctx, fmt.Sprintf("%d", fetchSlot))
if err != nil {
return errors.Wrap(err, "failed to obtain state")
}
if state == nil {
return errors.New("state not returned by beacon node")
}
if c.debug {
data, err := json.Marshal(state)
if err == nil {
fmt.Printf("%s\n", string(data))
}
}
switch state.Version {
case spec.DataVersionPhase0:
c.slot = phase0.Slot(state.Phase0.Slot)
c.incumbent = state.Phase0.ETH1Data
c.eth1DataVotes = state.Phase0.ETH1DataVotes
case spec.DataVersionAltair:
c.slot = phase0.Slot(state.Altair.Slot)
c.incumbent = state.Altair.ETH1Data
c.eth1DataVotes = state.Altair.ETH1DataVotes
case spec.DataVersionBellatrix:
c.slot = phase0.Slot(state.Bellatrix.Slot)
c.incumbent = state.Bellatrix.ETH1Data
c.eth1DataVotes = state.Bellatrix.ETH1DataVotes
default:
return fmt.Errorf("unhandled beacon state version %v", state.Version)
}
c.period = uint64(c.epoch) / c.epochsPerEth1VotingPeriod
c.votes = make(map[string]*vote)
for _, eth1Vote := range c.eth1DataVotes {
key := fmt.Sprintf("%#x:%d", eth1Vote.BlockHash, eth1Vote.DepositCount)
if _, exists := c.votes[key]; !exists {
c.votes[key] = &vote{
Vote: eth1Vote,
}
}
c.votes[key].Count++
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
var isProvider bool
c.beaconStateProvider, isProvider = c.eth2Client.(eth2client.BeaconStateProvider)
if !isProvider {
return errors.New("connection does not provide beacon state")
}
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
if !isProvider {
return errors.New("connection does not provide spec information")
}
spec, err := specProvider.Spec(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["SLOTS_PER_EPOCH"]
if !exists {
return errors.New("spec did not contain SLOTS_PER_EPOCH")
}
var good bool
c.slotsPerEpoch, good = tmp.(uint64)
if !good {
return errors.New("SLOTS_PER_EPOCH value invalid")
}
tmp, exists = spec["EPOCHS_PER_ETH1_VOTING_PERIOD"]
if !exists {
return errors.New("spec did not contain EPOCHS_PER_ETH1_VOTING_PERIOD")
}
c.epochsPerEth1VotingPeriod, good = tmp.(uint64)
if !good {
return errors.New("EPOCHS_PER_ETH1_VOTING_PERIOD value invalid")
}
return nil
}

View File

@@ -0,0 +1,66 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaineth1votes
import (
"context"
"os"
"testing"
"github.com/rs/zerolog"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
zerolog.SetGlobalLevel(zerolog.Disabled)
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "InvalidEpoch",
vars: map[string]interface{}{
"timeout": "5s",
"epoch": "invalid",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "failed to parse epoch: strconv.ParseInt: parsing \"invalid\": invalid syntax",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
cmd, err := newCommand(context.Background())
require.NoError(t, err)
err = cmd.process(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,50 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chaineth1votes
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
}

View File

@@ -0,0 +1,72 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chainqueues
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
json bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Input.
epoch string
// Data access.
eth2Client eth2client.Service
validatorsProvider eth2client.ValidatorsProvider
chainTime chaintime.Service
// Output.
activationQueue int
exitQueue int
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
json: viper.GetBool("json"),
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
if viper.GetString("epoch") != "" {
c.epoch = viper.GetString("epoch")
}
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
return c, nil
}

View File

@@ -0,0 +1,64 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chainqueues
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
_, err := newCommand(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,63 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chainqueues
import (
"context"
"encoding/json"
"fmt"
"strings"
)
type jsonOutput struct {
ActivationQueue int `json:"activation_queue"`
ExitQueue int `json:"exit_queue"`
}
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.json {
return c.outputJSON(ctx)
}
return c.outputText(ctx)
}
func (c *command) outputJSON(ctx context.Context) (string, error) {
output := &jsonOutput{
ActivationQueue: c.activationQueue,
ExitQueue: c.exitQueue,
}
data, err := json.Marshal(output)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputText(ctx context.Context) (string, error) {
builder := strings.Builder{}
if c.activationQueue > 0 {
builder.WriteString(fmt.Sprintf("Activation queue: %d\n", c.activationQueue))
}
if c.exitQueue > 0 {
builder.WriteString(fmt.Sprintf("Exit queue: %d\n", c.exitQueue))
}
return strings.TrimSuffix(builder.String(), "\n"), nil
}

View File

@@ -0,0 +1,82 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chainqueues
import (
"context"
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
epoch, err := util.ParseEpoch(ctx, c.chainTime, c.epoch)
if err != nil {
return err
}
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)), nil)
if err != nil {
return errors.Wrap(err, "failed to obtain validators")
}
for _, validator := range validators {
if validator.Validator == nil {
continue
}
if validator.Validator.ActivationEligibilityEpoch <= epoch && validator.Validator.ActivationEpoch > epoch {
c.activationQueue++
}
if validator.Validator.ExitEpoch != 0xffffffffffffffff && validator.Validator.ExitEpoch > epoch {
c.exitQueue++
}
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
var isProvider bool
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
if !isProvider {
return errors.New("connection does not provide validator information")
}
return nil
}

View File

@@ -0,0 +1,66 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chainqueues
import (
"context"
"os"
"testing"
"github.com/rs/zerolog"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
zerolog.SetGlobalLevel(zerolog.Disabled)
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "InvalidEpoch",
vars: map[string]interface{}{
"timeout": "5s",
"epoch": "invalid",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "failed to parse epoch: strconv.ParseInt: parsing \"invalid\": invalid syntax",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
cmd, err := newCommand(context.Background())
require.NoError(t, err)
err = cmd.process(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

50
cmd/chain/queues/run.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 chainqueues
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
}

View File

@@ -71,9 +71,6 @@ func input(ctx context.Context) (*dataIn, error) {
return nil, errors.New("one of timestamp, slot or epoch required")
}
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
data.connection = viper.GetString("connection")
data.allowInsecureConnections = viper.GetBool("allow-insecure-connections")

View File

@@ -60,14 +60,6 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
"slot": "1",
},
err: "connection is required",
},
{
name: "IDMissing",
vars: map[string]interface{}{

View File

@@ -76,9 +76,6 @@ func newCommand(ctx context.Context) (*command, error) {
}
c.data = viper.GetString("data")
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")

View File

@@ -44,14 +44,6 @@ func TestInput(t *testing.T) {
},
err: "data is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
"data": "{}",
},
err: "connection is required",
},
{
name: "Good",
vars: map[string]interface{}{

67
cmd/chaineth1votes.go Normal file
View File

@@ -0,0 +1,67 @@
// Copyright © 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
chaineth1votes "github.com/wealdtech/ethdo/cmd/chain/eth1votes"
)
var chainEth1VotesCmd = &cobra.Command{
Use: "eth1votes",
Short: "Show chain execution votes",
Long: `Show beacon chain execution votes. For example:
ethdo chain eth1votes
Note that this will fetch the votes made in blocks up to the end of the provided epoch.
In quiet mode this will return 0 if there is a majority for the votes, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := chaineth1votes.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
chainCmd.AddCommand(chainEth1VotesCmd)
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 {
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 {
panic(err)
}
}

61
cmd/chainqueues.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright © 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
chainqueues "github.com/wealdtech/ethdo/cmd/chain/queues"
)
var chainQueuesCmd = &cobra.Command{
Use: "queues",
Short: "Show chain queues",
Long: `Show beacon chain activation and exit queues. For example:
ethdo chain queues
In quiet mode this will return 0 if the entry and exit queues are 0, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := chainqueues.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
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 {
panic(err)
}
}

View File

@@ -21,7 +21,7 @@ import (
"os"
"strings"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/wealdtech/ethdo/util"
@@ -219,15 +219,15 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
outputIf(!quiet, "Validator public key verified")
}
var pubKey spec.BLSPubKey
var pubKey phase0.BLSPubKey
copy(pubKey[:], deposit.PublicKey)
var signature spec.BLSSignature
var signature phase0.BLSSignature
copy(signature[:], deposit.Signature)
depositData := &spec.DepositData{
depositData := &phase0.DepositData{
PublicKey: pubKey,
WithdrawalCredentials: deposit.WithdrawalCredentials,
Amount: spec.Gwei(deposit.Amount),
Amount: phase0.Gwei(deposit.Amount),
Signature: signature,
}
depositDataRoot, err := depositData.HashTreeRoot()
@@ -248,7 +248,7 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
}
} else {
if depositVerifyForkVersion == "" {
outputIf(!quiet, "fork version not supplied; NOT checked")
outputIf(!quiet, "fork version not supplied; not checked")
} else {
forkVersion, err := hex.DecodeString(strings.TrimPrefix(depositVerifyForkVersion, "0x"))
if err != nil {
@@ -260,6 +260,56 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
outputIf(!quiet, "Fork version incorrect")
return false, nil
}
if len(deposit.DepositMessageRoot) != 32 {
outputIf(!quiet, "Deposit message root not supplied; not checked")
} else {
// We can also verify the deposit message signature.
depositMessage := &phase0.DepositMessage{
PublicKey: pubKey,
WithdrawalCredentials: withdrawalCredentials,
Amount: phase0.Gwei(deposit.Amount),
}
depositMessageRoot, err := depositMessage.HashTreeRoot()
if err != nil {
return false, errors.Wrap(err, "failed to generate deposit message root")
}
if bytes.Equal(deposit.DepositMessageRoot, depositMessageRoot[:]) {
outputIf(!quiet, "Deposit message root verified")
} else {
outputIf(!quiet, "Deposit message root incorrect")
return false, nil
}
domainBytes := e2types.Domain(e2types.DomainDeposit, forkVersion, e2types.ZeroGenesisValidatorsRoot)
var domain phase0.Domain
copy(domain[:], domainBytes)
container := &phase0.SigningData{
ObjectRoot: depositMessageRoot,
Domain: domain,
}
containerRoot, err := container.HashTreeRoot()
if err != nil {
return false, errors.New("failed to generate root for container")
}
validatorPubKey, err := e2types.BLSPublicKeyFromBytes(pubKey[:])
if err != nil {
return false, errors.Wrap(err, "failed to generate validator public key")
}
blsSig, err := e2types.BLSSignatureFromBytes(signature[:])
if err != nil {
return false, errors.New("failed to verify BLS signature")
}
signatureVerified := blsSig.Verify(containerRoot[:], validatorPubKey)
if signatureVerified {
outputIf(!quiet, "Deposit message signature verified")
} else {
outputIf(!quiet, "Deposit message signature NOT verified")
return false, nil
}
}
}
}

40
cmd/epoch.go Normal file
View File

@@ -0,0 +1,40 @@
// Copyright © 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// epochCmd represents the epoch command
var epochCmd = &cobra.Command{
Use: "epoch",
Short: "Obtain information about Ethereum 2 epochs",
Long: "Obtain information about Ethereum 2 epochs",
}
func init() {
RootCmd.AddCommand(epochCmd)
}
func epochFlags(cmd *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 {
panic(err)
}
}

View File

@@ -0,0 +1,105 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 epochsummary
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Operation.
epoch string
stream bool
jsonOutput bool
// Data access.
eth2Client eth2client.Service
chainTime chaintime.Service
proposerDutiesProvider eth2client.ProposerDutiesProvider
blocksProvider eth2client.SignedBeaconBlockProvider
syncCommitteesProvider eth2client.SyncCommitteesProvider
validatorsProvider eth2client.ValidatorsProvider
beaconCommitteesProvider eth2client.BeaconCommitteesProvider
// Results.
summary *epochSummary
}
type epochSummary struct {
Epoch phase0.Epoch `json:"epoch"`
FirstSlot phase0.Slot `json:"first_slot"`
LastSlot phase0.Slot `json:"last_slot"`
Proposals []*epochProposal `json:"proposals"`
SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
ActiveValidators int `json:"active_validators"`
ParticipatingValidators int `json:"participating_validators"`
NonParticipatingValidators []*nonParticipatingValidator `json:"nonparticipating_validators"`
}
type epochProposal struct {
Slot phase0.Slot `json:"slot"`
Proposer phase0.ValidatorIndex `json:"proposer"`
Block bool `json:"block"`
}
type epochSyncCommittee struct {
Index phase0.ValidatorIndex `json:"index"`
Missed int `json:"missed"`
}
type nonParticipatingValidator struct {
Validator phase0.ValidatorIndex `json:"validator_index"`
Slot phase0.Slot `json:"slot"`
Committee phase0.CommitteeIndex `json:"committee_index"`
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
summary: &epochSummary{},
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
c.epoch = viper.GetString("epoch")
c.stream = viper.GetBool("stream")
c.jsonOutput = viper.GetBool("json")
return c, nil
}

View File

@@ -0,0 +1,64 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 epochsummary
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
_, err := newCommand(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

105
cmd/epoch/summary/output.go Normal file
View File

@@ -0,0 +1,105 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 epochsummary
import (
"context"
"encoding/json"
"fmt"
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.jsonOutput {
return c.outputJSON(ctx)
}
return c.outputTxt(ctx)
}
func (c *command) outputJSON(_ context.Context) (string, error) {
data, err := json.Marshal(c.summary)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputTxt(_ context.Context) (string, error) {
builder := strings.Builder{}
builder.WriteString("Epoch ")
builder.WriteString(fmt.Sprintf("%d:\n", c.summary.Epoch))
proposedBlocks := 0
missedProposals := make([]string, 0, len(c.summary.Proposals))
for _, proposal := range c.summary.Proposals {
if !proposal.Block {
missedProposals = append(missedProposals, fmt.Sprintf("\n Slot %d (validator %d)", proposal.Slot, proposal.Proposer))
} else {
proposedBlocks++
}
}
builder.WriteString(fmt.Sprintf(" Proposals: %d/%d (%0.2f%%)", proposedBlocks, len(missedProposals)+proposedBlocks, 100.0*float64(proposedBlocks)/float64(len(missedProposals)+proposedBlocks)))
if c.verbose {
for _, proposal := range c.summary.Proposals {
if proposal.Block {
continue
}
builder.WriteString("\n Slot ")
builder.WriteString(fmt.Sprintf("%d (%d/%d)", proposal.Slot, uint64(proposal.Slot)%uint64(len(c.summary.Proposals)), len(c.summary.Proposals)))
builder.WriteString(" validator ")
builder.WriteString(fmt.Sprintf("%d", proposal.Proposer))
builder.WriteString(" not proposed or not included")
}
}
builder.WriteString(fmt.Sprintf("\n Attestations: %d/%d (%0.2f%%)", c.summary.ParticipatingValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.ParticipatingValidators)/float64(c.summary.ActiveValidators)))
if c.verbose {
// Sort list by validator index.
for _, validator := range c.summary.NonParticipatingValidators {
builder.WriteString("\n Slot ")
builder.WriteString(fmt.Sprintf("%d", validator.Slot))
builder.WriteString(" committee ")
builder.WriteString(fmt.Sprintf("%d", validator.Committee))
builder.WriteString(" validator ")
builder.WriteString(fmt.Sprintf("%d", validator.Validator))
builder.WriteString(" failed to participate")
}
}
if c.summary.Epoch >= c.chainTime.AltairInitialEpoch() {
contributions := proposedBlocks * 512 // SYNC_COMMITTEE_SIZE
totalMissed := 0
for _, contribution := range c.summary.SyncCommittee {
totalMissed += contribution.Missed
}
builder.WriteString(fmt.Sprintf("\n Sync committees: %d/%d (%0.2f%%)", contributions-totalMissed, contributions, 100.0*float64(contributions-totalMissed)/float64(contributions)))
if c.verbose {
for _, syncCommittee := range c.summary.SyncCommittee {
builder.WriteString("\n Validator ")
builder.WriteString(fmt.Sprintf("%d", syncCommittee.Index))
builder.WriteString(" included ")
builder.WriteString(fmt.Sprintf("%d/%d", proposedBlocks-syncCommittee.Missed, proposedBlocks))
builder.WriteString(fmt.Sprintf(" (%0.2f%%)", 100.0*float64(proposedBlocks-syncCommittee.Missed)/float64(proposedBlocks)))
}
}
}
return builder.String(), nil
}

View File

@@ -0,0 +1,291 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 epochsummary
import (
"context"
"fmt"
"sort"
eth2client "github.com/attestantio/go-eth2-client"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
err := c.setup(ctx)
if err != nil {
return err
}
c.summary.Epoch, err = util.ParseEpoch(ctx, c.chainTime, c.epoch)
if err != nil {
return errors.Wrap(err, "failed to parse epoch")
}
c.summary.FirstSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)
c.summary.LastSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) - 1
if err := c.processProposerDuties(ctx); err != nil {
return err
}
if err := c.processAttesterDuties(ctx); err != nil {
return err
}
if err := c.processSyncCommitteeDuties(ctx); err != nil {
return err
}
return nil
}
func (c *command) processProposerDuties(ctx context.Context) error {
duties, err := c.proposerDutiesProvider.ProposerDuties(ctx, c.summary.Epoch, nil)
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))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", duty.Slot))
}
present := block != nil
c.summary.Proposals = append(c.summary.Proposals, &epochProposal{
Slot: duty.Slot,
Proposer: duty.ValidatorIndex,
Block: present,
})
}
return nil
}
func (c *command) processAttesterDuties(ctx context.Context) error {
// Obtain all active validators for the given epoch.
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), nil)
if err != nil {
return errors.Wrap(err, "failed to obtain validators for epoch")
}
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
for _, validator := range validators {
if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch {
activeValidators[validator.Index] = validator
}
}
// Obtain number of validators that voted for blocks in the epoch.
// These votes can be included anywhere from the second slot of
// the epoch to the first slot of the next-but-one epoch.
firstSlot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) + 1
lastSlot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch + 2)
if lastSlot > c.chainTime.CurrentSlot() {
lastSlot = c.chainTime.CurrentSlot()
}
votes := make(map[phase0.ValidatorIndex]struct{})
allCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
participations := make(map[phase0.ValidatorIndex]*nonParticipatingValidator)
for slot := firstSlot; slot <= lastSlot; slot++ {
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
}
if block == nil {
// No block at this slot; that's fine.
continue
}
attestations, err := block.Attestations()
if err != nil {
return err
}
for _, attestation := range attestations {
if attestation.Data.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestation.Data.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
// Outside of this epoch's range.
continue
}
slotCommittees, exists := allCommittees[attestation.Data.Slot]
if !exists {
beaconCommittees, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", attestation.Data.Slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
}
for _, beaconCommittee := range beaconCommittees {
if _, exists := allCommittees[beaconCommittee.Slot]; !exists {
allCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
}
allCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
for _, index := range beaconCommittee.Validators {
participations[index] = &nonParticipatingValidator{
Validator: index,
Slot: beaconCommittee.Slot,
Committee: beaconCommittee.Index,
}
}
}
slotCommittees = allCommittees[attestation.Data.Slot]
}
committee := slotCommittees[attestation.Data.Index]
for i := uint64(0); i < attestation.AggregationBits.Len(); i++ {
if attestation.AggregationBits.BitAt(i) {
votes[committee[int(i)]] = struct{}{}
}
}
}
}
c.summary.ActiveValidators = len(activeValidators)
c.summary.ParticipatingValidators = len(votes)
c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0, len(activeValidators)-len(votes))
for activeValidatorIndex := range activeValidators {
if _, exists := votes[activeValidatorIndex]; !exists {
if _, exists := participations[activeValidatorIndex]; exists {
c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, participations[activeValidatorIndex])
}
}
}
sort.Slice(c.summary.NonParticipatingValidators, func(i int, j int) bool {
if c.summary.NonParticipatingValidators[i].Slot != c.summary.NonParticipatingValidators[j].Slot {
return c.summary.NonParticipatingValidators[i].Slot < c.summary.NonParticipatingValidators[j].Slot
}
if c.summary.NonParticipatingValidators[i].Committee != c.summary.NonParticipatingValidators[j].Committee {
return c.summary.NonParticipatingValidators[i].Committee < c.summary.NonParticipatingValidators[j].Committee
}
return c.summary.NonParticipatingValidators[i].Validator < c.summary.NonParticipatingValidators[j].Validator
})
return nil
}
func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
if c.summary.Epoch < c.chainTime.AltairInitialEpoch() {
// The epoch is pre-Altair. No info but no error.
return nil
}
committee, err := c.syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", c.summary.FirstSlot))
if err != nil {
return errors.Wrap(err, "failed to obtain sync committee")
}
if len(committee.Validators) == 0 {
return errors.Wrap(err, "empty sync committee")
}
missed := make(map[phase0.ValidatorIndex]int)
for _, index := range committee.Validators {
missed[index] = 0
}
for slot := c.summary.FirstSlot; slot <= c.summary.LastSlot; slot++ {
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
}
if block == nil {
// If the block is missed we don't count the sync aggregate miss.
continue
}
var aggregate *altair.SyncAggregate
switch block.Version {
case spec.DataVersionPhase0:
// No sync committees in this fork.
return nil
case spec.DataVersionAltair:
aggregate = block.Altair.Message.Body.SyncAggregate
case spec.DataVersionBellatrix:
aggregate = block.Bellatrix.Message.Body.SyncAggregate
default:
return fmt.Errorf("unhandled block version %v", block.Version)
}
for i := uint64(0); i < aggregate.SyncCommitteeBits.Len(); i++ {
if !aggregate.SyncCommitteeBits.BitAt(i) {
missed[committee.Validators[int(i)]]++
}
}
}
c.summary.SyncCommittee = make([]*epochSyncCommittee, 0, len(missed))
for index, count := range missed {
if count > 0 {
c.summary.SyncCommittee = append(c.summary.SyncCommittee, &epochSyncCommittee{
Index: index,
Missed: count,
})
}
}
sort.Slice(c.summary.SyncCommittee, func(i int, j int) bool {
missedDiff := c.summary.SyncCommittee[i].Missed - c.summary.SyncCommittee[j].Missed
if missedDiff != 0 {
// Actually want to order by missed descending, so invert the expected condition.
return missedDiff > 0
}
// Then order by validator index.
return c.summary.SyncCommittee[i].Index < c.summary.SyncCommittee[j].Index
})
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
var isProvider bool
c.proposerDutiesProvider, isProvider = c.eth2Client.(eth2client.ProposerDutiesProvider)
if !isProvider {
return errors.New("connection does not provide proposer duties")
}
c.blocksProvider, isProvider = c.eth2Client.(eth2client.SignedBeaconBlockProvider)
if !isProvider {
return errors.New("connection does not provide signed beacon blocks")
}
c.syncCommitteesProvider, isProvider = c.eth2Client.(eth2client.SyncCommitteesProvider)
if !isProvider {
return errors.New("connection does not provide sync committee duties")
}
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
if !isProvider {
return errors.New("connection does not provide validators")
}
c.beaconCommitteesProvider, isProvider = c.eth2Client.(eth2client.BeaconCommitteesProvider)
if !isProvider {
return errors.New("connection does not provide beacon committees")
}
return nil
}

View File

@@ -0,0 +1,62 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 epochsummary
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "InvalidData",
vars: map[string]interface{}{
"timeout": "60s",
"data": "[[",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
cmd, err := newCommand(context.Background())
require.NoError(t, err)
err = cmd.process(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

50
cmd/epoch/summary/run.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 epochsummary
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
}

58
cmd/epochsummary.go Normal file
View File

@@ -0,0 +1,58 @@
// Copyright © 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
epochsummary "github.com/wealdtech/ethdo/cmd/epoch/summary"
)
var epochSummaryCmd = &cobra.Command{
Use: "summary",
Short: "Obtain summary information about an epoch",
Long: `Obtain summary information about an epoch. For example:
ethdo epoch summary --epoch=12345
In quiet mode this will return 0 if information for the epoch is found, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := epochsummary.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
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)
}
}

View File

@@ -66,7 +66,7 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to any beacon node",
},
{
name: "ConnectionBad",
@@ -75,7 +75,7 @@ func TestInput(t *testing.T) {
"connection": "localhost:1",
"topics": []string{"one", "two"},
},
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
},
{
name: "TopicsNil",

View File

@@ -27,3 +27,6 @@ var proposerCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(proposerCmd)
}
func proposerFlags(cmd *cobra.Command) {
}

View File

@@ -0,0 +1,77 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 proposerduties
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Operation.
epoch string
jsonOutput bool
// Data access.
eth2Client eth2client.Service
chainTime chaintime.Service
proposerDutiesProvider eth2client.ProposerDutiesProvider
// Results.
results *results
}
type results struct {
Epoch phase0.Epoch `json:"epoch"`
Duties []*apiv1.ProposerDuty `json:"duties"`
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
results: &results{},
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
c.epoch = viper.GetString("epoch")
c.jsonOutput = viper.GetBool("json")
return c, nil
}

View File

@@ -0,0 +1,72 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 proposerduties
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
{
name: "GoodWithEpoch",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"epoch": "-1",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
_, err := newCommand(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,62 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 proposerduties
import (
"context"
"encoding/json"
"fmt"
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.jsonOutput {
return c.outputJSON(ctx)
}
return c.outputTxt(ctx)
}
func (c *command) outputJSON(_ context.Context) (string, error) {
data, err := json.Marshal(c.results)
if err != nil {
return "", err
}
return string(data), nil
}
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 ")
builder.WriteString(fmt.Sprintf("%d", duty.ValidatorIndex))
if c.verbose {
builder.WriteString(" (pubkey ")
builder.WriteString(fmt.Sprintf("%#x)", duty.PubKey))
}
builder.WriteString("\n")
}
return strings.TrimSuffix(builder.String(), "\n"), nil
}

View File

@@ -0,0 +1,70 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 proposerduties
import (
"context"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
err := c.setup(ctx)
if err != nil {
return err
}
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)
if err != nil {
return errors.Wrap(err, "failed to obtain proposer duties")
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
var isProvider bool
c.proposerDutiesProvider, isProvider = c.eth2Client.(eth2client.ProposerDutiesProvider)
if !isProvider {
return errors.New("connection does not provide proposer duties")
}
return nil
}

View File

@@ -0,0 +1,62 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 proposerduties
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "InvalidData",
vars: map[string]interface{}{
"timeout": "60s",
"data": "[[",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
cmd, err := newCommand(context.Background())
require.NoError(t, err)
err = cmd.process(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,50 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 proposerduties
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
}

61
cmd/proposerduties.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright © 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
proposerduties "github.com/wealdtech/ethdo/cmd/proposer/duties"
)
var proposerDutiesCmd = &cobra.Command{
Use: "duties",
Short: "Obtain information about duties of an proposer",
Long: `Obtain information about dutes of an proposer. For example:
ethdo proposer duties --epoch=12345
In quiet mode this will return 0 if duties can be obtained, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := proposerduties.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
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")
}
func proposerDutiesBindings() {
if err := viper.BindPFlag("epoch", proposerDutiesCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", proposerDutiesCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
}

View File

@@ -77,6 +77,7 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
return util.SetupStore()
}
// nolint:gocyclo
func includeCommandBindings(cmd *cobra.Command) {
switch commandPath(cmd) {
case "account/create":
@@ -93,20 +94,30 @@ func includeCommandBindings(cmd *cobra.Command) {
blockAnalyzeBindings()
case "block/info":
blockInfoBindings()
case "chain/eth1votes":
chainEth1VotesBindings()
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/depositdata":
validatorDepositdataBindings()
case "validator/duties":
@@ -117,6 +128,8 @@ func includeCommandBindings(cmd *cobra.Command) {
validatorInfoBindings()
case "validator/keycheck":
validatorKeycheckBindings()
case "validator/yield":
validatorYieldBindings()
case "validator/expectation":
validatorExpectationBindings()
case "wallet/create":
@@ -209,7 +222,7 @@ func init() {
if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().String("connection", "http://localhost:3500", "URL to an Ethereum 2 node's RET API endpoint")
RootCmd.PersistentFlags().String("connection", "", "URL to an Ethereum 2 node's RET API endpoint")
if err := viper.BindPFlag("connection", RootCmd.PersistentFlags().Lookup("connection")); err != nil {
panic(err)
}

View File

@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
"timeout": "5s",
"slot": "1",
},
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to any beacon node",
},
}

View File

@@ -62,9 +62,7 @@ func newCommand(ctx context.Context) (*command, error) {
}
c.timeout = viper.GetDuration("timeout")
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
// Connection.
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")

View File

@@ -37,14 +37,6 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"validators": "1",
"timeout": "5s",
},
err: "connection is required",
},
{
name: "NoValidator",
vars: map[string]interface{}{

View File

@@ -32,6 +32,14 @@ func TestProcess(t *testing.T) {
vars map[string]interface{}
err string
}{
{
name: "MissingConnection",
vars: map[string]interface{}{
"timeout": "5s",
"index": "1",
},
err: "failed to connect to any beacon node",
},
{
name: "InvalidConnection",
vars: map[string]interface{}{
@@ -39,7 +47,7 @@ func TestProcess(t *testing.T) {
"index": "1",
"connection": "invalid",
},
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://invalid/eth/v1/beacon/genesis\": dial tcp: lookup invalid: no such host",
},
{
name: "Good",

View File

@@ -65,7 +65,15 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to any beacon node",
},
{
name: "ConnectionInvalid",
vars: map[string]interface{}{
"timeout": "5s",
"connection": "localhost:1",
},
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
},
}

View File

@@ -0,0 +1,86 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatorcredentialsget
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type command struct {
quiet bool
verbose bool
debug bool
// Input.
account string
index string
pubKey string
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Data access.
consensusClient eth2client.Service
validatorsProvider eth2client.ValidatorsProvider
// Output.
validator *apiv1.Validator
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
c.account = viper.GetString("account")
c.index = viper.GetString("index")
c.pubKey = viper.GetString("pubkey")
nonNil := 0
if c.account != "" {
nonNil++
}
if c.index != "" {
nonNil++
}
if c.pubKey != "" {
nonNil++
}
if nonNil == 0 {
return nil, errors.New("one of account, index or pubkey required")
}
if nonNil > 1 {
return nil, errors.New("only one of account, index and pubkey allowed")
}
return c, nil
}

View File

@@ -0,0 +1,83 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatorcredentialsget
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "NoValidatorInfo",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "one of account, index or pubkey required",
},
{
name: "MultipleValidatorInfo",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"index": "1",
"pubkey": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
},
err: "only one of account, index and pubkey allowed",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"index": "1",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
_, err := newCommand(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,44 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatorcredentialsget
import (
"context"
"fmt"
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
builder := strings.Builder{}
switch c.validator.Validator.WithdrawalCredentials[0] {
case 0:
builder.WriteString("BLS credentials: ")
builder.WriteString(fmt.Sprintf("%#x", c.validator.Validator.WithdrawalCredentials))
case 1:
builder.WriteString("Ethereum execution address: ")
builder.WriteString(fmt.Sprintf("%#x", c.validator.Validator.WithdrawalCredentials[12:]))
if c.verbose {
builder.WriteString("\n")
builder.WriteString("Withdrawal credentials: ")
builder.WriteString(fmt.Sprintf("%#x", c.validator.Validator.WithdrawalCredentials))
}
}
return builder.String(), nil
}

View File

@@ -0,0 +1,139 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatorcredentialsget
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"strconv"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
// Work out which validator we are dealing with.
if err := c.fetchValidator(ctx); err != nil {
return err
}
if c.debug {
data, err := json.Marshal(c.validator)
if err == nil {
fmt.Println(string(data))
}
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the consensus node.
c.consensusClient, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to consensus node")
}
// Obtain the validators provider.
var isProvider bool
c.validatorsProvider, isProvider = c.consensusClient.(eth2client.ValidatorsProvider)
if !isProvider {
return errors.New("consensu node does not provide validator information")
}
return nil
}
func (c *command) fetchValidator(ctx context.Context) error {
if c.account != "" {
_, account, err := util.WalletAndAccountFromInput(ctx)
if err != nil {
return errors.Wrap(err, "unable to obtain account")
}
accPubKey, err := util.BestPublicKey(account)
if err != nil {
return errors.Wrap(err, "unable to obtain public key for account")
}
pubKey := phase0.BLSPubKey{}
copy(pubKey[:], accPubKey.Marshal())
validators, err := c.validatorsProvider.ValidatorsByPubKey(ctx,
"head",
[]phase0.BLSPubKey{pubKey},
)
if err != nil {
return errors.Wrap(err, "failed to obtain validator information")
}
if len(validators) == 0 {
return errors.New("unknown validator")
}
for _, validator := range validators {
c.validator = validator
}
}
if c.index != "" {
tmp, err := strconv.ParseUint(c.index, 10, 64)
if err != nil {
return errors.Wrap(err, "invalid validator index")
}
index := phase0.ValidatorIndex(tmp)
validators, err := c.validatorsProvider.Validators(ctx,
"head",
[]phase0.ValidatorIndex{index},
)
if err != nil {
return errors.Wrap(err, "failed to obtain validator information")
}
if _, exists := validators[index]; !exists {
return errors.New("unknown validator")
}
c.validator = validators[index]
}
if c.pubKey != "" {
bytes, err := hex.DecodeString(strings.TrimPrefix(c.pubKey, "0x"))
if err != nil {
return errors.Wrap(err, "invalid validator public key")
}
pubKey := phase0.BLSPubKey{}
copy(pubKey[:], bytes)
validators, err := c.validatorsProvider.ValidatorsByPubKey(ctx,
"head",
[]phase0.BLSPubKey{pubKey},
)
if err != nil {
return errors.Wrap(err, "failed to obtain validator information")
}
if len(validators) == 0 {
return errors.New("unknown validator")
}
for _, validator := range validators {
c.validator = validator
}
}
return nil
}

View File

@@ -0,0 +1,50 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatorcredentialsget
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
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -110,6 +110,8 @@ func validatorDepositDataOutputLaunchpad(datum *dataOut) (string, error) {
[4]byte{0x00, 0x00, 0x00, 0x00}: "mainnet",
[4]byte{0x00, 0x00, 0x20, 0x09}: "pyrmont",
[4]byte{0x00, 0x00, 0x10, 0x20}: "prater",
[4]byte{0x80, 0x00, 0x00, 0x69}: "ropsten",
[4]byte{0x90, 0x00, 0x00, 0x69}: "sepolia",
}
if datum.validatorPubKey == nil {

View File

@@ -49,9 +49,6 @@ func input(ctx context.Context) (*dataIn, error) {
// Ethereum 2 connection.
data.eth2Client = viper.GetString("connection")
if data.eth2Client == "" {
return nil, errors.New("connection is required")
}
data.allowInsecure = viper.GetBool("allow-insecure-connections")
// Account.

View File

@@ -71,14 +71,6 @@ func TestInput(t *testing.T) {
},
err: "account, pubkey or index required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
err: "connection is required",
},
}
for _, test := range tests {

View File

@@ -158,7 +158,7 @@ func TestInput(t *testing.T) {
"timeout": "5s",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
},
{
name: "EpochProvided",

View File

@@ -58,9 +58,6 @@ func newCommand(ctx context.Context) (*command, error) {
}
c.timeout = viper.GetDuration("timeout")
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")

View File

@@ -37,14 +37,6 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"validators": "1",
"timeout": "5s",
},
err: "connection is required",
},
{
name: "ValidatorsZero",
vars: map[string]interface{}{

View File

@@ -0,0 +1,81 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatoryield
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
"github.com/shopspring/decimal"
"github.com/spf13/viper"
)
type command struct {
quiet bool
verbose bool
debug bool
json bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Input.
validators string
// Data access.
eth2Client eth2client.Service
// Output.
results *output
}
type output struct {
BaseReward decimal.Decimal `json:"base_reward"`
ActiveValidators decimal.Decimal `json:"active_validators"`
ActiveValidatorBalance decimal.Decimal `json:"active_validator_balance"`
ValidatorRewardsPerEpoch decimal.Decimal `json:"validator_rewards_per_epoch"`
ValidatorRewardsPerYear decimal.Decimal `json:"validator_rewards_per_year"`
ValidatorRewardsAllCorrect decimal.Decimal `json:"validator_rewards_all_correct"`
ExpectedValidatorRewardsPerEpoch decimal.Decimal `json:"expected_validator_rewards_per_epoch"`
MaxIssuancePerEpoch decimal.Decimal `json:"max_issuance_per_epoch"`
MaxIssuancePerYear decimal.Decimal `json:"max_issuance_per_year"`
Yield decimal.Decimal `json:"yield"`
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
json: viper.GetBool("json"),
results: &output{},
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
c.validators = viper.GetString("validators")
return c, nil
}

View File

@@ -0,0 +1,65 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatoryield
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "Good",
vars: map[string]interface{}{
"validators": "1",
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
_, err := newCommand(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,67 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatoryield
import (
"context"
"encoding/json"
"strings"
"github.com/shopspring/decimal"
"github.com/wealdtech/go-string2eth"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.json {
data, err := json.Marshal(c.results)
if err != nil {
return "", err
}
return string(data), nil
}
builder := strings.Builder{}
if c.verbose {
builder.WriteString("Per-validator rewards per epoch: ")
builder.WriteString(string2eth.WeiToGWeiString(c.results.ValidatorRewardsPerEpoch.BigInt()))
builder.WriteString("\n")
builder.WriteString("Per-validator rewards per year: ")
builder.WriteString(string2eth.WeiToString(c.results.ValidatorRewardsPerYear.BigInt(), true))
builder.WriteString("\n")
builder.WriteString("Expected per-validator rewards per epoch (with full participation): ")
builder.WriteString(string2eth.WeiToGWeiString(c.results.ExpectedValidatorRewardsPerEpoch.BigInt()))
builder.WriteString("\n")
builder.WriteString("Maximum chain issuance per epoch: ")
builder.WriteString(string2eth.WeiToString(c.results.MaxIssuancePerEpoch.BigInt(), true))
builder.WriteString("\n")
builder.WriteString("Maximum chain issuance per year: ")
builder.WriteString(string2eth.WeiToString(c.results.MaxIssuancePerYear.BigInt(), true))
builder.WriteString("\n")
}
builder.WriteString("Yield: ")
builder.WriteString(c.results.Yield.Mul(decimal.New(100, 0)).StringFixed(2))
builder.WriteString("%\n")
return builder.String(), nil
}

View File

@@ -0,0 +1,170 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 validatoryield
import (
"context"
"fmt"
"math/big"
"strconv"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
"github.com/shopspring/decimal"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
if c.debug {
fmt.Printf("Active validators: %v\n", c.results.ActiveValidators)
fmt.Printf("Active validator balance: %v\n", c.results.ActiveValidatorBalance)
}
return c.calculateYield(ctx)
}
var weiPerGwei = decimal.New(1e9, 0)
var one = decimal.New(1, 0)
var epochsPerYear = decimal.New(225*365, 0)
// 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)
if err != nil {
return err
}
tmp, exists := spec["BASE_REWARD_FACTOR"]
if !exists {
return errors.New("spec missing BASE_REWARD_FACTOR")
}
baseReward, isType := tmp.(uint64)
if !isType {
return errors.New("BASE_REWARD_FACTOR of incorrect type")
}
if c.debug {
fmt.Printf("Base reward: %v\n", baseReward)
}
c.results.BaseReward = decimal.New(int64(baseReward), 0)
numerator := decimal.New(32, 0).Mul(weiPerGwei).Mul(c.results.BaseReward)
if c.debug {
fmt.Printf("Numerator: %v\n", numerator)
}
activeValidatorsBalanceInGwei := c.results.ActiveValidatorBalance.Div(weiPerGwei)
denominator := decimal.NewFromBigInt(new(big.Int).Sqrt(activeValidatorsBalanceInGwei.BigInt()), 0)
if c.debug {
fmt.Printf("Denominator: %v\n", denominator)
}
c.results.ValidatorRewardsPerEpoch = numerator.Div(denominator).RoundDown(0).Mul(weiPerGwei)
if c.debug {
fmt.Printf("Validator rewards per epoch: %v\n", c.results.ValidatorRewardsPerEpoch)
}
c.results.ValidatorRewardsPerYear = c.results.ValidatorRewardsPerEpoch.Mul(epochsPerYear)
if c.debug {
fmt.Printf("Validator rewards per year: %v\n", c.results.ValidatorRewardsPerYear)
}
// Expected validator rewards assume that there is no proposal and no sync committee participation,
// but that head/source/target are correct and timely: this gives 54/64 of the reward.
// These values are obtained from https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#incentivization-weights
c.results.ExpectedValidatorRewardsPerEpoch = c.results.ValidatorRewardsPerEpoch.Mul(decimal.New(54, 0)).Div(decimal.New(64, 0)).Div(weiPerGwei).RoundDown(0).Mul(weiPerGwei)
if c.debug {
fmt.Printf("Expected validator rewards per epoch: %v\n", c.results.ExpectedValidatorRewardsPerEpoch)
}
c.results.MaxIssuancePerEpoch = c.results.ValidatorRewardsPerEpoch.Mul(c.results.ActiveValidators)
if c.debug {
fmt.Printf("Chain rewards per epoch: %v\n", c.results.MaxIssuancePerEpoch)
}
c.results.MaxIssuancePerYear = c.results.MaxIssuancePerEpoch.Mul(epochsPerYear)
if c.debug {
fmt.Printf("Chain rewards per year: %v\n", c.results.MaxIssuancePerYear)
}
c.results.Yield = c.results.ValidatorRewardsPerYear.Div(weiPerGwei).Div(weiPerGwei).Div(decimal.New(32, 0))
if c.debug {
fmt.Printf("Yield: %v\n", c.results.Yield)
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
if c.validators == "" {
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
// Obtain the number of active validators.
var isProvider bool
validatorsProvider, isProvider := c.eth2Client.(eth2client.ValidatorsProvider)
if !isProvider {
return errors.New("connection does not provide validator information")
}
validators, err := validatorsProvider.Validators(ctx, "head", nil)
if err != nil {
return errors.Wrap(err, "failed to obtain validators")
}
currentEpoch := chainTime.CurrentEpoch()
activeValidators := decimal.Zero
activeValidatorBalance := decimal.Zero
for _, validator := range validators {
if validator.Validator.ActivationEpoch <= currentEpoch &&
validator.Validator.ExitEpoch > currentEpoch {
activeValidators = activeValidators.Add(one)
activeValidatorBalance = activeValidatorBalance.Add(decimal.NewFromInt(int64(validator.Validator.EffectiveBalance)))
}
}
c.results.ActiveValidators = activeValidators
c.results.ActiveValidatorBalance = activeValidatorBalance.Mul(weiPerGwei)
} else {
activeValidators, err := strconv.ParseInt(c.validators, 0, 64)
if err != nil {
return errors.Wrap(err, "failed to parse number of validators")
}
if activeValidators <= 0 {
return errors.New("number of validators must be greater than 0")
}
c.results.ActiveValidators = decimal.New(activeValidators, 0)
c.results.ActiveValidatorBalance = decimal.New(32, 0).Mul(c.results.ActiveValidators).Mul(weiPerGwei).Mul(weiPerGwei)
if c.debug {
fmt.Println("Assuming 32Ξ per validator")
}
}
return nil
}

View File

@@ -0,0 +1,90 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatoryield
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "InvalidData",
vars: map[string]interface{}{
"timeout": "60s",
"validators": "1",
"data": "[[",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
{
name: "ValidatorsInvalid",
vars: map[string]interface{}{
"timeout": "60s",
"validators": "invalid",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "failed to parse number of validators: strconv.ParseInt: parsing \"invalid\": invalid syntax",
},
{
name: "ValidatorsNegative",
vars: map[string]interface{}{
"timeout": "60s",
"validators": "-1",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "number of validators must be greater than 0",
},
{
name: "ValidatorsZero",
vars: map[string]interface{}{
"timeout": "60s",
"validators": "0",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "number of validators must be greater than 0",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
cmd, err := newCommand(context.Background())
require.NoError(t, err)
err = cmd.process(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,50 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatoryield
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
}

View File

@@ -0,0 +1,32 @@
// Copyright © 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 (
"github.com/spf13/cobra"
)
// validatorCredentialsCmd represents the validator credentials command
var validatorCredentialsCmd = &cobra.Command{
Use: "credentials",
Short: "Manage Ethereum consensus validator credentials",
Long: `Manage Ethereum consensus validator credentials.`,
}
func init() {
validatorCmd.AddCommand(validatorCredentialsCmd)
}
func validatorCredentialsFlags(cmd *cobra.Command) {
}

View File

@@ -0,0 +1,65 @@
// Copyright © 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
validatorcredentialsget "github.com/wealdtech/ethdo/cmd/validator/credentials/get"
)
var validatorCredentialsGetCmd = &cobra.Command{
Use: "get",
Short: "Obtain withdrawal credentials for an Ethereum consensus validator",
Long: `Obtain withdrawal credentials for an Ethereum consensus validator. For example:
ethdo validator credentials get --account=primary/validator
In quiet mode this will return 0 if the validator exists, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatorcredentialsget.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
validatorCredentialsCmd.AddCommand(validatorCredentialsGetCmd)
validatorCredentialsFlags(validatorCredentialsGetCmd)
validatorCredentialsGetCmd.Flags().String("account", "", "Account for which to fetch validator credentials")
validatorCredentialsGetCmd.Flags().String("index", "", "Validator index for which to fetch validator credentials")
validatorCredentialsGetCmd.Flags().String("pubkey", "", "Validator public key for which to fetch validator credentials")
}
func validatorCredentialsGetBindings() {
if err := viper.BindPFlag("account", validatorCredentialsGetCmd.Flags().Lookup("account")); err != nil {
panic(err)
}
if err := viper.BindPFlag("index", validatorCredentialsGetCmd.Flags().Lookup("index")); err != nil {
panic(err)
}
if err := viper.BindPFlag("pubkey", validatorCredentialsGetCmd.Flags().Lookup("pubkey")); err != nil {
panic(err)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -55,7 +55,7 @@ func init() {
validatorDepositDataCmd.Flags().String("withdrawaladdress", "", "Ethereum 1 address of the account to which the validator funds will be withdrawn")
validatorDepositDataCmd.Flags().String("depositvalue", "", "Value of the amount to be deposited")
validatorDepositDataCmd.Flags().Bool("raw", false, "Print raw deposit data transaction data")
validatorDepositDataCmd.Flags().String("forkversion", "", "Use a hard-coded fork version (default is to fetch it from the node)")
validatorDepositDataCmd.Flags().String("forkversion", "", "Use a hard-coded fork version (default is to use mainnet value)")
validatorDepositDataCmd.Flags().Bool("launchpad", false, "Print launchpad-compatible JSON")
}

View File

@@ -80,7 +80,7 @@ In quiet mode this will return 0 if the validator information can be obtained, o
pubKey, err := bestPublicKey(account)
if err == nil {
deposits, totalDeposited, err := graphData(network, pubKey.Marshal())
if err == nil {
if err == nil && deposits > 0 {
fmt.Printf("Number of deposits: %d\n", deposits)
fmt.Printf("Total deposited: %s\n", string2eth.GWeiToString(uint64(totalDeposited), true))
}
@@ -91,9 +91,14 @@ In quiet mode this will return 0 if the validator information can be obtained, o
os.Exit(_exitSuccess)
}
if validator.Status.IsPending() || validator.Status.HasActivated() {
fmt.Printf("Index: %d\n", validator.Index)
}
if verbose {
if validator.Status.IsPending() {
fmt.Printf("Activation eligibility epoch: %d\n", validator.Validator.ActivationEligibilityEpoch)
}
if validator.Status.HasActivated() {
fmt.Printf("Index: %d\n", validator.Index)
fmt.Printf("Activation epoch: %d\n", validator.Validator.ActivationEpoch)
}
fmt.Printf("Public key: %#x\n", validator.Validator.PublicKey)
@@ -148,7 +153,7 @@ func validatorInfoAccount(ctx context.Context, eth2Client eth2client.Service) (e
index,
})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator information.")
return nil, errors.Wrap(err, "failed to obtain validator information")
}
if len(validators) == 0 {
return nil, errors.New("unknown validator index")

61
cmd/validatoryield.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright © 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
validatoryield "github.com/wealdtech/ethdo/cmd/validator/yield"
)
var validatorYieldCmd = &cobra.Command{
Use: "yield",
Short: "Calculate yield for validators",
Long: `Calculate yield for validators. For example:
ethdo validator yield
It is important to understand the yield is both probabilistic and dependent on network conditions.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatoryield.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
res = strings.TrimRight(res, "\n")
fmt.Println(res)
return nil
},
}
func init() {
validatorCmd.AddCommand(validatorYieldCmd)
validatorFlags(validatorYieldCmd)
validatorYieldCmd.Flags().String("validators", "", "Number of active validators (default fetches from chain)")
validatorYieldCmd.Flags().Bool("json", false, "JSON output")
}
func validatorYieldBindings() {
if err := viper.BindPFlag("validators", validatorYieldCmd.Flags().Lookup("validators")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", validatorYieldCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
}

View File

@@ -24,7 +24,7 @@ import (
// ReleaseVersion is the release version of the codebase.
// Usually overridden by tag names when building binaries.
var ReleaseVersion = "local build (latest release 1.18.1)"
var ReleaseVersion = "local build (latest release 1.25.2)"
// versionCmd represents the version command
var versionCmd = &cobra.Command{

View File

@@ -35,11 +35,11 @@ import (
func TestInput(t *testing.T) {
require.NoError(t, e2types.InitBLS())
dir := os.TempDir()
datFile := filepath.Join(dir, "backup.dat")
err := ioutil.WriteFile(datFile, []byte("dummy"), 0600)
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)
// defer os.RemoveAll(dir)
datFile := filepath.Join(dir, "backup.dat")
require.NoError(t, ioutil.WriteFile(datFile, []byte("dummy"), 0600))
defer os.RemoveAll(dir)
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))

View File

@@ -37,10 +37,10 @@ func TestProcess(t *testing.T) {
"ed2166659f7b5412a169ec83627386bc6ff1a31e67735d405b2bf7cb122ad7ced35c87e42c8e8f7ba90b5899a94be506687a9c5b353af2a216018d9f1bf61745a5",
}
dir := os.TempDir()
datFile := filepath.Join(dir, "backup.dat")
err := ioutil.WriteFile(datFile, export, 0600)
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)
datFile := filepath.Join(dir, "backup.dat")
require.NoError(t, ioutil.WriteFile(datFile, export, 0600))
defer os.RemoveAll(dir)
tests := []struct {

View File

@@ -174,6 +174,13 @@ Public key: 0x99b1f1d84d76185466d86c34bde1101316afddae76217aa86cd066979b19858c2c
$ ethdo account import --account=Validators/123 --key=6dd12d588d1c05ba40e80880ac7e894aa20babdbf16da52eae26b3f267d68032 --passphrase="my account secret"
```
You can also import from an existing keystore such as those generated by the deposit CLI. For this you need the keystore and the keystore passphrase. For example:
```sh
$ ethdo account import --account=Validators/123 --keystore=/path/to/keystore.json --keystore-passphrase="the keystore secret" --passphrase="my account secret"
```
`--keystore` can either be the path to the keystore file, or the contents of the keystore file.
#### `info`
`ethdo account info` provides information about the given account. Options include:
@@ -330,6 +337,22 @@ Voluntary exits: 0
Chain commands focus on providing information about Ethereum 2 chains.
#### `eth1votes`
`ethdo chain eth1votes` obtains information about the votes for the next Ethereum 1 block to be incorporated in to the chain for deposits. Options include:
- `epoch` show the votes at the end of the given epoch
- `json` provide JSON output
```sh
$ ethdo chain eth1votes
Voting period: 6
Slots through period: 1000
Votes this period: 959
Leading vote is for block 0x0ae5716ac1906592dbfb243ccadf90191f706d6f8c925b4f2712d2e24687553a with 356 votes
```
Additional information is supplied when using `--verbose`
#### `info`
`ethdo chain info` obtains information about an Ethereum 2 chain.
@@ -349,6 +372,17 @@ Seconds per slot: 12
Slots per epoch: 32
```
#### `queues`
`ethdo chain queues` obtains the activation and exit queue lengths of an Ethereum chain from the node's point of view. Options include:
- `epoch` show the queue length at a given epoch
- `json` provide JSON output
```sh
$ ethdo chain queues
Activation queue: 14798
```
#### `status`
`ethdo chain status` obtains the status of an Ethereum 2 chain from the node's point of view. Options include:
@@ -407,6 +441,39 @@ Deposit commands focus on information about deposit data information in a JSON f
$ ethdo deposit verify --data=${HOME}/depositdata.json --withdrawalpubkey=0xad1868210a0cff7aff22633c003c503d4c199c8dcca13bba5b3232fc784d39d3855936e94ce184c3ce27bf15d4347695 --validatorpubkey=0xa951530887ae2494a8cc4f11cf186963b0051ac4f7942375585b9cf98324db1e532a67e521d0fcaab510edad1352394c --depositvalue=32Ether
```
### `epoch` comands
Epoch commands focus on information about a beacon chain epoch.
#### `summary`
`ethdo epoch summary` provides a summary of the given epoch. Options include:
- `epoch`: the epoch for which to provide a summary; defaults to last complete epoch
- `json`: provide JSON output
```sh
$ ethdo epoch summary
Epoch 380:
Proposals: 31/32 (96.88%)
Attestations: 1530/1572 (97.33%)
Sync committees: 13086/15872 (82.45%)
```
More detailed information can be obtained with the `--verbose` flag:
```sh
$ ethdo epoch summary --verbose
Epoch 380:
Proposals: 31/32 (96.88%)
Slot 12188 (28/32) validator 1518 not proposed or not included
Attestations: 1530/1572 (97.33%)
Slot 12160 committee 0 validator 292 failed to participate
Slot 12162 committee 0 validator 204 failed to participate
Slot 12163 committee 0 validator 297 failed to participate
Slot 12164 committee 0 validator 209 failed to participate
...
```
### `exit` comands
Exit commands focus on information about validator exits generated by the `ethdo validator exit` command.
@@ -511,14 +578,26 @@ $ ethdo synccommittee members
Validator commands focus on interaction with Ethereum 2 validators.
#### `credentials get`
`ethdo validator credentials get` provides information about the withdrawal credentials for the provided validator. Options include:
- `account` the account for which to obtain the withdrawal credentials (in format "wallet/account")
- `pubkey` the public key of the validator for which to obtain the withdrawal credentials
- `index` the index of the validator for which to obtain the withdrawal credentials
```sh
$ ethdo validator credentials get --account=Validators/1
```
#### `depositdata`
`ethdo validator depositdata` generates the data required to deposit one or more Ethereum 2 validators. Options include:
- `withdrawalaccount` specify the account to be used for the withdrawal credentials (if withdrawalpubkey is not supplied)
- `withdrawaladdress` specify the Ethereum execution address to be used for the withdrawal credentials (if withdrawalpubkey is not supplied)
- `withdrawalpubkey` specify the public key to be used for the withdrawal credentials (if withdrawalaccount is not supplied)
- `validatoraccount` specify the account to be used for the validator
- `depositvalue` specify the amount of the deposit
- `forkversion` specify the fork version for the deposit signature; this should not be included unless the deposit is being generated offline. Note that supplying an incorrect value could result in the loss of your deposit, so only supply this value if you are sure you know what you are doing
- `forkversion` specify the fork version for the deposit signature; this defaults to mainnet. Note that supplying an incorrect value could result in the loss of your deposit, so only supply this value if you are sure you know what you are doing. You can find the value for other chains by fetching the value supplied in "Genesis fork version" of the `ethdo chain info` command
- `raw` generate raw hex output that can be supplied as the data to an Ethereum 1 deposit transaction
#### `exit`
@@ -621,6 +700,37 @@ $ ethdo attester inclusion --account=Validators/1 --epoch=6484
Attestation included in block 207492 (inclusion delay 1)
```
#### `yield`
`ethdo validator yield` calculates the expected yield given the number of validators. Options include:
- `validators` use a specified number of validators rather than the current number of active validators
- `json` obtain detailed information in JSON format
```sh
$ ethdo validator yield
Yield: 4.64%
```
### `proposer` commands
Proposer commands focus on Ethereum 2 validators' actions as proposers.
#### `duties`
`ethdo proposer duties` provides information on the proposal duties for a given epoch. Options include:
- `epoch` the epoch in which to obtain the duties (defaults to current epoch)
- `json` obtain detailed information in JSON format
```sh
$ ethdo proposer duties --epoch=5
Epoch 5:
Slot 160: validator 8221
Slot 161: validator 11193
Slot 162: validator 4116
Slot 163: validator 631
...
```
## Maintainers
Jim McDonald: [@mcdee](https://github.com/mcdee).

3
go.mod
View File

@@ -22,6 +22,7 @@ require (
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388
github.com/rs/zerolog v1.26.1
github.com/shopspring/decimal v1.3.1
github.com/spf13/afero v1.8.0 // indirect
github.com/spf13/cobra v1.3.0
github.com/spf13/pflag v1.0.5
@@ -42,7 +43,7 @@ require (
github.com/wealdtech/go-eth2-wallet-store-s3 v1.10.0
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.0
github.com/wealdtech/go-eth2-wallet-types/v2 v2.9.0
github.com/wealdtech/go-string2eth v1.1.0
github.com/wealdtech/go-string2eth v1.2.0
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed // indirect
golang.org/x/text v0.3.7
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 // indirect

9
go.sum
View File

@@ -75,8 +75,6 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
github.com/attestantio/dirk v1.1.0 h1:hwMTYZkwj/Y0um3OD0LQxg2xSl4/5xqVWV2MRePE4ec=
github.com/attestantio/dirk v1.1.0/go.mod h1:2jkOw/XHjvIDdhDcmj+Z3kuVPpxMcQ6zxzzjSSv71PY=
github.com/attestantio/go-eth2-client v0.8.1/go.mod h1:kEK9iAAOBoADO5wEkd84FEOzjT1zXgVWveQsqn+uBGg=
github.com/attestantio/go-eth2-client v0.10.0 h1:nmOmzErfz4I2gEkucHKOaFwkbwD4i6JbIX38Z8Dm4Tc=
github.com/attestantio/go-eth2-client v0.10.0/go.mod h1:ijuXoXJCBFMexUYaBOl8PXfZKwYUFJy7cV03TMdw8Bo=
github.com/attestantio/go-eth2-client v0.11.0 h1:8/Jn5AAfd+4tOggLi+FvOv9/ORaObECv42ab7vK2FJc=
github.com/attestantio/go-eth2-client v0.11.0/go.mod h1:zXL/BxC0cBBhxj+tP7QG7t9Ufoa8GwQLdlbvZRd9+dM=
github.com/aws/aws-sdk-go v1.33.17/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
@@ -354,7 +352,6 @@ github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQ
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.11 h1:i2lw1Pm7Yi/4O6XCSyJWqEHI2MDw2FzUK6o/D21xn2A=
github.com/klauspost/cpuid/v2 v2.0.11/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
@@ -495,6 +492,8 @@ github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDN
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
@@ -589,6 +588,8 @@ github.com/wealdtech/go-indexer v1.0.0/go.mod h1:u1cjsbsOXsm5jzJDyLmZY7GsrdX8KYX
github.com/wealdtech/go-majordomo v1.0.1/go.mod h1:QoT4S1nUQwdQK19+CfepDwV+Yr7cc3dbF+6JFdQnIqY=
github.com/wealdtech/go-string2eth v1.1.0 h1:USJQmysUrBYYmZs7d45pMb90hRSyEwizP7lZaOZLDAw=
github.com/wealdtech/go-string2eth v1.1.0/go.mod h1:RUzsLjJtbZaJ/3UKn9kY19a/vCCUHtEWoUW3uiK6yGU=
github.com/wealdtech/go-string2eth v1.2.0 h1:C0E5p78tecZTsGccJc9r/kreFah4EfDs5uUPnS6XXMs=
github.com/wealdtech/go-string2eth v1.2.0/go.mod h1:RUzsLjJtbZaJ/3UKn9kY19a/vCCUHtEWoUW3uiK6yGU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -843,8 +844,6 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

View File

@@ -21,17 +21,41 @@ import (
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/auto"
"github.com/attestantio/go-eth2-client/http"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
// defaultBeaconNodeAddresses are default REST endpoint addresses for beacon nodes.
var defaultBeaconNodeAddresses = []string{
"localhost:5052", // Lighthouse, Nimbus
"localhost:5051", // Teku
"localhost:3500", // Prysm
}
// ConnectToBeaconNode connects to a beacon node at the given address.
func ConnectToBeaconNode(ctx context.Context, address string, timeout time.Duration, allowInsecure bool) (eth2client.Service, error) {
if timeout == 0 {
return nil, errors.New("no timeout specified")
}
if address != "" {
// We have an explicit address; use it.
return connectToBeaconNode(ctx, address, timeout, allowInsecure)
}
// Try the defaults.
for _, address := range defaultBeaconNodeAddresses {
client, err := connectToBeaconNode(ctx, address, timeout, allowInsecure)
if err == nil {
return client, nil
}
}
return nil, errors.New("failed to connect to any beacon node")
}
func connectToBeaconNode(ctx context.Context, address string, timeout time.Duration, allowInsecure bool) (eth2client.Service, error) {
if !strings.HasPrefix(address, "http") {
address = fmt.Sprintf("http://%s", address)
}
@@ -49,10 +73,10 @@ func ConnectToBeaconNode(ctx context.Context, address string, timeout time.Durat
fmt.Println("Connections to remote beacon nodes should be secure. This warning can be silenced with --allow-insecure-connections")
}
}
eth2Client, err := auto.New(ctx,
auto.WithLogLevel(zerolog.Disabled),
auto.WithAddress(address),
auto.WithTimeout(timeout),
eth2Client, err := http.New(ctx,
http.WithLogLevel(zerolog.Disabled),
http.WithAddress(address),
http.WithTimeout(timeout),
)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to beacon node")

42
util/epoch.go Normal file
View File

@@ -0,0 +1,42 @@
// Copyright © 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 util
import (
"context"
"strconv"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/services/chaintime"
)
// ParseEpoch parses input to calculate the desired epoch.
func ParseEpoch(ctx context.Context, chainTime chaintime.Service, epochStr string) (phase0.Epoch, error) {
switch epochStr {
case "", "current":
return chainTime.CurrentEpoch(), nil
case "last":
return chainTime.CurrentEpoch() - 1, nil
default:
val, err := strconv.ParseInt(epochStr, 10, 64)
if err != nil {
return 0, errors.Wrap(err, "failed to parse epoch")
}
if val >= 0 {
return phase0.Epoch(val), nil
}
return chainTime.CurrentEpoch() + phase0.Epoch(val), nil
}
}

View File

@@ -27,6 +27,7 @@ var networks = map[string]string{
"07b39f4fde4a38bace212b546dac87c58dfe3fdc": "Medalla",
"8c5fecdc472e27bc447696f431e425d02dd46a8c": "Pyrmont",
"ff50ed3d0ec03ac01d4c79aad74928bff48a7b2b": "Prater",
"6f22ffbc56eff051aecf839396dd1ed9ad6bba9d": "Ropsten",
}
// Network returns the name of the network., calculated from the deposit contract information.