mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16a94d726a | ||
|
|
9d00f6bafc | ||
|
|
d274ab3db0 | ||
|
|
ad3d8606fd | ||
|
|
30455e7c43 | ||
|
|
7eb2c68a19 | ||
|
|
2d96f7cb13 | ||
|
|
fd1e4a97bb | ||
|
|
be2270c543 | ||
|
|
0c36239b8b | ||
|
|
f78b2922ec | ||
|
|
bcf6ffdaf0 | ||
|
|
9fc184f6a1 | ||
|
|
c9a30a6e4b | ||
|
|
1ec6ddc914 | ||
|
|
2c96ef958e | ||
|
|
3c10131c45 | ||
|
|
fe0bfd4f87 | ||
|
|
290413f115 | ||
|
|
4aa6bef6a3 | ||
|
|
1b0f4e2803 | ||
|
|
301224748c | ||
|
|
1e15b836c2 | ||
|
|
1e709b7592 | ||
|
|
8744a85cb7 | ||
|
|
92ad77d8f5 | ||
|
|
2298640e4c | ||
|
|
5baef59672 | ||
|
|
e54e8affa7 | ||
|
|
97fa04a7b2 | ||
|
|
4977ee82e5 | ||
|
|
090680366c | ||
|
|
531c86847f | ||
|
|
446e437531 | ||
|
|
63d8ccf1a0 | ||
|
|
77abe0e158 | ||
|
|
547f8d9e71 | ||
|
|
e144217f25 | ||
|
|
d919810ce1 | ||
|
|
0bdf68edf6 | ||
|
|
b24341b7da | ||
|
|
384ee3dcaa | ||
|
|
3e8b1a6dad | ||
|
|
d2dec4a444 | ||
|
|
7e171bdb1e | ||
|
|
0cedf79a89 | ||
|
|
65ad1248ce | ||
|
|
e1180f97ce | ||
|
|
394b4a7cd2 | ||
|
|
fd574aae34 | ||
|
|
7fe503f51d | ||
|
|
6bfb0ef098 | ||
|
|
46c667d387 | ||
|
|
50f4a9cace | ||
|
|
cd20875744 | ||
|
|
84f682a0da | ||
|
|
6389b7dfbd | ||
|
|
0ef65b8bda | ||
|
|
4426c3279d | ||
|
|
883f9f834e | ||
|
|
e0fd3df9dd | ||
|
|
e6d3c67e39 | ||
|
|
6d0a0225c2 | ||
|
|
d60d8beb0b | ||
|
|
a657b3bc24 | ||
|
|
28b90414d2 | ||
|
|
879a20a7af | ||
|
|
3d49e091e5 | ||
|
|
3b51c67e7d | ||
|
|
1d559c167b | ||
|
|
824c53f6f2 | ||
|
|
50ffdcd97c | ||
|
|
0e79334863 | ||
|
|
c6c3143dd5 | ||
|
|
4d718f614c | ||
|
|
9a1db9b0a4 | ||
|
|
7ede620ce7 | ||
|
|
a41cc77c18 | ||
|
|
ad83006069 | ||
|
|
976d758cac | ||
|
|
1be72d9ea8 | ||
|
|
175c33a494 | ||
|
|
b712f70667 | ||
|
|
d4ef9d43b5 | ||
|
|
58de55b40f | ||
|
|
22dad263db | ||
|
|
81fa11ad45 | ||
|
|
f5c4551c0c | ||
|
|
a00d09e28f | ||
|
|
6bfd5677e6 | ||
|
|
dbe0a9d9f1 | ||
|
|
fa390ecdf7 | ||
|
|
f70abb2165 |
31
.github/workflows/golangci-lint.yml
vendored
31
.github/workflows/golangci-lint.yml
vendored
@@ -1,23 +1,22 @@
|
||||
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
|
||||
args: --timeout 5m
|
||||
|
||||
62
.github/workflows/release.yml
vendored
62
.github/workflows/release.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- 't*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -12,10 +13,9 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ^1.16
|
||||
id: go
|
||||
go-version: 1.17
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
@@ -36,9 +36,9 @@ jobs:
|
||||
echo "RELEASE_TAG=${RELEASE_TAG}" >> $GITHUB_ENV
|
||||
echo "::set-output name=RELEASE_TAG::${RELEASE_TAG}"
|
||||
# Ensure the release tag has expected format.
|
||||
echo ${RELEASE_TAG} | grep -q '^v' || exit 1
|
||||
echo ${RELEASE_TAG} | grep -q '^[vt]' || exit 1
|
||||
# Release version is same as release tag without leading 'v'.
|
||||
RELEASE_VERSION=$(echo ${GITHUB_REF} | sed -e 's!.*/v!!')
|
||||
RELEASE_VERSION=$(echo ${GITHUB_REF} | sed -e 's!.*/[vt]!!')
|
||||
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> $GITHUB_ENV
|
||||
echo "::set-output name=RELEASE_VERSION::${RELEASE_VERSION}"
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
- 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
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,5 +18,8 @@ coverage.html
|
||||
# Vim
|
||||
*.sw?
|
||||
|
||||
# Local JSON files
|
||||
*.json
|
||||
|
||||
# Local TODO
|
||||
TODO.md
|
||||
|
||||
75
CHANGELOG.md
75
CHANGELOG.md
@@ -1,3 +1,78 @@
|
||||
1.26.1
|
||||
- add ability to generate validator credentials change operations prior to the fork in which they become usable
|
||||
|
||||
1.26.0
|
||||
- add commands and documentation to set user validator credentials (not usable until capella)
|
||||
|
||||
1.25.3
|
||||
- add more information to "epoch summary"
|
||||
- add "validator summary"
|
||||
|
||||
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
|
||||
|
||||
1.18.0:
|
||||
- add "-ssz" option to "block info"
|
||||
- add "block analyze" command
|
||||
- support bellatrix
|
||||
|
||||
1.17.0:
|
||||
- add sync committee information to "chain time"
|
||||
- add details of vote success to "attester inclusion --verbose"
|
||||
- add "synccommittee inclusion"
|
||||
|
||||
1.15.1:
|
||||
- provide sync committee slots in "chain status"
|
||||
- clarify that --connection should be a URL
|
||||
|
||||
1.15.0:
|
||||
- add --period to "synccommittee members", can be "current", "next"
|
||||
- add "validator expectation"
|
||||
|
||||
1.14.0:
|
||||
- add "chain verify signedcontributionandproof"
|
||||
- show both block and body root in "block info"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.16-buster as builder
|
||||
FROM golang:1.17-bullseye as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -10,7 +10,9 @@ COPY . .
|
||||
|
||||
RUN go build
|
||||
|
||||
FROM debian:buster-slim
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
31
README.md
31
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)
|
||||
@@ -34,7 +35,7 @@ docker pull wealdtech/ethdo
|
||||
`ethdo` is a standard Go program which can be installed with:
|
||||
|
||||
```sh
|
||||
GO111MODULE=on go get github.com/wealdtech/ethdo
|
||||
go install github.com/wealdtech/ethdo@latest
|
||||
```
|
||||
|
||||
Note that `ethdo` requires at least version 1.13 of go to operate. The version of go can be found with `go version`.
|
||||
@@ -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,315 +0,0 @@
|
||||
// Copyright © 2019, 2020 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 accountcreate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/attestantio/dirk/testing/daemon"
|
||||
"github.com/attestantio/dirk/testing/resources"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
dirk "github.com/wealdtech/go-eth2-wallet-dirk"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
hd "github.com/wealdtech/go-eth2-wallet-hd/v2"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
testNDWallet, err := nd.CreateWallet(context.Background(),
|
||||
"Test",
|
||||
scratch.New(),
|
||||
keystorev4.New(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
testHDWallet, err := hd.CreateWallet(context.Background(),
|
||||
"Test",
|
||||
[]byte("pass"),
|
||||
scratch.New(),
|
||||
keystorev4.New(),
|
||||
[]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// #nosec G404
|
||||
port1 := uint32(12000 + rand.Intn(4000))
|
||||
// #nosec G404
|
||||
port2 := uint32(12000 + rand.Intn(4000))
|
||||
// #nosec G404
|
||||
port3 := uint32(12000 + rand.Intn(4000))
|
||||
peers := map[uint64]string{
|
||||
1: fmt.Sprintf("signer-test01:%d", port1),
|
||||
2: fmt.Sprintf("signer-test02:%d", port2),
|
||||
3: fmt.Sprintf("signer-test03:%d", port3),
|
||||
}
|
||||
_, path, err := daemon.New(context.Background(), "", 1, port1, peers)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(path)
|
||||
_, path, err = daemon.New(context.Background(), "", 2, port2, peers)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(path)
|
||||
_, path, err = daemon.New(context.Background(), "", 3, port3, peers)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(path)
|
||||
endpoints := []*dirk.Endpoint{
|
||||
dirk.NewEndpoint("signer-test01", port1),
|
||||
dirk.NewEndpoint("signer-test02", port2),
|
||||
dirk.NewEndpoint("signer-test03", port3),
|
||||
}
|
||||
credentials, err := credentialsFromCerts(context.Background(), resources.ClientTest01Crt, resources.ClientTest01Key, resources.CACrt)
|
||||
require.NoError(t, err)
|
||||
testDistributedWallet, err := dirk.OpenWallet(context.Background(), "Wallet 3", credentials, endpoints)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "WalletPassphraseIncorrect",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Good",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "bad",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
},
|
||||
err: "failed to unlock wallet: incorrect passphrase",
|
||||
},
|
||||
{
|
||||
name: "PassphraseMissing",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Good",
|
||||
passphrase: "",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
},
|
||||
err: "passphrase is required",
|
||||
},
|
||||
{
|
||||
name: "PassphraseWeak",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Good",
|
||||
passphrase: "poor",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
},
|
||||
err: "supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Good",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PathMalformed",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Pathed",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
path: "n/12381/3600/1/2/3",
|
||||
},
|
||||
err: "path does not match expected format m/…",
|
||||
},
|
||||
{
|
||||
name: "PathPassphraseMissing",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Pathed",
|
||||
passphrase: "",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
path: "m/12381/3600/1/2/3",
|
||||
},
|
||||
err: "passphrase is required",
|
||||
},
|
||||
{
|
||||
name: "PathNotSupported",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testNDWallet,
|
||||
accountName: "Pathed",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
path: "m/12381/3600/1/2/3",
|
||||
},
|
||||
err: "wallet does not support account creation with an explicit path",
|
||||
},
|
||||
{
|
||||
name: "GoodWithPath",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Pathed",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
path: "m/12381/3600/1/2/3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DistributedSigningThresholdZero",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testDistributedWallet,
|
||||
accountName: "Remote",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 3,
|
||||
signingThreshold: 0,
|
||||
},
|
||||
err: "signing threshold required",
|
||||
},
|
||||
{
|
||||
name: "DistributedSigningThresholdNotHalf",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testDistributedWallet,
|
||||
accountName: "Remote",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 3,
|
||||
signingThreshold: 1,
|
||||
},
|
||||
err: "signing threshold must be more than half the number of participants",
|
||||
},
|
||||
{
|
||||
name: "DistributedSigningThresholdTooHigh",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testDistributedWallet,
|
||||
accountName: "Remote",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 3,
|
||||
signingThreshold: 4,
|
||||
},
|
||||
err: "signing threshold cannot be higher than the number of participants",
|
||||
},
|
||||
{
|
||||
name: "DistributedNotSupported",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testNDWallet,
|
||||
accountName: "Remote",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 3,
|
||||
signingThreshold: 2,
|
||||
},
|
||||
err: "wallet does not support distributed account creation",
|
||||
},
|
||||
{
|
||||
name: "DistributedGood",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testDistributedWallet,
|
||||
accountName: "Remote",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 3,
|
||||
signingThreshold: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.dataIn.accountName, res.account.Name())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilData(t *testing.T) {
|
||||
_, err := processStandard(context.Background(), nil)
|
||||
require.EqualError(t, err, "no data")
|
||||
_, err = processPathed(context.Background(), nil)
|
||||
require.EqualError(t, err, "no data")
|
||||
_, err = processDistributed(context.Background(), nil)
|
||||
require.EqualError(t, err, "no data")
|
||||
}
|
||||
|
||||
func credentialsFromCerts(ctx context.Context, clientCert []byte, clientKey []byte, caCert []byte) (credentials.TransportCredentials, error) {
|
||||
clientPair, err := tls.X509KeyPair(clientCert, clientKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load client keypair")
|
||||
}
|
||||
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{clientPair},
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
if caCert != nil {
|
||||
cp := x509.NewCertPool()
|
||||
if !cp.AppendCertsFromPEM(caCert) {
|
||||
return nil, errors.New("failed to add CA certificate")
|
||||
}
|
||||
tlsCfg.RootCAs = cp
|
||||
}
|
||||
|
||||
return credentials.NewTLS(tlsCfg), nil
|
||||
}
|
||||
@@ -42,11 +42,13 @@ func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data.showPrivateKey {
|
||||
builder.WriteString(fmt.Sprintf("Private key: %#x\n", data.key.Marshal()))
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("Public key: %#x", data.key.PublicKey().Marshal()))
|
||||
if data.showWithdrawalCredentials {
|
||||
withdrawalCredentials := util.SHA256(data.key.PublicKey().Marshal())
|
||||
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
|
||||
builder.WriteString(fmt.Sprintf("\nWithdrawal credentials: %#x", withdrawalCredentials))
|
||||
builder.WriteString(fmt.Sprintf("Withdrawal credentials: %#x\n", withdrawalCredentials))
|
||||
}
|
||||
if !(data.showPrivateKey || data.showWithdrawalCredentials) {
|
||||
builder.WriteString(fmt.Sprintf("Public key: %#x\n", data.key.PublicKey().Marshal()))
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestOutput(t *testing.T) {
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
showPrivateKey: true,
|
||||
},
|
||||
needs: []string{"Public key", "Private key"},
|
||||
needs: []string{"Private key"},
|
||||
},
|
||||
{
|
||||
name: "WithdrawalCredentials",
|
||||
@@ -72,7 +72,7 @@ func TestOutput(t *testing.T) {
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
showWithdrawalCredentials: true,
|
||||
},
|
||||
needs: []string{"Public key", "Withdrawal credentials"},
|
||||
needs: []string{"Withdrawal credentials"},
|
||||
},
|
||||
{
|
||||
name: "All",
|
||||
@@ -81,7 +81,7 @@ func TestOutput(t *testing.T) {
|
||||
showPrivateKey: true,
|
||||
showWithdrawalCredentials: true,
|
||||
},
|
||||
needs: []string{"Public key", "Private key", "Withdrawal credentials"},
|
||||
needs: []string{"Private key", "Withdrawal credentials"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -15,57 +15,32 @@ package accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tyler-smith/go-bip39"
|
||||
util "github.com/wealdtech/go-eth2-util"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
// pathRegex is the regular expression that matches an HD path.
|
||||
var pathRegex = regexp.MustCompile("^m/[0-9]+/[0-9]+(/[0-9+])+")
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
// If there are more than 24 words we treat the additional characters as the passphrase.
|
||||
mnemonicParts := strings.Split(data.mnemonic, " ")
|
||||
mnemonicPassphrase := ""
|
||||
if len(mnemonicParts) > 24 {
|
||||
data.mnemonic = strings.Join(mnemonicParts[:24], " ")
|
||||
mnemonicPassphrase = strings.Join(mnemonicParts[24:], " ")
|
||||
}
|
||||
// Normalise the input.
|
||||
data.mnemonic = string(norm.NFKD.Bytes([]byte(data.mnemonic)))
|
||||
mnemonicPassphrase = string(norm.NFKD.Bytes([]byte(mnemonicPassphrase)))
|
||||
|
||||
if !bip39.IsMnemonicValid(data.mnemonic) {
|
||||
return nil, errors.New("mnemonic is invalid")
|
||||
}
|
||||
|
||||
// Create seed from mnemonic and passphrase.
|
||||
seed := bip39.NewSeed(data.mnemonic, mnemonicPassphrase)
|
||||
|
||||
// Ensure the path is valid.
|
||||
match := pathRegex.Match([]byte(data.path))
|
||||
if !match {
|
||||
return nil, errors.New("path does not match expected format m/…")
|
||||
}
|
||||
|
||||
// Derive private key from seed and path.
|
||||
key, err := util.PrivateKeyFromSeedAndPath(seed, data.path)
|
||||
account, err := util.ParseAccount(ctx, data.mnemonic, []string{data.path}, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate key")
|
||||
return nil, errors.Wrap(err, "failed to derive account")
|
||||
}
|
||||
|
||||
key, err := account.(e2wtypes.AccountPrivateKeyProvider).PrivateKey(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain account private key")
|
||||
}
|
||||
|
||||
results := &dataOut{
|
||||
showPrivateKey: data.showPrivateKey,
|
||||
showPrivateKey: data.showPrivateKey,
|
||||
showWithdrawalCredentials: data.showWithdrawalCredentials,
|
||||
key: key,
|
||||
key: key.(*e2types.BLSPrivateKey),
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestProcess(t *testing.T) {
|
||||
dataIn: &dataIn{
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
err: "mnemonic is invalid",
|
||||
err: "failed to derive account: no account specified",
|
||||
},
|
||||
{
|
||||
name: "MnemonicInvalid",
|
||||
@@ -48,14 +48,14 @@ func TestProcess(t *testing.T) {
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
err: "mnemonic is invalid",
|
||||
err: "failed to derive account: mnemonic is invalid",
|
||||
},
|
||||
{
|
||||
name: "PathMissing",
|
||||
dataIn: &dataIn{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
},
|
||||
err: "path does not match expected format m/…",
|
||||
err: "failed to derive account: path does not match expected format m/…",
|
||||
},
|
||||
{
|
||||
name: "PathInvalid",
|
||||
@@ -63,7 +63,7 @@ func TestProcess(t *testing.T) {
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
path: "n/12381/3600/0/0",
|
||||
},
|
||||
err: "path does not match expected format m/…",
|
||||
err: "failed to derive account: path does not match expected format m/…",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
|
||||
@@ -15,6 +15,7 @@ package accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -45,5 +46,5 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
return strings.TrimSuffix(results, "\n"), nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ func init() {
|
||||
accountFlags(accountCreateCmd)
|
||||
accountCreateCmd.Flags().Uint32("participants", 1, "Number of participants (1 for non-distributed accounts, >1 for distributed accounts)")
|
||||
accountCreateCmd.Flags().Uint32("signing-threshold", 1, "Signing threshold (1 for non-distributed accounts)")
|
||||
accountCreateCmd.Flags().String("path", "", "path of account (for hierarchical deterministic accounts)")
|
||||
}
|
||||
|
||||
func accountCreateBindings() {
|
||||
@@ -59,7 +58,4 @@ func accountCreateBindings() {
|
||||
if err := viper.BindPFlag("signing-threshold", accountCreateCmd.Flags().Lookup("signing-threshold")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("path", accountCreateCmd.Flags().Lookup("path")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,19 +47,11 @@ In quiet mode this will return 0 if the inputs can derive an account account, ot
|
||||
func init() {
|
||||
accountCmd.AddCommand(accountDeriveCmd)
|
||||
accountFlags(accountDeriveCmd)
|
||||
accountDeriveCmd.Flags().String("mnemonic", "", "mnemonic from which to derive the HD seed")
|
||||
accountDeriveCmd.Flags().String("path", "", "path from which to derive the account")
|
||||
accountDeriveCmd.Flags().Bool("show-private-key", false, "show private key for derived account")
|
||||
accountDeriveCmd.Flags().Bool("show-withdrawal-credentials", false, "show withdrawal credentials for derived account")
|
||||
}
|
||||
|
||||
func accountDeriveBindings() {
|
||||
if err := viper.BindPFlag("mnemonic", accountDeriveCmd.Flags().Lookup("mnemonic")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("path", accountDeriveCmd.Flags().Lookup("path")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("show-private-key", accountDeriveCmd.Flags().Lookup("show-private-key")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,17 +63,19 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Required data.
|
||||
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
|
||||
}
|
||||
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
|
||||
|
||||
// Epoch
|
||||
epoch := viper.GetInt64("epoch")
|
||||
if epoch == -1 {
|
||||
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
|
||||
}
|
||||
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
|
||||
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
|
||||
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
|
||||
"timeout": "5s",
|
||||
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@ package attesterinclusion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
@@ -34,9 +36,11 @@ type dataIn struct {
|
||||
slotsPerEpoch uint64
|
||||
// Operation.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
epoch spec.Epoch
|
||||
account string
|
||||
pubKey string
|
||||
index string
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
@@ -50,18 +54,24 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
data.verbose = viper.GetBool("verbose")
|
||||
data.debug = viper.GetBool("debug")
|
||||
|
||||
// Account or pubkey.
|
||||
if viper.GetString("account") == "" && viper.GetString("pubkey") == "" {
|
||||
return nil, errors.New("account or pubkey is required")
|
||||
}
|
||||
// Account.
|
||||
data.account = viper.GetString("account")
|
||||
|
||||
// PubKey.
|
||||
data.pubKey = viper.GetString("pubkey")
|
||||
|
||||
// ID.
|
||||
data.index = viper.GetString("index")
|
||||
|
||||
if viper.GetString("account") == "" && viper.GetString("index") == "" && viper.GetString("pubkey") == "" {
|
||||
return nil, errors.New("account, index or pubkey is required")
|
||||
}
|
||||
|
||||
// Ethereum 2 client.
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
@@ -84,6 +94,9 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
}
|
||||
}
|
||||
data.epoch = spec.Epoch(epoch)
|
||||
if data.debug {
|
||||
fmt.Printf("Epoch is %d\n", data.epoch)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -61,11 +61,11 @@ func TestInput(t *testing.T) {
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "AccountMissing",
|
||||
name: "IndexMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "account or pubkey is required",
|
||||
err: "account, index or pubkey is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
|
||||
"timeout": "5s",
|
||||
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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,8 +16,9 @@ package attesterinclusion
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -25,22 +26,67 @@ type dataOut struct {
|
||||
debug bool
|
||||
quiet bool
|
||||
verbose bool
|
||||
slot spec.Slot
|
||||
attestation *phase0.Attestation
|
||||
slot phase0.Slot
|
||||
attestationIndex uint64
|
||||
inclusionDelay spec.Slot
|
||||
inclusionDelay phase0.Slot
|
||||
found bool
|
||||
headCorrect bool
|
||||
headTimely bool
|
||||
sourceTimely bool
|
||||
targetCorrect bool
|
||||
targetTimely bool
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
buf := strings.Builder{}
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
return buf.String(), errors.New("no data")
|
||||
}
|
||||
|
||||
if !data.quiet {
|
||||
if data.found {
|
||||
return fmt.Sprintf("Attestation included in block %d, attestation %d (inclusion delay %d)", data.slot, data.attestationIndex, data.inclusionDelay), nil
|
||||
buf.WriteString("Attestation included in block ")
|
||||
buf.WriteString(fmt.Sprintf("%d", data.slot))
|
||||
buf.WriteString(", index ")
|
||||
buf.WriteString(fmt.Sprintf("%d", data.attestationIndex))
|
||||
if data.verbose {
|
||||
buf.WriteString("\nInclusion delay: ")
|
||||
buf.WriteString(fmt.Sprintf("%d", data.inclusionDelay))
|
||||
buf.WriteString("\nHead correct: ")
|
||||
if data.headCorrect {
|
||||
buf.WriteString("✓")
|
||||
} else {
|
||||
buf.WriteString("✕")
|
||||
}
|
||||
buf.WriteString("\nHead timely: ")
|
||||
if data.headTimely {
|
||||
buf.WriteString("✓")
|
||||
} else {
|
||||
buf.WriteString("✕")
|
||||
}
|
||||
buf.WriteString("\nSource timely: ")
|
||||
if data.sourceTimely {
|
||||
buf.WriteString("✓")
|
||||
} else {
|
||||
buf.WriteString("✕")
|
||||
}
|
||||
buf.WriteString("\nTarget correct: ")
|
||||
if data.targetCorrect {
|
||||
buf.WriteString("✓")
|
||||
} else {
|
||||
buf.WriteString("✕")
|
||||
}
|
||||
buf.WriteString("\nTarget timely: ")
|
||||
if data.targetTimely {
|
||||
buf.WriteString("✓")
|
||||
} else {
|
||||
buf.WriteString("✕")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buf.WriteString("Attestation not found")
|
||||
}
|
||||
return "Attestation not found", nil
|
||||
}
|
||||
return "", nil
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
@@ -44,7 +44,29 @@ func TestOutput(t *testing.T) {
|
||||
attestationIndex: 456,
|
||||
inclusionDelay: 7,
|
||||
},
|
||||
res: "Attestation included in block 123, attestation 456 (inclusion delay 7)",
|
||||
res: `Attestation included in block 123, index 456`,
|
||||
},
|
||||
{
|
||||
name: "Verbose",
|
||||
dataOut: &dataOut{
|
||||
verbose: true,
|
||||
found: true,
|
||||
slot: 123,
|
||||
attestationIndex: 456,
|
||||
inclusionDelay: 7,
|
||||
headCorrect: true,
|
||||
headTimely: false,
|
||||
sourceTimely: false,
|
||||
targetCorrect: true,
|
||||
targetTimely: true,
|
||||
},
|
||||
res: `Attestation included in block 123, index 456
|
||||
Inclusion delay: 7
|
||||
Head correct: ✓
|
||||
Head timely: ✕
|
||||
Source timely: ✕
|
||||
Target correct: ✓
|
||||
Target timely: ✓`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -14,17 +14,16 @@
|
||||
package attesterinclusion
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
@@ -32,34 +31,23 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
var account e2wtypes.Account
|
||||
var err error
|
||||
if data.account != "" {
|
||||
ctx, cancel := context.WithTimeout(ctx, data.timeout)
|
||||
defer cancel()
|
||||
_, account, err = util.WalletAndAccountFromPath(ctx, data.account)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain account")
|
||||
}
|
||||
} else {
|
||||
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.pubKey, "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", data.pubKey))
|
||||
}
|
||||
account, err = util.NewScratchAccount(nil, pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", data.pubKey))
|
||||
}
|
||||
|
||||
data.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithForkScheduleProvider(data.eth2Client.(eth2client.ForkScheduleProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
// Fetch validator
|
||||
pubKeys := make([]phase0.BLSPubKey, 1)
|
||||
pubKey, err := util.BestPublicKey(account)
|
||||
validatorIndex, err := util.ValidatorIndex(ctx, data.eth2Client, data.account, data.pubKey, data.index)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain public key for account")
|
||||
return nil, errors.Wrap(err, "failed to obtain validator index")
|
||||
}
|
||||
copy(pubKeys[0][:], pubKey.Marshal())
|
||||
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), pubKeys)
|
||||
|
||||
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), []phase0.ValidatorIndex{validatorIndex})
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to obtain validator information")
|
||||
}
|
||||
@@ -81,6 +69,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain duty for validator")
|
||||
}
|
||||
if data.debug {
|
||||
fmt.Printf("Duty is %s\n", duty.String())
|
||||
}
|
||||
|
||||
startSlot := duty.Slot + 1
|
||||
endSlot := startSlot + 32
|
||||
@@ -110,10 +101,32 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if attestation.Data.Slot == duty.Slot &&
|
||||
attestation.Data.Index == duty.CommitteeIndex &&
|
||||
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
|
||||
|
||||
headCorrect := false
|
||||
targetCorrect := false
|
||||
if data.verbose {
|
||||
headCorrect, err = calcHeadCorrect(ctx, data, attestation)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain head correct result")
|
||||
}
|
||||
targetCorrect, err = calcTargetCorrect(ctx, data, attestation)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain target correct result")
|
||||
}
|
||||
}
|
||||
results.found = true
|
||||
results.attestation = attestation
|
||||
results.slot = slot
|
||||
results.attestationIndex = uint64(i)
|
||||
results.inclusionDelay = slot - duty.Slot
|
||||
results.found = true
|
||||
results.sourceTimely = results.inclusionDelay <= 5 // sqrt(32)
|
||||
results.targetCorrect = targetCorrect
|
||||
results.targetTimely = targetCorrect && results.inclusionDelay <= 32
|
||||
results.headCorrect = headCorrect
|
||||
results.headTimely = headCorrect && results.inclusionDelay == 1
|
||||
if data.debug {
|
||||
fmt.Printf("Attestation is %s\n", attestation.String())
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
}
|
||||
@@ -121,6 +134,49 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
|
||||
slot := attestation.Data.Slot
|
||||
for {
|
||||
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
if header == nil {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
if !header.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
return bytes.Equal(header.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
|
||||
// Start with first slot of the target epoch.
|
||||
slot := data.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
|
||||
for {
|
||||
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
if header == nil {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
if !header.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
return bytes.Equal(header.Root[:], attestation.Data.Target.Root[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
|
||||
// Find the attesting slot for the given epoch.
|
||||
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []phase0.ValidatorIndex{validator.Index})
|
||||
|
||||
@@ -49,6 +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().String("index", "", "the index of the attester")
|
||||
}
|
||||
|
||||
func attesterInclusionBindings() {
|
||||
@@ -58,4 +59,7 @@ func attesterInclusionBindings() {
|
||||
if err := viper.BindPFlag("pubkey", attesterInclusionCmd.Flags().Lookup("pubkey")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("index", attesterInclusionCmd.Flags().Lookup("index")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
133
cmd/block/analyze/command.go
Normal file
133
cmd/block/analyze/command.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// 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 blockanalyze
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prysmaticlabs/go-bitfield"
|
||||
"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.
|
||||
blockID string
|
||||
stream bool
|
||||
jsonOutput bool
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
blocksProvider eth2client.SignedBeaconBlockProvider
|
||||
blockHeadersProvider eth2client.BeaconBlockHeadersProvider
|
||||
|
||||
// Constants.
|
||||
timelySourceWeight uint64
|
||||
timelyTargetWeight uint64
|
||||
timelyHeadWeight uint64
|
||||
syncRewardWeight uint64
|
||||
proposerWeight uint64
|
||||
weightDenominator uint64
|
||||
|
||||
// Processing.
|
||||
priorAttestations map[string]*attestationData
|
||||
// Head roots provides the root of the head slot at given slots.
|
||||
headRoots map[phase0.Slot]phase0.Root
|
||||
// Target roots provides the root of the target epoch at given slots.
|
||||
targetRoots map[phase0.Slot]phase0.Root
|
||||
|
||||
// Block info.
|
||||
// Map is slot -> committee index -> validator committee index -> votes.
|
||||
votes map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist
|
||||
|
||||
// Results.
|
||||
analysis *blockAnalysis
|
||||
}
|
||||
|
||||
type blockAnalysis struct {
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
Attestations []*attestationAnalysis `json:"attestations"`
|
||||
SyncCommitee *syncCommitteeAnalysis `json:"sync_committee"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
type attestationAnalysis struct {
|
||||
Head phase0.Root `json:"head"`
|
||||
Target phase0.Root `json:"target"`
|
||||
Distance int `json:"distance"`
|
||||
Duplicate *attestationData `json:"duplicate,omitempty"`
|
||||
NewVotes int `json:"new_votes"`
|
||||
Votes int `json:"votes"`
|
||||
PossibleVotes int `json:"possible_votes"`
|
||||
HeadCorrect bool `json:"head_correct"`
|
||||
HeadTimely bool `json:"head_timely"`
|
||||
SourceTimely bool `json:"source_timely"`
|
||||
TargetCorrect bool `json:"target_correct"`
|
||||
TargetTimely bool `json:"target_timely"`
|
||||
Score float64 `json:"score"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
type syncCommitteeAnalysis struct {
|
||||
Contributions int `json:"contributions"`
|
||||
PossibleContributions int `json:"possible_contributions"`
|
||||
Score float64 `json:"score"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
type attestationData struct {
|
||||
Block phase0.Slot `json:"block"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
priorAttestations: make(map[string]*attestationData),
|
||||
headRoots: make(map[phase0.Slot]phase0.Root),
|
||||
targetRoots: make(map[phase0.Slot]phase0.Root),
|
||||
votes: make(map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist),
|
||||
}
|
||||
|
||||
// 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.blockID = viper.GetString("blockid")
|
||||
c.stream = viper.GetBool("stream")
|
||||
c.jsonOutput = viper.GetBool("json")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
65
cmd/block/analyze/command_internal_test.go
Normal file
65
cmd/block/analyze/command_internal_test.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 blockanalyze
|
||||
|
||||
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{}{
|
||||
"blockid": "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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
157
cmd/block/analyze/output.go
Normal file
157
cmd/block/analyze/output.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// 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 blockanalyze
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type attestationAnalysisJSON struct {
|
||||
Head string `json:"head"`
|
||||
Target string `json:"target"`
|
||||
Distance int `json:"distance"`
|
||||
Duplicate *attestationData `json:"duplicate,omitempty"`
|
||||
NewVotes int `json:"new_votes"`
|
||||
Votes int `json:"votes"`
|
||||
PossibleVotes int `json:"possible_votes"`
|
||||
HeadCorrect bool `json:"head_correct"`
|
||||
HeadTimely bool `json:"head_timely"`
|
||||
SourceTimely bool `json:"source_timely"`
|
||||
TargetCorrect bool `json:"target_correct"`
|
||||
TargetTimely bool `json:"target_timely"`
|
||||
Score float64 `json:"score"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
func (a *attestationAnalysis) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(attestationAnalysisJSON{
|
||||
Head: fmt.Sprintf("%#x", a.Head),
|
||||
Target: fmt.Sprintf("%#x", a.Target),
|
||||
Distance: a.Distance,
|
||||
Duplicate: a.Duplicate,
|
||||
NewVotes: a.NewVotes,
|
||||
Votes: a.Votes,
|
||||
PossibleVotes: a.PossibleVotes,
|
||||
HeadCorrect: a.HeadCorrect,
|
||||
HeadTimely: a.HeadTimely,
|
||||
SourceTimely: a.SourceTimely,
|
||||
TargetCorrect: a.TargetCorrect,
|
||||
TargetTimely: a.TargetTimely,
|
||||
Score: a.Score,
|
||||
Value: a.Value,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *command) outputJSON(_ context.Context) (string, error) {
|
||||
data, err := json.Marshal(c.analysis)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (c *command) outputTxt(_ context.Context) (string, error) {
|
||||
builder := strings.Builder{}
|
||||
|
||||
for i, attestation := range c.analysis.Attestations {
|
||||
if c.verbose {
|
||||
builder.WriteString("Attestation ")
|
||||
builder.WriteString(fmt.Sprintf("%d", i))
|
||||
builder.WriteString(": ")
|
||||
builder.WriteString("distance ")
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.Distance))
|
||||
builder.WriteString(", ")
|
||||
|
||||
if attestation.Duplicate != nil {
|
||||
builder.WriteString("duplicate of attestation ")
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Index))
|
||||
builder.WriteString(" in block ")
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Block))
|
||||
builder.WriteString("\n")
|
||||
continue
|
||||
}
|
||||
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.NewVotes))
|
||||
builder.WriteString("/")
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.Votes))
|
||||
builder.WriteString("/")
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.PossibleVotes))
|
||||
builder.WriteString(" new/total/possible votes")
|
||||
if attestation.NewVotes == 0 {
|
||||
builder.WriteString("\n")
|
||||
continue
|
||||
} else {
|
||||
builder.WriteString(", ")
|
||||
}
|
||||
switch {
|
||||
case !attestation.HeadCorrect:
|
||||
builder.WriteString("head vote incorrect, ")
|
||||
case !attestation.HeadTimely:
|
||||
builder.WriteString("head vote correct but late, ")
|
||||
}
|
||||
|
||||
if !attestation.SourceTimely {
|
||||
builder.WriteString("source vote late, ")
|
||||
}
|
||||
|
||||
switch {
|
||||
case !attestation.TargetCorrect:
|
||||
builder.WriteString("target vote incorrect, ")
|
||||
case !attestation.TargetTimely:
|
||||
builder.WriteString("target vote correct but late, ")
|
||||
}
|
||||
|
||||
builder.WriteString("score ")
|
||||
builder.WriteString(fmt.Sprintf("%0.3f", attestation.Score))
|
||||
builder.WriteString(", value ")
|
||||
builder.WriteString(fmt.Sprintf("%0.3f", attestation.Value))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
if c.analysis.SyncCommitee.Contributions > 0 {
|
||||
if c.verbose {
|
||||
builder.WriteString("Sync committee contributions: ")
|
||||
builder.WriteString(fmt.Sprintf("%d", c.analysis.SyncCommitee.Contributions))
|
||||
builder.WriteString(" contributions, score ")
|
||||
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.SyncCommitee.Score))
|
||||
builder.WriteString(", value ")
|
||||
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.SyncCommitee.Value))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteString("Value for block ")
|
||||
builder.WriteString(fmt.Sprintf("%d", c.analysis.Slot))
|
||||
builder.WriteString(": ")
|
||||
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.Value))
|
||||
builder.WriteString("\n")
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
438
cmd/block/analyze/process.go
Normal file
438
cmd/block/analyze/process.go
Normal file
@@ -0,0 +1,438 @@
|
||||
// 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 blockanalyze
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prysmaticlabs/go-bitfield"
|
||||
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
|
||||
}
|
||||
|
||||
block, err := c.blocksProvider.SignedBeaconBlock(ctx, c.blockID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain beacon block")
|
||||
}
|
||||
if block == nil {
|
||||
return errors.New("empty beacon block")
|
||||
}
|
||||
|
||||
slot, err := block.Slot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.analysis = &blockAnalysis{
|
||||
Slot: slot,
|
||||
}
|
||||
|
||||
// Calculate how many parents we need to fetch.
|
||||
minSlot := slot
|
||||
for _, attestation := range attestations {
|
||||
if attestation.Data.Slot < minSlot {
|
||||
minSlot = attestation.Data.Slot
|
||||
}
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Printf("Need to fetch blocks to slot %d\n", minSlot)
|
||||
}
|
||||
|
||||
if err := c.fetchParents(ctx, block, minSlot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.analyze(ctx, block)
|
||||
}
|
||||
|
||||
func (c *command) analyze(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
|
||||
if err := c.analyzeAttestations(ctx, block); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.analyzeSyncCommittees(ctx, block); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) analyzeAttestations(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slot, err := block.Slot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.analysis.Attestations = make([]*attestationAnalysis, len(attestations))
|
||||
|
||||
blockVotes := make(map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist)
|
||||
for i, attestation := range attestations {
|
||||
if c.debug {
|
||||
fmt.Printf("Processing attestation %d\n", i)
|
||||
}
|
||||
analysis := &attestationAnalysis{
|
||||
Head: attestation.Data.BeaconBlockRoot,
|
||||
Target: attestation.Data.Target.Root,
|
||||
Distance: int(slot - attestation.Data.Slot),
|
||||
}
|
||||
|
||||
root, err := attestation.HashTreeRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
|
||||
analysis.Duplicate = info
|
||||
} else {
|
||||
data := attestation.Data
|
||||
_, exists := blockVotes[data.Slot]
|
||||
if !exists {
|
||||
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
|
||||
}
|
||||
_, exists = blockVotes[data.Slot][data.Index]
|
||||
if !exists {
|
||||
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
|
||||
}
|
||||
|
||||
// Count new votes.
|
||||
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
|
||||
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
|
||||
if attestation.AggregationBits.BitAt(j) {
|
||||
analysis.Votes++
|
||||
if blockVotes[data.Slot][data.Index].BitAt(j) {
|
||||
// Already attested to in this block; skip.
|
||||
continue
|
||||
}
|
||||
if c.votes[data.Slot][data.Index].BitAt(j) {
|
||||
// Already attested to in a previous block; skip.
|
||||
continue
|
||||
}
|
||||
analysis.NewVotes++
|
||||
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
|
||||
}
|
||||
}
|
||||
// Calculate head correct.
|
||||
var err error
|
||||
analysis.HeadCorrect, err = c.calcHeadCorrect(ctx, attestation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Calculate head timely.
|
||||
analysis.HeadTimely = attestation.Data.Slot == slot-1
|
||||
|
||||
// Calculate source timely.
|
||||
analysis.SourceTimely = attestation.Data.Slot >= slot-5
|
||||
|
||||
// Calculate target correct.
|
||||
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Calculate target timely.
|
||||
analysis.TargetTimely = attestation.Data.Slot >= slot-32
|
||||
}
|
||||
|
||||
// Calculate score and value.
|
||||
if analysis.TargetCorrect && analysis.TargetTimely {
|
||||
analysis.Score += float64(c.timelyTargetWeight) / float64(c.weightDenominator)
|
||||
}
|
||||
if analysis.SourceTimely {
|
||||
analysis.Score += float64(c.timelySourceWeight) / float64(c.weightDenominator)
|
||||
}
|
||||
if analysis.HeadCorrect && analysis.HeadTimely {
|
||||
analysis.Score += float64(c.timelyHeadWeight) / float64(c.weightDenominator)
|
||||
}
|
||||
analysis.Value = analysis.Score * float64(analysis.NewVotes)
|
||||
c.analysis.Value += analysis.Value
|
||||
|
||||
c.analysis.Attestations[i] = analysis
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedBeaconBlock, minSlot phase0.Slot) error {
|
||||
parentRoot, err := block.ParentRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Obtain the parent block.
|
||||
parentBlock, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%#x", parentRoot))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if parentBlock == nil {
|
||||
return fmt.Errorf("unable to obtain parent block %s", parentBlock)
|
||||
}
|
||||
|
||||
parentSlot, err := parentBlock.Slot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if parentSlot < minSlot {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.processParentBlock(ctx, parentBlock); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.fetchParents(ctx, parentBlock, minSlot)
|
||||
}
|
||||
|
||||
func (c *command) processParentBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slot, err := block.Slot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Printf("Processing block %d\n", slot)
|
||||
}
|
||||
|
||||
for i, attestation := range attestations {
|
||||
root, err := attestation.HashTreeRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
|
||||
Block: slot,
|
||||
Index: i,
|
||||
}
|
||||
|
||||
data := attestation.Data
|
||||
_, exists := c.votes[data.Slot]
|
||||
if !exists {
|
||||
c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
|
||||
}
|
||||
_, exists = c.votes[data.Slot][data.Index]
|
||||
if !exists {
|
||||
c.votes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
|
||||
}
|
||||
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
|
||||
if attestation.AggregationBits.BitAt(j) {
|
||||
c.votes[data.Slot][data.Index].SetBitAt(j, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// Obtain the number of active validators.
|
||||
var isProvider bool
|
||||
c.blocksProvider, isProvider = c.eth2Client.(eth2client.SignedBeaconBlockProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide signed beacon block information")
|
||||
}
|
||||
c.blockHeadersProvider, isProvider = c.eth2Client.(eth2client.BeaconBlockHeadersProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide beacon block header information")
|
||||
}
|
||||
|
||||
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["TIMELY_SOURCE_WEIGHT"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(14)
|
||||
}
|
||||
var ok bool
|
||||
c.timelySourceWeight, ok = tmp.(uint64)
|
||||
if !ok {
|
||||
return errors.New("TIMELY_SOURCE_WEIGHT of unexpected type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["TIMELY_TARGET_WEIGHT"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(26)
|
||||
}
|
||||
c.timelyTargetWeight, ok = tmp.(uint64)
|
||||
if !ok {
|
||||
return errors.New("TIMELY_TARGET_WEIGHT of unexpected type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["TIMELY_HEAD_WEIGHT"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(14)
|
||||
}
|
||||
c.timelyHeadWeight, ok = tmp.(uint64)
|
||||
if !ok {
|
||||
return errors.New("TIMELY_HEAD_WEIGHT of unexpected type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["SYNC_REWARD_WEIGHT"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(2)
|
||||
}
|
||||
c.syncRewardWeight, ok = tmp.(uint64)
|
||||
if !ok {
|
||||
return errors.New("SYNC_REWARD_WEIGHT of unexpected type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["PROPOSER_WEIGHT"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(8)
|
||||
}
|
||||
c.proposerWeight, ok = tmp.(uint64)
|
||||
if !ok {
|
||||
return errors.New("PROPOSER_WEIGHT of unexpected type")
|
||||
}
|
||||
|
||||
tmp, exists = spec["WEIGHT_DENOMINATOR"]
|
||||
if !exists {
|
||||
// Set a default value based on the Altair spec.
|
||||
tmp = uint64(64)
|
||||
}
|
||||
c.weightDenominator, ok = tmp.(uint64)
|
||||
if !ok {
|
||||
return errors.New("WEIGHT_DENOMINATOR of unexpected type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
|
||||
slot := attestation.Data.Slot
|
||||
root, exists := c.headRoots[slot]
|
||||
if !exists {
|
||||
for {
|
||||
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
if header == nil {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
if !header.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
c.headRoots[attestation.Data.Slot] = header.Root
|
||||
root = header.Root
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return bytes.Equal(root[:], attestation.Data.BeaconBlockRoot[:]), nil
|
||||
}
|
||||
|
||||
func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
|
||||
root, exists := c.targetRoots[attestation.Data.Slot]
|
||||
if !exists {
|
||||
// Start with first slot of the target epoch.
|
||||
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
|
||||
for {
|
||||
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
if header == nil {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
if !header.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
c.targetRoots[attestation.Data.Slot] = header.Root
|
||||
root = header.Root
|
||||
break
|
||||
}
|
||||
}
|
||||
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
|
||||
}
|
||||
|
||||
func (c *command) analyzeSyncCommittees(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
|
||||
c.analysis.SyncCommitee = &syncCommitteeAnalysis{}
|
||||
switch block.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
return nil
|
||||
case spec.DataVersionAltair:
|
||||
c.analysis.SyncCommitee.Contributions = int(block.Altair.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
|
||||
c.analysis.SyncCommitee.PossibleContributions = int(block.Altair.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
|
||||
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)
|
||||
}
|
||||
}
|
||||
63
cmd/block/analyze/process_internal_test.go
Normal file
63
cmd/block/analyze/process_internal_test.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 blockanalyze
|
||||
|
||||
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: "NoBlock",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "60s",
|
||||
"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\"}",
|
||||
},
|
||||
}
|
||||
|
||||
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/block/analyze/run.go
Normal file
50
cmd/block/analyze/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 blockanalyze
|
||||
|
||||
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,6 +32,7 @@ type dataIn struct {
|
||||
// Operation.
|
||||
eth2Client eth2client.Service
|
||||
jsonOutput bool
|
||||
sszOutput bool
|
||||
// Chain information.
|
||||
blockID string
|
||||
stream bool
|
||||
@@ -48,13 +49,14 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
data.verbose = viper.GetBool("verbose")
|
||||
data.debug = viper.GetBool("debug")
|
||||
data.jsonOutput = viper.GetBool("json")
|
||||
data.sszOutput = viper.GetBool("ssz")
|
||||
|
||||
data.stream = viper.GetBool("stream")
|
||||
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if viper.GetString("blockid") == "" {
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
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 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 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",
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prysmaticlabs/go-bitfield"
|
||||
@@ -75,6 +77,7 @@ 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", string(graffiti)))
|
||||
} else {
|
||||
@@ -111,13 +114,15 @@ func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service,
|
||||
if !exists {
|
||||
beaconCommittees, err := beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", att.Data.Slot))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain beacon committees")
|
||||
}
|
||||
for _, beaconCommittee := range beaconCommittees {
|
||||
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
|
||||
validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
// Failed to get it; create an empty committee to stop us continually attempting to re-fetch.
|
||||
validatorCommittees[att.Data.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
} else {
|
||||
for _, beaconCommittee := range beaconCommittees {
|
||||
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
|
||||
validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
}
|
||||
validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
|
||||
}
|
||||
validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
|
||||
}
|
||||
committees = validatorCommittees[att.Data.Slot]
|
||||
}
|
||||
@@ -125,7 +130,9 @@ func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service,
|
||||
res.WriteString(fmt.Sprintf(" Committee index: %d\n", att.Data.Index))
|
||||
res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()))
|
||||
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitlistToString(att.AggregationBits)))
|
||||
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index])))
|
||||
if _, exists := committees[att.Data.Index]; exists {
|
||||
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index])))
|
||||
}
|
||||
res.WriteString(fmt.Sprintf(" Slot: %d\n", att.Data.Slot))
|
||||
res.WriteString(fmt.Sprintf(" Beacon block root: %#x\n", att.Data.BeaconBlockRoot))
|
||||
res.WriteString(fmt.Sprintf(" Source epoch: %d\n", att.Data.Source.Epoch))
|
||||
@@ -263,6 +270,96 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
|
||||
if signedBlock == nil {
|
||||
return "", errors.New("no block supplied")
|
||||
}
|
||||
|
||||
body := signedBlock.Message.Body
|
||||
|
||||
res := strings.Builder{}
|
||||
|
||||
// General info.
|
||||
blockRoot, err := signedBlock.Message.HashTreeRoot()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain block root")
|
||||
}
|
||||
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to generate body root")
|
||||
}
|
||||
|
||||
tmp, err := outputBlockGeneral(ctx,
|
||||
data.verbose,
|
||||
signedBlock.Message.Slot,
|
||||
blockRoot,
|
||||
bodyRoot,
|
||||
signedBlock.Message.ParentRoot,
|
||||
signedBlock.Message.StateRoot,
|
||||
signedBlock.Message.Body.Graffiti[:],
|
||||
data.genesisTime,
|
||||
data.slotDuration,
|
||||
data.slotsPerEpoch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Eth1 data.
|
||||
if data.verbose {
|
||||
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
}
|
||||
|
||||
// Sync aggregate.
|
||||
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Attestations.
|
||||
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Attester slashings.
|
||||
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
|
||||
// Add verbose proposer slashings.
|
||||
|
||||
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Voluntary exits.
|
||||
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
tmp, err = outputBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputAltairBlockText(ctx context.Context, data *dataOut, signedBlock *altair.SignedBeaconBlock) (string, error) {
|
||||
if signedBlock == nil {
|
||||
return "", errors.New("no block supplied")
|
||||
@@ -289,7 +386,7 @@ func outputAltairBlockText(ctx context.Context, data *dataOut, signedBlock *alta
|
||||
bodyRoot,
|
||||
signedBlock.Message.ParentRoot,
|
||||
signedBlock.Message.StateRoot,
|
||||
signedBlock.Message.Body.Graffiti,
|
||||
signedBlock.Message.Body.Graffiti[:],
|
||||
data.genesisTime,
|
||||
data.slotDuration,
|
||||
data.slotsPerEpoch)
|
||||
@@ -372,7 +469,7 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
|
||||
bodyRoot,
|
||||
signedBlock.Message.ParentRoot,
|
||||
signedBlock.Message.StateRoot,
|
||||
signedBlock.Message.Body.Graffiti,
|
||||
signedBlock.Message.Body.Graffiti[:],
|
||||
data.genesisTime,
|
||||
data.slotDuration,
|
||||
data.slotsPerEpoch)
|
||||
@@ -423,6 +520,68 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBlockExecutionPayload(ctx context.Context,
|
||||
verbose bool,
|
||||
payload *bellatrix.ExecutionPayload,
|
||||
) (
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
if payload == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// If the block number is 0 then we're before the merge.
|
||||
if payload.BlockNumber == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
res := strings.Builder{}
|
||||
if !verbose {
|
||||
res.WriteString("Execution block number: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
|
||||
res.WriteString("Transactions: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
|
||||
} else {
|
||||
res.WriteString("Execution payload:\n")
|
||||
res.WriteString(" Execution block number: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
|
||||
baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas))
|
||||
for i := 0; i < 32; i++ {
|
||||
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
|
||||
}
|
||||
baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBEBytes)
|
||||
res.WriteString(" Base fee per gas: ")
|
||||
res.WriteString(string2eth.WeiToString(baseFeePerGas, true))
|
||||
res.WriteString("\n Block hash: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
|
||||
res.WriteString(" Parent hash: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
|
||||
res.WriteString(" Fee recipient: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
|
||||
res.WriteString(" Gas limit: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
|
||||
res.WriteString(" Gas used: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
|
||||
res.WriteString(" Timestamp: ")
|
||||
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
|
||||
res.WriteString(" Prev RANDAO: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
|
||||
res.WriteString(" Receipts root: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
|
||||
res.WriteString(" State root: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
|
||||
res.WriteString(" Extra data: ")
|
||||
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
|
||||
}
|
||||
|
||||
// intersection returns a list of items common between the two sets.
|
||||
func intersection(set1 []uint64, set2 []uint64) []phase0.ValidatorIndex {
|
||||
sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] })
|
||||
|
||||
@@ -23,11 +23,13 @@ import (
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var jsonOutput bool
|
||||
var sszOutput bool
|
||||
var results *dataOut
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
@@ -66,7 +68,11 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
case spec.DataVersionAltair:
|
||||
if err := outputAltairBlock(ctx, data.jsonOutput, signedBlock.Altair); err != nil {
|
||||
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Altair); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
case spec.DataVersionBellatrix:
|
||||
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Bellatrix); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
default:
|
||||
@@ -75,6 +81,10 @@ 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")
|
||||
@@ -94,28 +104,48 @@ 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 {
|
||||
fmt.Printf("Failed to obtain block: %v\n", err)
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to obtain block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if signedBlock == nil {
|
||||
fmt.Println("Empty beacon block")
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Println("Empty beacon block")
|
||||
}
|
||||
return
|
||||
}
|
||||
switch signedBlock.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
case spec.DataVersionAltair:
|
||||
if err := outputAltairBlock(context.Background(), jsonOutput, signedBlock.Altair); err != nil {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
if err := outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair); err != nil {
|
||||
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 && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
default:
|
||||
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
|
||||
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 {
|
||||
@@ -131,12 +161,12 @@ 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
|
||||
}
|
||||
|
||||
func outputAltairBlock(ctx context.Context, jsonOutput bool, signedBlock *altair.SignedBeaconBlock) error {
|
||||
func outputAltairBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *altair.SignedBeaconBlock) error {
|
||||
switch {
|
||||
case jsonOutput:
|
||||
data, err := json.Marshal(signedBlock)
|
||||
@@ -144,12 +174,42 @@ func outputAltairBlock(ctx context.Context, jsonOutput bool, signedBlock *altair
|
||||
return errors.Wrap(err, "failed to generate JSON")
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
case sszOutput:
|
||||
data, err := signedBlock.MarshalSSZ()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate SSZ")
|
||||
}
|
||||
fmt.Printf("%x\n", data)
|
||||
default:
|
||||
data, err := outputAltairBlockText(ctx, results, signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputBellatrixBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *bellatrix.SignedBeaconBlock) error {
|
||||
switch {
|
||||
case jsonOutput:
|
||||
data, err := json.Marshal(signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate JSON")
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
case sszOutput:
|
||||
data, err := signedBlock.MarshalSSZ()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate SSZ")
|
||||
}
|
||||
fmt.Printf("%x\n", data)
|
||||
default:
|
||||
data, err := outputBellatrixBlockText(ctx, results, signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestProcess(t *testing.T) {
|
||||
dataIn: &dataIn{
|
||||
eth2Client: eth2Client,
|
||||
},
|
||||
err: "failed to output block: failed to generate text: no block supplied",
|
||||
err: "empty beacon block",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
65
cmd/blockanalyze.go
Normal file
65
cmd/blockanalyze.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"
|
||||
blockanalyze "github.com/wealdtech/ethdo/cmd/block/analyze"
|
||||
)
|
||||
|
||||
var blockAnalyzeCmd = &cobra.Command{
|
||||
Use: "analyze",
|
||||
Short: "Analyze a block",
|
||||
Long: `Analyze the contents of a block. For example:
|
||||
|
||||
ethdo block analyze --blockid=12345
|
||||
|
||||
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := blockanalyze.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Print(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
blockCmd.AddCommand(blockAnalyzeCmd)
|
||||
blockFlags(blockAnalyzeCmd)
|
||||
blockAnalyzeCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
|
||||
blockAnalyzeCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
|
||||
blockAnalyzeCmd.Flags().Bool("json", false, "output data in JSON format")
|
||||
}
|
||||
|
||||
func blockAnalyzeBindings() {
|
||||
if err := viper.BindPFlag("blockid", blockAnalyzeCmd.Flags().Lookup("blockid")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("stream", blockAnalyzeCmd.Flags().Lookup("stream")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", blockAnalyzeCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ var blockInfoCmd = &cobra.Command{
|
||||
Short: "Obtain information about a block",
|
||||
Long: `Obtain information about a block. For example:
|
||||
|
||||
ethdo block info --slot=12345
|
||||
ethdo block info --blockid=12345
|
||||
|
||||
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -50,6 +50,7 @@ func init() {
|
||||
blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
|
||||
blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
|
||||
blockInfoCmd.Flags().Bool("json", false, "output data in JSON format")
|
||||
blockInfoCmd.Flags().Bool("ssz", false, "output data in SSZ format")
|
||||
}
|
||||
|
||||
func blockInfoBindings() {
|
||||
@@ -62,4 +63,7 @@ func blockInfoBindings() {
|
||||
if err := viper.BindPFlag("json", blockInfoCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("ssz", blockInfoCmd.Flags().Lookup("ssz")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
84
cmd/chain/eth1votes/command.go
Normal file
84
cmd/chain/eth1votes/command.go
Normal 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
|
||||
}
|
||||
64
cmd/chain/eth1votes/command_internal_test.go
Normal file
64
cmd/chain/eth1votes/command_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
125
cmd/chain/eth1votes/output.go
Normal file
125
cmd/chain/eth1votes/output.go
Normal 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
|
||||
}
|
||||
165
cmd/chain/eth1votes/process.go
Normal file
165
cmd/chain/eth1votes/process.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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 = state.Altair.Slot
|
||||
c.incumbent = state.Altair.ETH1Data
|
||||
c.eth1DataVotes = state.Altair.ETH1DataVotes
|
||||
case spec.DataVersionBellatrix:
|
||||
c.slot = state.Bellatrix.Slot
|
||||
c.incumbent = state.Bellatrix.ETH1Data
|
||||
c.eth1DataVotes = state.Bellatrix.ETH1DataVotes
|
||||
case spec.DataVersionCapella:
|
||||
c.slot = state.Capella.Slot
|
||||
c.incumbent = state.Capella.ETH1Data
|
||||
c.eth1DataVotes = state.Capella.ETH1DataVotes
|
||||
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
|
||||
}
|
||||
66
cmd/chain/eth1votes/process_internal_test.go
Normal file
66
cmd/chain/eth1votes/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 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/chain/eth1votes/run.go
Normal file
50
cmd/chain/eth1votes/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 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
|
||||
}
|
||||
72
cmd/chain/queues/command.go
Normal file
72
cmd/chain/queues/command.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"
|
||||
"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
|
||||
}
|
||||
64
cmd/chain/queues/command_internal_test.go
Normal file
64
cmd/chain/queues/command_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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{}{
|
||||
|
||||
@@ -28,12 +28,17 @@ type dataOut struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
|
||||
epoch spec.Epoch
|
||||
epochStart time.Time
|
||||
epochEnd time.Time
|
||||
slot spec.Slot
|
||||
slotStart time.Time
|
||||
slotEnd time.Time
|
||||
epoch spec.Epoch
|
||||
epochStart time.Time
|
||||
epochEnd time.Time
|
||||
slot spec.Slot
|
||||
slotStart time.Time
|
||||
slotEnd time.Time
|
||||
syncCommitteePeriod uint64
|
||||
syncCommitteePeriodStart time.Time
|
||||
syncCommitteePeriodEpochStart spec.Epoch
|
||||
syncCommitteePeriodEnd time.Time
|
||||
syncCommitteePeriodEpochEnd spec.Epoch
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
@@ -60,7 +65,18 @@ func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
builder.WriteString(data.slotStart.Format("2006-01-02 15:04:05"))
|
||||
builder.WriteString("\n Slot end ")
|
||||
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
|
||||
builder.WriteString("\n")
|
||||
|
||||
builder.WriteString("\nSync committee period ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriod))
|
||||
builder.WriteString("\n Sync committee period start ")
|
||||
builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05"))
|
||||
builder.WriteString(" (epoch ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart))
|
||||
builder.WriteString(")\n Sync committee period end ")
|
||||
builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05"))
|
||||
builder.WriteString(" (epoch ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd))
|
||||
builder.WriteString(")\n")
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
@@ -41,6 +41,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
|
||||
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
|
||||
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
|
||||
epochsPerSyncCommitteePeriod := config["EPOCHS_PER_SYNC_COMMITTEE_PERIOD"].(uint64)
|
||||
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain genesis data")
|
||||
@@ -59,13 +60,13 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse slot")
|
||||
}
|
||||
results.slot = spec.Slot(slot)
|
||||
results.slot = phase0.Slot(slot)
|
||||
case data.epoch != "":
|
||||
epoch, err := strconv.ParseUint(data.epoch, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse epoch")
|
||||
}
|
||||
results.slot = spec.Slot(epoch * slotsPerEpoch)
|
||||
results.slot = phase0.Slot(epoch * slotsPerEpoch)
|
||||
case data.timestamp != "":
|
||||
timestamp, err := time.Parse("2006-01-02T15:04:05-0700", data.timestamp)
|
||||
if err != nil {
|
||||
@@ -75,15 +76,20 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if secs < 0 {
|
||||
return nil, errors.New("timestamp prior to genesis")
|
||||
}
|
||||
results.slot = spec.Slot(secs / slotDuration)
|
||||
results.slot = phase0.Slot(secs / slotDuration)
|
||||
}
|
||||
|
||||
// Fill in the info given the slot.
|
||||
results.slotStart = genesis.GenesisTime.Add(time.Duration(results.slot) * slotDuration)
|
||||
results.slotEnd = genesis.GenesisTime.Add(time.Duration(results.slot+1) * slotDuration)
|
||||
results.epoch = spec.Epoch(uint64(results.slot) / slotsPerEpoch)
|
||||
results.epoch = phase0.Epoch(uint64(results.slot) / slotsPerEpoch)
|
||||
results.epochStart = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch)*slotsPerEpoch) * slotDuration)
|
||||
results.epochEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch+1)*slotsPerEpoch) * slotDuration)
|
||||
results.syncCommitteePeriod = uint64(results.epoch) / epochsPerSyncCommitteePeriod
|
||||
results.syncCommitteePeriodEpochStart = phase0.Epoch(results.syncCommitteePeriod * epochsPerSyncCommitteePeriod)
|
||||
results.syncCommitteePeriodEpochEnd = phase0.Epoch((results.syncCommitteePeriod+1)*epochsPerSyncCommitteePeriod) - 1
|
||||
results.syncCommitteePeriodStart = genesis.GenesisTime.Add(time.Duration(uint64(results.syncCommitteePeriodEpochStart)*slotsPerEpoch) * slotDuration)
|
||||
results.syncCommitteePeriodEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.syncCommitteePeriodEpochEnd)*slotsPerEpoch) * slotDuration)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ package chaintime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -46,11 +47,16 @@ func TestProcess(t *testing.T) {
|
||||
slot: "1",
|
||||
},
|
||||
expected: &dataOut{
|
||||
epochStart: time.Unix(1606824023, 0),
|
||||
epochEnd: time.Unix(1606824407, 0),
|
||||
slot: 1,
|
||||
slotStart: time.Unix(1606824035, 0),
|
||||
slotEnd: time.Unix(1606824047, 0),
|
||||
epochStart: time.Unix(1606824023, 0),
|
||||
epochEnd: time.Unix(1606824407, 0),
|
||||
slot: 1,
|
||||
slotStart: time.Unix(1606824035, 0),
|
||||
slotEnd: time.Unix(1606824047, 0),
|
||||
syncCommitteePeriod: 0,
|
||||
syncCommitteePeriodStart: time.Unix(1606824023, 0),
|
||||
syncCommitteePeriodEnd: time.Unix(1606921943, 0),
|
||||
syncCommitteePeriodEpochStart: 0,
|
||||
syncCommitteePeriodEpochEnd: 255,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -62,12 +68,17 @@ func TestProcess(t *testing.T) {
|
||||
epoch: "2",
|
||||
},
|
||||
expected: &dataOut{
|
||||
epoch: 2,
|
||||
epochStart: time.Unix(1606824791, 0),
|
||||
epochEnd: time.Unix(1606825175, 0),
|
||||
slot: 64,
|
||||
slotStart: time.Unix(1606824791, 0),
|
||||
slotEnd: time.Unix(1606824803, 0),
|
||||
epoch: 2,
|
||||
epochStart: time.Unix(1606824791, 0),
|
||||
epochEnd: time.Unix(1606825175, 0),
|
||||
slot: 64,
|
||||
slotStart: time.Unix(1606824791, 0),
|
||||
slotEnd: time.Unix(1606824803, 0),
|
||||
syncCommitteePeriod: 0,
|
||||
syncCommitteePeriodStart: time.Unix(1606824023, 0),
|
||||
syncCommitteePeriodEnd: time.Unix(1606921943, 0),
|
||||
syncCommitteePeriodEpochStart: 0,
|
||||
syncCommitteePeriodEpochEnd: 255,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -76,15 +87,20 @@ func TestProcess(t *testing.T) {
|
||||
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
timeout: 10 * time.Second,
|
||||
allowInsecureConnections: true,
|
||||
timestamp: "2021-01-01T00:00:00",
|
||||
timestamp: "2021-01-01T00:00:00+0000",
|
||||
},
|
||||
expected: &dataOut{
|
||||
epoch: 6862,
|
||||
epochStart: time.Unix(1609459031, 0),
|
||||
epochEnd: time.Unix(1609459415, 0),
|
||||
slot: 219598,
|
||||
slotStart: time.Unix(1609459199, 0),
|
||||
slotEnd: time.Unix(1609459211, 0),
|
||||
epoch: 6862,
|
||||
epochStart: time.Unix(1609459031, 0),
|
||||
epochEnd: time.Unix(1609459415, 0),
|
||||
slot: 219598,
|
||||
slotStart: time.Unix(1609459199, 0),
|
||||
slotEnd: time.Unix(1609459211, 0),
|
||||
syncCommitteePeriod: 26,
|
||||
syncCommitteePeriodStart: time.Unix(1609379927, 0),
|
||||
syncCommitteePeriodEnd: time.Unix(1609477847, 0),
|
||||
syncCommitteePeriodEpochStart: 6656,
|
||||
syncCommitteePeriodEpochEnd: 6911,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -96,6 +112,7 @@ func TestProcess(t *testing.T) {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
fmt.Printf("****** %d %d\n", res.syncCommitteePeriodStart.Unix(), res.syncCommitteePeriodEnd.Unix())
|
||||
require.Equal(t, test.expected, res)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
67
cmd/chaineth1votes.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -53,6 +53,11 @@ In quiet mode this will return 0 if the chain information can be obtained, other
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
if viper.GetBool("prepare-offline") {
|
||||
fmt.Printf("Add the following to your command to run it offline:\n --offline --genesis-validators=root=%#x --fork-version=%#x\n", genesis.GenesisValidatorsRoot, fork.CurrentVersion)
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
if genesis.GenesisTime.Unix() == 0 {
|
||||
fmt.Println("Genesis time: undefined")
|
||||
} else {
|
||||
@@ -84,4 +89,11 @@ In quiet mode this will return 0 if the chain information can be obtained, other
|
||||
func init() {
|
||||
chainCmd.AddCommand(chainInfoCmd)
|
||||
chainFlags(chainInfoCmd)
|
||||
chainInfoCmd.Flags().Bool("prepare-offline", false, "Provide information useful for offline commands")
|
||||
}
|
||||
|
||||
func chainInfoBindings() {
|
||||
if err := viper.BindPFlag("prepare-offline", chainInfoCmd.Flags().Lookup("prepare-offline")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -119,9 +119,11 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
|
||||
if epoch >= chainTime.AltairInitialEpoch() {
|
||||
period := chainTime.SlotToSyncCommitteePeriod(slot)
|
||||
periodStartEpoch := chainTime.FirstEpochOfSyncPeriod(period)
|
||||
periodStartSlot := chainTime.FirstSlotOfEpoch(periodStartEpoch)
|
||||
nextPeriod := period + 1
|
||||
nextPeriodStartEpoch := chainTime.FirstEpochOfSyncPeriod(nextPeriod)
|
||||
periodEndEpoch := nextPeriodStartEpoch - 1
|
||||
periodEndSlot := chainTime.FirstSlotOfEpoch(periodEndEpoch+1) - 1
|
||||
nextPeriodTimestamp := chainTime.StartOfEpoch(nextPeriodStartEpoch)
|
||||
|
||||
res.WriteString("Sync committee period: ")
|
||||
@@ -135,6 +137,12 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
|
||||
res.WriteString(fmt.Sprintf("%d", periodEndEpoch))
|
||||
res.WriteString("\n")
|
||||
|
||||
res.WriteString("Sync committee slots: ")
|
||||
res.WriteString(fmt.Sprintf("%d", periodStartSlot))
|
||||
res.WriteString("-")
|
||||
res.WriteString(fmt.Sprintf("%d", periodEndSlot))
|
||||
res.WriteString("\n")
|
||||
|
||||
res.WriteString("Time until next sync committee period: ")
|
||||
res.WriteString(time.Until(nextPeriodTimestamp).Round(time.Second).String())
|
||||
res.WriteString("\n")
|
||||
|
||||
@@ -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
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)
|
||||
}
|
||||
}
|
||||
111
cmd/epoch/summary/command.go
Normal file
111
cmd/epoch/summary/command.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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
|
||||
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
|
||||
|
||||
// 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"`
|
||||
HeadCorrectValidators int `json:"head_correct_validators"`
|
||||
HeadTimelyValidators int `json:"head_timely_validators"`
|
||||
SourceTimelyValidators int `json:"source_timely_validators"`
|
||||
TargetCorrectValidators int `json:"target_correct_validators"`
|
||||
TargetTimelyValidators int `json:"target_timely_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
|
||||
}
|
||||
64
cmd/epoch/summary/command_internal_test.go
Normal file
64
cmd/epoch/summary/command_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
110
cmd/epoch/summary/output.go
Normal file
110
cmd/epoch/summary/output.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// 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)))
|
||||
builder.WriteString(fmt.Sprintf("\n Source timely: %d/%d (%0.2f%%)", c.summary.SourceTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.SourceTimelyValidators)/float64(c.summary.ActiveValidators)))
|
||||
builder.WriteString(fmt.Sprintf("\n Target correct: %d/%d (%0.2f%%)", c.summary.TargetCorrectValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.TargetCorrectValidators)/float64(c.summary.ActiveValidators)))
|
||||
builder.WriteString(fmt.Sprintf("\n Target timely: %d/%d (%0.2f%%)", c.summary.TargetTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.TargetTimelyValidators)/float64(c.summary.ActiveValidators)))
|
||||
builder.WriteString(fmt.Sprintf("\n Head correct: %d/%d (%0.2f%%)", c.summary.HeadCorrectValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.HeadCorrectValidators)/float64(c.summary.ActiveValidators)))
|
||||
builder.WriteString(fmt.Sprintf("\n Head timely: %d/%d (%0.2f%%)", c.summary.HeadTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.HeadTimelyValidators)/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
|
||||
}
|
||||
370
cmd/epoch/summary/process.go
Normal file
370
cmd/epoch/summary/process.go
Normal file
@@ -0,0 +1,370 @@
|
||||
// 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) activeValidators(ctx context.Context) (map[phase0.ValidatorIndex]*apiv1.Validator, error) {
|
||||
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain validators for epoch")
|
||||
}
|
||||
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
|
||||
for _, validator := range validators {
|
||||
if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch {
|
||||
activeValidators[validator.Index] = validator
|
||||
}
|
||||
}
|
||||
|
||||
return activeValidators, nil
|
||||
}
|
||||
func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
activeValidators, err := c.activeValidators(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.summary.ActiveValidators = len(activeValidators)
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
var votes map[phase0.ValidatorIndex]struct{}
|
||||
var participations map[phase0.ValidatorIndex]*nonParticipatingValidator
|
||||
c.summary.ParticipatingValidators, c.summary.HeadCorrectValidators, c.summary.HeadTimelyValidators, c.summary.SourceTimelyValidators, c.summary.TargetCorrectValidators, c.summary.TargetTimelyValidators, votes, participations, err = c.processSlots(ctx, firstSlot, lastSlot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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) processSlots(ctx context.Context,
|
||||
firstSlot phase0.Slot,
|
||||
lastSlot phase0.Slot,
|
||||
) (
|
||||
int,
|
||||
int,
|
||||
int,
|
||||
int,
|
||||
int,
|
||||
int,
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]*nonParticipatingValidator,
|
||||
error,
|
||||
) {
|
||||
votes := make(map[phase0.ValidatorIndex]struct{})
|
||||
headCorrects := make(map[phase0.ValidatorIndex]struct{})
|
||||
headTimelys := make(map[phase0.ValidatorIndex]struct{})
|
||||
sourceTimelys := make(map[phase0.ValidatorIndex]struct{})
|
||||
targetCorrects := make(map[phase0.ValidatorIndex]struct{})
|
||||
targetTimelys := make(map[phase0.ValidatorIndex]struct{})
|
||||
allCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
participations := make(map[phase0.ValidatorIndex]*nonParticipatingValidator)
|
||||
|
||||
// Need a cache of beacon block headers to reduce lookup times.
|
||||
headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider)
|
||||
|
||||
for slot := firstSlot; slot <= lastSlot; slot++ {
|
||||
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
|
||||
}
|
||||
if block == nil {
|
||||
// No block at this slot; that's fine.
|
||||
continue
|
||||
}
|
||||
slot, err := block.Slot()
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, err
|
||||
}
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, err
|
||||
}
|
||||
for _, attestation := range attestations {
|
||||
if attestation.Data.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestation.Data.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
|
||||
// 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 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
|
||||
}
|
||||
for _, beaconCommittee := range beaconCommittees {
|
||||
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]
|
||||
|
||||
inclusionDistance := slot - attestation.Data.Slot
|
||||
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, err
|
||||
}
|
||||
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, err
|
||||
}
|
||||
|
||||
for i := uint64(0); i < attestation.AggregationBits.Len(); i++ {
|
||||
if attestation.AggregationBits.BitAt(i) {
|
||||
votes[committee[int(i)]] = struct{}{}
|
||||
if _, exists := headCorrects[committee[int(i)]]; !exists && headCorrect {
|
||||
headCorrects[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
if _, exists := headTimelys[committee[int(i)]]; !exists && headCorrect && inclusionDistance == 1 {
|
||||
headTimelys[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
if _, exists := sourceTimelys[committee[int(i)]]; !exists && inclusionDistance <= 5 {
|
||||
sourceTimelys[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
if _, exists := targetCorrects[committee[int(i)]]; !exists && targetCorrect {
|
||||
targetCorrects[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
if _, exists := targetTimelys[committee[int(i)]]; !exists && targetCorrect && inclusionDistance <= 32 {
|
||||
targetTimelys[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(votes),
|
||||
len(headCorrects),
|
||||
len(headTimelys),
|
||||
len(sourceTimelys),
|
||||
len(targetCorrects),
|
||||
len(targetTimelys),
|
||||
votes,
|
||||
participations,
|
||||
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")
|
||||
}
|
||||
c.beaconBlockHeadersProvider, isProvider = c.eth2Client.(eth2client.BeaconBlockHeadersProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide beacon block headers")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
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 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 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",
|
||||
|
||||
@@ -27,3 +27,6 @@ var proposerCmd = &cobra.Command{
|
||||
func init() {
|
||||
RootCmd.AddCommand(proposerCmd)
|
||||
}
|
||||
|
||||
func proposerFlags(cmd *cobra.Command) {
|
||||
}
|
||||
|
||||
77
cmd/proposer/duties/command.go
Normal file
77
cmd/proposer/duties/command.go
Normal 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
|
||||
}
|
||||
72
cmd/proposer/duties/command_internal_test.go
Normal file
72
cmd/proposer/duties/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 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
62
cmd/proposer/duties/output.go
Normal file
62
cmd/proposer/duties/output.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 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
|
||||
}
|
||||
70
cmd/proposer/duties/process.go
Normal file
70
cmd/proposer/duties/process.go
Normal 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
|
||||
}
|
||||
62
cmd/proposer/duties/process_internal_test.go
Normal file
62
cmd/proposer/duties/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 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/proposer/duties/run.go
Normal file
50
cmd/proposer/duties/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 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
61
cmd/proposerduties.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"
|
||||
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)
|
||||
}
|
||||
}
|
||||
87
cmd/root.go
87
cmd/root.go
@@ -64,7 +64,21 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
|
||||
quiet = viper.GetBool("quiet")
|
||||
verbose = viper.GetBool("verbose")
|
||||
debug = viper.GetBool("debug")
|
||||
// Command-specific bindings.
|
||||
|
||||
includeCommandBindings(cmd)
|
||||
|
||||
if quiet && verbose {
|
||||
fmt.Println("Cannot supply both quiet and verbose flags")
|
||||
}
|
||||
if quiet && debug {
|
||||
fmt.Println("Cannot supply both quiet and debug flags")
|
||||
}
|
||||
|
||||
return util.SetupStore()
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func includeCommandBindings(cmd *cobra.Command) {
|
||||
switch commandPath(cmd) {
|
||||
case "account/create":
|
||||
accountCreateBindings()
|
||||
@@ -76,20 +90,38 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
|
||||
attesterDutiesBindings()
|
||||
case "attester/inclusion":
|
||||
attesterInclusionBindings()
|
||||
case "block/analyze":
|
||||
blockAnalyzeBindings()
|
||||
case "block/info":
|
||||
blockInfoBindings()
|
||||
case "chain/eth1votes":
|
||||
chainEth1VotesBindings()
|
||||
case "chain/info":
|
||||
chainInfoBindings()
|
||||
case "chain/queues":
|
||||
chainQueuesBindings()
|
||||
case "chain/time":
|
||||
chainTimeBindings()
|
||||
case "chain/verify/signedcontributionandproof":
|
||||
chainVerifySignedContributionAndProofBindings(cmd)
|
||||
case "epoch/summary":
|
||||
epochSummaryBindings()
|
||||
case "exit/verify":
|
||||
exitVerifyBindings()
|
||||
case "node/events":
|
||||
nodeEventsBindings()
|
||||
case "proposer/duties":
|
||||
proposerDutiesBindings()
|
||||
case "slot/time":
|
||||
slotTimeBindings()
|
||||
case "synccommittee/inclusion":
|
||||
synccommitteeInclusionBindings()
|
||||
case "synccommittee/members":
|
||||
synccommitteeMembersBindings()
|
||||
case "validator/credentials/get":
|
||||
validatorCredentialsGetBindings()
|
||||
case "validator/credentials/set":
|
||||
validatorCredentialsSetBindings()
|
||||
case "validator/depositdata":
|
||||
validatorDepositdataBindings()
|
||||
case "validator/duties":
|
||||
@@ -100,6 +132,12 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
|
||||
validatorInfoBindings()
|
||||
case "validator/keycheck":
|
||||
validatorKeycheckBindings()
|
||||
case "validator/summary":
|
||||
validatorSummaryBindings()
|
||||
case "validator/yield":
|
||||
validatorYieldBindings()
|
||||
case "validator/expectation":
|
||||
validatorExpectationBindings()
|
||||
case "wallet/create":
|
||||
walletCreateBindings()
|
||||
case "wallet/import":
|
||||
@@ -109,15 +147,6 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
|
||||
case "wallet/sharedimport":
|
||||
walletSharedImportBindings()
|
||||
}
|
||||
|
||||
if quiet && verbose {
|
||||
fmt.Println("Cannot supply both quiet and verbose flags")
|
||||
}
|
||||
if quiet && debug {
|
||||
fmt.Println("Cannot supply both quiet and debug flags")
|
||||
}
|
||||
|
||||
return util.SetupStore()
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
@@ -146,10 +175,26 @@ func init() {
|
||||
if err := viper.BindPFlag("store", RootCmd.PersistentFlags().Lookup("store")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("account", "", "Account name (in format \"wallet/account\")")
|
||||
RootCmd.PersistentFlags().String("account", "", `Account name (in format "<wallet>/<account>")`)
|
||||
if err := viper.BindPFlag("account", RootCmd.PersistentFlags().Lookup("account")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("mnemonic", "", "Mnemonic to provide access to an account")
|
||||
if err := viper.BindPFlag("mnemonic", RootCmd.PersistentFlags().Lookup("mnemonic")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("path", "", "Hierarchical derivation path used with mnemonic to provide access to an account")
|
||||
if err := viper.BindPFlag("path", RootCmd.PersistentFlags().Lookup("path")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("private-key", "", "Private key to provide access to an account")
|
||||
if err := viper.BindPFlag("private-key", RootCmd.PersistentFlags().Lookup("private-key")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("public-key", "", "public key to provide access to an account")
|
||||
if err := viper.BindPFlag("public-key", RootCmd.PersistentFlags().Lookup("public-key")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("basedir", "", "Base directory for filesystem wallets")
|
||||
if err := viper.BindPFlag("basedir", RootCmd.PersistentFlags().Lookup("basedir")); err != nil {
|
||||
panic(err)
|
||||
@@ -199,7 +244,7 @@ func init() {
|
||||
if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("connection", "localhost:4000", "connection to an Ethereum 2 node")
|
||||
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)
|
||||
}
|
||||
@@ -352,24 +397,6 @@ func walletAndAccountFromPath(ctx context.Context, path string) (e2wtypes.Wallet
|
||||
return wallet, account, nil
|
||||
}
|
||||
|
||||
// bestPublicKey returns the best public key for operations.
|
||||
// It prefers the composite public key if present, otherwise the public key.
|
||||
func bestPublicKey(account e2wtypes.Account) (e2types.PublicKey, error) {
|
||||
var pubKey e2types.PublicKey
|
||||
publicKeyProvider, isCompositePublicKeyProvider := account.(e2wtypes.AccountCompositePublicKeyProvider)
|
||||
if isCompositePublicKeyProvider {
|
||||
pubKey = publicKeyProvider.CompositePublicKey()
|
||||
} else {
|
||||
publicKeyProvider, isPublicKeyProvider := account.(e2wtypes.AccountPublicKeyProvider)
|
||||
if isPublicKeyProvider {
|
||||
pubKey = publicKeyProvider.PublicKey()
|
||||
} else {
|
||||
return nil, errors.New("account does not provide a public key")
|
||||
}
|
||||
}
|
||||
return pubKey, nil
|
||||
}
|
||||
|
||||
// remotesToEndpoints generates endpoints from remote addresses.
|
||||
func remotesToEndpoints(remotes []string) ([]*dirk.Endpoint, error) {
|
||||
endpoints := make([]*dirk.Endpoint, 0)
|
||||
|
||||
@@ -54,7 +54,7 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
|
||||
"timeout": "5s",
|
||||
"slot": "1",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestOutput(t *testing.T) {
|
||||
dataOut: &dataOut{
|
||||
startTime: time.Unix(1606824023, 0),
|
||||
},
|
||||
res: "2020-12-01 12:00:23 +0000 GMT",
|
||||
res: "2020-12-01 12:00:23 +0000 UTC",
|
||||
},
|
||||
{
|
||||
name: "Verbose",
|
||||
@@ -46,7 +46,7 @@ func TestOutput(t *testing.T) {
|
||||
endTime: time.Unix(1606824035, 0),
|
||||
verbose: true,
|
||||
},
|
||||
res: "2020-12-01 12:00:23 +0000 GMT - 2020-12-01 12:00:35 +0000 GMT",
|
||||
res: "2020-12-01 12:00:23 +0000 UTC - 2020-12-01 12:00:35 +0000 UTC",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
81
cmd/synccommittee/inclusion/command.go
Normal file
81
cmd/synccommittee/inclusion/command.go
Normal 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 inclusion
|
||||
|
||||
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
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Input.
|
||||
account string
|
||||
pubKey string
|
||||
index string
|
||||
epoch int64
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
|
||||
// Output.
|
||||
inCommittee bool
|
||||
committeeIndex uint64
|
||||
inclusions []int
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
// Connection.
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
// Validator.
|
||||
c.account = viper.GetString("account")
|
||||
c.pubKey = viper.GetString("pubkey")
|
||||
c.index = viper.GetString("index")
|
||||
if c.account == "" && c.pubKey == "" && c.index == "" {
|
||||
return nil, errors.New("account, pubkey or index required")
|
||||
}
|
||||
|
||||
// Epoch.
|
||||
c.epoch = viper.GetInt64("epoch")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
74
cmd/synccommittee/inclusion/command_internal_test.go
Normal file
74
cmd/synccommittee/inclusion/command_internal_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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 inclusion
|
||||
|
||||
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: "NoValidator",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "account, pubkey or index required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"validators": "1",
|
||||
"timeout": "5s",
|
||||
"index": "1",
|
||||
"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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
81
cmd/synccommittee/inclusion/output.go
Normal file
81
cmd/synccommittee/inclusion/output.go
Normal 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 inclusion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
|
||||
if c.verbose {
|
||||
builder.WriteString("Epoch: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", c.epoch))
|
||||
}
|
||||
|
||||
if !c.inCommittee {
|
||||
builder.WriteString("Validator not in sync committee")
|
||||
} else {
|
||||
if c.verbose {
|
||||
builder.WriteString("Validator sync committee index ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", c.committeeIndex))
|
||||
}
|
||||
|
||||
noBlock := 0
|
||||
included := 0
|
||||
missed := 0
|
||||
for _, inclusion := range c.inclusions {
|
||||
switch inclusion {
|
||||
case 0:
|
||||
noBlock++
|
||||
case 1:
|
||||
included++
|
||||
case 2:
|
||||
missed++
|
||||
}
|
||||
}
|
||||
builder.WriteString("Expected: ")
|
||||
builder.WriteString(fmt.Sprintf("%d", len(c.inclusions)))
|
||||
builder.WriteString("\nIncluded: ")
|
||||
builder.WriteString(fmt.Sprintf("%d", included))
|
||||
builder.WriteString("\nMissed: ")
|
||||
builder.WriteString(fmt.Sprintf("%d", missed))
|
||||
builder.WriteString("\nNo block: ")
|
||||
builder.WriteString(fmt.Sprintf("%d", noBlock))
|
||||
|
||||
builder.WriteString("\nPer-slot result: ")
|
||||
for i, inclusion := range c.inclusions {
|
||||
switch inclusion {
|
||||
case 0:
|
||||
builder.WriteString("-")
|
||||
case 1:
|
||||
builder.WriteString("✓")
|
||||
case 2:
|
||||
builder.WriteString("✕")
|
||||
}
|
||||
if i%8 == 7 && i != len(c.inclusions)-1 {
|
||||
builder.WriteString(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
130
cmd/synccommittee/inclusion/process.go
Normal file
130
cmd/synccommittee/inclusion/process.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// 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 inclusion
|
||||
|
||||
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.
|
||||
if err := c.setup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
firstSlot, lastSlot := c.calculateSlots(ctx)
|
||||
|
||||
validatorIndex, err := util.ValidatorIndex(ctx, c.eth2Client, c.account, c.pubKey, c.index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
syncCommittee, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", phase0.Epoch(c.epoch))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain sync committee information")
|
||||
}
|
||||
|
||||
if syncCommittee == nil {
|
||||
return errors.New("no sync committee returned")
|
||||
}
|
||||
|
||||
for i := range syncCommittee.Validators {
|
||||
if syncCommittee.Validators[i] == validatorIndex {
|
||||
c.inCommittee = true
|
||||
c.committeeIndex = uint64(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if c.inCommittee {
|
||||
// This validator is in the sync committee. Check blocks to see where it has been included.
|
||||
c.inclusions = make([]int, 0)
|
||||
if lastSlot > c.chainTime.CurrentSlot() {
|
||||
lastSlot = c.chainTime.CurrentSlot()
|
||||
}
|
||||
for slot := firstSlot; slot < lastSlot; slot++ {
|
||||
block, err := c.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if block == nil {
|
||||
c.inclusions = append(c.inclusions, 0)
|
||||
continue
|
||||
}
|
||||
var aggregate *altair.SyncAggregate
|
||||
switch block.Version {
|
||||
case spec.DataVersionAltair:
|
||||
aggregate = block.Altair.Message.Body.SyncAggregate
|
||||
if aggregate.SyncCommitteeBits.BitAt(c.committeeIndex) {
|
||||
c.inclusions = append(c.inclusions, 1)
|
||||
} else {
|
||||
c.inclusions = append(c.inclusions, 2)
|
||||
}
|
||||
case spec.DataVersionBellatrix:
|
||||
aggregate = block.Bellatrix.Message.Body.SyncAggregate
|
||||
if aggregate.SyncCommitteeBits.BitAt(c.committeeIndex) {
|
||||
c.inclusions = append(c.inclusions, 1)
|
||||
} else {
|
||||
c.inclusions = append(c.inclusions, 2)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unhandled block version %v", block.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 err
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) calculateSlots(ctx context.Context) (phase0.Slot, phase0.Slot) {
|
||||
var firstSlot phase0.Slot
|
||||
var lastSlot phase0.Slot
|
||||
if c.epoch == -1 {
|
||||
c.epoch = int64(c.chainTime.CurrentEpoch()) - 1
|
||||
}
|
||||
firstSlot = c.chainTime.FirstSlotOfEpoch(phase0.Epoch(c.epoch))
|
||||
lastSlot = c.chainTime.FirstSlotOfEpoch(phase0.Epoch(c.epoch) + 1)
|
||||
|
||||
return firstSlot, lastSlot
|
||||
}
|
||||
80
cmd/synccommittee/inclusion/process_internal_test.go
Normal file
80
cmd/synccommittee/inclusion/process_internal_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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 inclusion
|
||||
|
||||
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: "MissingConnection",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"index": "1",
|
||||
},
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
{
|
||||
name: "InvalidConnection",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"index": "1",
|
||||
"connection": "invalid",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://invalid/eth/v1/beacon/genesis\": dial tcp: lookup invalid: no such host",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"index": "1",
|
||||
"epoch": "-1",
|
||||
"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/synccommittee/inclusion/run.go
Normal file
50
cmd/synccommittee/inclusion/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 inclusion
|
||||
|
||||
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
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
@@ -35,7 +34,8 @@ type dataIn struct {
|
||||
// Operation.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
epoch spec.Epoch
|
||||
epoch int64
|
||||
period string
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
@@ -53,7 +53,7 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Chain time.
|
||||
@@ -67,12 +67,8 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
}
|
||||
|
||||
// Epoch
|
||||
epoch := viper.GetInt64("epoch")
|
||||
if epoch == -1 {
|
||||
data.epoch = data.chainTime.CurrentEpoch()
|
||||
} else {
|
||||
data.epoch = spec.Epoch(epoch)
|
||||
}
|
||||
data.epoch = viper.GetInt64("epoch")
|
||||
data.period = viper.GetString("period")
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -65,7 +65,15 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,10 @@ package members
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -26,11 +28,12 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
if data.epoch < data.chainTime.AltairInitialEpoch() {
|
||||
return nil, errors.New("not an Altair epoch")
|
||||
epoch, err := calculateEpoch(ctx, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
syncCommittee, err := data.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommittee(ctx, fmt.Sprintf("%d", data.chainTime.FirstSlotOfEpoch(data.epoch)))
|
||||
syncCommittee, err := data.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", epoch)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain sync committee information")
|
||||
}
|
||||
@@ -48,3 +51,27 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func calculateEpoch(ctx context.Context, data *dataIn) (phase0.Epoch, error) {
|
||||
var epoch phase0.Epoch
|
||||
if data.epoch != -1 {
|
||||
epoch = phase0.Epoch(data.epoch)
|
||||
} else {
|
||||
switch strings.ToLower(data.period) {
|
||||
case "", "current":
|
||||
epoch = data.chainTime.CurrentEpoch()
|
||||
case "next":
|
||||
period := data.chainTime.SlotToSyncCommitteePeriod(data.chainTime.CurrentSlot())
|
||||
nextPeriod := period + 1
|
||||
epoch = data.chainTime.FirstEpochOfSyncPeriod(nextPeriod)
|
||||
default:
|
||||
return 0, fmt.Errorf("period %s not known", data.period)
|
||||
}
|
||||
}
|
||||
|
||||
if data.debug {
|
||||
fmt.Printf("epoch is %d\n", epoch)
|
||||
}
|
||||
|
||||
return epoch, nil
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestProcess(t *testing.T) {
|
||||
dataIn: &dataIn{
|
||||
eth2Client: eth2Client,
|
||||
chainTime: chainTime,
|
||||
epoch: 61650,
|
||||
epoch: -1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
67
cmd/synccommitteeinclusion.go
Normal file
67
cmd/synccommitteeinclusion.go
Normal 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"
|
||||
synccommitteeinclusion "github.com/wealdtech/ethdo/cmd/synccommittee/inclusion"
|
||||
)
|
||||
|
||||
var synccommitteeInclusionCmd = &cobra.Command{
|
||||
Use: "inclusion",
|
||||
Short: "Obtain sync committee inclusion data for a validator",
|
||||
Long: `Obtain sync committee inclusion data for a validator. For example:
|
||||
|
||||
ethdo synccommittee inclusion --epoch=12345 --index=11111
|
||||
|
||||
In quiet mode this will return 0 if the validator was in the sync committee, otherwise 1.
|
||||
|
||||
epoch can be a specific epoch; If not supplied all slots for the current sync committee period will be provided`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := synccommitteeinclusion.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
synccommitteeCmd.AddCommand(synccommitteeInclusionCmd)
|
||||
synccommitteeFlags(synccommitteeInclusionCmd)
|
||||
synccommitteeInclusionCmd.Flags().Int64("epoch", -1, "the epoch for which to fetch sync committee inclusion")
|
||||
synccommitteeInclusionCmd.Flags().String("pubkey", "", "validator public key for sync committee")
|
||||
synccommitteeInclusionCmd.Flags().String("index", "", "validator index for sync committee")
|
||||
}
|
||||
|
||||
func synccommitteeInclusionBindings() {
|
||||
if err := viper.BindPFlag("epoch", synccommitteeInclusionCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("pubkey", synccommitteeInclusionCmd.Flags().Lookup("pubkey")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("index", synccommitteeInclusionCmd.Flags().Lookup("index")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,9 @@ var synccommitteeMembersCmd = &cobra.Command{
|
||||
|
||||
ethdo synccommittee members --epoch=12345
|
||||
|
||||
In quiet mode this will return 0 if the synccommittee members are found, otherwise 1.`,
|
||||
In quiet mode this will return 0 if the synccommittee members are found, otherwise 1.
|
||||
|
||||
epoch can be a specific epoch. period can be 'current' for the current sync period or 'next' for the next sync period`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := synccommitteemembers.Run(cmd)
|
||||
if err != nil {
|
||||
@@ -48,10 +50,14 @@ func init() {
|
||||
synccommitteeCmd.AddCommand(synccommitteeMembersCmd)
|
||||
synccommitteeFlags(synccommitteeMembersCmd)
|
||||
synccommitteeMembersCmd.Flags().Int64("epoch", -1, "the epoch for which to fetch sync committees")
|
||||
synccommitteeMembersCmd.Flags().String("period", "", "the sync committee period for which to fetch sync committees ('current', 'next')")
|
||||
}
|
||||
|
||||
func synccommitteeMembersBindings() {
|
||||
if err := viper.BindPFlag("epoch", synccommitteeMembersCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("period", synccommitteeMembersCmd.Flags().Lookup("period")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +30,6 @@ func init() {
|
||||
|
||||
func validatorFlags(cmd *cobra.Command) {
|
||||
}
|
||||
|
||||
func validatorBindings() {
|
||||
}
|
||||
|
||||
69
cmd/validator/credentials/get/command.go
Normal file
69
cmd/validator/credentials/get/command.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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.
|
||||
validator string
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Data access.
|
||||
consensusClient eth2client.Service
|
||||
validatorsProvider eth2client.ValidatorsProvider
|
||||
|
||||
// Output.
|
||||
validatorInfo *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")
|
||||
|
||||
if viper.GetString("validator") == "" {
|
||||
return nil, errors.New("validator is required")
|
||||
}
|
||||
c.validator = viper.GetString("validator")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
83
cmd/validator/credentials/get/command_internal_test.go
Normal file
83
cmd/validator/credentials/get/command_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user