mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
446 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b450e96dde | ||
|
|
30d7f6989a | ||
|
|
00ea75e5c8 | ||
|
|
34647927ab | ||
|
|
44cfb68e2c | ||
|
|
70df67e6ab | ||
|
|
08fb16ff9e | ||
|
|
903ecc8581 | ||
|
|
93f4f6d68c | ||
|
|
1eee5a1349 | ||
|
|
9c4e9bcb2f | ||
|
|
83a950e5d1 | ||
|
|
b2c0ae3fa2 | ||
|
|
3059aa270f | ||
|
|
8b7aab7180 | ||
|
|
c46483586d | ||
|
|
e4f0b934d7 | ||
|
|
25a5bd917f | ||
|
|
889a884f6e | ||
|
|
1c23926295 | ||
|
|
992a969eaf | ||
|
|
58d8587471 | ||
|
|
b89154ada3 | ||
|
|
b9fff0dbde | ||
|
|
0238130895 | ||
|
|
60b6ce44f1 | ||
|
|
24486f0175 | ||
|
|
503bf9a996 | ||
|
|
1d14a57204 | ||
|
|
842d603524 | ||
|
|
c3471240a5 | ||
|
|
7b4ea7e27e | ||
|
|
4d3bd966e0 | ||
|
|
0488b13ba1 | ||
|
|
f8a611d63d | ||
|
|
772f07f8ec | ||
|
|
f6e23d803b | ||
|
|
86e872294d | ||
|
|
d7d9c66052 | ||
|
|
3e173f141e | ||
|
|
5d95e93b76 | ||
|
|
005eed1360 | ||
|
|
34b752edcc | ||
|
|
f17fe2f5cb | ||
|
|
8322353af5 | ||
|
|
7d0e607f96 | ||
|
|
fcafa037f8 | ||
|
|
8e1c8c5300 | ||
|
|
5af1476bc3 | ||
|
|
24a4220804 | ||
|
|
fa1d4d60fa | ||
|
|
7d00d1261f | ||
|
|
ac85a9539b | ||
|
|
099e434f43 | ||
|
|
5bab79bd79 | ||
|
|
1defa3b121 | ||
|
|
6cb7b034aa | ||
|
|
bd9659d71f | ||
|
|
68ca31e034 | ||
|
|
a37a5f4af3 | ||
|
|
47cf033feb | ||
|
|
72a9390f97 | ||
|
|
0276a72de6 | ||
|
|
fd0a89c258 | ||
|
|
1ac505f0bd | ||
|
|
b4124b7a27 | ||
|
|
f3ae2baf8f | ||
|
|
8aaf16ab3f | ||
|
|
61179045dd | ||
|
|
e50b4f015b | ||
|
|
6e522d6b5a | ||
|
|
d467a8ef9c | ||
|
|
d51683426c | ||
|
|
4879443e51 | ||
|
|
cf9d5718a3 | ||
|
|
7e09068d30 | ||
|
|
2362a1a058 | ||
|
|
80373ae237 | ||
|
|
bd2eea3a60 | ||
|
|
a23650a5ae | ||
|
|
327df13fba | ||
|
|
eaa0a19711 | ||
|
|
8aba766208 | ||
|
|
dd68af4884 | ||
|
|
564228ff00 | ||
|
|
697b6d4230 | ||
|
|
c548058190 | ||
|
|
daa2ac7b6e | ||
|
|
3b8a98bb83 | ||
|
|
f1021387b3 | ||
|
|
cd27401514 | ||
|
|
faf3c8afa4 | ||
|
|
6ad8e7afe4 | ||
|
|
1cffa7051d | ||
|
|
0e089bc8ff | ||
|
|
c6f90a69af | ||
|
|
58c3a7e279 | ||
|
|
cd1bf4dcfc | ||
|
|
0a24f4ffe6 | ||
|
|
77dd02a8f1 | ||
|
|
f4c99ac2b1 | ||
|
|
062b968055 | ||
|
|
8083dd7eeb | ||
|
|
841ba8e8f0 | ||
|
|
32a34fb9c1 | ||
|
|
e356e9e1b7 | ||
|
|
82f5200296 | ||
|
|
36ddb1a2fe | ||
|
|
d4791ac8bd | ||
|
|
fff6469748 | ||
|
|
b11f9cf3a3 | ||
|
|
9d50a72270 | ||
|
|
9191cac389 | ||
|
|
f1a1d4f64c | ||
|
|
1f1ddc1719 | ||
|
|
92d964f092 | ||
|
|
972e35f7d7 | ||
|
|
5f4d7a389d | ||
|
|
696ba64238 | ||
|
|
aeb8142b27 | ||
|
|
a1ea298983 | ||
|
|
1c23e7cc54 | ||
|
|
b53e42aeb7 | ||
|
|
fa325e20f8 | ||
|
|
0ce563470f | ||
|
|
abd3567d05 | ||
|
|
399d0eaa64 | ||
|
|
655b9cbbc8 | ||
|
|
50c22818cb | ||
|
|
4991787b9d | ||
|
|
b652d2e083 | ||
|
|
ef58db3307 | ||
|
|
a95851dc9e | ||
|
|
ac7e0985fb | ||
|
|
7837165d46 | ||
|
|
46020396e4 | ||
|
|
105de0a139 | ||
|
|
a7da10360e | ||
|
|
d5351cbe7f | ||
|
|
874839754c | ||
|
|
4db0cdae3a | ||
|
|
6f0f3e4c91 | ||
|
|
713bbdd60c | ||
|
|
46ca70a615 | ||
|
|
2f24bb7884 | ||
|
|
9136c053b1 | ||
|
|
8efab62f8b | ||
|
|
7d723148ab | ||
|
|
2880ec9bdd | ||
|
|
871d1694ef | ||
|
|
f26c9e9c4a | ||
|
|
b28d5b2693 | ||
|
|
695f62bbd5 | ||
|
|
0b8de1f615 | ||
|
|
c1e5f1dd23 | ||
|
|
2e4337fe6d | ||
|
|
dbe45d5c27 | ||
|
|
c963ea8ba5 | ||
|
|
484361c034 | ||
|
|
d65f51d5af | ||
|
|
7857e97057 | ||
|
|
545665a79f | ||
|
|
394fd2bb04 | ||
|
|
a7631a6a7f | ||
|
|
d174219ddc | ||
|
|
854f3061b9 | ||
|
|
6c34d25ebb | ||
|
|
df34cef2bd | ||
|
|
e8513e60b2 | ||
|
|
4aadee3fad | ||
|
|
c7adf03aeb | ||
|
|
35944ca8c5 | ||
|
|
083d625813 | ||
|
|
263db812b3 | ||
|
|
2c01d18195 | ||
|
|
df99d43415 | ||
|
|
d81546f2de | ||
|
|
3d87a917af | ||
|
|
9240ed4857 | ||
|
|
18f9e8dca2 | ||
|
|
fbc24b81d2 | ||
|
|
f898466395 | ||
|
|
e496fa1977 | ||
|
|
d016326779 | ||
|
|
ee14b5ee8e | ||
|
|
9ae927feab | ||
|
|
7087a0a55c | ||
|
|
793a8d6d79 | ||
|
|
e15b22dc3c | ||
|
|
6ddd453900 | ||
|
|
24755099c0 | ||
|
|
a5f97c2765 | ||
|
|
df59bc22de | ||
|
|
5fdd59dee2 | ||
|
|
76b9010bc8 | ||
|
|
15d58e20d4 | ||
|
|
ec9f5b8012 | ||
|
|
5c907bb8f8 | ||
|
|
5e2cf9697c | ||
|
|
7d3201826d | ||
|
|
5e0a341eb1 | ||
|
|
954a972a36 | ||
|
|
c0dd5dcfc6 | ||
|
|
fd394e3475 | ||
|
|
5dcdf9c11f | ||
|
|
14f559ab8b | ||
|
|
6bb79f821c | ||
|
|
6dcd3c9978 | ||
|
|
d5acd2f842 | ||
|
|
1395b7159f | ||
|
|
548442c33b | ||
|
|
f8f7eb26e8 | ||
|
|
ca10ba7411 | ||
|
|
d8ccf67be8 | ||
|
|
8fba19597e | ||
|
|
c034dfaf53 | ||
|
|
4576978347 | ||
|
|
97c409fde6 | ||
|
|
5b2e62c29e | ||
|
|
c7025d99dd | ||
|
|
111f5bf627 | ||
|
|
7de8ad8a59 | ||
|
|
31336dd5ce | ||
|
|
47104b31a4 | ||
|
|
59200e796a | ||
|
|
c91538644f | ||
|
|
3140fc5b8a | ||
|
|
a2afd37a97 | ||
|
|
d453ba9303 | ||
|
|
d0b278c0ec | ||
|
|
27a59c031b | ||
|
|
1bc139c591 | ||
|
|
7e8db1cd2e | ||
|
|
864bb30244 | ||
|
|
4209f725ba | ||
|
|
d01e789c8a | ||
|
|
85a0590d55 | ||
|
|
b9ba1ec1c2 | ||
|
|
29bffd0dbe | ||
|
|
e7a2c600f1 | ||
|
|
f2a5a93195 | ||
|
|
095c246efb | ||
|
|
581d22c7d7 | ||
|
|
1ca1e1f2d6 | ||
|
|
7c88b7c082 | ||
|
|
4d351e6d3d | ||
|
|
c46e9740d4 | ||
|
|
f9cb1054c0 | ||
|
|
4e2fa63f30 | ||
|
|
ca5eec9c8c | ||
|
|
582930d982 | ||
|
|
5a572abc6f | ||
|
|
a2d018c321 | ||
|
|
12f3154157 | ||
|
|
51689db315 | ||
|
|
1a8897ff0f | ||
|
|
5ee92bee78 | ||
|
|
eba7a0c88a | ||
|
|
f6a6224968 | ||
|
|
60e8878dc3 | ||
|
|
41d9160b63 | ||
|
|
b3d4c9af08 | ||
|
|
5a8a13d8f3 | ||
|
|
d94e1551a7 | ||
|
|
03e7f15d04 | ||
|
|
f14233ccea | ||
|
|
d5fc8fed37 | ||
|
|
2bd8fdee5e | ||
|
|
ed830d7901 | ||
|
|
6b127796f9 | ||
|
|
720edf8475 | ||
|
|
e27fae11e2 | ||
|
|
f5d31cf827 | ||
|
|
317c22366e | ||
|
|
98717b9da0 | ||
|
|
c95ba66a0b | ||
|
|
40a7c57abd | ||
|
|
7743481b8c | ||
|
|
6a5251c738 | ||
|
|
ceb5195b69 | ||
|
|
53f0de3fb9 | ||
|
|
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 | ||
|
|
ac18cbab3e | ||
|
|
2f1c89d0a6 | ||
|
|
a3ad4181d3 | ||
|
|
f8ac23e8d7 | ||
|
|
b6815d1a2a | ||
|
|
a79b813bd0 | ||
|
|
ad971145f0 | ||
|
|
602948921c | ||
|
|
607e969a30 | ||
|
|
79f1ae9930 | ||
|
|
a98f681f98 | ||
|
|
e0e1f697d3 | ||
|
|
1b70a66120 | ||
|
|
94eba96a6e | ||
|
|
f052d8e307 | ||
|
|
df45686828 | ||
|
|
84d228877a | ||
|
|
b2b26742b0 | ||
|
|
9dc630c809 | ||
|
|
452430db56 | ||
|
|
b0d676a734 | ||
|
|
ff73470085 | ||
|
|
a41349999f | ||
|
|
004f4bc41a | ||
|
|
64c8e1a051 | ||
|
|
d95d48f6b2 | ||
|
|
3e702f0c51 | ||
|
|
2e36fcc3ce | ||
|
|
aa0cda306b | ||
|
|
aa79f83f35 | ||
|
|
8de7e75c77 | ||
|
|
4a1b419c0e | ||
|
|
b6a08d5073 | ||
|
|
65d2ab5d53 | ||
|
|
34b03f9d53 | ||
|
|
dca513b8c9 | ||
|
|
446941be92 | ||
|
|
b76cdb01d1 | ||
|
|
ce5b250ef0 | ||
|
|
2c4ccf62af | ||
|
|
c7ad5194e6 | ||
|
|
ddb866131b | ||
|
|
49fb03aa3a | ||
|
|
1ed3a51117 | ||
|
|
4d5660ccbb | ||
|
|
7596d271ad | ||
|
|
943f9350f3 | ||
|
|
07863846e6 | ||
|
|
cc59ab618d | ||
|
|
9794949e8a | ||
|
|
5c741d2b27 | ||
|
|
52c76deb5e | ||
|
|
c986118f16 | ||
|
|
df6694e3b7 | ||
|
|
a55ad238e6 | ||
|
|
be21db030e | ||
|
|
16488c8a40 | ||
|
|
a7489aa675 | ||
|
|
b1647d2f3d | ||
|
|
c7f3275dfa | ||
|
|
7aeba43338 | ||
|
|
688db9ef8c | ||
|
|
173883da3e | ||
|
|
6077e04619 | ||
|
|
95c57363a2 | ||
|
|
9b96b4e34f | ||
|
|
40ba1987cd | ||
|
|
9c08c0a1a4 | ||
|
|
b2360fa2f6 | ||
|
|
e7cc6ce18b | ||
|
|
a83e206c89 | ||
|
|
ac87f51047 |
45
.github/workflows/docker.yml
vendored
Normal file
45
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Docker
|
||||
|
||||
# This workflow is triggered on a push to a tag that follows semantic versioning
|
||||
# e.g., v1.2.3, v2.0.0-rc1
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+**'
|
||||
|
||||
jobs:
|
||||
# Build and push the Docker image
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# This step extracts the version number from the tag
|
||||
# e.g., if the tag is 'v1.2.3', this will output '1.2.3'
|
||||
- name: Extract release version
|
||||
id: release_version
|
||||
run: |
|
||||
echo "version=$(echo ${{ github.ref_name }} | sed -e 's/^v//')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: |
|
||||
wealdtech/ethdo:${{ steps.release_version.outputs.version }}
|
||||
wealdtech/ethdo:latest
|
||||
29
.github/workflows/golangci-lint.yml
vendored
Normal file
29
.github/workflows/golangci-lint.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: 'golangci-lint'
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
pull-requests: 'read'
|
||||
checks: 'write'
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: 'lint'
|
||||
runs-on: 'ubuntu-24.04'
|
||||
steps:
|
||||
- uses: 'actions/setup-go@v5'
|
||||
with:
|
||||
cache: false
|
||||
go-version: '^1.22'
|
||||
- uses: 'actions/checkout@v4'
|
||||
- uses: 'golangci/golangci-lint-action@v6'
|
||||
with:
|
||||
version: 'latest'
|
||||
args: '--timeout=60m'
|
||||
only-new-issues: true
|
||||
skip-cache: true
|
||||
288
.github/workflows/release.yml
vendored
288
.github/workflows/release.yml
vendored
@@ -3,118 +3,208 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- 'v*'
|
||||
- 't*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
# Set variables that will be available to all builds.
|
||||
env_vars:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release_version: ${{ steps.release_version.outputs.release_version }}
|
||||
binary: ${{ steps.binary.outputs.binary }}
|
||||
steps:
|
||||
- id: release_version
|
||||
run: |
|
||||
RELEASE_VERSION=$(echo ${{ github.ref_name }} | sed -e 's/^[vt]//')
|
||||
echo "release_version=${RELEASE_VERSION}" >> $GITHUB_OUTPUT
|
||||
- id: binary
|
||||
run: |
|
||||
BINARY=$(basename ${{ github.repository }})
|
||||
echo "binary=${BINARY}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.13
|
||||
id: go
|
||||
# Create a github release to hold the assets once built.
|
||||
create_release:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: env_vars
|
||||
outputs:
|
||||
upload_url: ${{ steps.release_action.outputs.upload_url }}
|
||||
steps:
|
||||
- name: Create Release
|
||||
id: release_action
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: Release ${{ needs.env_vars.outputs.release_version }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
# Build and pack the binaries for linux.
|
||||
build_linux:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [create_release, env_vars]
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: false
|
||||
go-version: '^1.22'
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
if [ -f Gopkg.toml ]; then
|
||||
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
dep ensure
|
||||
fi
|
||||
- name: Check out repository into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set env
|
||||
run: |
|
||||
echo "GO111MODULE=on" >> $GITHUB_ENV
|
||||
# Release tag comes from the github reference.
|
||||
RELEASE_TAG=$(echo ${GITHUB_REF} | sed -e 's!.*/!!')
|
||||
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
|
||||
# Release version is same as release tag without leading 'v'.
|
||||
RELEASE_VERSION=$(echo ${GITHUB_REF} | sed -e 's!.*/v!!')
|
||||
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> $GITHUB_ENV
|
||||
echo "::set-output name=RELEASE_VERSION::${RELEASE_VERSION}"
|
||||
- name: Select correct tag
|
||||
run: git checkout ${{ github.ref_name }}
|
||||
|
||||
- name: Build
|
||||
run: go build -v -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION}" .
|
||||
- name: Fetch repository dependencies
|
||||
run: go get -v -t ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v .
|
||||
- name: Compile
|
||||
run: |
|
||||
# Do not attempt to upgrade grub, as it errors on github (24th Feb 2023)
|
||||
sudo apt-mark hold grub-efi-amd64-signed grub-efi-amd64-bin
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade
|
||||
go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
|
||||
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz ${{ needs.env_vars.outputs.binary }}
|
||||
sha256sum ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz | sed -e 's/ .*//' >${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz.sha256
|
||||
- name: Upload binary
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Fetch xgo
|
||||
run: |
|
||||
go get github.com/suburbandad/xgo
|
||||
- name: Upload hash
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz.sha256
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz.sha256
|
||||
asset_content_type: text/plain
|
||||
|
||||
- name: Cross-compile linux
|
||||
run: |
|
||||
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 (ARM64)
|
||||
run: |
|
||||
sudo apt install -y gcc-aarch64-linux-gnu libstdc++-11-pic-arm64-cross
|
||||
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
|
||||
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz ${{ needs.env_vars.outputs.binary }}
|
||||
sha256sum ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz | sed -e 's/ .*//' >${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz.sha256
|
||||
|
||||
- name: Cross-compile windows
|
||||
run: |
|
||||
xgo -v -x -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION} -s -w -extldflags -static" --targets="windows/amd64" github.com/wealdtech/ethdo
|
||||
- name: Upload binary (ARM64)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Create windows zip file
|
||||
run: |
|
||||
mv ethdo-windows-4.0-amd64.exe ethdo.exe
|
||||
zip --junk-paths ethdo-${RELEASE_VERSION}-windows-exe.zip ethdo.exe
|
||||
- name: Upload hash (ARM64)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz.sha256
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz.sha256
|
||||
asset_content_type: text/plain
|
||||
|
||||
- name: Create linux AMD64 tgz file
|
||||
run: |
|
||||
mv ethdo-linux-amd64 ethdo
|
||||
tar zcf ethdo-${RELEASE_VERSION}-linux-amd64.tar.gz ethdo
|
||||
# Build and pack the binaries for OSX.
|
||||
build_macos:
|
||||
runs-on: macos-latest
|
||||
needs: [create_release, env_vars]
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: false
|
||||
go-version: '^1.22'
|
||||
|
||||
- name: Create linux ARM64 tgz file
|
||||
run: |
|
||||
mv ethdo-linux-arm64 ethdo
|
||||
tar zcf ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz ethdo
|
||||
- name: Check out repository into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ env.RELEASE_VERSION }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Upload windows zip file
|
||||
id: upload-release-asset-windows
|
||||
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 }}-windows-exe.zip
|
||||
asset_name: ethdo-${{ env.RELEASE_VERSION }}-windows-exe.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload linux AMD64 tgz file
|
||||
id: upload-release-asset-linux-amd64
|
||||
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-amd64.tar.gz
|
||||
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-amd64.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
|
||||
- name: Select correct tag
|
||||
run: git checkout ${{ github.ref_name }}
|
||||
|
||||
- name: Fetch repository dependencies
|
||||
run: go get -v -t ./...
|
||||
|
||||
- name: Compile
|
||||
run: |
|
||||
go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }}"
|
||||
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz ${{ needs.env_vars.outputs.binary }}
|
||||
brew install coreutils
|
||||
sha256sum ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz | sed -e 's/ .*//' >${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz.sha256
|
||||
- name: Upload binary
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload hash
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz.sha256
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz.sha256
|
||||
asset_content_type: text/plain
|
||||
|
||||
# Build and pack the binaries for Windows.
|
||||
build_windows:
|
||||
runs-on: windows-latest
|
||||
needs: [create_release, env_vars]
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: false
|
||||
go-version: '^1.22'
|
||||
|
||||
- name: Check out repository into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Select correct tag
|
||||
run: git checkout ${{ github.ref_name }}
|
||||
|
||||
- name: Fetch repository dependencies
|
||||
run: go get -v -t ./...
|
||||
|
||||
- name: Compile
|
||||
run: |
|
||||
go build -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
|
||||
choco install zip
|
||||
zip --junk-paths ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe.zip ${{ needs.env_vars.outputs.binary }}.exe
|
||||
$FileHash=(certutil -hashfile ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe.zip SHA256 | findstr /v hash | findstr /v SHA).replace(" ", "")
|
||||
echo "$FileHash" > ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe-zip.sha256
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe.zip
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe.zip
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload hash
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe-zip.sha256
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe-zip.sha256
|
||||
asset_content_type: text/plain
|
||||
|
||||
18
.github/workflows/test.yml
vendored
Normal file
18
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: test
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: false
|
||||
go-version: '^1.22'
|
||||
- uses: actions/checkout@v4
|
||||
- uses: n8maninger/action-golang-test@v2
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -15,8 +15,17 @@ coverage.html
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
|
||||
# Intellij
|
||||
.idea/
|
||||
|
||||
# Makefile
|
||||
Makefile
|
||||
|
||||
# Vim
|
||||
*.sw?
|
||||
|
||||
# Local JSON files
|
||||
*.json
|
||||
|
||||
# Local TODO
|
||||
TODO.md
|
||||
|
||||
143
.golangci.yml
Normal file
143
.golangci.yml
Normal file
@@ -0,0 +1,143 @@
|
||||
# This file contains all available configuration options
|
||||
# with their default values (in comments).
|
||||
#
|
||||
# This file is not a configuration example,
|
||||
# it contains the exhaustive configuration with explanations of the options.
|
||||
|
||||
issues:
|
||||
# Which files to exclude: they will be analyzed, but issues from them won't be reported.
|
||||
# There is no need to include all autogenerated files,
|
||||
# we confidently recognize autogenerated files.
|
||||
# If it's not, please let us know.
|
||||
# "/" will be replaced by current OS file path separator to properly work on Windows.
|
||||
# Default: []
|
||||
exclude-files:
|
||||
- ".*_ssz\\.go$"
|
||||
|
||||
# Options for analysis running.
|
||||
run:
|
||||
# The default concurrency value is the number of available CPU.
|
||||
# concurrency: 4
|
||||
|
||||
# Timeout for analysis, e.g. 30s, 5m.
|
||||
# Default: 1m
|
||||
timeout: 10m
|
||||
|
||||
# Exit code when at least one issue was found.
|
||||
# Default: 1
|
||||
# issues-exit-code: 2
|
||||
|
||||
# Include test files or not.
|
||||
# Default: true
|
||||
tests: false
|
||||
|
||||
# List of build tags, all linters use it.
|
||||
# Default: [].
|
||||
# build-tags:
|
||||
# - mytag
|
||||
|
||||
# Which dirs to skip: issues from them won't be reported.
|
||||
# Can use regexp here: `generated.*`, regexp is applied on full path.
|
||||
# Default value is empty list,
|
||||
# but default dirs are skipped independently of this option's value (see skip-dirs-use-default).
|
||||
# "/" will be replaced by current OS file path separator to properly work on Windows.
|
||||
# skip-dirs:
|
||||
# - autogenerated_by_my_lib
|
||||
|
||||
# Enables skipping of directories:
|
||||
# - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
||||
# Default: true
|
||||
# skip-dirs-use-default: false
|
||||
|
||||
# If set we pass it to "go list -mod={option}". From "go help modules":
|
||||
# If invoked with -mod=readonly, the go command is disallowed from the implicit
|
||||
# automatic updating of go.mod described above. Instead, it fails when any changes
|
||||
# to go.mod are needed. This setting is most useful to check that go.mod does
|
||||
# not need updates, such as in a continuous integration and testing system.
|
||||
# If invoked with -mod=vendor, the go command assumes that the vendor
|
||||
# directory holds the correct copies of dependencies and ignores
|
||||
# the dependency descriptions in go.mod.
|
||||
#
|
||||
# Allowed values: readonly|vendor|mod
|
||||
# By default, it isn't set.
|
||||
modules-download-mode: readonly
|
||||
|
||||
# Allow multiple parallel golangci-lint instances running.
|
||||
# If false (default) - golangci-lint acquires file lock on start.
|
||||
allow-parallel-runners: true
|
||||
|
||||
# Define the Go version limit.
|
||||
# Mainly related to generics support since go1.18.
|
||||
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18
|
||||
# go: '1.19'
|
||||
|
||||
|
||||
# output configuration options
|
||||
output:
|
||||
formats:
|
||||
- format: colored-line-number
|
||||
path: stderr
|
||||
|
||||
# All available settings of specific linters.
|
||||
linters-settings:
|
||||
gosec:
|
||||
excludes:
|
||||
# Flags for potentially-unsafe casting of ints, but generates a lot of false positives.
|
||||
- 'G115'
|
||||
|
||||
lll:
|
||||
line-length: 132
|
||||
|
||||
stylecheck:
|
||||
checks: [ "all", "-ST1000" ]
|
||||
|
||||
tagliatelle:
|
||||
case:
|
||||
# use-field-name: true
|
||||
rules:
|
||||
json: snake
|
||||
yaml: snake
|
||||
|
||||
linters:
|
||||
# Enable all available linters.
|
||||
# Default: false
|
||||
enable-all: true
|
||||
# Disable specific linter
|
||||
# https://golangci-lint.run/usage/linters/#disabled-by-default
|
||||
disable:
|
||||
- contextcheck
|
||||
- cyclop
|
||||
- depguard
|
||||
- dupl
|
||||
- err113
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exhaustruct
|
||||
- forbidigo
|
||||
- forcetypeassert
|
||||
- funlen
|
||||
- gci
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- goheader
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
- mnd
|
||||
- musttag
|
||||
- nestif
|
||||
- nilnil
|
||||
- nlreturn
|
||||
- nolintlint
|
||||
- perfsprint
|
||||
- promlinter
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- tenv
|
||||
- unparam
|
||||
- varnamelen
|
||||
- wastedassign
|
||||
- wrapcheck
|
||||
- wsl
|
||||
287
CHANGELOG.md
287
CHANGELOG.md
@@ -1,5 +1,289 @@
|
||||
dev:
|
||||
|
||||
1.39.0:
|
||||
- support Fulu
|
||||
|
||||
1.38.0:
|
||||
- update latest version of go-eth2-client to support complex Spec types
|
||||
- adapt event handling to use new event handler structures in go-eth2-client
|
||||
|
||||
1.37.4:
|
||||
- add support for eip-7044 in exit verify command
|
||||
- provide ETH values as well as validator numbers in "epoch summary"
|
||||
|
||||
1.37.3:
|
||||
- add "hoodi" to the list of supported networks
|
||||
|
||||
1.37.2:
|
||||
- add "block trail"
|
||||
|
||||
1.37.1:
|
||||
- handle missing blobs for block info
|
||||
- fix `--epoch` flag for epoch summary
|
||||
|
||||
1.37.0:
|
||||
- support Electra
|
||||
- add `--compounding` flag when creating validator deposit data
|
||||
|
||||
1.36.6:
|
||||
- allow specification of blockid for validator info
|
||||
- validator depositdata orders deposits from an HD wallet by path
|
||||
|
||||
1.36.5:
|
||||
- avoid corner case mnemonic derivation with 25th word
|
||||
|
||||
1.36.2:
|
||||
- avoid crash when signing and verifing signatures using keys rather than accounts
|
||||
|
||||
1.36.1:
|
||||
- more JSON data for epoch summary
|
||||
- fix crash when block ifno had no blobs
|
||||
|
||||
1.36.0:
|
||||
- support keystore wallets
|
||||
|
||||
1.35.6:
|
||||
- provide more JSON data in "epoch summary"
|
||||
|
||||
1.35.5:
|
||||
- allow keystore to be output to the console
|
||||
|
||||
1.35.4:
|
||||
- provide consensus and execution client info in block info output
|
||||
|
||||
1.35.3:
|
||||
- provide better error message on context deadlline exceeded
|
||||
- update launchpad output to match latest version
|
||||
- add deposit contract address to "chain info"
|
||||
|
||||
1.35.2:
|
||||
- update dependencies
|
||||
|
||||
1.35.1:
|
||||
- fix output for various commands that may encounter an empty slot
|
||||
|
||||
1.35.0:
|
||||
- support Deneb
|
||||
- add start and end dates for eth1votes period
|
||||
|
||||
1.34.1:
|
||||
- fix period parsing for "synccommittee members" command
|
||||
|
||||
1.34.0:
|
||||
- update dependencies
|
||||
- use Capella fork for all exits
|
||||
- support Deneb beta 5
|
||||
|
||||
1.33.2:
|
||||
- fix windows build
|
||||
|
||||
1.33.1:
|
||||
- add "slot" to "proposer duties" command
|
||||
- add activation epoch and time to "validator info" command where applicable
|
||||
- add "holesky" to the list of supported networks
|
||||
- avoid crash when requesting validators from beacon node without debug enabled
|
||||
|
||||
1.33.0:
|
||||
- show all slots with 'synccommittee inclusion'
|
||||
- add "wallet batch" command
|
||||
|
||||
1.32.0:
|
||||
- fix incorrect error when "deposit verify" is not given a withdrawal address
|
||||
- allow truncated mnemonics (first four characters of each word)
|
||||
- add deneb information to "block info"
|
||||
- add epoch parameter to "validator yield"
|
||||
- add proposer index to "block info"
|
||||
- "block info" honours "--quiet" flag
|
||||
- "block info" accepts "--block-time" option
|
||||
- increase default operation timeout from 10s to 30s
|
||||
- "epoch summary" JSON lists number of blobs
|
||||
|
||||
1.31.0:
|
||||
- initial support for deneb
|
||||
- add "--generate-keystore" option for "account derive"
|
||||
- update "validator exit" command to be able to generate multiple exits
|
||||
- support for 12-word and 18-word mnemonics with single-word (no whitespace) passphrases
|
||||
- add JSON output for "validator expectation"
|
||||
|
||||
1.30.0:
|
||||
- add "chain spec" command
|
||||
- add "validator withdrawal" command
|
||||
|
||||
1.29.2:
|
||||
- fix regression where validator index could not be used as an account specifier
|
||||
|
||||
1.29.0:
|
||||
- allow use of keystores with validator credentials set
|
||||
- tidy up various command options to provide more standard usage
|
||||
- add mainnet fallback beacon node
|
||||
|
||||
1.28.4:
|
||||
- allow validator exit to use a keystore as its validator parameter
|
||||
|
||||
1.28.2:
|
||||
- fix bix stopping validator exit creation by direct validator specification
|
||||
|
||||
1.28.1:
|
||||
- generate error message if "validator credentials set" process fails to generate any credentials
|
||||
- allow import of accounts with null name field in their keystore
|
||||
- show text of execution payload extra data if available
|
||||
|
||||
1.28.0:
|
||||
- support additional mnemonic word list languages
|
||||
- increase minimum timeout for commands that fetch all validators to 2 minutes
|
||||
- provide better error messages when offline preparation file cannot be read
|
||||
- allow creation of all credential change operations related to a private key (thanks to @joaocenoura)
|
||||
|
||||
1.27.1:
|
||||
- fix issue with voluntary exits using incorrect domain (thanks to @0xTylerHolmes)
|
||||
|
||||
1.27.0:
|
||||
- use new build system
|
||||
- support S3 credentials
|
||||
- update operation of validator exit to match validator credentials set
|
||||
|
||||
1.26.5:
|
||||
- provide validator information in "chain status" verbose output
|
||||
|
||||
1.26.4:
|
||||
- provide details of BLS to execution change operations with verbose block output
|
||||
|
||||
1.26.3:
|
||||
- provide support for additional S3 store options
|
||||
- show error when attempting to delete non-filesystem wallets
|
||||
- provide additional support for Capella
|
||||
|
||||
1.26.2
|
||||
- remove check that requires capella prior to generating validator credentials change operations
|
||||
|
||||
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"
|
||||
- add exit / withdrawable epoch to "validator info"
|
||||
|
||||
1.13.0:
|
||||
- rework and provide additional information to "chain status" output
|
||||
|
||||
1.12.0:
|
||||
- add "synccommittee members"
|
||||
|
||||
1.11.0
|
||||
- add Altair information to "block info"
|
||||
- add more information to "chain info"
|
||||
|
||||
1.10.2
|
||||
- use local shamir code (copied from github.com/hashicorp/vault)
|
||||
|
||||
1.10.0
|
||||
- add "wallet sharedexport" and "wallet sharedimport"
|
||||
|
||||
1.9.1
|
||||
- Avoid crash when required interfaces for chain status command are not supported
|
||||
- Avoid crash with latest version of herumi/go-bls
|
||||
|
||||
1.9.0
|
||||
- allow use of Ethereum 1 address as withdrawal credentials
|
||||
|
||||
1.8.1
|
||||
- fix issue where 'attester duties' and 'attester inclusion' could crash
|
||||
|
||||
1.8.0
|
||||
- add "chain time"
|
||||
- add "validator keycheck"
|
||||
|
||||
1.7.5:
|
||||
- add "slot time"
|
||||
- add "attester duties"
|
||||
- add "node events"
|
||||
- add activation epoch to "validator info"
|
||||
|
||||
1.7.3:
|
||||
- fix issue where base directory was ignored for wallet creation
|
||||
- new "validator duties" command to display known duties for a given validator
|
||||
- update go-eth2-client to display correct validator status from prysm
|
||||
|
||||
1.7.2:
|
||||
- new "account derive" command to derive keys directly from a mnemonic and derivation path
|
||||
- add more output to "deposit verify" to explain operation
|
||||
|
||||
1.7.1:
|
||||
- fix "store not set" issue
|
||||
|
||||
1.7.0:
|
||||
- "validator depositdata" now defaults to mainnet, does not silently fetch fork version from chain
|
||||
- update deposit data output to version 3, to allow for better deposit checking
|
||||
@@ -10,12 +294,15 @@
|
||||
- renamed "--exportpassphrase" and "--importpassphrase" flags to "--passphrase"
|
||||
- reworked internal structure of account-related commands
|
||||
- reject weak passphrases by default
|
||||
|
||||
1.6.1:
|
||||
- "attester inclusion" defaults to previous epoch
|
||||
- output array for launchpad deposit data JSON in all situations
|
||||
|
||||
1.6.0:
|
||||
- update BLS HKDF function to match spec 04
|
||||
- add --launchpad option to "validator depositdata" to output data in launchpad format
|
||||
|
||||
1.5.9:
|
||||
- fix issue where wallet mnemonics were not normalised to NFKD
|
||||
- "block info" supports fetching the gensis block (--slot=0)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.14-buster as builder
|
||||
FROM golang:1.23-bookworm AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -10,10 +10,12 @@ COPY . .
|
||||
|
||||
RUN go build
|
||||
|
||||
FROM debian:buster-slim
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/ethdo /app
|
||||
|
||||
ENTRYPOINT ["/app/ethdo"]
|
||||
ENTRYPOINT ["/app/ethdo"]
|
||||
|
||||
75
README.md
75
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,10 +35,10 @@ 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`.
|
||||
Note that `ethdo` requires at least version 1.20 of go to operate. The version of go can be found with `go version`.
|
||||
|
||||
If this does not work please see the [troubleshooting](https://github.com/wealdtech/ethdo/blob/master/docs/troubleshooting.md) page.
|
||||
|
||||
@@ -61,24 +62,53 @@ 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/) using the `--connection <beacon-node:port>` argument. 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.
|
||||
|
||||
### Lodestar
|
||||
Lodestar enables the REST API by default and should just work locally. If you want to access the REST API from a remote server then you should also look to change the `--rest.address` to `0.0.0.0` as per the Lodestar documentation.
|
||||
|
||||
The default port for the REST API is 9596, which can be changed with the `--rest.port` parameter.
|
||||
|
||||
## Usage
|
||||
|
||||
ethdo contains a large number of features that are useful for day-to-day interactions with the Ethereum 2 blockchain.
|
||||
`ethdo` contains a large number of features that are useful for day-to-day interactions with the different consensus clients.
|
||||
|
||||
### 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
|
||||
- for Windows: %APPDATA%\ethereum2\wallets
|
||||
|
||||
If using the filesystem store, the additional parameter `basedir` can be supplied to change this location.
|
||||
If using the filesystem store, the additional parameter `base-dir` can be supplied to change this location.
|
||||
|
||||
> If using docker as above you can make this directory accessible to docker to make wallets and accounts persistent. For example, for linux you could use the following command to list your wallets on Linux:
|
||||
>
|
||||
> ```
|
||||
> docker run -v $HOME/.config/ethereum2/wallets:/data ethdo --basedir=/data wallet list
|
||||
> docker run -v $HOME/.config/ethereum2/wallets:/data ethdo --base-dir=/data wallet list
|
||||
> ```
|
||||
>
|
||||
> This will allow you to use `ethdo` with or without docker, with the same location for wallets and accounts.
|
||||
@@ -113,6 +143,28 @@ ethdo also supports environment variables. Environment variables are prefixed w
|
||||
export ETHDO_PASSPHRASE="my account passphrase"
|
||||
```
|
||||
|
||||
### S3 store options
|
||||
|
||||
Amazon S3-compatible stores have additional options available, which can be configured under the "stores.s3" key. An example configuration is as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"stores": {
|
||||
"s3": {
|
||||
"region": "us-west-1",
|
||||
"bucket": "my-s3-store",
|
||||
"path": "/wallets",
|
||||
"credentials": {
|
||||
"id": "ABCDEF123",
|
||||
"secret": "XXXXXXXXX"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Information on these and other options can be found in the S3 store repository.
|
||||
|
||||
### Output and exit status
|
||||
|
||||
If set, the `--quiet` argument will suppress all output.
|
||||
@@ -123,6 +175,15 @@ If set, the `--debug` argument will output additional information about the oper
|
||||
|
||||
Commands will have an exit status of 0 on success and 1 on failure. The specific definition of success is specified in the help for each command.
|
||||
|
||||
### Validator specifier
|
||||
|
||||
Ethereum validators can be specified in a number of different ways. The options are:
|
||||
|
||||
- an `ethdo` account, in the format _wallet_/_account_. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied, with a passphrase (for local accounts) or authority (for remote accounts)
|
||||
- the validator's 48-byte public key. It is not possible to use the a validator specified in this way to sign validator-related operations
|
||||
- a keystore, supplied either as direct JSON or as a path to a keystore on the local filesystem. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied
|
||||
- the validator's numeric index. It is not possible to use a validator specified in this way to sign validator-related operations. Note that this only works with on-chain operations, as the validator's index must be resolved to its public key
|
||||
|
||||
## Passphrase strength
|
||||
|
||||
`ethdo` will by default not allow creation or export of accounts or wallets with weak passphrases. If a weak pasphrase is used then `ethdo` will refuse to continue.
|
||||
@@ -153,7 +214,7 @@ There is a [HOWTO](https://github.com/wealdtech/ethdo/blob/master/docs/howto.md)
|
||||
|
||||
Jim McDonald: [@mcdee](https://github.com/mcdee).
|
||||
|
||||
Special thanks to [@SuburbanDad](https://github.com/SuburbanDad) for updating xgo to allow for cross-compilation of `ethdo` releaes.
|
||||
Special thanks to [@SuburbanDad](https://github.com/SuburbanDad) for updating xgo to allow for cross-compilation of `ethdo` releases.
|
||||
|
||||
## Contribute
|
||||
|
||||
|
||||
335
beacon/chaininfo.go
Normal file
335
beacon/chaininfo.go
Normal file
@@ -0,0 +1,335 @@
|
||||
// Copyright © 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package beacon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
consensusclient "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
type ChainInfo struct {
|
||||
Version uint64
|
||||
Validators []*ValidatorInfo
|
||||
GenesisValidatorsRoot phase0.Root
|
||||
Epoch phase0.Epoch
|
||||
GenesisForkVersion phase0.Version
|
||||
ExitForkVersion phase0.Version
|
||||
CurrentForkVersion phase0.Version
|
||||
BLSToExecutionChangeDomainType phase0.DomainType
|
||||
VoluntaryExitDomainType phase0.DomainType
|
||||
}
|
||||
|
||||
type chainInfoJSON struct {
|
||||
Version string `json:"version"`
|
||||
Validators []*ValidatorInfo `json:"validators"`
|
||||
GenesisValidatorsRoot string `json:"genesis_validators_root"`
|
||||
Epoch string `json:"epoch"`
|
||||
GenesisForkVersion string `json:"genesis_fork_version"`
|
||||
ExitForkVersion string `json:"exit_fork_version"`
|
||||
CurrentForkVersion string `json:"current_fork_version"`
|
||||
BLSToExecutionChangeDomainType string `json:"bls_to_execution_change_domain_type"`
|
||||
VoluntaryExitDomainType string `json:"voluntary_exit_domain_type"`
|
||||
}
|
||||
|
||||
type chainInfoVersionJSON struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (c *ChainInfo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&chainInfoJSON{
|
||||
Version: strconv.FormatUint(c.Version, 10),
|
||||
Validators: c.Validators,
|
||||
GenesisValidatorsRoot: fmt.Sprintf("%#x", c.GenesisValidatorsRoot),
|
||||
Epoch: fmt.Sprintf("%d", c.Epoch),
|
||||
GenesisForkVersion: fmt.Sprintf("%#x", c.GenesisForkVersion),
|
||||
ExitForkVersion: fmt.Sprintf("%#x", c.ExitForkVersion),
|
||||
CurrentForkVersion: fmt.Sprintf("%#x", c.CurrentForkVersion),
|
||||
BLSToExecutionChangeDomainType: fmt.Sprintf("%#x", c.BLSToExecutionChangeDomainType),
|
||||
VoluntaryExitDomainType: fmt.Sprintf("%#x", c.VoluntaryExitDomainType),
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (c *ChainInfo) UnmarshalJSON(input []byte) error {
|
||||
// See which version we are dealing with.
|
||||
var metadata chainInfoVersionJSON
|
||||
if err := json.Unmarshal(input, &metadata); err != nil {
|
||||
return errors.Wrap(err, "invalid JSON")
|
||||
}
|
||||
if metadata.Version == "" {
|
||||
return errors.New("version missing")
|
||||
}
|
||||
version, err := strconv.ParseUint(metadata.Version, 10, 64)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "version invalid")
|
||||
}
|
||||
if version < 3 {
|
||||
return errors.New("outdated version; please regenerate your offline data")
|
||||
}
|
||||
c.Version = version
|
||||
|
||||
var data chainInfoJSON
|
||||
if err := json.Unmarshal(input, &data); err != nil {
|
||||
return errors.Wrap(err, "invalid JSON")
|
||||
}
|
||||
|
||||
if len(data.Validators) == 0 {
|
||||
return errors.New("validators missing")
|
||||
}
|
||||
c.Validators = data.Validators
|
||||
|
||||
if data.GenesisValidatorsRoot == "" {
|
||||
return errors.New("genesis validators root missing")
|
||||
}
|
||||
genesisValidatorsRootBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisValidatorsRoot, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "genesis validators root invalid")
|
||||
}
|
||||
if len(genesisValidatorsRootBytes) != phase0.RootLength {
|
||||
return errors.New("genesis validators root incorrect length")
|
||||
}
|
||||
copy(c.GenesisValidatorsRoot[:], genesisValidatorsRootBytes)
|
||||
|
||||
if data.Epoch == "" {
|
||||
return errors.New("epoch missing")
|
||||
}
|
||||
epoch, err := strconv.ParseUint(data.Epoch, 10, 64)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "epoch invalid")
|
||||
}
|
||||
c.Epoch = phase0.Epoch(epoch)
|
||||
|
||||
if data.GenesisForkVersion == "" {
|
||||
return errors.New("genesis fork version missing")
|
||||
}
|
||||
genesisForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisForkVersion, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "genesis fork version invalid")
|
||||
}
|
||||
if len(genesisForkVersionBytes) != phase0.ForkVersionLength {
|
||||
return errors.New("genesis fork version incorrect length")
|
||||
}
|
||||
copy(c.GenesisForkVersion[:], genesisForkVersionBytes)
|
||||
|
||||
if data.ExitForkVersion == "" {
|
||||
return errors.New("exit fork version missing")
|
||||
}
|
||||
exitForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.ExitForkVersion, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "exit fork version invalid")
|
||||
}
|
||||
if len(exitForkVersionBytes) != phase0.ForkVersionLength {
|
||||
return errors.New("exit fork version incorrect length")
|
||||
}
|
||||
copy(c.ExitForkVersion[:], exitForkVersionBytes)
|
||||
|
||||
if data.CurrentForkVersion == "" {
|
||||
return errors.New("current fork version missing")
|
||||
}
|
||||
currentForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.CurrentForkVersion, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "current fork version invalid")
|
||||
}
|
||||
if len(currentForkVersionBytes) != phase0.ForkVersionLength {
|
||||
return errors.New("current fork version incorrect length")
|
||||
}
|
||||
copy(c.CurrentForkVersion[:], currentForkVersionBytes)
|
||||
|
||||
if data.BLSToExecutionChangeDomainType == "" {
|
||||
return errors.New("bls to execution domain type missing")
|
||||
}
|
||||
blsToExecutionChangeDomainType, err := hex.DecodeString(strings.TrimPrefix(data.BLSToExecutionChangeDomainType, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "bls to execution domain type invalid")
|
||||
}
|
||||
if len(blsToExecutionChangeDomainType) != phase0.DomainTypeLength {
|
||||
return errors.New("bls to execution domain type incorrect length")
|
||||
}
|
||||
copy(c.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType)
|
||||
|
||||
if data.VoluntaryExitDomainType == "" {
|
||||
return errors.New("voluntary exit domain type missing")
|
||||
}
|
||||
voluntaryExitDomainType, err := hex.DecodeString(strings.TrimPrefix(data.VoluntaryExitDomainType, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "voluntary exit domain type invalid")
|
||||
}
|
||||
if len(voluntaryExitDomainType) != phase0.DomainTypeLength {
|
||||
return errors.New("voluntary exit domain type incorrect length")
|
||||
}
|
||||
copy(c.VoluntaryExitDomainType[:], voluntaryExitDomainType)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FetchValidatorInfo fetches validator info given a validator identifier.
|
||||
func (c *ChainInfo) FetchValidatorInfo(ctx context.Context, id string) (*ValidatorInfo, error) {
|
||||
var validatorInfo *ValidatorInfo
|
||||
switch {
|
||||
case id == "":
|
||||
return nil, errors.New("no validator specified")
|
||||
case strings.HasPrefix(id, "0x"):
|
||||
// ID is a public key.
|
||||
// Check that the key is the correct length.
|
||||
if len(id) != 98 {
|
||||
return nil, errors.New("invalid public key: incorrect length")
|
||||
}
|
||||
for _, validator := range c.Validators {
|
||||
if strings.EqualFold(id, fmt.Sprintf("%#x", validator.Pubkey)) {
|
||||
validatorInfo = validator
|
||||
break
|
||||
}
|
||||
}
|
||||
case strings.Contains(id, "/"):
|
||||
// An account.
|
||||
_, account, err := util.WalletAndAccountFromPath(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to obtain account")
|
||||
}
|
||||
accPubKey, err := util.BestPublicKey(account)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to obtain public key for account")
|
||||
}
|
||||
pubkey := fmt.Sprintf("%#x", accPubKey.Marshal())
|
||||
for _, validator := range c.Validators {
|
||||
if strings.EqualFold(pubkey, fmt.Sprintf("%#x", validator.Pubkey)) {
|
||||
validatorInfo = validator
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
// An index.
|
||||
index, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse validator index")
|
||||
}
|
||||
validatorIndex := phase0.ValidatorIndex(index)
|
||||
for _, validator := range c.Validators {
|
||||
if validator.Index == validatorIndex {
|
||||
validatorInfo = validator
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if validatorInfo == nil {
|
||||
return nil, errors.New("unknown validator")
|
||||
}
|
||||
|
||||
return validatorInfo, nil
|
||||
}
|
||||
|
||||
// ObtainChainInfoFromNode obtains the chain information from a node.
|
||||
func ObtainChainInfoFromNode(ctx context.Context,
|
||||
consensusClient consensusclient.Service,
|
||||
chainTime chaintime.Service,
|
||||
) (
|
||||
*ChainInfo,
|
||||
error,
|
||||
) {
|
||||
res := &ChainInfo{
|
||||
Version: 3,
|
||||
Validators: make([]*ValidatorInfo, 0),
|
||||
Epoch: chainTime.CurrentEpoch(),
|
||||
}
|
||||
|
||||
// Obtain validators.
|
||||
validatorsResponse, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{State: "head"})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain validators")
|
||||
}
|
||||
|
||||
for _, validator := range validatorsResponse.Data {
|
||||
res.Validators = append(res.Validators, &ValidatorInfo{
|
||||
Index: validator.Index,
|
||||
Pubkey: validator.Validator.PublicKey,
|
||||
WithdrawalCredentials: validator.Validator.WithdrawalCredentials,
|
||||
State: validator.Status,
|
||||
})
|
||||
}
|
||||
// Order validators by index.
|
||||
sort.Slice(res.Validators, func(i int, j int) bool {
|
||||
return res.Validators[i].Index < res.Validators[j].Index
|
||||
})
|
||||
|
||||
// Genesis validators root obtained from beacon node.
|
||||
genesisResponse, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain genesis information")
|
||||
}
|
||||
res.GenesisValidatorsRoot = genesisResponse.Data.GenesisValidatorsRoot
|
||||
|
||||
// Fetch the genesis fork version from the specification.
|
||||
specResponse, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain spec")
|
||||
}
|
||||
tmp, exists := specResponse.Data["GENESIS_FORK_VERSION"]
|
||||
if !exists {
|
||||
return nil, errors.New("genesis fork version not known by chain")
|
||||
}
|
||||
var isForkVersion bool
|
||||
res.GenesisForkVersion, isForkVersion = tmp.(phase0.Version)
|
||||
if !isForkVersion {
|
||||
return nil, errors.New("could not obtain GENESIS_FORK_VERSION")
|
||||
}
|
||||
|
||||
// Fetch the exit fork version (Capella) from the specification.
|
||||
tmp, exists = specResponse.Data["CAPELLA_FORK_VERSION"]
|
||||
if !exists {
|
||||
return nil, errors.New("capella fork version not known by chain")
|
||||
}
|
||||
res.ExitForkVersion, isForkVersion = tmp.(phase0.Version)
|
||||
if !isForkVersion {
|
||||
return nil, errors.New("could not obtain CAPELLA_FORK_VERSION")
|
||||
}
|
||||
|
||||
// Fetch the current fork version from the fork schedule.
|
||||
forkScheduleResponse, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx, &api.ForkScheduleOpts{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain fork schedule")
|
||||
}
|
||||
for i := range forkScheduleResponse.Data {
|
||||
if forkScheduleResponse.Data[i].Epoch <= res.Epoch {
|
||||
res.CurrentForkVersion = forkScheduleResponse.Data[i].CurrentVersion
|
||||
}
|
||||
}
|
||||
|
||||
blsToExecutionChangeDomainType, exists := specResponse.Data["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
|
||||
if !exists {
|
||||
return nil, errors.New("failed to obtain DOMAIN_BLS_TO_EXECUTION_CHANGE")
|
||||
}
|
||||
copy(res.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType[:])
|
||||
|
||||
voluntaryExitDomainType, exists := specResponse.Data["DOMAIN_VOLUNTARY_EXIT"].(phase0.DomainType)
|
||||
if !exists {
|
||||
return nil, errors.New("failed to obtain DOMAIN_VOLUNTARY_EXIT")
|
||||
}
|
||||
copy(res.VoluntaryExitDomainType[:], voluntaryExitDomainType[:])
|
||||
|
||||
return res, nil
|
||||
}
|
||||
106
beacon/validatorinfo.go
Normal file
106
beacon/validatorinfo.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright © 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package beacon
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ValidatorInfo struct {
|
||||
Index phase0.ValidatorIndex
|
||||
Pubkey phase0.BLSPubKey
|
||||
State apiv1.ValidatorState
|
||||
WithdrawalCredentials []byte
|
||||
}
|
||||
|
||||
type validatorInfoJSON struct {
|
||||
Index string `json:"index"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
State apiv1.ValidatorState `json:"state"`
|
||||
WithdrawalCredentials string `json:"withdrawal_credentials"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (v *ValidatorInfo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&validatorInfoJSON{
|
||||
Index: fmt.Sprintf("%d", v.Index),
|
||||
Pubkey: fmt.Sprintf("%#x", v.Pubkey),
|
||||
State: v.State,
|
||||
WithdrawalCredentials: fmt.Sprintf("%#x", v.WithdrawalCredentials),
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (v *ValidatorInfo) UnmarshalJSON(input []byte) error {
|
||||
var data validatorInfoJSON
|
||||
if err := json.Unmarshal(input, &data); err != nil {
|
||||
return errors.Wrap(err, "invalid JSON")
|
||||
}
|
||||
|
||||
if data.Index == "" {
|
||||
return errors.New("index missing")
|
||||
}
|
||||
index, err := strconv.ParseUint(data.Index, 10, 64)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid value for index")
|
||||
}
|
||||
v.Index = phase0.ValidatorIndex(index)
|
||||
|
||||
if data.Pubkey == "" {
|
||||
return errors.New("public key missing")
|
||||
}
|
||||
pubkey, err := hex.DecodeString(strings.TrimPrefix(data.Pubkey, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid value for public key")
|
||||
}
|
||||
if len(pubkey) != phase0.PublicKeyLength {
|
||||
return fmt.Errorf("incorrect length %d for public key", len(pubkey))
|
||||
}
|
||||
copy(v.Pubkey[:], pubkey)
|
||||
|
||||
if data.State == apiv1.ValidatorStateUnknown {
|
||||
return errors.New("state unknown")
|
||||
}
|
||||
v.State = data.State
|
||||
|
||||
if data.WithdrawalCredentials == "" {
|
||||
return errors.New("withdrawal credentials missing")
|
||||
}
|
||||
v.WithdrawalCredentials, err = hex.DecodeString(strings.TrimPrefix(data.WithdrawalCredentials, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid value for withdrawal credentials")
|
||||
}
|
||||
if len(v.WithdrawalCredentials) != phase0.HashLength {
|
||||
return fmt.Errorf("incorrect length %d for withdrawal credentials", len(v.WithdrawalCredentials))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (v *ValidatorInfo) String() string {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Err: %v\n", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// accountCmd represents the account command
|
||||
// accountCmd represents the account command.
|
||||
var accountCmd = &cobra.Command{
|
||||
Use: "account",
|
||||
Short: "Manage account",
|
||||
@@ -28,5 +28,5 @@ func init() {
|
||||
RootCmd.AddCommand(accountCmd)
|
||||
}
|
||||
|
||||
func accountFlags(cmd *cobra.Command) {
|
||||
func accountFlags(_ *cobra.Command) {
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/core"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
@@ -63,7 +62,7 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
// Wallet.
|
||||
ctx, cancel := context.WithTimeout(ctx, data.timeout)
|
||||
defer cancel()
|
||||
data.wallet, err = core.WalletFromInput(ctx)
|
||||
data.wallet, err = util.WalletFromInput(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain wallet")
|
||||
|
||||
@@ -25,7 +25,7 @@ type dataOut struct {
|
||||
account e2wtypes.Account
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
func output(_ context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func processPathed(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data.passphrase == "" {
|
||||
return nil, errors.New("passphrase is required")
|
||||
}
|
||||
match, err := regexp.Match("^m/[0-9]+/[0-9]+(/[0-9+])+", []byte(data.path))
|
||||
match, err := regexp.MatchString("^m/[0-9]+/[0-9]+(/[0-9+])+", data.path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to match path to regular expression")
|
||||
}
|
||||
|
||||
@@ -1,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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 - 2024 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,8 +15,8 @@ package accountcreate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
return "", errors.Join(errors.New("failed to set up command"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if !viper.GetBool("verbose") {
|
||||
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
||||
66
cmd/account/derive/input.go
Normal file
66
cmd/account/derive/input.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright © 2020, 2023 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
quiet bool
|
||||
json bool
|
||||
// Derivation information.
|
||||
mnemonic string
|
||||
path string
|
||||
// Output options.
|
||||
showPrivateKey bool
|
||||
showWithdrawalCredentials bool
|
||||
generateKeystore bool
|
||||
}
|
||||
|
||||
func input(_ context.Context) (*dataIn, error) {
|
||||
data := &dataIn{}
|
||||
|
||||
// Quiet.
|
||||
data.quiet = viper.GetBool("quiet")
|
||||
|
||||
// JSON.
|
||||
data.json = viper.GetBool("json")
|
||||
|
||||
// Mnemonic.
|
||||
if viper.GetString("mnemonic") == "" {
|
||||
return nil, errors.New("mnemonic is required")
|
||||
}
|
||||
data.mnemonic = viper.GetString("mnemonic")
|
||||
|
||||
// Path.
|
||||
if viper.GetString("path") == "" {
|
||||
return nil, errors.New("path is required")
|
||||
}
|
||||
data.path = viper.GetString("path")
|
||||
|
||||
// Show private key.
|
||||
data.showPrivateKey = viper.GetBool("show-private-key")
|
||||
|
||||
// Show withdrawal credentials.
|
||||
data.showWithdrawalCredentials = viper.GetBool("show-withdrawal-credentials")
|
||||
|
||||
// Generate keystore.
|
||||
data.generateKeystore = viper.GetBool("generate-keystore")
|
||||
|
||||
return data, nil
|
||||
}
|
||||
78
cmd/account/derive/input_internal_test.go
Normal file
78
cmd/account/derive/input_internal_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright © 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "MnemonicMissing",
|
||||
vars: map[string]interface{}{
|
||||
"path": "m/12381/3600/0/0",
|
||||
},
|
||||
err: "mnemonic is required",
|
||||
},
|
||||
{
|
||||
name: "PathMissing",
|
||||
vars: map[string]interface{}{
|
||||
"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 is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"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": "m/12381/3600/0/0",
|
||||
},
|
||||
res: &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",
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
// Cannot compare accounts directly, so need to check each element individually.
|
||||
require.Equal(t, test.res.mnemonic, res.mnemonic)
|
||||
require.Equal(t, test.res.path, res.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
108
cmd/account/derive/output.go
Normal file
108
cmd/account/derive/output.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright © 2020, 2023 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
ethutil "github.com/wealdtech/go-eth2-util"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
json bool
|
||||
showPrivateKey bool
|
||||
showWithdrawalCredentials bool
|
||||
generateKeystore bool
|
||||
key *e2types.BLSPrivateKey
|
||||
path string
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
if data.key == nil {
|
||||
return "", errors.New("no key")
|
||||
}
|
||||
|
||||
if data.generateKeystore {
|
||||
return outputKeystore(ctx, data)
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
|
||||
if data.showPrivateKey {
|
||||
builder.WriteString(fmt.Sprintf("Private key: %#x\n", data.key.Marshal()))
|
||||
}
|
||||
if data.showWithdrawalCredentials {
|
||||
withdrawalCredentials := ethutil.SHA256(data.key.PublicKey().Marshal())
|
||||
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
|
||||
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
|
||||
}
|
||||
|
||||
func outputKeystore(_ context.Context, data *dataOut) (string, error) {
|
||||
passphrase, err := util.GetPassphrase()
|
||||
if err != nil {
|
||||
return "", errors.New("no passphrase supplied")
|
||||
}
|
||||
|
||||
encryptor := keystorev4.New()
|
||||
crypto, err := encryptor.Encrypt(data.key.Marshal(), passphrase)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to encrypt private key")
|
||||
}
|
||||
|
||||
uuid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return "", errors.New("failed to generate UUID")
|
||||
}
|
||||
ks := make(map[string]interface{})
|
||||
ks["uuid"] = uuid.String()
|
||||
ks["pubkey"] = hex.EncodeToString(data.key.PublicKey().Marshal())
|
||||
ks["version"] = 4
|
||||
ks["path"] = data.path
|
||||
ks["crypto"] = crypto
|
||||
out, err := json.Marshal(ks)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal keystore JSON")
|
||||
}
|
||||
|
||||
if data.json {
|
||||
fmt.Fprintf(os.Stdout, "%s\n", string(out))
|
||||
} else {
|
||||
keystoreFilename := fmt.Sprintf("keystore-%s-%d.json", strings.ReplaceAll(data.path, "/", "_"), time.Now().Unix())
|
||||
|
||||
if err := os.WriteFile(keystoreFilename, out, 0o600); err != nil {
|
||||
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", keystoreFilename))
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
101
cmd/account/derive/output_internal_test.go
Normal file
101
cmd/account/derive/output_internal_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright © 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
)
|
||||
|
||||
func blsPrivateKey(input string) *e2types.BLSPrivateKey {
|
||||
data, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
key, err := e2types.BLSPrivateKeyFromBytes(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
needs []string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "KeyMissing",
|
||||
dataOut: &dataOut{},
|
||||
err: "no key",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
dataOut: &dataOut{
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
},
|
||||
needs: []string{"Public key"},
|
||||
},
|
||||
{
|
||||
name: "PrivatKey",
|
||||
dataOut: &dataOut{
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
showPrivateKey: true,
|
||||
},
|
||||
needs: []string{"Private key"},
|
||||
},
|
||||
{
|
||||
name: "WithdrawalCredentials",
|
||||
dataOut: &dataOut{
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
showWithdrawalCredentials: true,
|
||||
},
|
||||
needs: []string{"Withdrawal credentials"},
|
||||
},
|
||||
{
|
||||
name: "All",
|
||||
dataOut: &dataOut{
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
showPrivateKey: true,
|
||||
showWithdrawalCredentials: true,
|
||||
},
|
||||
needs: []string{"Private key", "Withdrawal credentials"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := output(context.Background(), test.dataOut)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
for _, need := range test.needs {
|
||||
require.Contains(t, res, need)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/account/derive/process.go
Normal file
50
cmd/account/derive/process.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2020, 2023 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
account, err := util.ParseAccount(ctx, data.mnemonic, []string{data.path}, true)
|
||||
if err != nil {
|
||||
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{
|
||||
json: data.json,
|
||||
showPrivateKey: data.showPrivateKey,
|
||||
showWithdrawalCredentials: data.showWithdrawalCredentials,
|
||||
generateKeystore: data.generateKeystore,
|
||||
key: key.(*e2types.BLSPrivateKey),
|
||||
path: data.path,
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
97
cmd/account/derive/process_internal_test.go
Normal file
97
cmd/account/derive/process_internal_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright © 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
privKey []byte
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "MnemonicMissing",
|
||||
dataIn: &dataIn{
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
err: "failed to derive account: no account specified",
|
||||
},
|
||||
{
|
||||
name: "MnemonicInvalid",
|
||||
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 art",
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
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: "failed to derive account: path does not match expected format m/…",
|
||||
},
|
||||
{
|
||||
name: "PathInvalid",
|
||||
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",
|
||||
path: "n/12381/3600/0/0",
|
||||
},
|
||||
err: "failed to derive account: path does not match expected format m/…",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
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",
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
privKey: testutil.HexToBytes("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
},
|
||||
{
|
||||
name: "Extended",
|
||||
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 extended",
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
privKey: testutil.HexToBytes("0x58c8b280ae035de0452797b52fb62555f27f78541ea2f04b23e7bb0fcd0fc2d6"),
|
||||
},
|
||||
}
|
||||
|
||||
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.privKey, res.key.Marshal())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
cmd/account/derive/run.go
Normal file
55
cmd/account/derive/run.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2020, 2024 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Run runs the account create data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to obtain input"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if dataIn.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
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,24 +16,26 @@ package accountimport
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/core"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
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) {
|
||||
@@ -60,7 +62,7 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
// Wallet.
|
||||
ctx, cancel := context.WithTimeout(ctx, data.timeout)
|
||||
defer cancel()
|
||||
data.wallet, err = core.WalletFromInput(ctx)
|
||||
data.wallet, err = util.WalletFromInput(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain wallet")
|
||||
@@ -75,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 = os.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{}{
|
||||
|
||||
@@ -25,7 +25,7 @@ type dataOut struct {
|
||||
account e2wtypes.Account
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
func output(_ context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
|
||||
@@ -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,15 @@ package accountimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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,13 +49,73 @@ 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")
|
||||
return nil, errors.Wrap(err, "failed to import wallet")
|
||||
}
|
||||
results.account = account
|
||||
|
||||
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.
|
||||
var keystore map[string]any
|
||||
if err := json.Unmarshal(data.keystore, &keystore); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal keystore")
|
||||
}
|
||||
keystore["name"] = data.accountName
|
||||
keystore["encryptor"] = "keystore"
|
||||
keystoreData, err := json.Marshal(keystore)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal keystore")
|
||||
}
|
||||
|
||||
walletData := fmt.Sprintf(`{"wallet":{"name":"Import","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 account")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 - 2024 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,8 +15,8 @@ package accountimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
return "", errors.Join(errors.New("failed to obtain input"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if !viper.GetBool("verbose") {
|
||||
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/core"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
@@ -40,7 +39,7 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
|
||||
// Account.
|
||||
_, data.account, err = core.WalletAndAccountFromInput(ctx)
|
||||
_, data.account, err = util.WalletAndAccountFromInput(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain acount")
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func TestInput(t *testing.T) {
|
||||
"account": "Unknown/Interop 0",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "failed to obtain acount: faild to open wallet for account: wallet not found",
|
||||
err: "failed to obtain acount: failed to open wallet for account: wallet not found",
|
||||
},
|
||||
{
|
||||
name: "AccountMissing",
|
||||
@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
|
||||
"timeout": "5s",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "failed to obtain acount: faild to open wallet for account: invalid account format",
|
||||
err: "failed to obtain acount: failed to open wallet for account: invalid account format",
|
||||
},
|
||||
{
|
||||
name: "AccountWalletOnly",
|
||||
@@ -91,7 +91,7 @@ func TestInput(t *testing.T) {
|
||||
"account": "//",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "failed to obtain acount: faild to open wallet for account: invalid account format",
|
||||
err: "failed to obtain acount: failed to open wallet for account: invalid account format",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
|
||||
@@ -24,7 +24,7 @@ type dataOut struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
func output(_ context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 - 2024 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,8 +15,8 @@ package accountkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
return "", errors.Join(errors.New("failed to set up command"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
||||
@@ -29,7 +29,7 @@ var accountCreateCmd = &cobra.Command{
|
||||
ethdo account create --account="primary/operations" --passphrase="my secret"
|
||||
|
||||
In quiet mode this will return 0 if the account is created successfully, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
res, err := accountcreate.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -49,17 +49,13 @@ 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() {
|
||||
if err := viper.BindPFlag("participants", accountCreateCmd.Flags().Lookup("participants")); err != nil {
|
||||
func accountCreateBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("participants", cmd.Flags().Lookup("participants")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("signing-threshold", accountCreateCmd.Flags().Lookup("signing-threshold")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("path", accountCreateCmd.Flags().Lookup("path")); err != nil {
|
||||
if err := viper.BindPFlag("signing-threshold", cmd.Flags().Lookup("signing-threshold")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
69
cmd/accountderive.go
Normal file
69
cmd/accountderive.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
accountderive "github.com/wealdtech/ethdo/cmd/account/derive"
|
||||
)
|
||||
|
||||
var accountDeriveCmd = &cobra.Command{
|
||||
Use: "derive",
|
||||
Short: "Derive an account",
|
||||
Long: `Derive an account from a mnemonic and path. For example:
|
||||
|
||||
ethdo account derive --mnemonic="..." --path="m/12381/3600/0/0"
|
||||
|
||||
In quiet mode this will return 0 if the inputs can derive an account account, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
res, err := accountderive.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
accountCmd.AddCommand(accountDeriveCmd)
|
||||
accountFlags(accountDeriveCmd)
|
||||
accountDeriveCmd.Flags().Bool("show-private-key", false, "show private key for derived account")
|
||||
accountDeriveCmd.Flags().Bool("show-withdrawal-credentials", false, "show withdrawal credentials for derived account")
|
||||
accountDeriveCmd.Flags().Bool("generate-keystore", false, "generate a keystore for the derived account")
|
||||
accountDeriveCmd.Flags().Bool("json", false, "display the JSON keystore for the derived account on stdout")
|
||||
}
|
||||
|
||||
func accountDeriveBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("show-private-key", cmd.Flags().Lookup("show-private-key")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("show-withdrawal-credentials", cmd.Flags().Lookup("show-withdrawal-credentials")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("generate-keystore", cmd.Flags().Lookup("generate-keystore")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", cmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ var accountImportCmd = &cobra.Command{
|
||||
ethdo account import --account="primary/testing" --key="0x..." --passphrase="my secret"
|
||||
|
||||
In quiet mode this will return 0 if the account is imported successfully, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
res, err := accountimport.Run(cmd)
|
||||
if err != nil {
|
||||
return 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 {
|
||||
func accountImportBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("key", cmd.Flags().Lookup("key")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("keystore", cmd.Flags().Lookup("keystore")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("keystore-passphrase", cmd.Flags().Lookup("keystore-passphrase")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ var accountInfoCmd = &cobra.Command{
|
||||
ethdo account info --account="primary/my funds"
|
||||
|
||||
In quiet mode this will return 0 if the account exists, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
|
||||
@@ -44,11 +44,11 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
|
||||
// Disallow wildcards (for now)
|
||||
assert(fmt.Sprintf("%s/%s", wallet.Name(), account.Name()) == viper.GetString("account"), "Mismatched account name")
|
||||
|
||||
if quiet {
|
||||
if viper.GetBool("quiet") {
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
outputIf(verbose, fmt.Sprintf("UUID: %v", account.ID()))
|
||||
outputIf(viper.GetBool("verbose"), fmt.Sprintf("UUID: %v", account.ID()))
|
||||
var withdrawalPubKey e2types.PublicKey
|
||||
if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok {
|
||||
fmt.Printf("Public key: %#x\n", pubKeyProvider.PublicKey().Marshal())
|
||||
@@ -58,7 +58,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
|
||||
if distributedAccount, ok := account.(e2wtypes.DistributedAccount); ok {
|
||||
fmt.Printf("Composite public key: %#x\n", distributedAccount.CompositePublicKey().Marshal())
|
||||
fmt.Printf("Signing threshold: %d/%d\n", distributedAccount.SigningThreshold(), len(distributedAccount.Participants()))
|
||||
if verbose {
|
||||
if viper.GetBool("verbose") {
|
||||
fmt.Printf("Participants:\n")
|
||||
for k, v := range distributedAccount.Participants() {
|
||||
fmt.Printf(" %d: %s\n", k, v)
|
||||
@@ -67,7 +67,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
|
||||
|
||||
withdrawalPubKey = distributedAccount.CompositePublicKey()
|
||||
}
|
||||
if verbose {
|
||||
if viper.GetBool("verbose") {
|
||||
withdrawalCredentials := util.SHA256(withdrawalPubKey.Marshal())
|
||||
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
|
||||
fmt.Printf("Withdrawal credentials: %#x\n", withdrawalCredentials)
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
accountkey "github.com/wealdtech/ethdo/cmd/account/key"
|
||||
)
|
||||
|
||||
// accountKeyCmd represents the account key command
|
||||
// accountKeyCmd represents the account key command.
|
||||
var accountKeyCmd = &cobra.Command{
|
||||
Use: "key",
|
||||
Short: "Obtain the private key of an account.",
|
||||
@@ -30,7 +30,7 @@ var accountKeyCmd = &cobra.Command{
|
||||
ethdo account key --account="Personal wallet/Operations" --passphrase="my account passphrase"
|
||||
|
||||
In quiet mode this will return 0 if the key can be obtained, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
res, err := accountkey.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -29,7 +29,7 @@ var accountLockCmd = &cobra.Command{
|
||||
ethdo account lock --account="primary/my funds"
|
||||
|
||||
In quiet mode this will return 0 if the account is locked, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ var accountUnlockCmd = &cobra.Command{
|
||||
ethdo account unlock --account="primary/my funds" --passphrase="secret"
|
||||
|
||||
In quiet mode this will return 0 if the account is unlocked, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
|
||||
|
||||
29
cmd/attestation.go
Normal file
29
cmd/attestation.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright © 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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// attestationCmd represents the attestation command.
|
||||
var attestationCmd = &cobra.Command{
|
||||
Use: "attestation",
|
||||
Short: "Obtain information about an Ethereum 2 attestation",
|
||||
Long: "Obtain information about an Ethereum 2 attestation",
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(attestationCmd)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// attesterCmd represents the attester command
|
||||
// attesterCmd represents the attester command.
|
||||
var attesterCmd = &cobra.Command{
|
||||
Use: "attester",
|
||||
Short: "Obtain information about Ethereum 2 attesters",
|
||||
@@ -28,5 +28,5 @@ func init() {
|
||||
RootCmd.AddCommand(attesterCmd)
|
||||
}
|
||||
|
||||
func attesterFlags(cmd *cobra.Command) {
|
||||
func attesterFlags(_ *cobra.Command) {
|
||||
}
|
||||
|
||||
88
cmd/attester/duties/input.go
Normal file
88
cmd/attester/duties/input.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package attesterduties
|
||||
|
||||
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"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
// System.
|
||||
timeout time.Duration
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
json bool
|
||||
// Operation.
|
||||
validator string
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
epoch phase0.Epoch
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
data := &dataIn{}
|
||||
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
data.quiet = viper.GetBool("quiet")
|
||||
data.verbose = viper.GetBool("verbose")
|
||||
data.debug = viper.GetBool("debug")
|
||||
data.json = viper.GetBool("json")
|
||||
|
||||
// Validator.
|
||||
data.validator = viper.GetString("validator")
|
||||
if data.validator == "" {
|
||||
return nil, errors.New("validator is required")
|
||||
}
|
||||
|
||||
// Ethereum 2 client.
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
|
||||
Address: viper.GetString("connection"),
|
||||
Timeout: viper.GetDuration("timeout"),
|
||||
AllowInsecure: viper.GetBool("allow-insecure-connections"),
|
||||
LogFallback: !data.quiet,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
// Epoch.
|
||||
data.epoch, err = util.ParseEpoch(ctx, data.chainTime, viper.GetString("epoch"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
99
cmd/attester/duties/input_internal_test.go
Normal file
99
cmd/attester/duties/input_internal_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
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"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ValidatorMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "validator is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"validator": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res.timeout, res.timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
cmd/attester/duties/output.go
Normal file
55
cmd/attester/duties/output.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
debug bool
|
||||
quiet bool
|
||||
verbose bool
|
||||
json bool
|
||||
duty *api.AttesterDuty
|
||||
}
|
||||
|
||||
func output(_ context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
|
||||
if data.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if data.duty == nil {
|
||||
return "No duties found", nil
|
||||
}
|
||||
|
||||
if data.json {
|
||||
bytes, err := json.Marshal(data.duty)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshalJSON")
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Validator attesting in slot %d committee %d", data.duty.Slot, data.duty.CommitteeIndex), nil
|
||||
}
|
||||
85
cmd/attester/duties/output_internal_test.go
Normal file
85
cmd/attester/duties/output_internal_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
)
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
res string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "Empty",
|
||||
dataOut: &dataOut{},
|
||||
res: "No duties found",
|
||||
},
|
||||
{
|
||||
name: "Present",
|
||||
dataOut: &dataOut{
|
||||
duty: &api.AttesterDuty{
|
||||
PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
|
||||
Slot: 1,
|
||||
ValidatorIndex: 2,
|
||||
CommitteeIndex: 3,
|
||||
CommitteeLength: 4,
|
||||
CommitteesAtSlot: 5,
|
||||
ValidatorCommitteeIndex: 6,
|
||||
},
|
||||
},
|
||||
res: "Validator attesting in slot 1 committee 3",
|
||||
},
|
||||
{
|
||||
name: "JSON",
|
||||
dataOut: &dataOut{
|
||||
json: true,
|
||||
duty: &api.AttesterDuty{
|
||||
PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
|
||||
Slot: 1,
|
||||
ValidatorIndex: 2,
|
||||
CommitteeIndex: 3,
|
||||
CommitteeLength: 4,
|
||||
CommitteesAtSlot: 5,
|
||||
ValidatorCommitteeIndex: 6,
|
||||
},
|
||||
},
|
||||
res: `{"pubkey":"0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95","slot":"1","validator_index":"2","committee_index":"3","committee_length":"4","committees_at_slot":"5","validator_committee_index":"6"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := output(context.Background(), test.dataOut)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
69
cmd/attester/duties/process.go
Normal file
69
cmd/attester/duties/process.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
validator, err := util.ParseValidator(ctx, data.eth2Client.(eth2client.ValidatorsProvider), data.validator, "head")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := &dataOut{
|
||||
debug: data.debug,
|
||||
quiet: data.quiet,
|
||||
verbose: data.verbose,
|
||||
}
|
||||
|
||||
duty, err := duty(ctx, data.eth2Client, validator, data.epoch)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain duty for validator")
|
||||
}
|
||||
|
||||
results.duty = duty
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func duty(ctx context.Context, eth2Client eth2client.Service, validator *apiv1.Validator, epoch spec.Epoch) (*apiv1.AttesterDuty, error) {
|
||||
// Find the attesting slot for the given epoch.
|
||||
dutiesResponse, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{
|
||||
Epoch: epoch,
|
||||
Indices: []spec.ValidatorIndex{validator.Index},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain attester duties")
|
||||
}
|
||||
duties := dutiesResponse.Data
|
||||
|
||||
if len(duties) == 0 {
|
||||
return nil, errors.New("validator does not have duty for that epoch")
|
||||
}
|
||||
|
||||
return duties[0], nil
|
||||
}
|
||||
73
cmd/attester/duties/process_internal_test.go
Normal file
73
cmd/attester/duties/process_internal_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/auto"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
eth2Client, err := auto.New(context.Background(),
|
||||
auto.WithLogLevel(zerolog.Disabled),
|
||||
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
chainTime, err := standardchaintime.New(context.Background(),
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "Client",
|
||||
dataIn: &dataIn{
|
||||
eth2Client: eth2Client,
|
||||
chainTime: chainTime,
|
||||
validator: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
cmd/attester/duties/run.go
Normal file
55
cmd/attester/duties/run.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2021, 2024 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 attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the wallet create data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to set up command"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -15,19 +15,15 @@ package attesterinclusion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/core"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
@@ -36,13 +32,11 @@ type dataIn struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
// Chain information.
|
||||
slotsPerEpoch uint64
|
||||
// Operation.
|
||||
validator *api.Validator
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
epoch spec.Epoch
|
||||
account e2wtypes.Account
|
||||
validator string
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
@@ -56,75 +50,36 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
data.verbose = viper.GetBool("verbose")
|
||||
data.debug = viper.GetBool("debug")
|
||||
|
||||
// Account.
|
||||
var err error
|
||||
data.account, err = attesterInclusionAccount()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain account")
|
||||
data.validator = viper.GetString("validator")
|
||||
if data.validator == "" {
|
||||
return nil, errors.New("validator is required")
|
||||
}
|
||||
|
||||
// Ethereum 2 client.
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
|
||||
Address: viper.GetString("connection"),
|
||||
Timeout: viper.GetDuration("timeout"),
|
||||
AllowInsecure: viper.GetBool("allow-insecure-connections"),
|
||||
LogFallback: !data.quiet,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, errors.Wrap(err, "failed to obtain genesis data")
|
||||
}
|
||||
epoch = int64(time.Since(genesis.GenesisTime).Seconds()) / (int64(slotDuration.Seconds()) * int64(data.slotsPerEpoch))
|
||||
if epoch > 0 {
|
||||
epoch--
|
||||
}
|
||||
data.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
data.epoch = spec.Epoch(epoch)
|
||||
|
||||
pubKeys := make([]spec.BLSPubKey, 1)
|
||||
pubKey, err := core.BestPublicKey(data.account)
|
||||
// Epoch.
|
||||
data.epoch, err = util.ParseEpoch(ctx, data.chainTime, viper.GetString("epoch"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain public key for account")
|
||||
return nil, err
|
||||
}
|
||||
copy(pubKeys[0][:], pubKey.Marshal())
|
||||
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), pubKeys)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to obtain validator information")
|
||||
}
|
||||
data.validator = validators[0]
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// attesterInclusionAccount obtains the account for the attester inclusion command.
|
||||
func attesterInclusionAccount() (e2wtypes.Account, error) {
|
||||
var account e2wtypes.Account
|
||||
var err error
|
||||
if viper.GetString("account") != "" {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
_, account, err = core.WalletAndAccountFromPath(ctx, viper.GetString("account"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain account")
|
||||
}
|
||||
} else {
|
||||
pubKey := viper.GetString("pubkey")
|
||||
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(pubKey, "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", pubKey))
|
||||
}
|
||||
account, err = util.NewScratchAccount(nil, pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", pubKey))
|
||||
}
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -61,19 +62,21 @@ func TestInput(t *testing.T) {
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "AccountMissing",
|
||||
name: "ValidatorMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to obtain account: invalid public key : public key must be 48 bytes",
|
||||
err: "validator is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
"timeout": "5s",
|
||||
"validator": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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,11 @@ package attesterinclusion
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -25,22 +28,67 @@ type dataOut struct {
|
||||
debug bool
|
||||
quiet bool
|
||||
verbose bool
|
||||
slot spec.Slot
|
||||
attestation *spec.VersionedAttestation
|
||||
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) {
|
||||
func output(_ 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(strconv.FormatUint(data.attestationIndex, 10))
|
||||
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,13 +14,19 @@
|
||||
package attesterinclusion
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
@@ -28,54 +34,183 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
validator, err := util.ParseValidator(ctx, data.eth2Client.(eth2client.ValidatorsProvider), data.validator, "head")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
results := &dataOut{
|
||||
debug: data.debug,
|
||||
quiet: data.quiet,
|
||||
verbose: data.verbose,
|
||||
}
|
||||
|
||||
duty, err := duty(ctx, data.eth2Client, data.validator, data.epoch, data.slotsPerEpoch)
|
||||
duty, err := duty(ctx, data.eth2Client, validator, data.epoch)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain duty for validator")
|
||||
}
|
||||
if data.debug {
|
||||
fmt.Printf("Duty is %s\n", duty.String())
|
||||
}
|
||||
|
||||
startSlot := duty.Slot + 1
|
||||
endSlot := startSlot + 32
|
||||
for slot := startSlot; slot < endSlot; slot++ {
|
||||
signedBlock, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
blockResponse, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
var apiErr *api.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
||||
// No block for this slot, that's fine.
|
||||
continue
|
||||
}
|
||||
return nil, errors.Wrap(err, "failed to obtain block")
|
||||
}
|
||||
if signedBlock == nil {
|
||||
block := blockResponse.Data
|
||||
if block == nil {
|
||||
continue
|
||||
}
|
||||
if signedBlock.Message.Slot != slot {
|
||||
blockSlot, err := block.Slot()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain block slot")
|
||||
}
|
||||
if blockSlot != slot {
|
||||
continue
|
||||
}
|
||||
if data.debug {
|
||||
fmt.Printf("Fetched block for slot %d\n", slot)
|
||||
}
|
||||
for i, attestation := range signedBlock.Message.Body.Attestations {
|
||||
if attestation.Data.Slot == duty.Slot &&
|
||||
attestation.Data.Index == duty.CommitteeIndex &&
|
||||
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain block attestations")
|
||||
}
|
||||
for i, attestation := range attestations {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
aggregationBits, err := attestation.AggregationBits()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain attestation aggregation bits")
|
||||
}
|
||||
|
||||
if attestationData.Slot == duty.Slot &&
|
||||
attestationData.Index == duty.CommitteeIndex &&
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New("not found")
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
|
||||
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *spec.VersionedAttestation) (bool, error) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
slot := attestationData.Slot
|
||||
for {
|
||||
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
var apiErr *api.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
if !response.Data.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
|
||||
return bytes.Equal(response.Data.Root[:], attestationData.BeaconBlockRoot[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *spec.VersionedAttestation) (bool, error) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
// Start with first slot of the target epoch.
|
||||
slot := data.chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
|
||||
for {
|
||||
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
var apiErr *api.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
if !response.Data.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
|
||||
return bytes.Equal(response.Data.Root[:], attestationData.Target.Root[:]), nil
|
||||
}
|
||||
}
|
||||
|
||||
func duty(ctx context.Context, eth2Client eth2client.Service, validator *apiv1.Validator, epoch phase0.Epoch) (*apiv1.AttesterDuty, error) {
|
||||
// Find the attesting slot for the given epoch.
|
||||
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
|
||||
dutiesResponse, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{
|
||||
Epoch: epoch,
|
||||
Indices: []phase0.ValidatorIndex{validator.Index},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain attester duties")
|
||||
}
|
||||
duties := dutiesResponse.Data
|
||||
|
||||
if len(duties) == 0 {
|
||||
return nil, errors.New("validator does not have duty for that epoch")
|
||||
|
||||
@@ -18,19 +18,26 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/auto"
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/http"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
eth2Client, err := auto.New(context.Background(),
|
||||
auto.WithLogLevel(zerolog.Disabled),
|
||||
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
|
||||
eth2Client, err := http.New(context.Background(),
|
||||
http.WithLogLevel(zerolog.Disabled),
|
||||
http.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
chainTime, err := standardchaintime.New(context.Background(),
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -46,12 +53,9 @@ func TestProcess(t *testing.T) {
|
||||
{
|
||||
name: "Client",
|
||||
dataIn: &dataIn{
|
||||
eth2Client: eth2Client,
|
||||
slotsPerEpoch: 32,
|
||||
validator: &api.Validator{
|
||||
Index: 0,
|
||||
},
|
||||
epoch: 100,
|
||||
eth2Client: eth2Client,
|
||||
chainTime: chainTime,
|
||||
validator: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 - 2024 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,8 +15,8 @@ package attesterinclusion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
return "", errors.Join(errors.New("failed to set up command"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
||||
61
cmd/attesterduties.go
Normal file
61
cmd/attesterduties.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright © 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
attesterduties "github.com/wealdtech/ethdo/cmd/attester/duties"
|
||||
)
|
||||
|
||||
var attesterDutiesCmd = &cobra.Command{
|
||||
Use: "duties",
|
||||
Short: "Obtain information about duties of an attester",
|
||||
Long: `Obtain information about dutes of an attester. For example:
|
||||
|
||||
ethdo attester duties --validator=Validators/00001 --epoch=12345
|
||||
|
||||
In quiet mode this will return 0 if a duty from the attester is found, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
res, err := attesterduties.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
attesterCmd.AddCommand(attesterDutiesCmd)
|
||||
attesterFlags(attesterDutiesCmd)
|
||||
attesterDutiesCmd.Flags().String("epoch", "head", "the epoch for which to obtain the duties")
|
||||
attesterDutiesCmd.Flags().String("validator", "", "the index, public key, or acount of the validator")
|
||||
}
|
||||
|
||||
func attesterDutiesBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,10 @@ var attesterInclusionCmd = &cobra.Command{
|
||||
Short: "Obtain information about attester inclusion",
|
||||
Long: `Obtain information about attester inclusion. For example:
|
||||
|
||||
ethdo attester inclusion --account=Validators/00001 --epoch=12345
|
||||
ethdo attester inclusion --validator=Validators/00001 --epoch=12345
|
||||
|
||||
In quiet mode this will return 0 if an attestation from the attester is found on the block of the given epoch, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
res, err := attesterinclusion.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -47,15 +47,19 @@ In quiet mode this will return 0 if an attestation from the attester is found on
|
||||
func init() {
|
||||
attesterCmd.AddCommand(attesterInclusionCmd)
|
||||
attesterFlags(attesterInclusionCmd)
|
||||
attesterInclusionCmd.Flags().Int64("epoch", -1, "the last complete epoch")
|
||||
attesterInclusionCmd.Flags().String("pubkey", "", "the public key of the attester")
|
||||
attesterInclusionCmd.Flags().String("epoch", "-1", "the epoch for which to obtain the inclusion")
|
||||
attesterInclusionCmd.Flags().String("validator", "", "the index, public key, or account of the validator")
|
||||
attesterInclusionCmd.Flags().String("index", "", "the index of the attester")
|
||||
}
|
||||
|
||||
func attesterInclusionBindings() {
|
||||
if err := viper.BindPFlag("epoch", attesterInclusionCmd.Flags().Lookup("epoch")); err != nil {
|
||||
func attesterInclusionBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("pubkey", attesterInclusionCmd.Flags().Lookup("pubkey")); err != nil {
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("index", cmd.Flags().Lookup("index")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// blockCmd represents the block command
|
||||
// blockCmd represents the block command.
|
||||
var blockCmd = &cobra.Command{
|
||||
Use: "block",
|
||||
Short: "Obtain information about an Ethereum 2 block",
|
||||
@@ -28,5 +28,5 @@ func init() {
|
||||
RootCmd.AddCommand(blockCmd)
|
||||
}
|
||||
|
||||
func blockFlags(cmd *cobra.Command) {
|
||||
func blockFlags(_ *cobra.Command) {
|
||||
}
|
||||
|
||||
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]*attestationDataInfo
|
||||
// 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 *attestationDataInfo `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 attestationDataInfo struct {
|
||||
Block phase0.Slot `json:"block"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
func newCommand(_ context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
priorAttestations: make(map[string]*attestationDataInfo),
|
||||
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"
|
||||
"strconv"
|
||||
"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 *attestationDataInfo `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(strconv.Itoa(i))
|
||||
builder.WriteString(": ")
|
||||
builder.WriteString("distance ")
|
||||
builder.WriteString(strconv.Itoa(attestation.Distance))
|
||||
builder.WriteString(", ")
|
||||
|
||||
if attestation.Duplicate != nil {
|
||||
builder.WriteString("duplicate of attestation ")
|
||||
builder.WriteString(strconv.Itoa(attestation.Duplicate.Index))
|
||||
builder.WriteString(" in block ")
|
||||
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Block))
|
||||
builder.WriteString("\n")
|
||||
continue
|
||||
}
|
||||
|
||||
builder.WriteString(strconv.Itoa(attestation.NewVotes))
|
||||
builder.WriteString("/")
|
||||
builder.WriteString(strconv.Itoa(attestation.Votes))
|
||||
builder.WriteString("/")
|
||||
builder.WriteString(strconv.Itoa(attestation.PossibleVotes))
|
||||
builder.WriteString(" new/total/possible votes")
|
||||
if attestation.NewVotes == 0 {
|
||||
builder.WriteString("\n")
|
||||
continue
|
||||
}
|
||||
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(strconv.Itoa(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
|
||||
}
|
||||
541
cmd/block/analyze/process.go
Normal file
541
cmd/block/analyze/process.go
Normal file
@@ -0,0 +1,541 @@
|
||||
// Copyright © 2022, 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// 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"
|
||||
"net/http"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"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
|
||||
}
|
||||
|
||||
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: c.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
var apiError *api.Error
|
||||
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
|
||||
return errors.New("empty beacon block")
|
||||
}
|
||||
return errors.Wrap(err, "failed to obtain beacon block")
|
||||
}
|
||||
block := blockResponse.Data
|
||||
|
||||
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 {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
if attestationData.Slot < minSlot {
|
||||
minSlot = attestationData.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
|
||||
}
|
||||
|
||||
return c.analyzeSyncCommittees(ctx, block)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
analysis := &attestationAnalysis{
|
||||
Head: attestationData.BeaconBlockRoot,
|
||||
Target: attestationData.Target.Root,
|
||||
Distance: int(slot - attestationData.Slot),
|
||||
}
|
||||
|
||||
root, err := attestation.HashTreeRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
|
||||
analysis.Duplicate = info
|
||||
} else {
|
||||
aggregationBits, err := attestation.AggregationBits()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, exists := blockVotes[attestationData.Slot]
|
||||
if !exists {
|
||||
blockVotes[attestationData.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
|
||||
}
|
||||
_, exists = blockVotes[attestationData.Slot][attestationData.Index]
|
||||
if !exists {
|
||||
blockVotes[attestationData.Slot][attestationData.Index] = bitfield.NewBitlist(aggregationBits.Len())
|
||||
}
|
||||
|
||||
// Count new votes.
|
||||
analysis.PossibleVotes = int(aggregationBits.Len())
|
||||
for j := range aggregationBits.Len() {
|
||||
if aggregationBits.BitAt(j) {
|
||||
analysis.Votes++
|
||||
if blockVotes[attestationData.Slot][attestationData.Index].BitAt(j) {
|
||||
// Already attested to in this block; skip.
|
||||
continue
|
||||
}
|
||||
if c.votes[attestationData.Slot][attestationData.Index].BitAt(j) {
|
||||
// Already attested to in a previous block; skip.
|
||||
continue
|
||||
}
|
||||
analysis.NewVotes++
|
||||
blockVotes[attestationData.Slot][attestationData.Index].SetBitAt(j, true)
|
||||
}
|
||||
}
|
||||
// Calculate head correct.
|
||||
analysis.HeadCorrect, err = c.calcHeadCorrect(ctx, attestation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Calculate head timely.
|
||||
analysis.HeadTimely = analysis.HeadCorrect && attestationData.Slot == slot-1
|
||||
|
||||
// Calculate source timely.
|
||||
analysis.SourceTimely = attestationData.Slot >= slot-5
|
||||
|
||||
// Calculate target correct.
|
||||
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Calculate target timely.
|
||||
if block.Version < spec.DataVersionDeneb {
|
||||
analysis.TargetTimely = attestationData.Slot >= slot-32
|
||||
} else {
|
||||
analysis.TargetTimely = true
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
root, err := block.Root()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
slot, err := block.Slot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Printf("Parent root of %#x@%d is %#x\n", root, slot, parentRoot)
|
||||
}
|
||||
|
||||
// Obtain the parent block.
|
||||
parentBlockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: fmt.Sprintf("%#x", parentRoot),
|
||||
})
|
||||
if err != nil {
|
||||
var apiError *api.Error
|
||||
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
|
||||
return errors.New("empty beacon block")
|
||||
}
|
||||
return err
|
||||
}
|
||||
parentBlock := parentBlockResponse.Data
|
||||
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(_ 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)] = &attestationDataInfo{
|
||||
Block: slot,
|
||||
Index: i,
|
||||
}
|
||||
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
aggregationBits, err := attestation.AggregationBits()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain attestation aggregation bits")
|
||||
}
|
||||
|
||||
_, exists := c.votes[attestationData.Slot]
|
||||
if !exists {
|
||||
c.votes[attestationData.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
|
||||
}
|
||||
_, exists = c.votes[attestationData.Slot][attestationData.Index]
|
||||
if !exists {
|
||||
c.votes[attestationData.Slot][attestationData.Index] = bitfield.NewBitlist(aggregationBits.Len())
|
||||
}
|
||||
for j := range aggregationBits.Len() {
|
||||
if aggregationBits.BitAt(j) {
|
||||
c.votes[attestationData.Slot][attestationData.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, &util.ConnectOpts{
|
||||
Address: c.connection,
|
||||
Timeout: c.timeout,
|
||||
AllowInsecure: c.allowInsecureConnections,
|
||||
LogFallback: !c.quiet,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to beacon node")
|
||||
}
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
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")
|
||||
}
|
||||
|
||||
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain spec")
|
||||
}
|
||||
|
||||
tmp, exists := specResponse.Data["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 = specResponse.Data["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 = specResponse.Data["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 = specResponse.Data["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 = specResponse.Data["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 = specResponse.Data["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 *spec.VersionedAttestation) (bool, error) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
slot := attestationData.Slot
|
||||
root, exists := c.headRoots[slot]
|
||||
if !exists {
|
||||
for {
|
||||
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
var apiError *api.Error
|
||||
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
|
||||
if c.debug {
|
||||
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if response.Data == nil {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
if !response.Data.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
c.headRoots[slot] = response.Data.Root
|
||||
root = response.Data.Root
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return bytes.Equal(root[:], attestationData.BeaconBlockRoot[:]), nil
|
||||
}
|
||||
|
||||
func (c *command) calcTargetCorrect(ctx context.Context, attestation *spec.VersionedAttestation) (bool, error) {
|
||||
attestationData, err := attestation.Data()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain attestation data")
|
||||
}
|
||||
|
||||
root, exists := c.targetRoots[attestationData.Slot]
|
||||
if !exists {
|
||||
// Start with first slot of the target epoch.
|
||||
slot := c.chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
|
||||
for {
|
||||
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
|
||||
Block: fmt.Sprintf("%d", slot),
|
||||
})
|
||||
if err != nil {
|
||||
var apiError *api.Error
|
||||
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
|
||||
if c.debug {
|
||||
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if response.Data == nil {
|
||||
// No block.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
if !response.Data.Canonical {
|
||||
// Not canonical.
|
||||
slot--
|
||||
continue
|
||||
}
|
||||
c.targetRoots[attestationData.Slot] = response.Data.Root
|
||||
root = response.Data.Root
|
||||
break
|
||||
}
|
||||
}
|
||||
return bytes.Equal(root[:], attestationData.Target.Root[:]), nil
|
||||
}
|
||||
|
||||
func (c *command) analyzeSyncCommittees(_ 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
|
||||
case spec.DataVersionCapella:
|
||||
c.analysis.SyncCommitee.Contributions = int(block.Capella.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
|
||||
c.analysis.SyncCommitee.PossibleContributions = int(block.Capella.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.DataVersionDeneb:
|
||||
c.analysis.SyncCommitee.Contributions = int(block.Deneb.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
|
||||
c.analysis.SyncCommitee.PossibleContributions = int(block.Deneb.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
|
||||
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
|
||||
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
|
||||
c.analysis.Value += c.analysis.SyncCommitee.Value
|
||||
return nil
|
||||
case spec.DataVersionElectra:
|
||||
c.analysis.SyncCommitee.Contributions = int(block.Electra.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
|
||||
c.analysis.SyncCommitee.PossibleContributions = int(block.Electra.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.DataVersionFulu:
|
||||
c.analysis.SyncCommitee.Contributions = int(block.Fulu.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
|
||||
c.analysis.SyncCommitee.PossibleContributions = int(block.Fulu.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\":\"BAD_REQUEST: Unsupported endpoint version: v2\",\"stacktraces\":[]}",
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
cmd/block/analyze/run.go
Normal file
55
cmd/block/analyze/run.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2022, 2024 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"
|
||||
"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.Join(errors.New("failed to set up command"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
if err := c.process(ctx); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := c.output(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 - 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -32,9 +32,11 @@ type dataIn struct {
|
||||
// Operation.
|
||||
eth2Client eth2client.Service
|
||||
jsonOutput bool
|
||||
sszOutput bool
|
||||
// Chain information.
|
||||
blockID string
|
||||
stream bool
|
||||
blockID string
|
||||
blockTime string
|
||||
stream bool
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
@@ -48,20 +50,20 @@ 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.blockID = viper.GetString("blockid")
|
||||
data.blockTime = viper.GetString("block-time")
|
||||
data.stream = viper.GetBool("stream")
|
||||
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
|
||||
Address: viper.GetString("connection"),
|
||||
Timeout: viper.GetDuration("timeout"),
|
||||
AllowInsecure: viper.GetBool("allow-insecure-connections"),
|
||||
LogFallback: !data.quiet,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
}
|
||||
|
||||
if viper.GetString("blockid") == "" {
|
||||
data.blockID = "head"
|
||||
} else {
|
||||
// Specific slot.
|
||||
data.blockID = viper.GetString("blockid")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
@@ -61,13 +61,6 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
},
|
||||
{
|
||||
name: "ConnectionBad",
|
||||
vars: map[string]interface{}{
|
||||
@@ -79,7 +72,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",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -175,3 +175,58 @@ func TestOutputBlockETH1Data(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockGraffiti(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
graffiti []byte
|
||||
res string
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
graffiti: []byte(""),
|
||||
},
|
||||
{
|
||||
name: "NoID",
|
||||
graffiti: []byte("No identifier"),
|
||||
res: "Graffiti: No identifier\n",
|
||||
},
|
||||
{
|
||||
name: "SingleClient",
|
||||
graffiti: []byte("Graffiti TK"),
|
||||
res: "Graffiti: Graffiti\nConsensus client: teku\n",
|
||||
},
|
||||
{
|
||||
name: "SingleClientImmediate",
|
||||
graffiti: []byte("TK"),
|
||||
res: "Consensus client: teku\n",
|
||||
},
|
||||
{
|
||||
name: "SingleClientAndHashImmediate",
|
||||
graffiti: []byte("TKa9f98260"),
|
||||
res: "Consensus client: teku (version hash a9f98260)\n",
|
||||
},
|
||||
{
|
||||
name: "DualClients",
|
||||
graffiti: []byte("LHGE"),
|
||||
res: "Consensus client: lighthouse\nExecution client: go-ethereum\n",
|
||||
},
|
||||
{
|
||||
name: "DualClientsReverseOrder",
|
||||
graffiti: []byte("GELH"),
|
||||
res: "Consensus client: lighthouse\nExecution client: go-ethereum\n",
|
||||
},
|
||||
{
|
||||
name: "DualClientsTruncatedHash",
|
||||
graffiti: []byte("Freedom To Transact TKa9f9NM220b"),
|
||||
res: "Graffiti: Freedom To Transact\nConsensus client: teku (version hash a9f9)\nExecution client: nethermind (version hash 220b)\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res := blockGraffiti(context.Background(), test.graffiti)
|
||||
require.Equal(t, test.res, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 - 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -17,21 +17,39 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
"github.com/attestantio/go-eth2-client/spec/capella"
|
||||
"github.com/attestantio/go-eth2-client/spec/deneb"
|
||||
"github.com/attestantio/go-eth2-client/spec/electra"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
)
|
||||
|
||||
var jsonOutput bool
|
||||
var results *dataOut
|
||||
var (
|
||||
jsonOutput bool
|
||||
sszOutput bool
|
||||
results *dataOut
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
if data.blockID == "" && data.blockTime == "" {
|
||||
return nil, errors.New("no block ID or block time")
|
||||
}
|
||||
|
||||
results = &dataOut{
|
||||
debug: data.debug,
|
||||
@@ -39,30 +57,58 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
eth2Client: data.eth2Client,
|
||||
}
|
||||
|
||||
config, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
err := populateResults(ctx, results)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
|
||||
}
|
||||
genesis, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
|
||||
}
|
||||
results.genesisTime = genesis.GenesisTime
|
||||
results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration)
|
||||
results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
|
||||
|
||||
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, data.blockID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon block")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := outputBlock(ctx, data.jsonOutput, signedBlock); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
if data.blockTime != "" {
|
||||
data.blockID, err = timeToBlockID(ctx, data.eth2Client, data.blockTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
block, err := obtainBlock(ctx, data, results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data.quiet {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
switch block.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
err = outputPhase0Block(ctx, data.jsonOutput, block.Phase0)
|
||||
case spec.DataVersionAltair:
|
||||
err = outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, block.Altair)
|
||||
case spec.DataVersionBellatrix:
|
||||
err = outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, block.Bellatrix)
|
||||
case spec.DataVersionCapella:
|
||||
err = outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, block.Capella)
|
||||
case spec.DataVersionDeneb:
|
||||
err = processDenebBlock(ctx, data, block)
|
||||
case spec.DataVersionElectra:
|
||||
err = processElectraBlock(ctx, data, block)
|
||||
case spec.DataVersionFulu:
|
||||
err = processFuluBlock(ctx, data, block)
|
||||
default:
|
||||
return nil, errors.New("unknown block version")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to process block")
|
||||
}
|
||||
|
||||
if data.stream {
|
||||
jsonOutput = data.jsonOutput
|
||||
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
|
||||
sszOutput = data.sszOutput
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Println("")
|
||||
}
|
||||
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, &api.EventsOpts{
|
||||
Topics: []string{"head"},
|
||||
HeadHandler: headEventHandler,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to start block stream")
|
||||
}
|
||||
@@ -72,23 +118,236 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return &dataOut{}, nil
|
||||
}
|
||||
|
||||
func headEventHandler(event *api.Event) {
|
||||
// Only interested in head events.
|
||||
if event.Topic != "head" {
|
||||
func populateResults(ctx context.Context, results *dataOut) error {
|
||||
specResponse, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to obtain configuration information")
|
||||
}
|
||||
genesisResponse, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to obtain genesis information")
|
||||
}
|
||||
genesis := genesisResponse.Data
|
||||
results.genesisTime = genesis.GenesisTime
|
||||
results.slotDuration = specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
|
||||
results.slotsPerEpoch = specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func obtainBlock(ctx context.Context, data *dataIn, results *dataOut,
|
||||
) (
|
||||
*spec.VersionedSignedBeaconBlock,
|
||||
error,
|
||||
) {
|
||||
blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
var apiErr *api.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
||||
if data.quiet {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nil, errors.New("empty beacon block")
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon block")
|
||||
}
|
||||
|
||||
return blockResponse.Data, nil
|
||||
}
|
||||
|
||||
func processDenebBlock(ctx context.Context,
|
||||
data *dataIn,
|
||||
block *spec.VersionedSignedBeaconBlock,
|
||||
) error {
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
kzgCommitments, err := block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
var apiErr *api.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode != http.StatusNotFound {
|
||||
return errors.Wrap(err, "failed to obtain blob sidecars")
|
||||
}
|
||||
} else {
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
}
|
||||
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, block.Deneb, blobSidecars); err != nil {
|
||||
return errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processElectraBlock(ctx context.Context,
|
||||
data *dataIn,
|
||||
block *spec.VersionedSignedBeaconBlock,
|
||||
) error {
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
kzgCommitments, err := block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
var apiErr *api.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode != http.StatusNotFound {
|
||||
return errors.Wrap(err, "failed to obtain blob sidecars")
|
||||
}
|
||||
} else {
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
}
|
||||
if err := outputElectraBlock(ctx, data.jsonOutput, data.sszOutput, block.Electra, blobSidecars); err != nil {
|
||||
return errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processFuluBlock(ctx context.Context,
|
||||
data *dataIn,
|
||||
block *spec.VersionedSignedBeaconBlock,
|
||||
) error {
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
kzgCommitments, err := block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: data.blockID,
|
||||
})
|
||||
if err != nil {
|
||||
var apiErr *api.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode != http.StatusNotFound {
|
||||
return errors.Wrap(err, "failed to obtain blob sidecars")
|
||||
}
|
||||
} else {
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
}
|
||||
if err := outputFuluBlock(ctx, data.jsonOutput, data.sszOutput, block.Fulu, blobSidecars); err != nil {
|
||||
return errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func headEventHandler(ctx context.Context, headEvent *apiv1.HeadEvent) {
|
||||
blockID := fmt.Sprintf("%#x", headEvent.Block[:])
|
||||
blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: blockID,
|
||||
})
|
||||
if err != nil {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to obtain block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
block := blockResponse.Data
|
||||
if block == nil {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Println("Empty beacon block")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
switch block.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
err = outputPhase0Block(ctx, jsonOutput, block.Phase0)
|
||||
case spec.DataVersionAltair:
|
||||
err = outputAltairBlock(ctx, jsonOutput, sszOutput, block.Altair)
|
||||
case spec.DataVersionBellatrix:
|
||||
err = outputBellatrixBlock(ctx, jsonOutput, sszOutput, block.Bellatrix)
|
||||
case spec.DataVersionCapella:
|
||||
err = outputCapellaBlock(ctx, jsonOutput, sszOutput, block.Capella)
|
||||
case spec.DataVersionDeneb:
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
var kzgCommitments []deneb.KZGCommitment
|
||||
kzgCommitments, err = block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to obtain KZG commitments: %v\n", err)
|
||||
return
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
var blobSidecarsResponse *api.Response[[]*deneb.BlobSidecar]
|
||||
blobSidecarsResponse, err = results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: blockID,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to obtain blob sidecars: %v\n", err)
|
||||
return
|
||||
}
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, block.Deneb, blobSidecars)
|
||||
case spec.DataVersionElectra:
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
var kzgCommitments []deneb.KZGCommitment
|
||||
kzgCommitments, err = block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to obtain KZG commitments: %v\n", err)
|
||||
return
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
var blobSidecarsResponse *api.Response[[]*deneb.BlobSidecar]
|
||||
blobSidecarsResponse, err = results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: blockID,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to obtain blob sidecars: %v\n", err)
|
||||
return
|
||||
}
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
err = outputElectraBlock(context.Background(), jsonOutput, sszOutput, block.Electra, blobSidecars)
|
||||
case spec.DataVersionFulu:
|
||||
var blobSidecars []*deneb.BlobSidecar
|
||||
var kzgCommitments []deneb.KZGCommitment
|
||||
kzgCommitments, err = block.BlobKZGCommitments()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to obtain KZG commitments: %v\n", err)
|
||||
return
|
||||
}
|
||||
if len(kzgCommitments) > 0 {
|
||||
var blobSidecarsResponse *api.Response[[]*deneb.BlobSidecar]
|
||||
blobSidecarsResponse, err = results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
|
||||
Block: blockID,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to obtain blob sidecars: %v\n", err)
|
||||
return
|
||||
}
|
||||
blobSidecars = blobSidecarsResponse.Data
|
||||
}
|
||||
err = outputFuluBlock(context.Background(), jsonOutput, sszOutput, block.Fulu, blobSidecars)
|
||||
default:
|
||||
err = errors.New("unknown block version")
|
||||
}
|
||||
if err := outputBlock(context.Background(), jsonOutput, signedBlock); err != nil {
|
||||
fmt.Printf("Failed to display block: %v\n", err)
|
||||
if err != nil && !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Println("")
|
||||
}
|
||||
}
|
||||
|
||||
func outputBlock(ctx context.Context, jsonOutput bool, signedBlock *spec.SignedBeaconBlock) error {
|
||||
func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0.SignedBeaconBlock) error {
|
||||
switch {
|
||||
case jsonOutput:
|
||||
data, err := json.Marshal(signedBlock)
|
||||
@@ -97,11 +356,208 @@ func outputBlock(ctx context.Context, jsonOutput bool, signedBlock *spec.SignedB
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
default:
|
||||
data, err := outputBlockText(ctx, results, signedBlock)
|
||||
data, err := outputPhase0BlockText(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 outputAltairBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *altair.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 := outputAltairBlockText(ctx, results, signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func outputCapellaBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *capella.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 := outputCapellaBlockText(ctx, results, signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputDenebBlock(ctx context.Context,
|
||||
jsonOutput bool,
|
||||
sszOutput bool,
|
||||
signedBlock *deneb.SignedBeaconBlock,
|
||||
blobs []*deneb.BlobSidecar,
|
||||
) error {
|
||||
switch {
|
||||
case jsonOutput:
|
||||
data, err := json.Marshal(signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate JSON")
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
case sszOutput:
|
||||
data, err := signedBlock.MarshalSSZ()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate SSZ")
|
||||
}
|
||||
fmt.Printf("%x\n", data)
|
||||
default:
|
||||
data, err := outputDenebBlockText(ctx, results, signedBlock, blobs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputElectraBlock(ctx context.Context,
|
||||
jsonOutput bool,
|
||||
sszOutput bool,
|
||||
signedBlock *electra.SignedBeaconBlock,
|
||||
blobs []*deneb.BlobSidecar,
|
||||
) error {
|
||||
switch {
|
||||
case jsonOutput:
|
||||
data, err := json.Marshal(signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate JSON")
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
case sszOutput:
|
||||
data, err := signedBlock.MarshalSSZ()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate SSZ")
|
||||
}
|
||||
fmt.Printf("%x\n", data)
|
||||
default:
|
||||
data, err := outputElectraBlockText(ctx, results, signedBlock, blobs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputFuluBlock(ctx context.Context,
|
||||
jsonOutput bool,
|
||||
sszOutput bool,
|
||||
signedBlock *electra.SignedBeaconBlock,
|
||||
blobs []*deneb.BlobSidecar,
|
||||
) error {
|
||||
switch {
|
||||
case jsonOutput:
|
||||
data, err := json.Marshal(signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate JSON")
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
case sszOutput:
|
||||
data, err := signedBlock.MarshalSSZ()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate SSZ")
|
||||
}
|
||||
fmt.Printf("%x\n", data)
|
||||
default:
|
||||
data, err := outputElectraBlockText(ctx, results, signedBlock, blobs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func timeToBlockID(ctx context.Context, eth2Client eth2client.Service, input string) (string, error) {
|
||||
var timestamp time.Time
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(input, "0x"):
|
||||
// Hex string.
|
||||
hexTime, err := strconv.ParseInt(strings.TrimPrefix(input, "0x"), 16, 64)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse block time as hex string")
|
||||
}
|
||||
timestamp = time.Unix(hexTime, 0)
|
||||
case !strings.Contains(input, ":"):
|
||||
// No colon, assume decimal string.
|
||||
decTime, err := strconv.ParseInt(input, 10, 64)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse block time as decimal string")
|
||||
}
|
||||
timestamp = time.Unix(decTime, 0)
|
||||
default:
|
||||
dateTime, err := time.Parse("2006-01-02T15:04:05", input)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse block time as datetime")
|
||||
}
|
||||
timestamp = dateTime
|
||||
}
|
||||
|
||||
// Assume timestamp.
|
||||
chainTime, err := standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d", chainTime.TimestampToSlot(timestamp)), nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 - 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -43,11 +43,11 @@ func TestProcess(t *testing.T) {
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "Client",
|
||||
name: "NoBlockID",
|
||||
dataIn: &dataIn{
|
||||
eth2Client: eth2Client,
|
||||
},
|
||||
err: "failed to output block: failed to generate text: no block supplied",
|
||||
err: "no block ID",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 - 2024 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,8 +15,8 @@ package blockinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
return "", errors.Join(errors.New("failed to set up command"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
||||
89
cmd/block/trail/command.go
Normal file
89
cmd/block/trail/command.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright © 2025 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 blocktrail
|
||||
|
||||
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.
|
||||
blockID string
|
||||
jsonOutput bool
|
||||
target string
|
||||
maxBlocks int
|
||||
|
||||
// Data access.
|
||||
consensusClient eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
blocksProvider eth2client.SignedBeaconBlockProvider
|
||||
blockHeadersProvider eth2client.BeaconBlockHeadersProvider
|
||||
|
||||
// Processing.
|
||||
justifiedCheckpoint *phase0.Checkpoint
|
||||
finalizedCheckpoint *phase0.Checkpoint
|
||||
|
||||
// Results.
|
||||
steps []*step
|
||||
found bool
|
||||
}
|
||||
|
||||
type step struct {
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
Root phase0.Root `json:"root"`
|
||||
ParentRoot phase0.Root `json:"parent_root"`
|
||||
State string `json:"state,omitempty"`
|
||||
// Not a slot, but we're using it to steal the JSON processing.
|
||||
ExecutionBlock phase0.Slot `json:"execution_block"`
|
||||
ExecutionHash phase0.Hash32 `json:"execution_hash"`
|
||||
}
|
||||
|
||||
func newCommand(_ context.Context) (*command, error) {
|
||||
c := &command{
|
||||
timeout: viper.GetDuration("timeout"),
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
jsonOutput: viper.GetBool("json"),
|
||||
connection: viper.GetString("connection"),
|
||||
allowInsecureConnections: viper.GetBool("allow-insecure-connections"),
|
||||
blockID: viper.GetString("blockid"),
|
||||
target: viper.GetString("target"),
|
||||
maxBlocks: viper.GetInt("max-blocks"),
|
||||
steps: make([]*step, 0),
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
if c.timeout == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
74
cmd/block/trail/output.go
Normal file
74
cmd/block/trail/output.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright © 2025 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 blocktrail
|
||||
|
||||
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 simpleOut struct {
|
||||
Start *step `json:"start"`
|
||||
End *step `json:"end"`
|
||||
Steps int `json:"distance"`
|
||||
}
|
||||
|
||||
func (c *command) outputJSON(_ context.Context) (string, error) {
|
||||
var err error
|
||||
var data []byte
|
||||
if c.verbose {
|
||||
data, err = json.Marshal(c.steps)
|
||||
} else {
|
||||
basic := &simpleOut{
|
||||
Start: c.steps[0],
|
||||
End: c.steps[len(c.steps)-1],
|
||||
Steps: len(c.steps) - 1,
|
||||
}
|
||||
data, err = json.Marshal(basic)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (c *command) outputTxt(_ context.Context) (string, error) {
|
||||
if !c.found {
|
||||
return "Target not found", nil
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
builder.WriteString("Target '")
|
||||
builder.WriteString(c.target)
|
||||
builder.WriteString("' found at a distance of ")
|
||||
builder.WriteString(fmt.Sprintf("%d", len(c.steps)-1))
|
||||
builder.WriteString(" block(s)")
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
182
cmd/block/trail/process.go
Normal file
182
cmd/block/trail/process.go
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright © 2025 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 blocktrail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
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
|
||||
}
|
||||
|
||||
untilRoot := phase0.Root{}
|
||||
var untilBlock phase0.Slot
|
||||
switch {
|
||||
case strings.ToLower(c.target) == "justified", strings.ToLower(c.target) == "finalized":
|
||||
// Nothing to do.
|
||||
case strings.HasPrefix(c.target, "0x"):
|
||||
// Assume a root.
|
||||
if err := json.Unmarshal([]byte(fmt.Sprintf("%q", c.target)), &untilRoot); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// Assume a block number.
|
||||
tmp, err := strconv.ParseUint(c.target, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
untilBlock = phase0.Slot(tmp)
|
||||
}
|
||||
|
||||
blockID := c.blockID
|
||||
for range c.maxBlocks {
|
||||
step := &step{}
|
||||
|
||||
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
|
||||
Block: blockID,
|
||||
})
|
||||
if err != nil {
|
||||
var apiError *api.Error
|
||||
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
|
||||
return errors.New("empty beacon block")
|
||||
}
|
||||
return errors.Wrap(err, "failed to obtain beacon block")
|
||||
}
|
||||
block := blockResponse.Data
|
||||
|
||||
step.Slot, err = block.Slot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
step.Root, err = block.Root()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
step.ParentRoot, err = block.ParentRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
executionBlock, err := block.ExecutionBlockNumber()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
step.ExecutionBlock = phase0.Slot(executionBlock)
|
||||
step.ExecutionHash, err = block.ExecutionBlockHash()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
data, err := json.Marshal(step)
|
||||
if err == nil {
|
||||
fmt.Fprintf(os.Stderr, "Step is %s\n", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
c.steps = append(c.steps, step)
|
||||
|
||||
blockID = step.ParentRoot.String()
|
||||
|
||||
if c.target == "justified" && bytes.Equal(step.Root[:], c.justifiedCheckpoint.Root[:]) {
|
||||
c.found = true
|
||||
break
|
||||
}
|
||||
if c.target == "finalized" && bytes.Equal(step.Root[:], c.finalizedCheckpoint.Root[:]) {
|
||||
c.found = true
|
||||
break
|
||||
}
|
||||
if untilBlock > 0 && step.Slot == untilBlock {
|
||||
c.found = true
|
||||
break
|
||||
}
|
||||
if (!untilRoot.IsZero()) && bytes.Equal(step.Root[:], untilRoot[:]) {
|
||||
c.found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) setup(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
// Connect to the client.
|
||||
c.consensusClient, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
|
||||
Address: c.connection,
|
||||
Timeout: c.timeout,
|
||||
AllowInsecure: c.allowInsecureConnections,
|
||||
LogFallback: !c.quiet,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to beacon node")
|
||||
}
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.consensusClient.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.consensusClient.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
var isProvider bool
|
||||
c.blocksProvider, isProvider = c.consensusClient.(eth2client.SignedBeaconBlockProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide signed beacon block information")
|
||||
}
|
||||
c.blockHeadersProvider, isProvider = c.consensusClient.(eth2client.BeaconBlockHeadersProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide beacon block header information")
|
||||
}
|
||||
|
||||
finalityProvider, isProvider := c.consensusClient.(eth2client.FinalityProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide finality information")
|
||||
}
|
||||
finalityResponse, err := finalityProvider.Finality(ctx, &api.FinalityOpts{
|
||||
State: "head",
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain finality")
|
||||
}
|
||||
finality := finalityResponse.Data
|
||||
c.justifiedCheckpoint = finality.Justified
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Justified checkpoint is %d / %#x\n", c.justifiedCheckpoint.Epoch, c.justifiedCheckpoint.Root)
|
||||
}
|
||||
c.finalizedCheckpoint = finality.Finalized
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Finalized checkpoint is %d / %#x\n", c.finalizedCheckpoint.Epoch, c.finalizedCheckpoint.Root)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
63
cmd/block/trail/process_internal_test.go
Normal file
63
cmd/block/trail/process_internal_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright © 2025 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 blocktrail
|
||||
|
||||
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\":\"BAD_REQUEST: Unsupported endpoint version: v2\",\"stacktraces\":[]}",
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
59
cmd/block/trail/run.go
Normal file
59
cmd/block/trail/run.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright © 2025 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 blocktrail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"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.Join(errors.New("failed to set up command"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
if err := c.process(ctx); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
if c.found {
|
||||
return "", nil
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
results, err := c.output(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
61
cmd/blockanalyze.go
Normal file
61
cmd/blockanalyze.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"
|
||||
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, _ []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")
|
||||
}
|
||||
|
||||
func blockAnalyzeBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("stream", cmd.Flags().Lookup("stream")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,10 @@ 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 {
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
res, err := blockinfo.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -48,18 +48,22 @@ func init() {
|
||||
blockCmd.AddCommand(blockInfoCmd)
|
||||
blockFlags(blockInfoCmd)
|
||||
blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
|
||||
blockInfoCmd.Flags().String("block-time", "", "the time of the block to fetch (format YYYY-MM-DDTHH:MM:SS, or a hex or decimal timestamp")
|
||||
blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
|
||||
blockInfoCmd.Flags().Bool("json", false, "output data in JSON format")
|
||||
blockInfoCmd.Flags().Bool("ssz", false, "output data in SSZ format")
|
||||
}
|
||||
|
||||
func blockInfoBindings() {
|
||||
if err := viper.BindPFlag("blockid", blockInfoCmd.Flags().Lookup("blockid")); err != nil {
|
||||
func blockInfoBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("stream", blockInfoCmd.Flags().Lookup("stream")); err != nil {
|
||||
if err := viper.BindPFlag("block-time", cmd.Flags().Lookup("block-time")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", blockInfoCmd.Flags().Lookup("json")); err != nil {
|
||||
if err := viper.BindPFlag("stream", cmd.Flags().Lookup("stream")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("ssz", cmd.Flags().Lookup("ssz")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
65
cmd/blocktrail.go
Normal file
65
cmd/blocktrail.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright © 2025 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"
|
||||
blocktrail "github.com/wealdtech/ethdo/cmd/block/trail"
|
||||
)
|
||||
|
||||
var blockTrailCmd = &cobra.Command{
|
||||
Use: "trail",
|
||||
Short: "Trail back in the chain from a given block.",
|
||||
Long: `Trail back in the chain for a given block. For example:
|
||||
|
||||
ethdo block trail --blockid=12345 --target=finalized
|
||||
|
||||
In quiet mode this will return 0 if the block trail ends up at the finalized state, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
res, err := blocktrail.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
blockCmd.AddCommand(blockTrailCmd)
|
||||
blockFlags(blockTrailCmd)
|
||||
blockTrailCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
|
||||
blockTrailCmd.Flags().String("target", "justified", "the target block (block number, hash, justified or finalized)")
|
||||
blockTrailCmd.Flags().Int("max-blocks", 16384, "the maximum number of blocks to look at before halting")
|
||||
}
|
||||
|
||||
func blockTrailBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("target", cmd.Flags().Lookup("target")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("max-blocks", cmd.Flags().Lookup("max-blocks")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// chainCmd represents the chain command
|
||||
// chainCmd represents the chain command.
|
||||
var chainCmd = &cobra.Command{
|
||||
Use: "chain",
|
||||
Short: "Obtain information about an Ethereum 2 chain",
|
||||
@@ -28,5 +28,5 @@ func init() {
|
||||
RootCmd.AddCommand(chainCmd)
|
||||
}
|
||||
|
||||
func chainFlags(cmd *cobra.Command) {
|
||||
func chainFlags(_ *cobra.Command) {
|
||||
}
|
||||
|
||||
86
cmd/chain/eth1votes/command.go
Normal file
86
cmd/chain/eth1votes/command.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright © 2022 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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
|
||||
periodStart time.Time
|
||||
periodEnd time.Time
|
||||
incumbent *phase0.ETH1Data
|
||||
eth1DataVotes []*phase0.ETH1Data
|
||||
votes map[string]*vote
|
||||
}
|
||||
|
||||
type vote struct {
|
||||
Vote *phase0.ETH1Data `json:"vote"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func newCommand(_ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
134
cmd/chain/eth1votes/output.go
Normal file
134
cmd/chain/eth1votes/output.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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"`
|
||||
PeriodStart int64 `json:"period_start"`
|
||||
PeriodEnd int64 `json:"period_end"`
|
||||
Epoch phase0.Epoch `json:"epoch"`
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
Incumbent *phase0.ETH1Data `json:"incumbent"`
|
||||
Votes []*vote `json:"votes"`
|
||||
}
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.json {
|
||||
return c.outputJSON(ctx)
|
||||
}
|
||||
return c.outputText(ctx)
|
||||
}
|
||||
|
||||
func (c *command) outputJSON(_ 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,
|
||||
PeriodStart: c.periodStart.Unix(),
|
||||
PeriodEnd: c.periodEnd.Unix(),
|
||||
Epoch: c.epoch,
|
||||
Slot: c.slot,
|
||||
Incumbent: c.incumbent,
|
||||
Votes: votes,
|
||||
}
|
||||
data, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (c *command) outputText(_ context.Context) (string, error) {
|
||||
builder := strings.Builder{}
|
||||
|
||||
builder.WriteString("Voting period: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", c.period))
|
||||
|
||||
if c.verbose {
|
||||
builder.WriteString("Period start: ")
|
||||
builder.WriteString(fmt.Sprintf("%s\n", c.periodStart))
|
||||
builder.WriteString("Period end: ")
|
||||
builder.WriteString(fmt.Sprintf("%s\n", c.periodEnd))
|
||||
|
||||
builder.WriteString("Incumbent: ")
|
||||
builder.WriteString(fmt.Sprintf("block %#x, deposit count %d\n", c.incumbent.BlockHash, c.incumbent.DepositCount))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
184
cmd/chain/eth1votes/process.go
Normal file
184
cmd/chain/eth1votes/process.go
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright © 2022, 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// 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/api"
|
||||
"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()
|
||||
}
|
||||
stateResponse, err := c.beaconStateProvider.BeaconState(ctx, &api.BeaconStateOpts{
|
||||
State: fmt.Sprintf("%d", fetchSlot),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain state")
|
||||
}
|
||||
state := stateResponse.Data
|
||||
if state == nil {
|
||||
return errors.New("state not returned by beacon node")
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
data, err := json.Marshal(state)
|
||||
if err == nil {
|
||||
fmt.Printf("%s\n", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
c.slot, err = state.Slot()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain slot")
|
||||
}
|
||||
switch state.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
c.incumbent = state.Phase0.ETH1Data
|
||||
c.eth1DataVotes = state.Phase0.ETH1DataVotes
|
||||
case spec.DataVersionAltair:
|
||||
c.incumbent = state.Altair.ETH1Data
|
||||
c.eth1DataVotes = state.Altair.ETH1DataVotes
|
||||
case spec.DataVersionBellatrix:
|
||||
c.incumbent = state.Bellatrix.ETH1Data
|
||||
c.eth1DataVotes = state.Bellatrix.ETH1DataVotes
|
||||
case spec.DataVersionCapella:
|
||||
c.incumbent = state.Capella.ETH1Data
|
||||
c.eth1DataVotes = state.Capella.ETH1DataVotes
|
||||
case spec.DataVersionDeneb:
|
||||
c.incumbent = state.Deneb.ETH1Data
|
||||
c.eth1DataVotes = state.Deneb.ETH1DataVotes
|
||||
case spec.DataVersionElectra:
|
||||
c.incumbent = state.Electra.ETH1Data
|
||||
c.eth1DataVotes = state.Electra.ETH1DataVotes
|
||||
case spec.DataVersionFulu:
|
||||
c.incumbent = state.Fulu.ETH1Data
|
||||
c.eth1DataVotes = state.Fulu.ETH1DataVotes
|
||||
default:
|
||||
return fmt.Errorf("unhandled beacon state version %v", state.Version)
|
||||
}
|
||||
|
||||
c.period = uint64(c.epoch) / c.epochsPerEth1VotingPeriod
|
||||
c.periodStart = c.chainTime.StartOfEpoch(phase0.Epoch(c.period * c.epochsPerEth1VotingPeriod))
|
||||
c.periodEnd = c.chainTime.StartOfEpoch(phase0.Epoch((c.period + 1) * c.epochsPerEth1VotingPeriod))
|
||||
|
||||
c.votes = make(map[string]*vote)
|
||||
for _, eth1Vote := range c.eth1DataVotes {
|
||||
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, &util.ConnectOpts{
|
||||
Address: c.connection,
|
||||
Timeout: c.timeout,
|
||||
AllowInsecure: c.allowInsecureConnections,
|
||||
LogFallback: !c.quiet,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to beacon node")
|
||||
}
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
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")
|
||||
}
|
||||
|
||||
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain spec")
|
||||
}
|
||||
|
||||
tmp, exists := specResponse.Data["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 = specResponse.Data["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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
cmd/chain/eth1votes/run.go
Normal file
55
cmd/chain/eth1votes/run.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2022, 2024 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"
|
||||
"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.Join(errors.New("failed to set up command"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
if err := c.process(ctx); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := c.output(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
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(_ 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(_ 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(_ 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
|
||||
}
|
||||
89
cmd/chain/queues/process.go
Normal file
89
cmd/chain/queues/process.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright © 2022 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package chainqueues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/api"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
|
||||
State: fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validators")
|
||||
}
|
||||
|
||||
for _, validator := range response.Data {
|
||||
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, &util.ConnectOpts{
|
||||
Address: c.connection,
|
||||
Timeout: c.timeout,
|
||||
AllowInsecure: c.allowInsecureConnections,
|
||||
LogFallback: !c.quiet,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to beacon node")
|
||||
}
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
cmd/chain/queues/run.go
Normal file
55
cmd/chain/queues/run.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2022, 2024 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"
|
||||
"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.Join(errors.New("failed to set up command"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
if err := c.process(ctx); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := c.output(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
78
cmd/chain/time/input.go
Normal file
78
cmd/chain/time/input.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package chaintime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
// System.
|
||||
timeout time.Duration
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
json bool
|
||||
// Input
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
timestamp string
|
||||
slot string
|
||||
epoch string
|
||||
}
|
||||
|
||||
func input(_ context.Context) (*dataIn, error) {
|
||||
data := &dataIn{}
|
||||
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
data.quiet = viper.GetBool("quiet")
|
||||
data.verbose = viper.GetBool("verbose")
|
||||
data.debug = viper.GetBool("debug")
|
||||
data.json = viper.GetBool("json")
|
||||
|
||||
haveInput := false
|
||||
if viper.GetString("timestamp") != "" {
|
||||
data.timestamp = viper.GetString("timestamp")
|
||||
haveInput = true
|
||||
}
|
||||
if viper.GetString("slot") != "" {
|
||||
if haveInput {
|
||||
return nil, errors.New("only one of timestamp, slot and epoch allowed")
|
||||
}
|
||||
data.slot = viper.GetString("slot")
|
||||
haveInput = true
|
||||
}
|
||||
if viper.GetString("epoch") != "" {
|
||||
if haveInput {
|
||||
return nil, errors.New("only one of timestamp, slot and epoch allowed")
|
||||
}
|
||||
data.epoch = viper.GetString("epoch")
|
||||
haveInput = true
|
||||
}
|
||||
if !haveInput {
|
||||
return nil, errors.New("one of timestamp, slot or epoch required")
|
||||
}
|
||||
|
||||
data.connection = viper.GetString("connection")
|
||||
data.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
return data, nil
|
||||
}
|
||||
89
cmd/chain/time/input_internal_test.go
Normal file
89
cmd/chain/time/input_internal_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package chaintime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
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"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "IDMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "one of timestamp, slot or epoch required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res.timeout, res.timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
116
cmd/chain/time/output.go
Normal file
116
cmd/chain/time/output.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package chaintime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
debug bool
|
||||
quiet bool
|
||||
verbose bool
|
||||
|
||||
epoch spec.Epoch
|
||||
epochStart time.Time
|
||||
epochEnd time.Time
|
||||
slot spec.Slot
|
||||
slotStart time.Time
|
||||
slotEnd time.Time
|
||||
hasSyncCommittees bool
|
||||
syncCommitteePeriod uint64
|
||||
syncCommitteePeriodStart time.Time
|
||||
syncCommitteePeriodEpochStart spec.Epoch
|
||||
syncCommitteePeriodEnd time.Time
|
||||
syncCommitteePeriodEpochEnd spec.Epoch
|
||||
}
|
||||
|
||||
func output(_ context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
|
||||
if data.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
|
||||
builder.WriteString("Epoch ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.epoch))
|
||||
builder.WriteString("\n Epoch start ")
|
||||
builder.WriteString(data.epochStart.Format("2006-01-02 15:04:05"))
|
||||
if data.verbose {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(strconv.FormatInt(data.epochStart.Unix(), 10))
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n Epoch end ")
|
||||
builder.WriteString(data.epochEnd.Format("2006-01-02 15:04:05"))
|
||||
if data.verbose {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(strconv.FormatInt(data.epochEnd.Unix(), 10))
|
||||
builder.WriteString(")")
|
||||
}
|
||||
|
||||
builder.WriteString("\nSlot ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.slot))
|
||||
builder.WriteString("\n Slot start ")
|
||||
builder.WriteString(data.slotStart.Format("2006-01-02 15:04:05"))
|
||||
if data.verbose {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(strconv.FormatInt(data.slotStart.Unix(), 10))
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n Slot end ")
|
||||
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
|
||||
if data.verbose {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(strconv.FormatInt(data.slotEnd.Unix(), 10))
|
||||
builder.WriteString(")")
|
||||
}
|
||||
|
||||
if data.hasSyncCommittees {
|
||||
builder.WriteString("\nSync committee period ")
|
||||
builder.WriteString(strconv.FormatUint(data.syncCommitteePeriod, 10))
|
||||
builder.WriteString("\n Sync committee period start ")
|
||||
builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05"))
|
||||
builder.WriteString(" (epoch ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart))
|
||||
if data.verbose {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString(strconv.FormatInt(data.syncCommitteePeriodStart.Unix(), 10))
|
||||
}
|
||||
builder.WriteString(")\n Sync committee period end ")
|
||||
builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05"))
|
||||
builder.WriteString(" (epoch ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd))
|
||||
if data.verbose {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString(strconv.FormatInt(data.syncCommitteePeriodEnd.Unix(), 10))
|
||||
}
|
||||
builder.WriteString(")")
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
85
cmd/chain/time/output_internal_test.go
Normal file
85
cmd/chain/time/output_internal_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package chaintime
|
||||
|
||||
// import (
|
||||
// "context"
|
||||
// "testing"
|
||||
//
|
||||
// api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
// "github.com/stretchr/testify/require"
|
||||
// "github.com/wealdtech/ethdo/testutil"
|
||||
// )
|
||||
//
|
||||
// func TestOutput(t *testing.T) {
|
||||
// tests := []struct {
|
||||
// name string
|
||||
// dataOut *dataOut
|
||||
// res string
|
||||
// err string
|
||||
// }{
|
||||
// {
|
||||
// name: "Nil",
|
||||
// err: "no data",
|
||||
// },
|
||||
// {
|
||||
// name: "Empty",
|
||||
// dataOut: &dataOut{},
|
||||
// res: "No duties found",
|
||||
// },
|
||||
// {
|
||||
// name: "Present",
|
||||
// dataOut: &dataOut{
|
||||
// duty: &api.AttesterDuty{
|
||||
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
|
||||
// Slot: 1,
|
||||
// ValidatorIndex: 2,
|
||||
// CommitteeIndex: 3,
|
||||
// CommitteeLength: 4,
|
||||
// CommitteesAtSlot: 5,
|
||||
// ValidatorCommitteeIndex: 6,
|
||||
// },
|
||||
// },
|
||||
// res: "Validator attesting in slot 1 committee 3",
|
||||
// },
|
||||
// {
|
||||
// name: "JSON",
|
||||
// dataOut: &dataOut{
|
||||
// json: true,
|
||||
// duty: &api.AttesterDuty{
|
||||
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
|
||||
// Slot: 1,
|
||||
// ValidatorIndex: 2,
|
||||
// CommitteeIndex: 3,
|
||||
// CommitteeLength: 4,
|
||||
// CommitteesAtSlot: 5,
|
||||
// ValidatorCommitteeIndex: 6,
|
||||
// },
|
||||
// },
|
||||
// res: `{"pubkey":"0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95","slot":"1","validator_index":"2","committee_index":"3","committee_length":"4","committees_at_slot":"5","validator_committee_index":"6"}`,
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// for _, test := range tests {
|
||||
// t.Run(test.name, func(t *testing.T) {
|
||||
// res, err := output(context.Background(), test.dataOut)
|
||||
// if test.err != "" {
|
||||
// require.EqualError(t, err, test.err)
|
||||
// } else {
|
||||
// require.NoError(t, err)
|
||||
// require.Equal(t, test.res, res)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
95
cmd/chain/time/process.go
Normal file
95
cmd/chain/time/process.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package chaintime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
|
||||
Address: data.connection,
|
||||
Timeout: data.timeout,
|
||||
AllowInsecure: data.allowInsecureConnections,
|
||||
LogFallback: !data.quiet,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
}
|
||||
|
||||
chainTime, err := standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
results := &dataOut{
|
||||
debug: data.debug,
|
||||
quiet: data.quiet,
|
||||
verbose: data.verbose,
|
||||
}
|
||||
|
||||
// Calculate the slot given the input.
|
||||
switch {
|
||||
case data.slot != "":
|
||||
slot, err := strconv.ParseUint(data.slot, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse slot")
|
||||
}
|
||||
results.slot = phase0.Slot(slot)
|
||||
case data.epoch != "":
|
||||
epoch, err := util.ParseEpoch(ctx, chainTime, data.epoch)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse epoch")
|
||||
}
|
||||
results.slot = chainTime.FirstSlotOfEpoch(epoch)
|
||||
case data.timestamp != "":
|
||||
timestamp, err := time.Parse("2006-01-02T15:04:05-0700", data.timestamp)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse timestamp")
|
||||
}
|
||||
results.slot = chainTime.TimestampToSlot(timestamp)
|
||||
}
|
||||
|
||||
// Fill in the info given the slot.
|
||||
results.slotStart = chainTime.StartOfSlot(results.slot)
|
||||
results.slotEnd = chainTime.StartOfSlot(results.slot + 1)
|
||||
results.epoch = chainTime.SlotToEpoch(results.slot)
|
||||
results.epochStart = chainTime.StartOfEpoch(results.epoch)
|
||||
results.epochEnd = chainTime.StartOfEpoch(results.epoch + 1)
|
||||
if results.epoch >= chainTime.FirstEpochOfSyncPeriod(chainTime.AltairInitialSyncCommitteePeriod()) {
|
||||
results.hasSyncCommittees = true
|
||||
results.syncCommitteePeriod = chainTime.SlotToSyncCommitteePeriod(results.slot)
|
||||
results.syncCommitteePeriodEpochStart = chainTime.FirstEpochOfSyncPeriod(results.syncCommitteePeriod)
|
||||
results.syncCommitteePeriodEpochEnd = chainTime.FirstEpochOfSyncPeriod(results.syncCommitteePeriod + 1)
|
||||
results.syncCommitteePeriodStart = chainTime.StartOfEpoch(results.syncCommitteePeriodEpochStart)
|
||||
results.syncCommitteePeriodEnd = chainTime.StartOfEpoch(results.syncCommitteePeriodEpochEnd)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
109
cmd/chain/time/process_internal_test.go
Normal file
109
cmd/chain/time/process_internal_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright © 2021 - 2023 Weald Technology Trading.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// 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 chaintime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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
|
||||
dataIn *dataIn
|
||||
expected *dataOut
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "Slot",
|
||||
dataIn: &dataIn{
|
||||
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
timeout: 10 * time.Second,
|
||||
allowInsecureConnections: true,
|
||||
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),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Epoch",
|
||||
dataIn: &dataIn{
|
||||
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
timeout: 10 * time.Second,
|
||||
allowInsecureConnections: true,
|
||||
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),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Timestamp",
|
||||
dataIn: &dataIn{
|
||||
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
timeout: 10 * time.Second,
|
||||
allowInsecureConnections: true,
|
||||
timestamp: "2023-01-01T00:00:00+0000",
|
||||
},
|
||||
expected: &dataOut{
|
||||
epoch: 171112,
|
||||
epochStart: time.Unix(1672531031, 0),
|
||||
epochEnd: time.Unix(1672531415, 0),
|
||||
slot: 5475598,
|
||||
slotStart: time.Unix(1672531199, 0),
|
||||
slotEnd: time.Unix(1672531211, 0),
|
||||
hasSyncCommittees: true,
|
||||
syncCommitteePeriod: 668,
|
||||
syncCommitteePeriodStart: time.Unix(1672491095, 0),
|
||||
syncCommitteePeriodEnd: time.Unix(1672589399, 0),
|
||||
syncCommitteePeriodEpochStart: 171008,
|
||||
syncCommitteePeriodEpochEnd: 171264,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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.expected, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
cmd/chain/time/run.go
Normal file
55
cmd/chain/time/run.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2021, 2024 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 chaintime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the wallet create data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to set up command"), err)
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "", errors.New("operation timed out; try increasing with --timeout option")
|
||||
default:
|
||||
return "", errors.Join(errors.New("failed to process"), err)
|
||||
}
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Join(errors.New("failed to obtain output"), err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user