mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bfb0ef098 | ||
|
|
46c667d387 | ||
|
|
50f4a9cace | ||
|
|
cd20875744 | ||
|
|
84f682a0da | ||
|
|
6389b7dfbd | ||
|
|
0ef65b8bda | ||
|
|
4426c3279d | ||
|
|
883f9f834e | ||
|
|
e0fd3df9dd |
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
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
|
||||
version: v1.45
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
56
.github/workflows/release.yml
vendored
56
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,3 +1,16 @@
|
||||
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"
|
||||
|
||||
|
||||
29
README.md
29
README.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
75
cmd/chain/queues/command.go
Normal file
75
cmd/chain/queues/command.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// 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")
|
||||
}
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
72
cmd/chain/queues/command_internal_test.go
Normal file
72
cmd/chain/queues/command_internal_test.go
Normal 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"
|
||||
"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: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"data": "{}",
|
||||
},
|
||||
err: "connection 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
63
cmd/chain/queues/output.go
Normal file
63
cmd/chain/queues/output.go
Normal 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
|
||||
}
|
||||
82
cmd/chain/queues/process.go
Normal file
82
cmd/chain/queues/process.go
Normal 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
|
||||
}
|
||||
66
cmd/chain/queues/process_internal_test.go
Normal file
66
cmd/chain/queues/process_internal_test.go
Normal 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
50
cmd/chain/queues/run.go
Normal 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
|
||||
}
|
||||
61
cmd/chainqueues.go
Normal file
61
cmd/chainqueues.go
Normal 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)
|
||||
}
|
||||
}
|
||||
40
cmd/epoch.go
Normal file
40
cmd/epoch.go
Normal 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)
|
||||
}
|
||||
}
|
||||
97
cmd/epoch/summary/command.go
Normal file
97
cmd/epoch/summary/command.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
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
|
||||
}
|
||||
71
cmd/epoch/summary/command_internal_test.go
Normal file
71
cmd/epoch/summary/command_internal_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "connection 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
107
cmd/epoch/summary/output.go
Normal file
107
cmd/epoch/summary/output.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// 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
|
||||
if c.verbose {
|
||||
for _, proposal := range c.summary.Proposals {
|
||||
builder.WriteString(" Slot ")
|
||||
builder.WriteString(fmt.Sprintf("%d (%d/%d):\n", proposal.Slot, uint64(proposal.Slot)%uint64(len(c.summary.Proposals)), len(c.summary.Proposals)))
|
||||
builder.WriteString(" Proposer: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", proposal.Proposer))
|
||||
builder.WriteString(" Proposed: ")
|
||||
if proposal.Block {
|
||||
proposedBlocks++
|
||||
builder.WriteString("✓\n")
|
||||
} else {
|
||||
builder.WriteString("✕\n")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
missedProposals := make([]string, 0, len(c.summary.Proposals))
|
||||
for _, proposal := range c.summary.Proposals {
|
||||
if !proposal.Block {
|
||||
missedProposals = append(missedProposals, fmt.Sprintf(" Slot %d (validator %d)\n", proposal.Slot, proposal.Proposer))
|
||||
} else {
|
||||
proposedBlocks++
|
||||
}
|
||||
}
|
||||
if len(missedProposals) > 0 {
|
||||
builder.WriteString(" Missed proposals:\n")
|
||||
for _, missedProposal := range missedProposals {
|
||||
builder.WriteString(missedProposal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.verbose {
|
||||
for _, syncCommittee := range c.summary.SyncCommittee {
|
||||
builder.WriteString(" Sync committee validator ")
|
||||
builder.WriteString(fmt.Sprintf("%d:\n", syncCommittee.Index))
|
||||
builder.WriteString(" Chances: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", proposedBlocks))
|
||||
builder.WriteString(" Included: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", proposedBlocks-syncCommittee.Missed))
|
||||
builder.WriteString(" Inclusion %: ")
|
||||
builder.WriteString(fmt.Sprintf("%0.2f\n", 100.0*float64(proposedBlocks-syncCommittee.Missed)/float64(proposedBlocks)))
|
||||
}
|
||||
} else {
|
||||
missedSyncCommittees := make([]string, 0, len(c.summary.SyncCommittee))
|
||||
for _, syncCommittee := range c.summary.SyncCommittee {
|
||||
missedPct := 100.0 * float64(syncCommittee.Missed) / float64(proposedBlocks)
|
||||
missedSyncCommittees = append(missedSyncCommittees, fmt.Sprintf(" %d (%0.2f%%) by validator %d\n", syncCommittee.Missed, missedPct, syncCommittee.Index))
|
||||
}
|
||||
if len(missedSyncCommittees) > 0 {
|
||||
builder.WriteString(" Missed sync committees (excluding missed blocks):\n")
|
||||
for _, missedSyncCommittee := range missedSyncCommittees {
|
||||
builder.WriteString(missedSyncCommittee)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
174
cmd/epoch/summary/process.go
Normal file
174
cmd/epoch/summary/process.go
Normal file
@@ -0,0 +1,174 @@
|
||||
// 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"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
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.
|
||||
// Do in future.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
|
||||
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.New("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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
62
cmd/epoch/summary/process_internal_test.go
Normal file
62
cmd/epoch/summary/process_internal_test.go
Normal 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
50
cmd/epoch/summary/run.go
Normal 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
58
cmd/epochsummary.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -93,10 +93,14 @@ func includeCommandBindings(cmd *cobra.Command) {
|
||||
blockAnalyzeBindings()
|
||||
case "block/info":
|
||||
blockInfoBindings()
|
||||
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":
|
||||
@@ -107,6 +111,8 @@ func includeCommandBindings(cmd *cobra.Command) {
|
||||
synccommitteeInclusionBindings()
|
||||
case "synccommittee/members":
|
||||
synccommitteeMembersBindings()
|
||||
case "validator/credentials/get":
|
||||
validatorCredentialsGetBindings()
|
||||
case "validator/depositdata":
|
||||
validatorDepositdataBindings()
|
||||
case "validator/duties":
|
||||
@@ -209,7 +215,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)
|
||||
}
|
||||
|
||||
89
cmd/validator/credentials/get/command.go
Normal file
89
cmd/validator/credentials/get/command.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// 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")
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
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
|
||||
}
|
||||
91
cmd/validator/credentials/get/command_internal_test.go
Normal file
91
cmd/validator/credentials/get/command_internal_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// 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: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"index": "1",
|
||||
},
|
||||
err: "connection 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
44
cmd/validator/credentials/get/output.go
Normal file
44
cmd/validator/credentials/get/output.go
Normal 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
|
||||
}
|
||||
139
cmd/validator/credentials/get/process.go
Normal file
139
cmd/validator/credentials/get/process.go
Normal 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
|
||||
}
|
||||
50
cmd/validator/credentials/get/run.go
Normal file
50
cmd/validator/credentials/get/run.go
Normal 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
|
||||
}
|
||||
32
cmd/validatorcredentials.go
Normal file
32
cmd/validatorcredentials.go
Normal 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 consensu validator credentials",
|
||||
Long: `Manage Ethereum consensu validator credentials.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
validatorCmd.AddCommand(validatorCredentialsCmd)
|
||||
}
|
||||
|
||||
func validatorCredentialsFlags(cmd *cobra.Command) {
|
||||
}
|
||||
65
cmd/validatorcredentialsget.go
Normal file
65
cmd/validatorcredentialsget.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,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")
|
||||
|
||||
@@ -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.2)"
|
||||
var ReleaseVersion = "local build (latest release 1.21.0)"
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
|
||||
@@ -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:
|
||||
@@ -349,6 +356,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 +425,37 @@ 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 1406:
|
||||
Slot 44992 (0/32):
|
||||
Proposer: 31501
|
||||
Proposed: ✓
|
||||
Slot 44993 (1/32):
|
||||
Proposer: 9302
|
||||
Proposed: ✓
|
||||
...
|
||||
Sync committee validator 71248:
|
||||
Chances: 29
|
||||
Included: 7
|
||||
Inclusion %: 24.14
|
||||
Sync committee validator 87371:
|
||||
Chances: 29
|
||||
Included: 0
|
||||
Inclusion %: 0.00
|
||||
...
|
||||
```
|
||||
|
||||
### `exit` comands
|
||||
|
||||
Exit commands focus on information about validator exits generated by the `ethdo validator exit` command.
|
||||
@@ -511,6 +560,17 @@ $ 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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
42
util/epoch.go
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user