Compare commits

...

187 Commits

Author SHA1 Message Date
Jim McDonald
bf1d265742 Support 12 and 18-word mnemonics with passphrases.
Only supports single-word (no whitespace) passphrases for 12 and 18 word
mnemonics.

Fixes #87
2023-05-18 23:54:26 +01:00
Jim McDonald
7857e97057 Generate multiple validator exits.
Fixes #88
2023-05-18 23:34:40 +01:00
Jim McDonald
545665a79f Add generate-keystore to account derive. 2023-05-08 09:21:20 +01:00
Jim McDonald
394fd2bb04 Use String() for execution address output. 2023-05-01 15:44:29 +01:00
Jim McDonald
a7631a6a7f Initial support for deneb. 2023-04-29 12:20:29 +01:00
Jim McDonald
d174219ddc Fix test. 2023-04-16 19:43:02 +01:00
Jim McDonald
854f3061b9 Update docs. 2023-04-16 19:42:07 +01:00
Jim McDonald
6c34d25ebb Update docs to include validator specifier. 2023-04-16 19:25:59 +01:00
Jim McDonald
df34cef2bd Bump version. 2023-04-16 18:59:38 +01:00
Jim McDonald
e8513e60b2 Add validator withdrawal. 2023-04-15 10:17:11 +01:00
Jim McDonald
4aadee3fad Promote --json to global flag. 2023-04-15 09:29:23 +01:00
Jim McDonald
c7adf03aeb Bump version. 2023-04-13 18:15:43 +01:00
Jim McDonald
35944ca8c5 Update changelog. 2023-04-13 18:12:45 +01:00
Jim McDonald
083d625813 Allow use of validator index in account specifier.
Fixes #83
2023-04-13 18:11:46 +01:00
Jim McDonald
263db812b3 Update dependencies. 2023-04-09 23:24:48 +01:00
Jim McDonald
2c01d18195 Update parameter names; add fallback connection.
Update parameter names to meet newer standards.

Provide a fallback beacon node if none other supplied.
2023-04-09 23:19:19 +01:00
Jim McDonald
df99d43415 Merge branch 'master' of github.com:wealdtech/ethdo 2023-04-09 19:23:08 +01:00
Jim McDonald
d81546f2de Update docs to match new validator info options. 2023-04-09 19:20:02 +01:00
Jim McDonald
3d87a917af Merge pull request #75 from nflaig/patch-1
Document Lodestar settings in README
2023-03-31 19:06:41 +01:00
Nico Flaig
9240ed4857 Document Lodestar settings in README 2023-03-31 17:54:06 +02:00
Jim McDonald
18f9e8dca2 Fix test. 2023-03-21 08:42:01 +00:00
Jim McDonald
fbc24b81d2 Linting. 2023-03-21 08:36:39 +00:00
Jim McDonald
f898466395 Increase timeout for fetching offline preparation. 2023-03-21 08:04:33 +00:00
Jim McDonald
e496fa1977 Do not mask account errors. 2023-03-07 13:50:50 +00:00
Jim McDonald
d016326779 Merge pull request #71 from lastperson/patch-1
Fix the 'credentials set' example.
2023-03-03 11:16:45 +00:00
Oleksii Matiiasevych
ee14b5ee8e Fix the 'credentials set' example. 2023-03-03 14:03:05 +07:00
Jim McDonald
9ae927feab Handle keystore in validator credentials set. 2023-02-27 22:07:45 +00:00
Jim McDonald
7087a0a55c Allow keystore without path. 2023-02-27 11:16:36 +00:00
Jim McDonald
793a8d6d79 Allow keystore as source of validator. 2023-02-26 22:43:45 +00:00
Jim McDonald
e15b22dc3c Updates for go 1.20. 2023-02-26 19:45:47 +00:00
Jim McDonald
6ddd453900 Update dependencies. 2023-02-26 12:58:49 +00:00
Jim McDonald
24755099c0 Update workflow. 2023-02-26 12:57:22 +00:00
Jim McDonald
a5f97c2765 Update dependencies. 2023-02-26 12:44:45 +00:00
Jim McDonald
df59bc22de Unlock validator account for exit generation. 2023-02-26 12:42:19 +00:00
Jim McDonald
5fdd59dee2 Update changelog. 2023-02-21 22:17:55 +00:00
Jim McDonald
76b9010bc8 Update docs re public endpoint. 2023-02-21 22:12:38 +00:00
Jim McDonald
15d58e20d4 Use public endpoint if necessary.
Provide access to public endpoint if no other connection available.
2023-02-21 20:59:09 +00:00
Jim McDonald
ec9f5b8012 Show extra data as text if possible. 2023-02-19 21:50:36 +00:00
Jim McDonald
5c907bb8f8 Allow import of accounts with null name.
Fixes #59.
2023-02-18 22:40:45 +00:00
Jim McDonald
5e2cf9697c Merge pull request #61 from ladidan/master
fixing typo
2023-02-16 19:22:18 +00:00
Jim McDonald
7d3201826d Merge pull request #63 from infosecual/ethdo_bls_tests
BLSToExecutionChange message generation error reporting and tests
2023-02-16 18:58:58 +00:00
David Theodore
5e0a341eb1 fixed linting issues and failing tests 2023-02-16 11:36:02 -06:00
Jim McDonald
954a972a36 Update dependencies. 2023-02-16 13:36:34 +00:00
Jim McDonald
c0dd5dcfc6 Update testing. 2023-02-16 13:36:26 +00:00
Jim McDonald
fd394e3475 Reduce code duplication. 2023-02-16 13:35:51 +00:00
Jim McDonald
5dcdf9c11f Handle account-based validator exit correctly. 2023-02-16 13:20:11 +00:00
David Theodore
14f559ab8b Merge branch 'wealdtech:master' into ethdo_bls_tests 2023-02-13 11:10:24 -06:00
infosecual
6bb79f821c more linting 2023-02-13 11:08:42 -06:00
infosecual
6dcd3c9978 Addressed issues for PR63 2023-02-13 10:51:06 -06:00
Jim McDonald
d5acd2f842 Update workflow. 2023-02-12 23:15:51 +00:00
Jim McDonald
1395b7159f Linting. 2023-02-12 21:22:39 +00:00
David Theodore
548442c33b renamed some tests 2023-02-12 12:59:01 -06:00
David Theodore
f8f7eb26e8 added/removed comments 2023-02-12 12:52:18 -06:00
David Theodore
ca10ba7411 generateOperationsFromPrivateKey errs and test++ 2023-02-10 14:47:13 -06:00
David Theodore
d8ccf67be8 spelling error in comments 2023-02-10 13:43:55 -06:00
David Theodore
8fba19597e more err reporting and tests 2023-02-10 13:42:25 -06:00
David Theodore
c034dfaf53 Merge branch 'wealdtech:master' into ethdo_bls_tests 2023-02-09 21:09:03 -06:00
David Theodore
4576978347 add tesing coverage for generateOperationsFromMnem 2023-02-09 21:07:56 -06:00
Jim McDonald
97c409fde6 Linting. 2023-02-09 19:40:19 +00:00
Jim McDonald
5b2e62c29e Error if no credential change operations generated. 2023-02-09 19:37:26 +00:00
David Theodore
c7025d99dd generateOperationFromMnemonicAndValidator err++ 2023-02-09 11:42:55 -06:00
David Theodore
111f5bf627 TestGenerateOperationFromMnemonicAndValidator++ 2023-02-08 17:07:15 -06:00
David Theodore
7de8ad8a59 Merge branch 'ethdo_bls_tests' of github.com:infosecual/ethdo into ethdo_bls_tests 2023-02-08 16:16:27 -06:00
David Theodore
31336dd5ce added to TestGenerateOperationFromMnemonicAndPath 2023-02-08 16:16:00 -06:00
ladidan
47104b31a4 Update validatorexit.go 2023-02-08 18:41:30 +01:00
Jim McDonald
59200e796a Fix issue obtaining capella epoch. 2023-02-02 09:28:44 +00:00
Jim McDonald
c91538644f Bump version. 2023-01-31 15:36:16 +00:00
Jim McDonald
3140fc5b8a Merge pull request #56 from joaocenoura/master
Allow ethdo credential set to use all validators from offline-preparation.json
2023-01-31 15:34:42 +00:00
Jim McDonald
a2afd37a97 Provide better error messages when offline preparation file cannot be read. 2023-01-31 15:25:40 +00:00
Joao Rodrigues
d453ba9303 post review changes: moving code outside for loop 2023-01-31 13:32:28 +00:00
Joao Rodrigues
d0b278c0ec removing return err 2023-01-31 11:17:28 +00:00
Joao Rodrigues
27a59c031b align comment with the rest of the code block 2023-01-30 17:45:02 +00:00
Joao Rodrigues
1bc139c591 post review changes 2023-01-30 17:40:37 +00:00
Jim McDonald
7e8db1cd2e Handle false positive linting issue. 2023-01-28 00:55:28 +00:00
Jim McDonald
864bb30244 Tweak linting. 2023-01-28 00:53:20 +00:00
Jim McDonald
4209f725ba Tweak linting. 2023-01-28 00:51:24 +00:00
Jim McDonald
d01e789c8a Downgrade golangci-lint. 2023-01-28 00:49:34 +00:00
Jim McDonald
85a0590d55 Linting. 2023-01-28 00:42:24 +00:00
Jim McDonald
b9ba1ec1c2 Linting. 2023-01-28 00:34:23 +00:00
Jim McDonald
29bffd0dbe Support additional word list languages.
Fixes #58
2023-01-27 23:28:36 +00:00
Jim McDonald
e7a2c600f1 Increase minimum timeout for some commands to 2 minutes. 2023-01-27 21:53:03 +00:00
Joao Rodrigues
f2a5a93195 document usage for changing withdrawal credentials with private key only 2023-01-27 12:54:46 +00:00
Jim McDonald
095c246efb Merge pull request #57 from fredriksvantes/patch-1
Fixing typo
2023-01-26 15:37:21 +00:00
Fredrik Svantes
581d22c7d7 Fixing typo
Changing "Not broadcasting credentials operations" to "Not broadcasting exit operation"
2023-01-26 13:48:39 +01:00
Joao Rodrigues
1ca1e1f2d6 skip validators from which validator credentials do not match supplied withdrawal credentials 2023-01-25 20:57:44 +00:00
Joao Rodrigues
7c88b7c082 Allow ethdo credential set to use all validators from offline-preparation.json 2023-01-25 19:33:11 +00:00
Jim McDonald
4d351e6d3d Update changelog 2023-01-25 06:55:55 +00:00
Jim McDonald
c46e9740d4 Merge pull request #55 from 0xTylerHolmes/master
Correct domain type for exits
2023-01-25 06:52:51 +00:00
z3n
f9cb1054c0 Correct domain type for exits 2023-01-24 17:08:43 -06:00
Jim McDonald
4e2fa63f30 Bump version. 2023-01-17 13:49:01 +00:00
Jim McDonald
ca5eec9c8c Update code for spec change. 2023-01-13 22:34:04 +00:00
Jim McDonald
582930d982 Verify operations on load. 2023-01-02 22:04:07 +00:00
Jim McDonald
5a572abc6f Tidy-ups. 2023-01-02 21:32:14 +00:00
Jim McDonald
a2d018c321 Update dependencies. 2022-12-28 19:39:16 +00:00
Jim McDonald
12f3154157 Update release workflow. 2022-12-22 01:26:09 +00:00
Jim McDonald
51689db315 Fix typo. 2022-12-21 23:23:36 +00:00
Jim McDonald
1a8897ff0f Fix typo. 2022-12-21 23:18:04 +00:00
Jim McDonald
5ee92bee78 Tidy up validator credentials set.
Provide more help and methods to generate credentials change operations.
2022-12-21 23:11:43 +00:00
Jim McDonald
eba7a0c88a Tidy workflow. 2022-12-21 18:28:35 +00:00
Jim McDonald
f6a6224968 Update dockerfile. 2022-12-19 22:43:29 +00:00
Jim McDonald
60e8878dc3 Update for altered BLSToExecutionChange API. 2022-12-19 22:42:56 +00:00
Jim McDonald
41d9160b63 Update release workflow. 2022-12-19 22:36:05 +00:00
Jim McDonald
b3d4c9af08 Allow S3 credentials. 2022-12-19 22:35:46 +00:00
Jim McDonald
5a8a13d8f3 Merge pull request #50 from Myu-Unix/master
Update root.go
2022-12-13 22:03:41 +00:00
Myu-Unix
d94e1551a7 Update root.go
Fix typo (RET vs REST)
2022-12-13 22:04:42 +01:00
Jim McDonald
03e7f15d04 Update workflow. 2022-12-12 14:20:43 +00:00
Jim McDonald
f14233ccea Update workflow. 2022-12-12 13:06:35 +00:00
Jim McDonald
d5fc8fed37 Remove ioutil. 2022-12-12 11:38:07 +00:00
Jim McDonald
2bd8fdee5e Update workflows. 2022-12-12 11:31:14 +00:00
Jim McDonald
ed830d7901 Bump version 2022-12-10 22:32:56 +00:00
Jim McDonald
6b127796f9 Add validator information to "chain status". 2022-12-10 13:37:13 +00:00
Jim McDonald
720edf8475 Bump version. 2022-12-10 11:59:53 +00:00
Jim McDonald
e27fae11e2 Better verbose block output. 2022-12-09 17:02:07 +00:00
Jim McDonald
f5d31cf827 Support capella in more commands. 2022-12-09 14:21:51 +00:00
Jim McDonald
317c22366e Merge pull request #49 from barnabasbusa/bbusa/smallfix
fix: small typo fixes, and clarifying --connections, base-dir
2022-12-07 13:58:00 +00:00
Barnabas Busa
98717b9da0 clarify README for connections 2022-12-07 14:49:33 +01:00
Jim McDonald
c95ba66a0b Ensure only one specifier is provided. 2022-12-06 14:40:41 +00:00
Jim McDonald
40a7c57abd Tidy up debug messages. 2022-12-05 20:39:06 +00:00
Jim McDonald
7743481b8c Use constant for file name. 2022-12-05 10:41:55 +00:00
Jim McDonald
6a5251c738 Do not allow deleting non-fileystem wallets. 2022-12-05 10:41:15 +00:00
Jim McDonald
ceb5195b69 Provide support for additional S3 store options. 2022-12-05 10:39:52 +00:00
Jim McDonald
53f0de3fb9 Remove check for capella.
Remove a check in the validator credentials set command for the chain to
be running capella prior, as it is not available offline and no longer
necessary online.

Fixes #48.
2022-11-30 06:41:21 +00:00
Jim McDonald
16a94d726a Withdrawal credentials operations in advance.
Allow withdrawal credentials change operations to be generated prior to
the Capella hard fork, allowing queueing if the beacon node supports it.
2022-11-29 14:20:46 +00:00
Jim McDonald
9d00f6bafc Update dependencies. 2022-11-29 12:41:22 +00:00
Jim McDonald
d274ab3db0 Release 1.26.0 2022-11-24 15:15:22 +00:00
Jim McDonald
ad3d8606fd Update workflow. 2022-10-31 17:56:35 +00:00
Jim McDonald
30455e7c43 Merge pull request #47 from wealdtech/credentials-set
Set withdrawal credentials.
2022-10-31 17:41:54 +00:00
Jim McDonald
7eb2c68a19 Update operation and docs. 2022-10-29 12:15:14 +01:00
Jim McDonald
2d96f7cb13 Update operation and docs. 2022-10-29 12:10:31 +01:00
Jim McDonald
fd1e4a97bb Initial work on validator summary. 2022-10-16 23:35:44 +01:00
Jim McDonald
be2270c543 Increase security of shamir number generation. 2022-10-16 22:50:30 +01:00
Jim McDonald
0c36239b8b Update output and associated documentation. 2022-10-06 18:19:38 +00:00
Jim McDonald
f78b2922ec Tidy-ups. 2022-10-06 16:15:45 +00:00
Jim McDonald
bcf6ffdaf0 Linting. 2022-10-06 15:33:01 +00:00
Jim McDonald
9fc184f6a1 Update documentation. 2022-10-06 15:24:15 +00:00
Jim McDonald
c9a30a6e4b Update documentation. 2022-10-06 15:20:08 +00:00
Jim McDonald
1ec6ddc914 Set withdrawal credentials. 2022-10-06 15:14:23 +00:00
Jim McDonald
2c96ef958e Use standard function to obtain best public key. 2022-10-05 13:54:41 +00:00
Jim McDonald
3c10131c45 Use util func to obtain validator from input. 2022-10-02 20:53:10 +01:00
Jim McDonald
fe0bfd4f87 Add more information to "epoch summary". 2022-10-01 22:52:20 +01:00
Jim McDonald
290413f115 Update install instructions. 2022-09-27 19:53:39 +01:00
Jim McDonald
4aa6bef6a3 Update release. 2022-08-28 21:28:54 +01:00
Jim McDonald
1b0f4e2803 Bump version. 2022-08-18 14:42:57 +01:00
Jim McDonald
301224748c Update workflow. 2022-08-18 14:28:23 +01:00
Jim McDonald
1e15b836c2 Bump version. 2022-08-11 08:16:55 +01:00
Jim McDonald
1e709b7592 Remove mandatory connection parameter.
The connection parameter is no longer mandatory, in that ethdo will
attempt to obtain a connection using well-known ports if no override is
supplied.  As such the `--connection` parameter can be omitted and so is
not force-required as part of the command initialisation.
2022-08-11 08:11:55 +01:00
Jim McDonald
8744a85cb7 Merge pull request #45 from tcrossland/master
feat: support block analyze on bellatrix
2022-08-06 08:16:58 +01:00
Tom Crossland
92ad77d8f5 feat: support block analyze on bellatrix 2022-08-04 17:25:20 +02:00
Jim McDonald
2298640e4c Merge pull request #44 from aaron-alderman/fix/deposit-message-root-verification
Add deposit message root match verification
2022-07-16 16:27:24 +01:00
Jim McDonald
5baef59672 Tidy up streaming output. 2022-07-13 11:21:09 +01:00
Aaron Alderman
e54e8affa7 Add deposit message root match verification 2022-07-12 10:47:27 +08:00
Jim McDonald
97fa04a7b2 Bump version. 2022-07-10 18:28:54 +01:00
Jim McDonald
4977ee82e5 Add dpeosit signature verirication to "deposit verify". 2022-07-10 12:33:19 +01:00
Jim McDonald
090680366c Do not print 0-value deposit validator information. 2022-06-23 09:52:45 +01:00
Jim McDonald
531c86847f Tidy up tests. 2022-06-22 07:51:22 +01:00
Jim McDonald
446e437531 Update docs. 2022-06-22 07:51:14 +01:00
Jim McDonald
63d8ccf1a0 Add "proposer duties". 2022-06-22 07:50:53 +01:00
Jim McDonald
77abe0e158 Add sepolia support. 2022-06-21 10:05:29 +01:00
Jim McDonald
547f8d9e71 Fix potential crash when new validator is activated. 2022-06-20 16:18:28 +01:00
Jim McDonald
e144217f25 Add "validator yield". 2022-06-12 11:17:59 +01:00
Jim McDonald
d919810ce1 Tidy up eth1 votes output. 2022-06-10 18:13:49 +01:00
Jim McDonald
0bdf68edf6 Do not fetch future states. 2022-06-01 12:42:40 +01:00
Jim McDonald
b24341b7da Do not fetch future states. 2022-06-01 12:42:12 +01:00
Jim McDonald
384ee3dcaa Bump version. 2022-06-01 12:22:23 +01:00
Jim McDonald
3e8b1a6dad Add "chain eth1votes". 2022-06-01 12:21:40 +01:00
Jim McDonald
d2dec4a444 Tidy up formatting. 2022-05-31 13:39:36 +01:00
Jim McDonald
7e171bdb1e Provide more epoch summary information. 2022-05-30 21:52:30 +01:00
Jim McDonald
0cedf79a89 Bump version. 2022-05-30 16:08:58 +01:00
Jim McDonald
65ad1248ce epoch summary no sync committee information pre-Altair. 2022-05-30 16:08:31 +01:00
Jim McDonald
e1180f97ce error on attester inclusion without required params. 2022-05-26 22:29:21 +01:00
Jim McDonald
394b4a7cd2 Bump version. 2022-05-19 13:00:38 +01:00
Jim McDonald
fd574aae34 Tidy up tests. 2022-05-19 13:00:05 +01:00
Jim McDonald
7fe503f51d Add ropsten support. 2022-05-19 12:59:48 +01:00
Jim McDonald
6bfb0ef098 Add validator credentials get command. 2022-05-10 15:02:41 +01:00
Jim McDonald
46c667d387 Add "chain queues". 2022-04-23 08:51:30 +01:00
Jim McDonald
50f4a9cace Update workflow. 2022-04-15 08:09:06 +01:00
Jim McDonald
cd20875744 Provide clearer error if attempting to import to an HD wallet. 2022-03-24 17:28:46 +00:00
Jim McDonald
84f682a0da Guess connection if none supplied. 2022-03-24 09:46:11 +00:00
Jim McDonald
6389b7dfbd Test coverage. 2022-03-23 22:14:34 +00:00
Jim McDonald
0ef65b8bda Allow account import from keystores. 2022-03-23 22:08:10 +00:00
Jim McDonald
4426c3279d Allow account import from keystores. 2022-03-23 22:06:48 +00:00
Jim McDonald
883f9f834e Update changelog. 2022-03-17 20:21:30 +00:00
Jim McDonald
e0fd3df9dd Add "epoch summary" command. 2022-03-17 20:20:52 +00:00
Jim McDonald
e6d3c67e39 Bump version. 2022-03-16 11:19:52 +00:00
Jim McDonald
6d0a0225c2 Tidy up graffiti and execution output for block info. 2022-03-16 08:52:28 +00:00
Jim McDonald
d60d8beb0b Bump version. 2022-03-11 14:16:49 +00:00
Jim McDonald
a657b3bc24 Do not show execution payload if empty. 2022-03-11 14:16:09 +00:00
263 changed files with 14210 additions and 3102 deletions

View File

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

View File

@@ -4,153 +4,203 @@ on:
push:
tags:
- 'v*'
- 't*'
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.16
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@v3
with:
go-version: '^1.20'
- 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@v3
- 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 install github.com/wealdtech/xgo@latest
- 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" 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 release files
run: |
mv ethdo-windows-4.0-amd64.exe ethdo.exe
zip --junk-paths ethdo-${RELEASE_VERSION}-windows-exe.zip ethdo.exe
sha256sum ethdo-${RELEASE_VERSION}-windows-exe.zip >ethdo-${RELEASE_VERSION}-windows.sha256
- 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
sha256sum ethdo-${RELEASE_VERSION}-linux-amd64.tar.gz >ethdo-${RELEASE_VERSION}-linux-amd64.sha256
# 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@v3
with:
go-version: '^1.20'
# - name: Create linux ARM64 tgz file
# run: |
# mv ethdo-linux-arm64 ethdo
# tar zcf ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz ethdo
# sha256sum ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz >ethdo-${RELEASE_VERSION}-linux-arm64.sha256
- name: Check out repository into the Go module directory
uses: actions/checkout@v3
- 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: true
prerelease: false
- name: Select correct tag
run: git checkout ${{ github.ref_name }}
- name: Upload windows checksum file
id: upload-release-asset-windows-checksum
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-windows.sha256
asset_name: ethdo-${{ env.RELEASE_VERSION }}-windows.sha256
asset_content_type: text/plain
- name: Fetch repository dependencies
run: go get -v -t ./...
- 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: 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 linux AMD64 checksum file
id: upload-release-asset-linux-amd64-checksum
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-amd64.sha256
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-amd64.sha256
asset_content_type: text/plain
- 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
- 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
# 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@v3
with:
go-version: '^1.20'
# - name: Upload linux ARM64 checksum file
# id: upload-release-asset-linux-arm64-checksum
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
# asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
# asset_content_type: text/plain
- name: Check out repository into the Go module directory
uses: actions/checkout@v3
# - 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 -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

15
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: test
on:
push:
branches:
- master
pull_request:
jobs:
test:
runs-on: ubuntu-22.04
steps:
- uses: actions/setup-go@v3
with:
go-version: '1.20'
- uses: actions/checkout@v3
- uses: n8maninger/action-golang-test@v1

3
.gitignore vendored
View File

@@ -18,5 +18,8 @@ coverage.html
# Vim
*.sw?
# Local JSON files
*.json
# Local TODO
TODO.md

170
.golangci.yml Normal file
View File

@@ -0,0 +1,170 @@
# 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.
# 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
# Which files to skip: they will be analyzed, but issues from them won't be reported.
# Default value is empty list,
# but 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.
skip-files:
- ".*_ssz\\.go$"
# 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:
# Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
#
# Multiple can be specified by separating them by comma, output can be provided
# for each of them by separating format name and path by colon symbol.
# Output path can be either `stdout`, `stderr` or path to the file to write to.
# Example: "checkstyle:report.json,colored-line-number"
#
# Default: colored-line-number
# format: json
# Print lines of code with issue.
# Default: true
# print-issued-lines: false
# Print linter name in the end of issue text.
# Default: true
# print-linter-name: false
# Make issues output unique by line.
# Default: true
# uniq-by-line: false
# Add a prefix to the output file references.
# Default is no prefix.
# path-prefix: ""
# Sort results by: filepath, line and column.
# sort-results: true
# All available settings of specific linters.
linters-settings:
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
- deadcode
- dupl
- errorlint
- exhaustive
- exhaustivestruct
- exhaustruct
- forbidigo
- forcetypeassert
- funlen
- gci
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- goerr113
- goheader
- golint
- gomnd
- ifshort
- interfacer
- ireturn
- lll
- maintidx
- maligned
- musttag
- nestif
- nilnil
- nlreturn
- nolintlint
- nosnakecase
- promlinter
- rowserrcheck
- scopelint
- sqlclosecheck
- structcheck
- unparam
- varcheck
- varnamelen
- wastedassign
- wrapcheck
- wsl

View File

@@ -1,3 +1,117 @@
dev:
- 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
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

View File

@@ -1,4 +1,4 @@
FROM golang:1.17-bullseye as builder
FROM golang:1.20-bullseye as builder
WORKDIR /app

View File

@@ -11,6 +11,7 @@ A command-line tool for managing common tasks in Ethereum 2.
- [Binaries](#binaries)
- [Docker](#docker)
- [Source](#source)
- [Setting up](#setting-up)
- [Usage](#usage)
- [Maintainers](#maintainers)
- [Contribute](#contribute)
@@ -34,7 +35,7 @@ docker pull wealdtech/ethdo
`ethdo` is a standard Go program which can be installed with:
```sh
GO111MODULE=on go get github.com/wealdtech/ethdo
go install github.com/wealdtech/ethdo@latest
```
Note that `ethdo` requires at least version 1.13 of go to operate. The version of go can be found with `go version`.
@@ -61,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,24 @@ 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": {
"bucket":"mybucketname",
"path":"path/in/bucket",
"passphrase":"secret"
}
}
}
```
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 +171,15 @@ If set, the `--debug` argument will output additional information about the oper
Commands will have an exit status of 0 on success and 1 on failure. The specific definition of success is specified in the help for each command.
### Validator specifier
Ethereum validators can be specified in a number of different ways. The options are:
- an `ethdo` account, in the format _wallet_/_account_. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied, with a passphrase (for local accounts) or authority (for remote accounts)
- the validator's 48-byte public key. It is not possible to use the a validator specified in this way to sign validator-related operations
- a keystore, supplied either as direct JSON or as a path to a keystore on the local filesystem. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied
- the validator's numeric index. It is not possible to use a validator specified in this way to sign validator-related operations. Note that this only works with on-chain operations, as the validator's index must be resolved to its public key
## Passphrase strength
`ethdo` will by default not allow creation or export of accounts or wallets with weak passphrases. If a weak pasphrase is used then `ethdo` will refuse to continue.
@@ -153,7 +210,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

304
beacon/chaininfo.go Normal file
View File

@@ -0,0 +1,304 @@
// 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"
"strconv"
"strings"
consensusclient "github.com/attestantio/go-eth2-client"
"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
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"`
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: fmt.Sprintf("%d", c.Version),
Validators: c.Validators,
GenesisValidatorsRoot: fmt.Sprintf("%#x", c.GenesisValidatorsRoot),
Epoch: fmt.Sprintf("%d", c.Epoch),
GenesisForkVersion: fmt.Sprintf("%#x", c.GenesisForkVersion),
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 < 2 {
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.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: 2,
Validators: make([]*ValidatorInfo, 0),
Epoch: chainTime.CurrentEpoch(),
}
// Obtain validators.
validators, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, "head", nil)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validators")
}
for _, validator := range validators {
res.Validators = append(res.Validators, &ValidatorInfo{
Index: validator.Index,
Pubkey: validator.Validator.PublicKey,
WithdrawalCredentials: validator.Validator.WithdrawalCredentials,
State: validator.Status,
})
}
// Genesis validators root obtained from beacon node.
genesis, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis information")
}
res.GenesisValidatorsRoot = genesis.GenesisValidatorsRoot
// Fetch the genesis fork version from the specification.
spec, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["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 current fork version from the fork schedule.
forkSchedule, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain fork schedule")
}
for i := range forkSchedule {
if forkSchedule[i].Epoch <= res.Epoch {
res.CurrentForkVersion = forkSchedule[i].CurrentVersion
}
}
blsToExecutionChangeDomainType, exists := spec["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
if !exists {
return nil, errors.New("failed to obtain DOMAIN_BLS_TO_EXECUTION_CHANGE")
}
copy(res.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType[:])
voluntaryExitDomainType, exists := spec["DOMAIN_VOLUNTARY_EXIT"].(phase0.DomainType)
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
View 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)
}

View File

@@ -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) {
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2023 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -28,9 +28,10 @@ type dataIn struct {
// Output options.
showPrivateKey bool
showWithdrawalCredentials bool
generateKeystore bool
}
func input(ctx context.Context) (*dataIn, error) {
func input(_ context.Context) (*dataIn, error) {
data := &dataIn{}
// Quiet.
@@ -54,5 +55,8 @@ func input(ctx context.Context) (*dataIn, error) {
// Show withdrawal credentials.
data.showWithdrawalCredentials = viper.GetBool("show-withdrawal-credentials")
// Generate keystore.
data.generateKeystore = viper.GetBool("generate-keystore")
return data, nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2023 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,18 +15,26 @@ package accountderive
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
ethutil "github.com/wealdtech/go-eth2-util"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
type dataOut struct {
showPrivateKey bool
showWithdrawalCredentials bool
generateKeystore bool
key *e2types.BLSPrivateKey
path string
}
func output(ctx context.Context, data *dataOut) (string, error) {
@@ -37,17 +45,58 @@ func output(ctx context.Context, data *dataOut) (string, error) {
return "", errors.New("no key")
}
if data.generateKeystore {
return outputKeystore(ctx, data)
}
builder := strings.Builder{}
if data.showPrivateKey {
builder.WriteString(fmt.Sprintf("Private key: %#x\n", data.key.Marshal()))
}
builder.WriteString(fmt.Sprintf("Public key: %#x", data.key.PublicKey().Marshal()))
if data.showWithdrawalCredentials {
withdrawalCredentials := util.SHA256(data.key.PublicKey().Marshal())
withdrawalCredentials := ethutil.SHA256(data.key.PublicKey().Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
builder.WriteString(fmt.Sprintf("\nWithdrawal credentials: %#x", withdrawalCredentials))
builder.WriteString(fmt.Sprintf("Withdrawal credentials: %#x\n", withdrawalCredentials))
}
if !(data.showPrivateKey || data.showWithdrawalCredentials) {
builder.WriteString(fmt.Sprintf("Public key: %#x\n", data.key.PublicKey().Marshal()))
}
return builder.String(), nil
}
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"] = fmt.Sprintf("%x", data.key.PublicKey().Marshal())
ks["version"] = 4
ks["path"] = data.path
ks["crypto"] = crypto
out, err := json.Marshal(ks)
if err != nil {
return "", errors.Wrap(err, "failed to marshal keystore JSON")
}
keystoreFilename := fmt.Sprintf("keystore-%s-%d.json", strings.ReplaceAll(data.path, "/", "_"), time.Now().Unix())
if err := os.WriteFile(keystoreFilename, out, 0o600); err != nil {
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", keystoreFilename))
}
return "", errors.New("not implemented")
}

View File

@@ -64,7 +64,7 @@ func TestOutput(t *testing.T) {
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
showPrivateKey: true,
},
needs: []string{"Public key", "Private key"},
needs: []string{"Private key"},
},
{
name: "WithdrawalCredentials",
@@ -72,7 +72,7 @@ func TestOutput(t *testing.T) {
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
showWithdrawalCredentials: true,
},
needs: []string{"Public key", "Withdrawal credentials"},
needs: []string{"Withdrawal credentials"},
},
{
name: "All",
@@ -81,7 +81,7 @@ func TestOutput(t *testing.T) {
showPrivateKey: true,
showWithdrawalCredentials: true,
},
needs: []string{"Public key", "Private key", "Withdrawal credentials"},
needs: []string{"Private key", "Withdrawal credentials"},
},
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2023 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,57 +15,34 @@ package accountderive
import (
"context"
"regexp"
"strings"
"github.com/pkg/errors"
"github.com/tyler-smith/go-bip39"
util "github.com/wealdtech/go-eth2-util"
"golang.org/x/text/unicode/norm"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// pathRegex is the regular expression that matches an HD path.
var pathRegex = regexp.MustCompile("^m/[0-9]+/[0-9]+(/[0-9+])+")
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
// If there are more than 24 words we treat the additional characters as the passphrase.
mnemonicParts := strings.Split(data.mnemonic, " ")
mnemonicPassphrase := ""
if len(mnemonicParts) > 24 {
data.mnemonic = strings.Join(mnemonicParts[:24], " ")
mnemonicPassphrase = strings.Join(mnemonicParts[24:], " ")
}
// Normalise the input.
data.mnemonic = string(norm.NFKD.Bytes([]byte(data.mnemonic)))
mnemonicPassphrase = string(norm.NFKD.Bytes([]byte(mnemonicPassphrase)))
if !bip39.IsMnemonicValid(data.mnemonic) {
return nil, errors.New("mnemonic is invalid")
}
// Create seed from mnemonic and passphrase.
seed := bip39.NewSeed(data.mnemonic, mnemonicPassphrase)
// Ensure the path is valid.
match := pathRegex.Match([]byte(data.path))
if !match {
return nil, errors.New("path does not match expected format m/…")
}
// Derive private key from seed and path.
key, err := util.PrivateKeyFromSeedAndPath(seed, data.path)
account, err := util.ParseAccount(ctx, data.mnemonic, []string{data.path}, true)
if err != nil {
return nil, errors.Wrap(err, "failed to generate key")
return nil, errors.Wrap(err, "failed to derive account")
}
key, err := account.(e2wtypes.AccountPrivateKeyProvider).PrivateKey(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account private key")
}
results := &dataOut{
showPrivateKey: data.showPrivateKey,
showPrivateKey: data.showPrivateKey,
showWithdrawalCredentials: data.showWithdrawalCredentials,
key: key,
generateKeystore: data.generateKeystore,
key: key.(*e2types.BLSPrivateKey),
path: data.path,
}
return results, nil

View File

@@ -40,7 +40,7 @@ func TestProcess(t *testing.T) {
dataIn: &dataIn{
path: "m/12381/3600/0/0",
},
err: "mnemonic is invalid",
err: "failed to derive account: no account specified",
},
{
name: "MnemonicInvalid",
@@ -48,14 +48,14 @@ func TestProcess(t *testing.T) {
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
path: "m/12381/3600/0/0",
},
err: "mnemonic is invalid",
err: "failed to derive account: mnemonic is invalid",
},
{
name: "PathMissing",
dataIn: &dataIn{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
},
err: "path does not match expected format m/…",
err: "failed to derive account: path does not match expected format m/…",
},
{
name: "PathInvalid",
@@ -63,7 +63,7 @@ func TestProcess(t *testing.T) {
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
path: "n/12381/3600/0/0",
},
err: "path does not match expected format m/…",
err: "failed to derive account: path does not match expected format m/…",
},
{
name: "Good",

View File

@@ -15,6 +15,7 @@ package accountderive
import (
"context"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@@ -45,5 +46,5 @@ func Run(cmd *cobra.Command) (string, error) {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
return strings.TrimSuffix(results, "\n"), nil
}

View File

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

View File

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

View File

@@ -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")
}

View File

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

View File

@@ -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")
}

View File

@@ -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)
}
}

View File

@@ -47,23 +47,19 @@ In quiet mode this will return 0 if the inputs can derive an account account, ot
func init() {
accountCmd.AddCommand(accountDeriveCmd)
accountFlags(accountDeriveCmd)
accountDeriveCmd.Flags().String("mnemonic", "", "mnemonic from which to derive the HD seed")
accountDeriveCmd.Flags().String("path", "", "path from which to derive the account")
accountDeriveCmd.Flags().Bool("show-private-key", false, "show private key for derived account")
accountDeriveCmd.Flags().Bool("show-withdrawal-credentials", false, "show withdrawal credentials for derived account")
accountDeriveCmd.Flags().Bool("generate-keystore", false, "generate a keystore for the derived account")
}
func accountDeriveBindings() {
if err := viper.BindPFlag("mnemonic", accountDeriveCmd.Flags().Lookup("mnemonic")); err != nil {
func accountDeriveBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("show-private-key", cmd.Flags().Lookup("show-private-key")); err != nil {
panic(err)
}
if err := viper.BindPFlag("path", accountDeriveCmd.Flags().Lookup("path")); err != nil {
if err := viper.BindPFlag("show-withdrawal-credentials", cmd.Flags().Lookup("show-withdrawal-credentials")); err != nil {
panic(err)
}
if err := viper.BindPFlag("show-private-key", accountDeriveCmd.Flags().Lookup("show-private-key")); err != nil {
panic(err)
}
if err := viper.BindPFlag("show-withdrawal-credentials", accountDeriveCmd.Flags().Lookup("show-withdrawal-credentials")); err != nil {
if err := viper.BindPFlag("generate-keystore", cmd.Flags().Lookup("generate-keystore")); err != nil {
panic(err)
}
}

View File

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

View File

@@ -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)

View File

@@ -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.",

View File

@@ -17,7 +17,7 @@ import (
"github.com/spf13/cobra"
)
// attestationCmd represents the attestation command
// attestationCmd represents the attestation command.
var attestationCmd = &cobra.Command{
Use: "attestation",
Short: "Obtain information about an Ethereum 2 attestation",

View File

@@ -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) {
}

View File

@@ -18,9 +18,11 @@ import (
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
@@ -31,13 +33,11 @@ type dataIn struct {
verbose bool
debug bool
json bool
// Chain information.
slotsPerEpoch uint64
// Operation.
account string
pubKey string
validator string
eth2Client eth2client.Service
epoch spec.Epoch
chainTime chaintime.Service
epoch phase0.Epoch
}
func input(ctx context.Context) (*dataIn, error) {
@@ -52,38 +52,37 @@ func input(ctx context.Context) (*dataIn, error) {
data.debug = viper.GetBool("debug")
data.json = viper.GetBool("json")
// Account or pubkey.
if viper.GetString("account") == "" && viper.GetString("pubkey") == "" {
return nil, errors.New("account or pubkey is required")
// Validator.
data.validator = viper.GetString("validator")
if data.validator == "" {
return nil, errors.New("validator is required")
}
data.account = viper.GetString("account")
data.pubKey = viper.GetString("pubkey")
// Ethereum 2 client.
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, err
}
// Required data.
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
return nil, errors.Wrap(err, "failed to set up chaintime service")
}
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
// Epoch
epoch := viper.GetInt64("epoch")
if epoch == -1 {
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))
// Epoch.
data.epoch, err = util.ParseEpoch(ctx, data.chainTime, viper.GetString("epoch"))
if err != nil {
return nil, err
}
data.epoch = spec.Epoch(epoch)
return data, nil
}

View File

@@ -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: "account or pubkey is required",
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 beacon node: failed to connect to Ethereum 2 client with any known method",
},
}

View File

@@ -30,7 +30,7 @@ type dataOut struct {
duty *api.AttesterDuty
}
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")
}

View File

@@ -15,16 +15,12 @@ package attesterduties
import (
"context"
"encoding/hex"
"fmt"
"strings"
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/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
@@ -32,43 +28,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("no data")
}
var account e2wtypes.Account
var err error
if data.account != "" {
ctx, cancel := context.WithTimeout(ctx, data.timeout)
defer cancel()
_, account, err = util.WalletAndAccountFromPath(ctx, data.account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.pubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", data.pubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", data.pubKey))
}
}
// Fetch validator
pubKeys := make([]spec.BLSPubKey, 1)
pubKey, err := util.BestPublicKey(account)
validator, err := util.ParseValidator(ctx, data.eth2Client.(eth2client.ValidatorsProvider), data.validator, "head")
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for account")
}
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")
}
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
var validator *api.Validator
for _, v := range validators {
validator = v
return nil, err
}
results := &dataOut{
@@ -77,7 +39,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
verbose: data.verbose,
}
duty, err := duty(ctx, data.eth2Client, 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")
}
@@ -87,7 +49,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
if err != nil {

View File

@@ -18,9 +18,11 @@ import (
"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) {
@@ -33,6 +35,12 @@ func TestProcess(t *testing.T) {
)
require.NoError(t, err)
chainTime, err := standardchaintime.New(context.Background(),
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
)
require.NoError(t, err)
tests := []struct {
name string
dataIn *dataIn
@@ -45,10 +53,9 @@ func TestProcess(t *testing.T) {
{
name: "Client",
dataIn: &dataIn{
eth2Client: eth2Client,
slotsPerEpoch: 32,
pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
epoch: 100,
eth2Client: eth2Client,
chainTime: chainTime,
validator: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
},
},
}

View File

@@ -15,7 +15,6 @@ package attesterinclusion
import (
"context"
"fmt"
"time"
eth2client "github.com/attestantio/go-eth2-client"
@@ -23,6 +22,7 @@ import (
"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"
)
@@ -32,15 +32,11 @@ type dataIn struct {
quiet bool
verbose bool
debug bool
// Chain information.
slotsPerEpoch uint64
// Operation.
eth2Client eth2client.Service
chainTime chaintime.Service
epoch spec.Epoch
account string
pubKey string
index string
validator string
}
func input(ctx context.Context) (*dataIn, error) {
@@ -54,48 +50,35 @@ func input(ctx context.Context) (*dataIn, error) {
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Account.
data.account = viper.GetString("account")
// PubKey.
data.pubKey = viper.GetString("pubkey")
// ID.
data.index = viper.GetString("index")
if viper.GetString("account") == "" && viper.GetString("index") == "" && viper.GetString("pubkey") == "" {
return nil, errors.New("account, index or pubkey is required")
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, 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, err
}
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
return nil, errors.Wrap(err, "failed to set up chaintime service")
}
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
// Epoch.
epoch := viper.GetInt64("epoch")
if epoch == -1 {
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.epoch = spec.Epoch(epoch)
if data.debug {
fmt.Printf("Epoch is %d\n", data.epoch)
data.epoch, err = util.ParseEpoch(ctx, data.chainTime, viper.GetString("epoch"))
if err != nil {
return nil, err
}
return data, nil

View File

@@ -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: "IndexMissing",
name: "ValidatorMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "account, index or pubkey is required",
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 beacon node: failed to connect to Ethereum 2 client with any known method",
},
}

View File

@@ -38,7 +38,7 @@ type dataOut struct {
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 buf.String(), errors.New("no data")

View File

@@ -31,41 +31,26 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("no data")
}
var err error
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.WithForkScheduleProvider(data.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to set up chaintime service")
}
validatorIndex, err := util.ValidatorIndex(ctx, data.eth2Client, data.account, data.pubKey, data.index)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator index")
}
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), []phase0.ValidatorIndex{validatorIndex})
if err != nil {
return nil, errors.New("failed to obtain validator information")
}
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
var validator *api.Validator
for _, v := range validators {
validator = v
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
duty, err := duty(ctx, data.eth2Client, 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")
}
@@ -101,7 +86,6 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if attestation.Data.Slot == duty.Slot &&
attestation.Data.Index == duty.CommitteeIndex &&
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
headCorrect := false
targetCorrect := false
if data.verbose {
@@ -139,7 +123,7 @@ func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Atte
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
return false, err
}
if header == nil {
// No block.
@@ -161,7 +145,7 @@ func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.At
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
return false, err
}
if header == nil {
// No block.
@@ -177,7 +161,7 @@ func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.At
}
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []phase0.ValidatorIndex{validator.Index})
if err != nil {

View File

@@ -18,18 +18,26 @@ import (
"os"
"testing"
"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.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
)
require.NoError(t, err)
@@ -45,10 +53,9 @@ func TestProcess(t *testing.T) {
{
name: "Client",
dataIn: &dataIn{
eth2Client: eth2Client,
slotsPerEpoch: 32,
pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
epoch: 100,
eth2Client: eth2Client,
chainTime: chainTime,
validator: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
},
},
}

View File

@@ -26,7 +26,7 @@ var attesterDutiesCmd = &cobra.Command{
Short: "Obtain information about duties of an attester",
Long: `Obtain information about dutes of an attester. For example:
ethdo attester duties --account=Validators/00001 --epoch=12345
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, args []string) error {
@@ -47,19 +47,15 @@ In quiet mode this will return 0 if a duty from the attester is found, otherwise
func init() {
attesterCmd.AddCommand(attesterDutiesCmd)
attesterFlags(attesterDutiesCmd)
attesterDutiesCmd.Flags().Int64("epoch", -1, "the last complete epoch")
attesterDutiesCmd.Flags().String("pubkey", "", "the public key of the attester")
attesterDutiesCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network")
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() {
if err := viper.BindPFlag("epoch", attesterDutiesCmd.Flags().Lookup("epoch")); err != nil {
func attesterDutiesBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("pubkey", attesterDutiesCmd.Flags().Lookup("pubkey")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", attesterDutiesCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
panic(err)
}
}

View File

@@ -26,7 +26,7 @@ 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 {
@@ -47,19 +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().Int64("index", -1, "the index 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", attesterInclusionCmd.Flags().Lookup("index")); err != nil {
if err := viper.BindPFlag("index", cmd.Flags().Lookup("index")); err != nil {
panic(err)
}
}

View File

@@ -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) {
}

View File

@@ -105,7 +105,7 @@ type attestationData struct {
Index int `json:"index"`
}
func newCommand(ctx context.Context) (*command, error) {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
@@ -122,9 +122,6 @@ func newCommand(ctx context.Context) (*command, error) {
}
c.timeout = viper.GetDuration("timeout")
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")

View File

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

View File

@@ -106,9 +106,8 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
if attestation.NewVotes == 0 {
builder.WriteString("\n")
continue
} else {
builder.WriteString(", ")
}
builder.WriteString(", ")
switch {
case !attestation.HeadCorrect:
builder.WriteString("head vote incorrect, ")

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -77,11 +77,7 @@ func (c *command) analyze(ctx context.Context, block *spec.VersionedSignedBeacon
return err
}
if err := c.analyzeSyncCommittees(ctx, block); err != nil {
return err
}
return nil
return c.analyzeSyncCommittees(ctx, block)
}
func (c *command) analyzeAttestations(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
@@ -213,7 +209,7 @@ func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedB
return c.fetchParents(ctx, parentBlock, minSlot)
}
func (c *command) processParentBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
func (c *command) processParentBlock(_ context.Context, block *spec.VersionedSignedBeaconBlock) error {
attestations, err := block.Attestations()
if err != nil {
return err
@@ -259,14 +255,18 @@ func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
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.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
@@ -364,7 +364,7 @@ func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attes
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
return false, err
}
if header == nil {
// No block.
@@ -393,7 +393,7 @@ func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Att
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
return false, err
}
if header == nil {
// No block.
@@ -413,7 +413,7 @@ func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Att
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
}
func (c *command) analyzeSyncCommittees(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
func (c *command) analyzeSyncCommittees(_ context.Context, block *spec.VersionedSignedBeaconBlock) error {
c.analysis.SyncCommitee = &syncCommitteeAnalysis{}
switch block.Version {
case spec.DataVersionPhase0:
@@ -425,6 +425,27 @@ func (c *command) analyzeSyncCommittees(ctx context.Context, block *spec.Version
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
case spec.DataVersionBellatrix:
c.analysis.SyncCommitee.Contributions = int(block.Bellatrix.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Bellatrix.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
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
default:
return fmt.Errorf("unsupported block version %d", block.Version)
}

View File

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

View File

@@ -54,7 +54,12 @@ func input(ctx context.Context) (*dataIn, error) {
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, err
}

View File

@@ -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 beacon node: failed to connect to Ethereum 2 client with any known method",
},
{
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 beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
},
{
name: "BlockIDNil",

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020, 2021 Weald Technology Trading
// Copyright © 2019 - 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -27,6 +27,8 @@ import (
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/capella"
"github.com/attestantio/go-eth2-client/spec/deneb"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
@@ -42,7 +44,7 @@ type dataOut struct {
slotsPerEpoch uint64
}
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")
}
@@ -50,7 +52,7 @@ func output(ctx context.Context, data *dataOut) (string, error) {
return "", nil
}
func outputBlockGeneral(ctx context.Context,
func outputBlockGeneral(_ context.Context,
verbose bool,
slot phase0.Slot,
blockRoot phase0.Root,
@@ -77,8 +79,9 @@ func outputBlockGeneral(ctx context.Context,
res.WriteString(fmt.Sprintf("State root: %#x\n", stateRoot))
}
if len(graffiti) > 0 && hex.EncodeToString(graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
graffiti = bytes.TrimRight(graffiti, "\u0000")
if utf8.Valid(graffiti) {
res.WriteString(fmt.Sprintf("Graffiti: %s\n", strings.TrimRight(string(graffiti), "\u0000")))
res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(graffiti)))
} else {
res.WriteString(fmt.Sprintf("Graffiti: %#x\n", graffiti))
}
@@ -87,7 +90,7 @@ func outputBlockGeneral(ctx context.Context,
return res.String(), nil
}
func outputBlockETH1Data(ctx context.Context, eth1Data *phase0.ETH1Data) (string, error) {
func outputBlockETH1Data(_ context.Context, eth1Data *phase0.ETH1Data) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Ethereum 1 deposit count: %d\n", eth1Data.DepositCount))
@@ -192,7 +195,7 @@ func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Ser
return res.String(), nil
}
func outputBlockDeposits(ctx context.Context, verbose bool, deposits []*phase0.Deposit) (string, error) {
func outputBlockDeposits(_ context.Context, verbose bool, deposits []*phase0.Deposit) (string, error) {
res := strings.Builder{}
// Deposits.
@@ -222,7 +225,7 @@ func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Servic
if err != nil {
res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err))
} else {
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[0].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[voluntaryExit.Message.ValidatorIndex].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" Epoch: %d\n", voluntaryExit.Message.Epoch))
}
}
@@ -231,6 +234,27 @@ func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Servic
return res.String(), nil
}
func outputBlockBLSToExecutionChanges(ctx context.Context, eth2Client eth2client.Service, verbose bool, ops []*capella.SignedBLSToExecutionChange) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("BLS to execution changes: %d\n", len(ops)))
if verbose {
for i, op := range ops {
res.WriteString(fmt.Sprintf(" %d:\n", i))
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []phase0.ValidatorIndex{op.Message.ValidatorIndex})
if err != nil {
res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err))
} else {
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[op.Message.ValidatorIndex].Validator.PublicKey, op.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" BLS public key: %#x\n", op.Message.FromBLSPubkey))
res.WriteString(fmt.Sprintf(" Execution address: %s\n", op.Message.ToExecutionAddress.String()))
}
}
}
return res.String(), nil
}
func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service, verbose bool, syncAggregate *altair.SyncAggregate, epoch phase0.Epoch) (string, error) {
res := strings.Builder{}
@@ -269,7 +293,7 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
return res.String(), nil
}
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
func outputCapellaBlockText(ctx context.Context, data *dataOut, signedBlock *capella.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
@@ -295,7 +319,7 @@ func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *b
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti,
signedBlock.Message.Body.Graffiti[:],
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
@@ -350,7 +374,205 @@ func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *b
}
res.WriteString(tmp)
tmp, err = outputBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
tmp, err = outputBlockBLSToExecutionChanges(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.BLSToExecutionChanges)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputCapellaBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputDenebBlockText(ctx context.Context, data *dataOut, signedBlock *deneb.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
blockRoot, err := signedBlock.Message.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to obtain block root")
}
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate body root")
}
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti[:],
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Eth1 data.
if data.verbose {
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
if err != nil {
return "", err
}
res.WriteString(tmp)
}
// Sync aggregate.
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attestations.
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
if err != nil {
return "", err
}
res.WriteString(tmp)
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
// Add verbose proposer slashings.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputBlockBLSToExecutionChanges(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.BLSToExecutionChanges)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputDenebBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputDenebBlobInfo(ctx, data.verbose, signedBlock.Message.Body)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
blockRoot, err := signedBlock.Message.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to obtain block root")
}
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate body root")
}
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti[:],
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Eth1 data.
if data.verbose {
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
if err != nil {
return "", err
}
res.WriteString(tmp)
}
// Sync aggregate.
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attestations.
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
if err != nil {
return "", err
}
res.WriteString(tmp)
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
// Add verbose proposer slashings.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputBellatrixBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
if err != nil {
return "", err
}
@@ -385,7 +607,7 @@ func outputAltairBlockText(ctx context.Context, data *dataOut, signedBlock *alta
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti,
signedBlock.Message.Body.Graffiti[:],
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
@@ -468,7 +690,7 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti,
signedBlock.Message.Body.Graffiti[:],
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
@@ -519,9 +741,9 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
return res.String(), nil
}
func outputBlockExecutionPayload(ctx context.Context,
func outputCapellaBlockExecutionPayload(_ context.Context,
verbose bool,
payload *bellatrix.ExecutionPayload,
payload *capella.ExecutionPayload,
) (
string,
error,
@@ -530,11 +752,21 @@ func outputBlockExecutionPayload(ctx context.Context,
return "", nil
}
// If the block number is 0 then we're before the merge.
if payload.BlockNumber == 0 {
return "", nil
}
res := strings.Builder{}
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
if verbose {
if !verbose {
res.WriteString("Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString("Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
} else {
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas))
for i := 0; i < 32; i++ {
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
@@ -547,7 +779,166 @@ func outputBlockExecutionPayload(ctx context.Context,
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
res.WriteString(payload.FeeRecipient.String())
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
res.WriteString(" Timestamp: ")
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
res.WriteString(" Prev RANDAO: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
res.WriteString(" Receipts root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
res.WriteString(" State root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
res.WriteString(" Extra data: ")
if utf8.Valid(payload.ExtraData) {
res.WriteString(fmt.Sprintf("%s\n", string(payload.ExtraData)))
} else {
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
}
res.WriteString(" Logs bloom: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
res.WriteString(" Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
res.WriteString(" Withdrawals: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Withdrawals)))
}
return res.String(), nil
}
func outputDenebBlockExecutionPayload(_ context.Context,
verbose bool,
payload *deneb.ExecutionPayload,
) (
string,
error,
) {
if payload == nil {
return "", nil
}
// If the block number is 0 then we're before the merge.
if payload.BlockNumber == 0 {
return "", nil
}
res := strings.Builder{}
if !verbose {
res.WriteString("Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString("Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
} else {
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString(" Base fee per gas: ")
res.WriteString(string2eth.WeiToString(payload.BaseFeePerGas.ToBig(), true))
res.WriteString("\n Block hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(payload.FeeRecipient.String())
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
res.WriteString(" Timestamp: ")
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
res.WriteString(" Prev RANDAO: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
res.WriteString(" Receipts root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
res.WriteString(" State root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
res.WriteString(" Extra data: ")
if utf8.Valid(payload.ExtraData) {
res.WriteString(fmt.Sprintf("%s\n", string(payload.ExtraData)))
} else {
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
}
res.WriteString(" Logs bloom: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
res.WriteString(" Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
res.WriteString(" Withdrawals: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Withdrawals)))
res.WriteString(" Excess data gas: ")
res.WriteString(payload.ExcessDataGas.Dec())
res.WriteString("\n")
}
return res.String(), nil
}
func outputDenebBlobInfo(_ context.Context,
verbose bool,
body *deneb.BeaconBlockBody,
) (
string,
error,
) {
if body == nil {
return "", nil
}
res := strings.Builder{}
if !verbose {
res.WriteString(fmt.Sprintf("Blob KZG commitments: %d\n", len(body.BlobKzgCommitments)))
} else if len(body.BlobKzgCommitments) > 0 {
res.WriteString("Blob KZG commitments:\n")
for i := range body.BlobKzgCommitments {
res.WriteString(fmt.Sprintf(" %s\n", body.BlobKzgCommitments[i].String()))
}
}
return res.String(), nil
}
func outputBellatrixBlockExecutionPayload(_ context.Context,
verbose bool,
payload *bellatrix.ExecutionPayload,
) (
string,
error,
) {
if payload == nil {
return "", nil
}
// If the block number is 0 then we're before the merge.
if payload.BlockNumber == 0 {
return "", nil
}
res := strings.Builder{}
if !verbose {
res.WriteString("Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString("Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
} else {
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas))
for i := 0; i < 32; i++ {
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
}
baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBEBytes)
res.WriteString(" Base fee per gas: ")
res.WriteString(string2eth.WeiToString(baseFeePerGas, true))
res.WriteString("\n Block hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(payload.FeeRecipient.String())
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
@@ -564,6 +955,8 @@ func outputBlockExecutionPayload(ctx context.Context,
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
res.WriteString(" Logs bloom: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
res.WriteString(" Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
}
return res.String(), nil

View File

@@ -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
@@ -24,18 +24,25 @@ import (
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/capella"
"github.com/attestantio/go-eth2-client/spec/deneb"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
var jsonOutput bool
var sszOutput 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 == "" {
return nil, errors.New("no block ID")
}
results = &dataOut{
debug: data.debug,
@@ -75,6 +82,14 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Bellatrix); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionCapella:
if err := outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Capella); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionDeneb:
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Deneb); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
default:
return nil, errors.New("unknown block version")
}
@@ -82,6 +97,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data.stream {
jsonOutput = data.jsonOutput
sszOutput = data.sszOutput
if !jsonOutput && !sszOutput {
fmt.Println("")
}
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
if err != nil {
return nil, errors.Wrap(err, "failed to start block stream")
@@ -101,45 +119,40 @@ func headEventHandler(event *api.Event) {
blockID := fmt.Sprintf("%#x", event.Data.(*api.HeadEvent).Block[:])
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID)
if err != nil {
if !jsonOutput {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to obtain block: %v\n", err)
}
return
}
if signedBlock == nil {
if !jsonOutput {
if !jsonOutput && !sszOutput {
fmt.Println("Empty beacon block")
}
return
}
switch signedBlock.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
if !jsonOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0)
case spec.DataVersionAltair:
if err := outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair); err != nil {
if !jsonOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair)
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix); err != nil {
if !jsonOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix)
case spec.DataVersionCapella:
err = outputCapellaBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Capella)
case spec.DataVersionDeneb:
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Deneb)
default:
if !jsonOutput {
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
}
err = errors.New("unknown block version")
}
if err != nil && !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
return
}
if !jsonOutput && !sszOutput {
fmt.Println("")
}
}
func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0.SignedBeaconBlock) error {
@@ -155,7 +168,7 @@ func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Printf("%s\n", data)
fmt.Print(data)
}
return nil
}
@@ -179,7 +192,7 @@ func outputAltairBlock(ctx context.Context, jsonOutput bool, sszOutput bool, sig
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Printf("%s\n", data)
fmt.Print(data)
}
return nil
}
@@ -203,7 +216,55 @@ func outputBellatrixBlock(ctx context.Context, jsonOutput bool, sszOutput bool,
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Printf("%s\n", data)
fmt.Print(data)
}
return nil
}
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) 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)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Print(data)
}
return nil
}

View File

@@ -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: "empty beacon block",
err: "no block ID",
},
}

View File

@@ -49,17 +49,13 @@ func init() {
blockFlags(blockAnalyzeCmd)
blockAnalyzeCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockAnalyzeCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockAnalyzeCmd.Flags().Bool("json", false, "output data in JSON format")
}
func blockAnalyzeBindings() {
if err := viper.BindPFlag("blockid", blockAnalyzeCmd.Flags().Lookup("blockid")); err != nil {
func blockAnalyzeBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stream", blockAnalyzeCmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", blockAnalyzeCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("stream", cmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
}

View File

@@ -49,21 +49,17 @@ func init() {
blockFlags(blockInfoCmd)
blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockInfoCmd.Flags().Bool("json", false, "output data in JSON format")
blockInfoCmd.Flags().Bool("ssz", false, "output data in SSZ format")
}
func blockInfoBindings() {
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("stream", cmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", blockInfoCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
if err := viper.BindPFlag("ssz", blockInfoCmd.Flags().Lookup("ssz")); err != nil {
if err := viper.BindPFlag("ssz", cmd.Flags().Lookup("ssz")); err != nil {
panic(err)
}
}

View File

@@ -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) {
}

View File

@@ -0,0 +1,84 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaineth1votes
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
json bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Input.
xepoch string
xperiod string
// Data access.
eth2Client eth2client.Service
chainTime chaintime.Service
beaconStateProvider eth2client.BeaconStateProvider
slotsPerEpoch uint64
epochsPerEth1VotingPeriod uint64
// Output.
slot phase0.Slot
epoch phase0.Epoch
period uint64
incumbent *phase0.ETH1Data
eth1DataVotes []*phase0.ETH1Data
votes map[string]*vote
}
type vote struct {
Vote *phase0.ETH1Data `json:"vote"`
Count int `json:"count"`
}
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
json: viper.GetBool("json"),
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
c.xepoch = viper.GetString("epoch")
c.xperiod = viper.GetString("period")
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
return c, nil
}

View File

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

View File

@@ -0,0 +1,125 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaineth1votes
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/attestantio/go-eth2-client/spec/phase0"
)
type jsonOutput struct {
Period uint64 `json:"period"`
Epoch phase0.Epoch `json:"epoch"`
Slot phase0.Slot `json:"slot"`
Incumbent *phase0.ETH1Data `json:"incumbent"`
Votes []*vote `json:"votes"`
}
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.json {
return c.outputJSON(ctx)
}
return c.outputText(ctx)
}
func (c *command) outputJSON(_ context.Context) (string, error) {
votes := make([]*vote, 0, len(c.votes))
totalVotes := 0
for _, vote := range c.votes {
votes = append(votes, vote)
totalVotes += vote.Count
}
sort.Slice(votes, func(i int, j int) bool {
if votes[i].Count != votes[j].Count {
return votes[i].Count > votes[j].Count
}
return votes[i].Vote.DepositCount < votes[j].Vote.DepositCount
})
output := &jsonOutput{
Period: c.period,
Epoch: c.epoch,
Slot: c.slot,
Incumbent: c.incumbent,
Votes: votes,
}
data, err := json.Marshal(output)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputText(_ context.Context) (string, error) {
builder := strings.Builder{}
builder.WriteString("Voting period: ")
builder.WriteString(fmt.Sprintf("%d\n", c.period))
if c.verbose {
builder.WriteString("Incumbent: ")
builder.WriteString(fmt.Sprintf("block %#x, deposit count %d\n", c.incumbent.BlockHash, c.incumbent.DepositCount))
}
votes := make([]*vote, 0, len(c.votes))
totalVotes := 0
for _, vote := range c.votes {
votes = append(votes, vote)
totalVotes += vote.Count
}
sort.Slice(votes, func(i int, j int) bool {
if votes[i].Count != votes[j].Count {
return votes[i].Count > votes[j].Count
}
return votes[i].Vote.DepositCount < votes[j].Vote.DepositCount
})
slot := c.chainTime.CurrentSlot()
if slot > c.slot {
slot = c.slot
}
slotsThroughPeriod := slot + 1 - phase0.Slot(c.period*(c.slotsPerEpoch*c.epochsPerEth1VotingPeriod))
builder.WriteString("Slots through period: ")
builder.WriteString(fmt.Sprintf("%d (%d)\n", slotsThroughPeriod, c.slot))
builder.WriteString("Votes this period: ")
builder.WriteString(fmt.Sprintf("%d\n", totalVotes))
if len(votes) > 0 {
if c.verbose {
for _, vote := range votes {
builder.WriteString(fmt.Sprintf(" block %#x, deposit count %d: %d vote", vote.Vote.BlockHash, vote.Vote.DepositCount, vote.Count))
if vote.Count != 1 {
builder.WriteString("s")
}
builder.WriteString(fmt.Sprintf(" (%0.2f%%)\n", 100.0*float64(vote.Count)/float64(slotsThroughPeriod)))
}
} else {
builder.WriteString(fmt.Sprintf("Leading vote is for block %#x with %d votes (%0.2f%%)\n", votes[0].Vote.BlockHash, votes[0].Count, 100.0*float64(votes[0].Count)/float64(slotsThroughPeriod)))
}
}
return strings.TrimSuffix(builder.String(), "\n"), nil
}

View File

@@ -0,0 +1,173 @@
// 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/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
var err error
if c.xperiod != "" {
period, err := strconv.ParseUint(c.xperiod, 10, 64)
if err != nil {
return err
}
c.epoch = phase0.Epoch(c.epochsPerEth1VotingPeriod*(period+1)) - 1
} else {
c.epoch, err = util.ParseEpoch(ctx, c.chainTime, c.xepoch)
if err != nil {
return err
}
}
// Do not fetch from the future.
if c.epoch > c.chainTime.CurrentEpoch() {
c.epoch = c.chainTime.CurrentEpoch()
}
// Need to fetch the state from the last slot of the epoch.
fetchSlot := c.chainTime.FirstSlotOfEpoch(c.epoch+1) - 1
// Do not fetch from the future.
if fetchSlot > c.chainTime.CurrentSlot() {
fetchSlot = c.chainTime.CurrentSlot()
}
state, err := c.beaconStateProvider.BeaconState(ctx, fmt.Sprintf("%d", fetchSlot))
if err != nil {
return errors.Wrap(err, "failed to obtain state")
}
if state == nil {
return errors.New("state not returned by beacon node")
}
if c.debug {
data, err := json.Marshal(state)
if err == nil {
fmt.Printf("%s\n", string(data))
}
}
switch state.Version {
case spec.DataVersionPhase0:
c.slot = state.Phase0.Slot
c.incumbent = state.Phase0.ETH1Data
c.eth1DataVotes = state.Phase0.ETH1DataVotes
case spec.DataVersionAltair:
c.slot = state.Altair.Slot
c.incumbent = state.Altair.ETH1Data
c.eth1DataVotes = state.Altair.ETH1DataVotes
case spec.DataVersionBellatrix:
c.slot = state.Bellatrix.Slot
c.incumbent = state.Bellatrix.ETH1Data
c.eth1DataVotes = state.Bellatrix.ETH1DataVotes
case spec.DataVersionCapella:
c.slot = state.Capella.Slot
c.incumbent = state.Capella.ETH1Data
c.eth1DataVotes = state.Capella.ETH1DataVotes
case spec.DataVersionDeneb:
c.slot = state.Deneb.Slot
c.incumbent = state.Deneb.ETH1Data
c.eth1DataVotes = state.Deneb.ETH1DataVotes
default:
return fmt.Errorf("unhandled beacon state version %v", state.Version)
}
c.period = uint64(c.epoch) / c.epochsPerEth1VotingPeriod
c.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.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
var isProvider bool
c.beaconStateProvider, isProvider = c.eth2Client.(eth2client.BeaconStateProvider)
if !isProvider {
return errors.New("connection does not provide beacon state")
}
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
if !isProvider {
return errors.New("connection does not provide spec information")
}
spec, err := specProvider.Spec(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["SLOTS_PER_EPOCH"]
if !exists {
return errors.New("spec did not contain SLOTS_PER_EPOCH")
}
var good bool
c.slotsPerEpoch, good = tmp.(uint64)
if !good {
return errors.New("SLOTS_PER_EPOCH value invalid")
}
tmp, exists = spec["EPOCHS_PER_ETH1_VOTING_PERIOD"]
if !exists {
return errors.New("spec did not contain EPOCHS_PER_ETH1_VOTING_PERIOD")
}
c.epochsPerEth1VotingPeriod, good = tmp.(uint64)
if !good {
return errors.New("EPOCHS_PER_ETH1_VOTING_PERIOD value invalid")
}
return nil
}

View File

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

View File

@@ -0,0 +1,50 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaineth1votes
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

View File

@@ -0,0 +1,72 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainqueues
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
json bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Input.
epoch string
// Data access.
eth2Client eth2client.Service
validatorsProvider eth2client.ValidatorsProvider
chainTime chaintime.Service
// Output.
activationQueue int
exitQueue int
}
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
json: viper.GetBool("json"),
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
if viper.GetString("epoch") != "" {
c.epoch = viper.GetString("epoch")
}
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
return c, nil
}

View File

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

View File

@@ -0,0 +1,63 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainqueues
import (
"context"
"encoding/json"
"fmt"
"strings"
)
type jsonOutput struct {
ActivationQueue int `json:"activation_queue"`
ExitQueue int `json:"exit_queue"`
}
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.json {
return c.outputJSON(ctx)
}
return c.outputText(ctx)
}
func (c *command) outputJSON(_ 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
}

View File

@@ -0,0 +1,86 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainqueues
import (
"context"
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
epoch, err := util.ParseEpoch(ctx, c.chainTime, c.epoch)
if err != nil {
return err
}
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)), nil)
if err != nil {
return errors.Wrap(err, "failed to obtain validators")
}
for _, validator := range validators {
if validator.Validator == nil {
continue
}
if validator.Validator.ActivationEligibilityEpoch <= epoch && validator.Validator.ActivationEpoch > epoch {
c.activationQueue++
}
if validator.Validator.ExitEpoch != 0xffffffffffffffff && validator.Validator.ExitEpoch > epoch {
c.exitQueue++
}
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &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.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
var isProvider bool
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
if !isProvider {
return errors.New("connection does not provide validator information")
}
return nil
}

View File

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

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

@@ -0,0 +1,50 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainqueues
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ type dataOut struct {
slot spec.Slot
slotStart time.Time
slotEnd time.Time
hasSyncCommittees bool
syncCommitteePeriod uint64
syncCommitteePeriodStart time.Time
syncCommitteePeriodEpochStart spec.Epoch
@@ -41,7 +42,7 @@ type dataOut struct {
syncCommitteePeriodEpochEnd spec.Epoch
}
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")
}
@@ -56,27 +57,59 @@ func output(ctx context.Context, data *dataOut) (string, error) {
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(fmt.Sprintf("%d", data.epochStart.Unix()))
builder.WriteString(")")
}
builder.WriteString("\n Epoch end ")
builder.WriteString(data.epochEnd.Format("2006-01-02 15:04:05"))
if data.verbose {
builder.WriteString(" (")
builder.WriteString(fmt.Sprintf("%d", data.epochEnd.Unix()))
builder.WriteString(")")
}
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(fmt.Sprintf("%d", data.slotStart.Unix()))
builder.WriteString(")")
}
builder.WriteString("\n Slot end ")
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
if data.verbose {
builder.WriteString(" (")
builder.WriteString(fmt.Sprintf("%d", data.slotEnd.Unix()))
builder.WriteString(")")
}
builder.WriteString("\nSync committee period ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriod))
builder.WriteString("\n Sync committee period start ")
builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart))
builder.WriteString(")\n Sync committee period end ")
builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd))
builder.WriteString(")\n")
if data.hasSyncCommittees {
builder.WriteString("\nSync committee period ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriod))
builder.WriteString("\n Sync committee period start ")
builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart))
if data.verbose {
builder.WriteString(", ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodStart.Unix()))
}
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(fmt.Sprintf("%d", data.syncCommitteePeriodEnd.Unix()))
}
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}

View File

@@ -21,6 +21,7 @@ import (
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"
)
@@ -29,22 +30,22 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("no data")
}
eth2Client, err := util.ConnectToBeaconNode(ctx, data.connection, data.timeout, data.allowInsecureConnections)
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")
}
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
epochsPerSyncCommitteePeriod := config["EPOCHS_PER_SYNC_COMMITTEE_PERIOD"].(uint64)
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis data")
return nil, errors.Wrap(err, "failed to set up chaintime service")
}
results := &dataOut{
@@ -62,34 +63,33 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
results.slot = phase0.Slot(slot)
case data.epoch != "":
epoch, err := strconv.ParseUint(data.epoch, 10, 64)
epoch, err := util.ParseEpoch(ctx, chainTime, data.epoch)
if err != nil {
return nil, errors.Wrap(err, "failed to parse epoch")
}
results.slot = phase0.Slot(epoch * slotsPerEpoch)
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")
}
secs := timestamp.Sub(genesis.GenesisTime)
if secs < 0 {
return nil, errors.New("timestamp prior to genesis")
}
results.slot = phase0.Slot(secs / slotDuration)
results.slot = chainTime.TimestampToSlot(timestamp)
}
// Fill in the info given the slot.
results.slotStart = genesis.GenesisTime.Add(time.Duration(results.slot) * slotDuration)
results.slotEnd = genesis.GenesisTime.Add(time.Duration(results.slot+1) * slotDuration)
results.epoch = phase0.Epoch(uint64(results.slot) / slotsPerEpoch)
results.epochStart = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch)*slotsPerEpoch) * slotDuration)
results.epochEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch+1)*slotsPerEpoch) * slotDuration)
results.syncCommitteePeriod = uint64(results.epoch) / epochsPerSyncCommitteePeriod
results.syncCommitteePeriodEpochStart = phase0.Epoch(results.syncCommitteePeriod * epochsPerSyncCommitteePeriod)
results.syncCommitteePeriodEpochEnd = phase0.Epoch((results.syncCommitteePeriod+1)*epochsPerSyncCommitteePeriod) - 1
results.syncCommitteePeriodStart = genesis.GenesisTime.Add(time.Duration(uint64(results.syncCommitteePeriodEpochStart)*slotsPerEpoch) * slotDuration)
results.syncCommitteePeriodEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.syncCommitteePeriodEpochEnd)*slotsPerEpoch) * slotDuration)
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
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2021 Weald Technology Trading
// Copyright © 2021 - 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,7 +15,6 @@ package chaintime
import (
"context"
"fmt"
"os"
"testing"
"time"
@@ -47,16 +46,11 @@ func TestProcess(t *testing.T) {
slot: "1",
},
expected: &dataOut{
epochStart: time.Unix(1606824023, 0),
epochEnd: time.Unix(1606824407, 0),
slot: 1,
slotStart: time.Unix(1606824035, 0),
slotEnd: time.Unix(1606824047, 0),
syncCommitteePeriod: 0,
syncCommitteePeriodStart: time.Unix(1606824023, 0),
syncCommitteePeriodEnd: time.Unix(1606921943, 0),
syncCommitteePeriodEpochStart: 0,
syncCommitteePeriodEpochEnd: 255,
epochStart: time.Unix(1606824023, 0),
epochEnd: time.Unix(1606824407, 0),
slot: 1,
slotStart: time.Unix(1606824035, 0),
slotEnd: time.Unix(1606824047, 0),
},
},
{
@@ -68,17 +62,12 @@ func TestProcess(t *testing.T) {
epoch: "2",
},
expected: &dataOut{
epoch: 2,
epochStart: time.Unix(1606824791, 0),
epochEnd: time.Unix(1606825175, 0),
slot: 64,
slotStart: time.Unix(1606824791, 0),
slotEnd: time.Unix(1606824803, 0),
syncCommitteePeriod: 0,
syncCommitteePeriodStart: time.Unix(1606824023, 0),
syncCommitteePeriodEnd: time.Unix(1606921943, 0),
syncCommitteePeriodEpochStart: 0,
syncCommitteePeriodEpochEnd: 255,
epoch: 2,
epochStart: time.Unix(1606824791, 0),
epochEnd: time.Unix(1606825175, 0),
slot: 64,
slotStart: time.Unix(1606824791, 0),
slotEnd: time.Unix(1606824803, 0),
},
},
{
@@ -87,20 +76,21 @@ func TestProcess(t *testing.T) {
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
timestamp: "2021-01-01T00:00:00+0000",
timestamp: "2023-01-01T00:00:00+0000",
},
expected: &dataOut{
epoch: 6862,
epochStart: time.Unix(1609459031, 0),
epochEnd: time.Unix(1609459415, 0),
slot: 219598,
slotStart: time.Unix(1609459199, 0),
slotEnd: time.Unix(1609459211, 0),
syncCommitteePeriod: 26,
syncCommitteePeriodStart: time.Unix(1609379927, 0),
syncCommitteePeriodEnd: time.Unix(1609477847, 0),
syncCommitteePeriodEpochStart: 6656,
syncCommitteePeriodEpochEnd: 6911,
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,
},
},
}
@@ -112,7 +102,6 @@ func TestProcess(t *testing.T) {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
fmt.Printf("****** %d %d\n", res.syncCommitteePeriodStart.Unix(), res.syncCommitteePeriodEnd.Unix())
require.Equal(t, test.expected, res)
}
})

View File

@@ -58,7 +58,7 @@ type command struct {
additionalInfo string
}
func newCommand(ctx context.Context) (*command, error) {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
@@ -76,9 +76,6 @@ func newCommand(ctx context.Context) (*command, error) {
}
c.data = viper.GetString("data")
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")

View File

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

View File

@@ -18,7 +18,7 @@ import (
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
func (c *command) output(_ context.Context) (string, error) {
if c.quiet {
return "", nil
}

View File

@@ -38,6 +38,7 @@ func (c *command) process(ctx context.Context) error {
err := json.Unmarshal([]byte(c.data), c.item)
if err != nil {
c.additionalInfo = err.Error()
//nolint:nilerr
return nil
}
c.itemStructureValid = true
@@ -84,7 +85,12 @@ func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
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")
}
@@ -124,7 +130,7 @@ func (c *command) setup(ctx context.Context) error {
return nil
}
// isAggregator returns true if the given
// isAggregator returns true if the given.
func (c *command) isAggregator(ctx context.Context) (bool, error) {
// Calculate the modulo.
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
@@ -204,6 +210,7 @@ func (c *command) confirmContributionSignature(ctx context.Context) error {
_, err := e2types.BLSSignatureFromBytes(sigBytes)
if err != nil {
c.additionalInfo = err.Error()
//nolint:nilerr
return nil
}
c.contributionSignatureValidFormat = true
@@ -256,6 +263,7 @@ func (c *command) confirmContributionAndProofSignature(ctx context.Context) erro
sig, err := e2types.BLSSignatureFromBytes(sigBytes)
if err != nil {
c.additionalInfo = err.Error()
//nolint:nilerr
return nil
}
c.contributionAndProofSignatureValidFormat = true

63
cmd/chaineth1votes.go Normal file
View File

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

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -37,7 +37,12 @@ In quiet mode this will return 0 if the chain information can be obtained, other
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !viper.GetBool("quiet"),
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
@@ -49,7 +54,7 @@ In quiet mode this will return 0 if the chain information can be obtained, other
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
errCheck(err, "Failed to obtain current fork")
if quiet {
if viper.GetBool("quiet") {
os.Exit(_exitSuccess)
}
@@ -57,12 +62,12 @@ In quiet mode this will return 0 if the chain information can be obtained, other
fmt.Println("Genesis time: undefined")
} else {
fmt.Printf("Genesis time: %s\n", genesis.GenesisTime.Format(time.UnixDate))
outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesis.GenesisTime.Unix()))
outputIf(viper.GetBool("verbose"), fmt.Sprintf("Genesis timestamp: %v", genesis.GenesisTime.Unix()))
}
fmt.Printf("Genesis validators root: %#x\n", genesis.GenesisValidatorsRoot)
fmt.Printf("Genesis fork version: %#x\n", config["GENESIS_FORK_VERSION"].(spec.Version))
fmt.Printf("Current fork version: %#x\n", fork.CurrentVersion)
if verbose {
if viper.GetBool("verbose") {
forkData := &spec.ForkData{
CurrentVersion: fork.CurrentVersion,
GenesisValidatorsRoot: genesis.GenesisValidatorsRoot,
@@ -85,3 +90,6 @@ func init() {
chainCmd.AddCommand(chainInfoCmd)
chainFlags(chainInfoCmd)
}
func chainInfoBindings(_ *cobra.Command) {
}

57
cmd/chainqueues.go Normal file
View File

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

97
cmd/chainspec.go Normal file
View File

@@ -0,0 +1,97 @@
// Copyright © 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"context"
"encoding/json"
"fmt"
"sort"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
)
var chainSpecCmd = &cobra.Command{
Use: "spec",
Short: "Obtain specification for a chain",
Long: `Obtain specification for a chain. For example:
ethdo chain spec
In quiet mode this will return 0 if the chain specification can be obtained, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !viper.GetBool("quiet"),
})
errCheck(err, "Failed to connect to Ethereum consensus node")
spec, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
errCheck(err, "Failed to obtain chain specification")
if viper.GetBool("quiet") {
return
}
// Tweak the spec for output.
for k, v := range spec {
switch t := v.(type) {
case phase0.Version:
spec[k] = fmt.Sprintf("%#x", t)
case phase0.DomainType:
spec[k] = fmt.Sprintf("%#x", t)
case time.Time:
spec[k] = fmt.Sprintf("%d", t.Unix())
case time.Duration:
spec[k] = fmt.Sprintf("%d", uint64(t.Seconds()))
case []byte:
spec[k] = fmt.Sprintf("%#x", t)
case uint64:
spec[k] = fmt.Sprintf("%d", t)
}
}
if viper.GetBool("json") {
data, err := json.Marshal(spec)
errCheck(err, "Failed to marshal JSON")
fmt.Printf("%s\n", string(data))
} else {
keys := make([]string, 0, len(spec))
for k := range spec {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("%s: %v\n", key, spec[key])
}
}
},
}
func init() {
chainCmd.AddCommand(chainSpecCmd)
chainFlags(chainSpecCmd)
}
func chainSpecBindings(_ *cobra.Command) {
}

View File

@@ -21,10 +21,13 @@ import (
"time"
eth2client "github.com/attestantio/go-eth2-client"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/cobra"
"github.com/spf13/viper"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
string2eth "github.com/wealdtech/go-string2eth"
)
var chainStatusCmd = &cobra.Command{
@@ -38,12 +41,16 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !viper.GetBool("quiet"),
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithForkScheduleProvider(eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
)
errCheck(err, "Failed to configure chaintime service")
@@ -76,7 +83,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
res.WriteString(fmt.Sprintf("%d", epoch))
res.WriteString("\n")
if verbose {
if viper.GetBool("verbose") {
res.WriteString("Epoch slots: ")
res.WriteString(fmt.Sprintf("%d", epochStartSlot))
res.WriteString("-")
@@ -99,7 +106,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
res.WriteString("Justified epoch: ")
res.WriteString(fmt.Sprintf("%d", finality.Justified.Epoch))
res.WriteString("\n")
if verbose {
if viper.GetBool("verbose") {
distance := epoch - finality.Justified.Epoch
res.WriteString("Justified epoch distance: ")
res.WriteString(fmt.Sprintf("%d", distance))
@@ -109,13 +116,41 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
res.WriteString("Finalized epoch: ")
res.WriteString(fmt.Sprintf("%d", finality.Finalized.Epoch))
res.WriteString("\n")
if verbose {
if viper.GetBool("verbose") {
distance := epoch - finality.Finalized.Epoch
res.WriteString("Finalized epoch distance: ")
res.WriteString(fmt.Sprintf("%d", distance))
res.WriteString("\n")
}
if viper.GetBool("verbose") {
validatorsProvider, isProvider := eth2Client.(eth2client.ValidatorsProvider)
if isProvider {
validators, err := validatorsProvider.Validators(ctx, "head", nil)
errCheck(err, "Failed to obtain validators information")
// Stats of inteest.
totalBalance := phase0.Gwei(0)
activeEffectiveBalance := phase0.Gwei(0)
validatorCount := make(map[apiv1.ValidatorState]int)
for _, validator := range validators {
validatorCount[validator.Status]++
totalBalance += validator.Balance
if validator.Status.IsActive() {
activeEffectiveBalance += validator.Validator.EffectiveBalance
}
}
res.WriteString(fmt.Sprintf("Total balance: %s\n", string2eth.GWeiToString(uint64(totalBalance), true)))
res.WriteString(fmt.Sprintf("Active effective balance: %s\n", string2eth.GWeiToString(uint64(activeEffectiveBalance), true)))
res.WriteString("Validator states:\n")
res.WriteString(fmt.Sprintf(" Pending: %d\n", validatorCount[apiv1.ValidatorStatePendingInitialized]))
res.WriteString(fmt.Sprintf(" Activating: %d\n", validatorCount[apiv1.ValidatorStatePendingQueued]))
res.WriteString(fmt.Sprintf(" Active: %d\n", validatorCount[apiv1.ValidatorStateActiveOngoing]+validatorCount[apiv1.ValidatorStateActiveSlashed]))
res.WriteString(fmt.Sprintf(" Exiting: %d\n", validatorCount[apiv1.ValidatorStateActiveExiting]))
res.WriteString(fmt.Sprintf(" Exited: %d\n", validatorCount[apiv1.ValidatorStateExitedUnslashed]+validatorCount[apiv1.ValidatorStateExitedSlashed]+validatorCount[apiv1.ValidatorStateWithdrawalPossible]+validatorCount[apiv1.ValidatorStateWithdrawalDone]))
res.WriteString(fmt.Sprintf(" Unknown: %d\n", validatorCount[apiv1.ValidatorStateUnknown]))
}
}
if epoch >= chainTime.AltairInitialEpoch() {
period := chainTime.SlotToSyncCommitteePeriod(slot)
periodStartEpoch := chainTime.FirstEpochOfSyncPeriod(period)
@@ -130,7 +165,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
res.WriteString(fmt.Sprintf("%d", period))
res.WriteString("\n")
if verbose {
if viper.GetBool("verbose") {
res.WriteString("Sync committee epochs: ")
res.WriteString(fmt.Sprintf("%d", periodStartEpoch))
res.WriteString("-")

View File

@@ -47,14 +47,14 @@ func init() {
chainTimeCmd.Flags().String("timestamp", "", "The timestamp for which to obtain information (format YYYY-MM-DDTHH:MM:SS+ZZZZ)")
}
func chainTimeBindings() {
if err := viper.BindPFlag("slot", chainTimeCmd.Flags().Lookup("slot")); err != nil {
func chainTimeBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("slot", cmd.Flags().Lookup("slot")); err != nil {
panic(err)
}
if err := viper.BindPFlag("epoch", chainTimeCmd.Flags().Lookup("epoch")); err != nil {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("timestamp", chainTimeCmd.Flags().Lookup("timestamp")); err != nil {
if err := viper.BindPFlag("timestamp", cmd.Flags().Lookup("timestamp")); err != nil {
panic(err)
}
}

View File

@@ -18,7 +18,7 @@ import (
"github.com/spf13/viper"
)
// chainVerifyCmd represents the chain verify command
// chainVerifyCmd represents the chain verify command.
var chainVerifyCmd = &cobra.Command{
Use: "verify",
Short: "Verify a beacon chain signature",

View File

@@ -17,7 +17,7 @@ import (
"github.com/spf13/cobra"
)
// depositCmd represents the deposit command
// depositCmd represents the deposit command.
var depositCmd = &cobra.Command{
Use: "deposit",
Short: "Manage Ethereum 2 deposits",
@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(depositCmd)
}
func depositFlags(cmd *cobra.Command) {
func depositFlags(_ *cobra.Command) {
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019-2021 Weald Technology Limited.
// Copyright © 2019-2022 Weald Technology Limited.
// 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,25 +17,27 @@ import (
"bytes"
"encoding/hex"
"fmt"
"io/ioutil"
"os"
"strings"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
eth2util "github.com/wealdtech/go-eth2-util"
string2eth "github.com/wealdtech/go-string2eth"
)
var depositVerifyData string
var depositVerifyWithdrawalPubKey string
var depositVerifyWithdrawalAddress string
var depositVerifyValidatorPubKey string
var depositVerifyDepositAmount string
var depositVerifyForkVersion string
var (
depositVerifyData string
depositVerifyWithdrawalPubKey string
depositVerifyWithdrawalAddress string
depositVerifyValidatorPubKey string
depositVerifyDepositAmount string
depositVerifyForkVersion string
)
var depositVerifyCmd = &cobra.Command{
Use: "verify",
@@ -46,7 +48,7 @@ var depositVerifyCmd = &cobra.Command{
The deposit data is compared to the supplied withdrawal account/public key, validator public key, and value to ensure they match.
In quiet mode this will return 0 if the the data is verified correctly, otherwise 1.`,
In quiet mode this will return 0 if the data is verified correctly, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
assert(depositVerifyData != "", "--data is required")
var data []byte
@@ -64,7 +66,7 @@ In quiet mode this will return 0 if the the data is verified correctly, otherwis
data = []byte(depositVerifyData)
default:
// Assume it's a path to JSON.
data, err = ioutil.ReadFile(depositVerifyData)
data, err = os.ReadFile(depositVerifyData)
errCheck(err, "Failed to read deposit data file")
if data[0] == '{' {
data = []byte("[" + string(data) + "]")
@@ -91,7 +93,7 @@ In quiet mode this will return 0 if the the data is verified correctly, otherwis
withdrawalCredentials[0] = 0x01 // ETH1_ADDRESS_WITHDRAWAL_PREFIX
copy(withdrawalCredentials[12:], withdrawalAddressBytes)
}
outputIf(debug, fmt.Sprintf("Withdrawal credentials are %#x", withdrawalCredentials))
outputIf(viper.GetBool("debug"), fmt.Sprintf("Withdrawal credentials are %#x", withdrawalCredentials))
depositAmount := uint64(0)
if depositVerifyDepositAmount != "" {
@@ -119,9 +121,9 @@ In quiet mode this will return 0 if the the data is verified correctly, otherwis
}
if !verified {
failures = true
outputIf(!quiet, fmt.Sprintf("%s failed verification", depositName))
outputIf(!viper.GetBool("quiet"), fmt.Sprintf("%s failed verification", depositName))
} else {
outputIf(!quiet, fmt.Sprintf("%s verified", depositName))
outputIf(!viper.GetBool("quiet"), fmt.Sprintf("%s verified", depositName))
}
}
@@ -155,7 +157,7 @@ func validatorPubKeysFromInput(input string) (map[[48]byte]bool, error) {
pubKeys[key] = true
} else {
// Assume it's a path to a file of public keys.
data, err = ioutil.ReadFile(input)
data, err = os.ReadFile(input)
if err != nil {
return nil, errors.Wrap(err, "failed to find public key file")
}
@@ -189,45 +191,45 @@ func validatorPubKeysFromInput(input string) (map[[48]byte]bool, error) {
func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, validatorPubKeys map[[48]byte]bool, amount uint64) (bool, error) {
if withdrawalCredentials == nil {
outputIf(!quiet, "Withdrawal public key or address not supplied; withdrawal credentials NOT checked")
outputIf(!viper.GetBool("quiet"), "Withdrawal public key or address not supplied; withdrawal credentials NOT checked")
} else {
if !bytes.Equal(deposit.WithdrawalCredentials, withdrawalCredentials) {
outputIf(!quiet, "Withdrawal credentials incorrect")
outputIf(!viper.GetBool("quiet"), "Withdrawal credentials incorrect")
return false, nil
}
outputIf(!quiet, "Withdrawal credentials verified")
outputIf(!viper.GetBool("quiet"), "Withdrawal credentials verified")
}
if amount == 0 {
outputIf(!quiet, "Amount not supplied; NOT checked")
outputIf(!viper.GetBool("quiet"), "Amount not supplied; NOT checked")
} else {
if deposit.Amount != amount {
outputIf(!quiet, "Amount incorrect")
outputIf(!viper.GetBool("quiet"), "Amount incorrect")
return false, nil
}
outputIf(!quiet, "Amount verified")
outputIf(!viper.GetBool("quiet"), "Amount verified")
}
if len(validatorPubKeys) == 0 {
outputIf(!quiet, "Validator public key not suppled; NOT checked")
outputIf(!viper.GetBool("quiet"), "Validator public key not suppled; NOT checked")
} else {
var key [48]byte
copy(key[:], deposit.PublicKey)
if _, exists := validatorPubKeys[key]; !exists {
outputIf(!quiet, "Validator public key incorrect")
outputIf(!viper.GetBool("quiet"), "Validator public key incorrect")
return false, nil
}
outputIf(!quiet, "Validator public key verified")
outputIf(!viper.GetBool("quiet"), "Validator public key verified")
}
var pubKey spec.BLSPubKey
var pubKey phase0.BLSPubKey
copy(pubKey[:], deposit.PublicKey)
var signature spec.BLSSignature
var signature phase0.BLSSignature
copy(signature[:], deposit.Signature)
depositData := &spec.DepositData{
depositData := &phase0.DepositData{
PublicKey: pubKey,
WithdrawalCredentials: deposit.WithdrawalCredentials,
Amount: spec.Gwei(deposit.Amount),
Amount: phase0.Gwei(deposit.Amount),
Signature: signature,
}
depositDataRoot, err := depositData.HashTreeRoot()
@@ -236,30 +238,80 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
}
if bytes.Equal(deposit.DepositDataRoot, depositDataRoot[:]) {
outputIf(!quiet, "Deposit data root verified")
outputIf(!viper.GetBool("quiet"), "Deposit data root verified")
} else {
outputIf(!quiet, "Deposit data root incorrect")
outputIf(!viper.GetBool("quiet"), "Deposit data root incorrect")
return false, nil
}
if len(deposit.ForkVersion) == 0 {
if depositVerifyForkVersion != "" {
outputIf(!quiet, "Data format does not contain fork version for verification; NOT verified")
outputIf(!viper.GetBool("quiet"), "Data format does not contain fork version for verification; NOT verified")
}
} else {
if depositVerifyForkVersion == "" {
outputIf(!quiet, "fork version not supplied; NOT checked")
outputIf(!viper.GetBool("quiet"), "fork version not supplied; not checked")
} else {
forkVersion, err := hex.DecodeString(strings.TrimPrefix(depositVerifyForkVersion, "0x"))
if err != nil {
return false, errors.Wrap(err, "failed to decode fork version")
}
if bytes.Equal(deposit.ForkVersion, forkVersion) {
outputIf(!quiet, "Fork version verified")
outputIf(!viper.GetBool("quiet"), "Fork version verified")
} else {
outputIf(!quiet, "Fork version incorrect")
outputIf(!viper.GetBool("quiet"), "Fork version incorrect")
return false, nil
}
if len(deposit.DepositMessageRoot) != 32 {
outputIf(!viper.GetBool("quiet"), "Deposit message root not supplied; not checked")
} else {
// We can also verify the deposit message signature.
depositMessage := &phase0.DepositMessage{
PublicKey: pubKey,
WithdrawalCredentials: withdrawalCredentials,
Amount: phase0.Gwei(deposit.Amount),
}
depositMessageRoot, err := depositMessage.HashTreeRoot()
if err != nil {
return false, errors.Wrap(err, "failed to generate deposit message root")
}
if bytes.Equal(deposit.DepositMessageRoot, depositMessageRoot[:]) {
outputIf(!viper.GetBool("quiet"), "Deposit message root verified")
} else {
outputIf(!viper.GetBool("quiet"), "Deposit message root incorrect")
return false, nil
}
domainBytes := e2types.Domain(e2types.DomainDeposit, forkVersion, e2types.ZeroGenesisValidatorsRoot)
var domain phase0.Domain
copy(domain[:], domainBytes)
container := &phase0.SigningData{
ObjectRoot: depositMessageRoot,
Domain: domain,
}
containerRoot, err := container.HashTreeRoot()
if err != nil {
return false, errors.New("failed to generate root for container")
}
validatorPubKey, err := e2types.BLSPublicKeyFromBytes(pubKey[:])
if err != nil {
return false, errors.Wrap(err, "failed to generate validator public key")
}
blsSig, err := e2types.BLSSignatureFromBytes(signature[:])
if err != nil {
return false, errors.New("failed to verify BLS signature")
}
signatureVerified := blsSig.Verify(containerRoot[:], validatorPubKey)
if signatureVerified {
outputIf(!viper.GetBool("quiet"), "Deposit message signature verified")
} else {
outputIf(!viper.GetBool("quiet"), "Deposit message signature NOT verified")
return false, nil
}
}
}
}

40
cmd/epoch.go Normal file
View File

@@ -0,0 +1,40 @@
// Copyright © 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// epochCmd represents the epoch command.
var epochCmd = &cobra.Command{
Use: "epoch",
Short: "Obtain information about Ethereum 2 epochs",
Long: "Obtain information about Ethereum 2 epochs",
}
func init() {
RootCmd.AddCommand(epochCmd)
}
func epochFlags(_ *cobra.Command) {
epochSummaryCmd.Flags().String("epoch", "", "the epoch for which to obtain information (default current, can be 'current', 'last' or a number)")
}
func epochBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,111 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package epochsummary
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Operation.
epoch string
stream bool
jsonOutput bool
// Data access.
eth2Client eth2client.Service
chainTime chaintime.Service
proposerDutiesProvider eth2client.ProposerDutiesProvider
blocksProvider eth2client.SignedBeaconBlockProvider
syncCommitteesProvider eth2client.SyncCommitteesProvider
validatorsProvider eth2client.ValidatorsProvider
beaconCommitteesProvider eth2client.BeaconCommitteesProvider
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Results.
summary *epochSummary
}
type epochSummary struct {
Epoch phase0.Epoch `json:"epoch"`
FirstSlot phase0.Slot `json:"first_slot"`
LastSlot phase0.Slot `json:"last_slot"`
Proposals []*epochProposal `json:"proposals"`
SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
ActiveValidators int `json:"active_validators"`
ParticipatingValidators int `json:"participating_validators"`
HeadCorrectValidators int `json:"head_correct_validators"`
HeadTimelyValidators int `json:"head_timely_validators"`
SourceTimelyValidators int `json:"source_timely_validators"`
TargetCorrectValidators int `json:"target_correct_validators"`
TargetTimelyValidators int `json:"target_timely_validators"`
NonParticipatingValidators []*nonParticipatingValidator `json:"nonparticipating_validators"`
}
type epochProposal struct {
Slot phase0.Slot `json:"slot"`
Proposer phase0.ValidatorIndex `json:"proposer"`
Block bool `json:"block"`
}
type epochSyncCommittee struct {
Index phase0.ValidatorIndex `json:"index"`
Missed int `json:"missed"`
}
type nonParticipatingValidator struct {
Validator phase0.ValidatorIndex `json:"validator_index"`
Slot phase0.Slot `json:"slot"`
Committee phase0.CommitteeIndex `json:"committee_index"`
}
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
summary: &epochSummary{},
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
c.epoch = viper.GetString("epoch")
c.stream = viper.GetBool("stream")
c.jsonOutput = viper.GetBool("json")
return c, nil
}

View File

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

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

@@ -0,0 +1,110 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package epochsummary
import (
"context"
"encoding/json"
"fmt"
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.jsonOutput {
return c.outputJSON(ctx)
}
return c.outputTxt(ctx)
}
func (c *command) outputJSON(_ context.Context) (string, error) {
data, err := json.Marshal(c.summary)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputTxt(_ context.Context) (string, error) {
builder := strings.Builder{}
builder.WriteString("Epoch ")
builder.WriteString(fmt.Sprintf("%d:\n", c.summary.Epoch))
proposedBlocks := 0
missedProposals := make([]string, 0, len(c.summary.Proposals))
for _, proposal := range c.summary.Proposals {
if !proposal.Block {
missedProposals = append(missedProposals, fmt.Sprintf("\n Slot %d (validator %d)", proposal.Slot, proposal.Proposer))
} else {
proposedBlocks++
}
}
builder.WriteString(fmt.Sprintf(" Proposals: %d/%d (%0.2f%%)", proposedBlocks, len(missedProposals)+proposedBlocks, 100.0*float64(proposedBlocks)/float64(len(missedProposals)+proposedBlocks)))
if c.verbose {
for _, proposal := range c.summary.Proposals {
if proposal.Block {
continue
}
builder.WriteString("\n Slot ")
builder.WriteString(fmt.Sprintf("%d (%d/%d)", proposal.Slot, uint64(proposal.Slot)%uint64(len(c.summary.Proposals)), len(c.summary.Proposals)))
builder.WriteString(" validator ")
builder.WriteString(fmt.Sprintf("%d", proposal.Proposer))
builder.WriteString(" not proposed or not included")
}
}
builder.WriteString(fmt.Sprintf("\n Attestations: %d/%d (%0.2f%%)", c.summary.ParticipatingValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.ParticipatingValidators)/float64(c.summary.ActiveValidators)))
builder.WriteString(fmt.Sprintf("\n Source timely: %d/%d (%0.2f%%)", c.summary.SourceTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.SourceTimelyValidators)/float64(c.summary.ActiveValidators)))
builder.WriteString(fmt.Sprintf("\n Target correct: %d/%d (%0.2f%%)", c.summary.TargetCorrectValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.TargetCorrectValidators)/float64(c.summary.ActiveValidators)))
builder.WriteString(fmt.Sprintf("\n Target timely: %d/%d (%0.2f%%)", c.summary.TargetTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.TargetTimelyValidators)/float64(c.summary.ActiveValidators)))
builder.WriteString(fmt.Sprintf("\n Head correct: %d/%d (%0.2f%%)", c.summary.HeadCorrectValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.HeadCorrectValidators)/float64(c.summary.ActiveValidators)))
builder.WriteString(fmt.Sprintf("\n Head timely: %d/%d (%0.2f%%)", c.summary.HeadTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.HeadTimelyValidators)/float64(c.summary.ActiveValidators)))
if c.verbose {
// Sort list by validator index.
for _, validator := range c.summary.NonParticipatingValidators {
builder.WriteString("\n Slot ")
builder.WriteString(fmt.Sprintf("%d", validator.Slot))
builder.WriteString(" committee ")
builder.WriteString(fmt.Sprintf("%d", validator.Committee))
builder.WriteString(" validator ")
builder.WriteString(fmt.Sprintf("%d", validator.Validator))
builder.WriteString(" failed to participate")
}
}
if c.summary.Epoch >= c.chainTime.AltairInitialEpoch() {
contributions := proposedBlocks * 512 // SYNC_COMMITTEE_SIZE
totalMissed := 0
for _, contribution := range c.summary.SyncCommittee {
totalMissed += contribution.Missed
}
builder.WriteString(fmt.Sprintf("\n Sync committees: %d/%d (%0.2f%%)", contributions-totalMissed, contributions, 100.0*float64(contributions-totalMissed)/float64(contributions)))
if c.verbose {
for _, syncCommittee := range c.summary.SyncCommittee {
builder.WriteString("\n Validator ")
builder.WriteString(fmt.Sprintf("%d", syncCommittee.Index))
builder.WriteString(" included ")
builder.WriteString(fmt.Sprintf("%d/%d", proposedBlocks-syncCommittee.Missed, proposedBlocks))
builder.WriteString(fmt.Sprintf(" (%0.2f%%)", 100.0*float64(proposedBlocks-syncCommittee.Missed)/float64(proposedBlocks)))
}
}
}
return builder.String(), nil
}

View File

@@ -0,0 +1,374 @@
// 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 epochsummary
import (
"context"
"fmt"
"sort"
eth2client "github.com/attestantio/go-eth2-client"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
err := c.setup(ctx)
if err != nil {
return err
}
c.summary.Epoch, err = util.ParseEpoch(ctx, c.chainTime, c.epoch)
if err != nil {
return errors.Wrap(err, "failed to parse epoch")
}
c.summary.FirstSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)
c.summary.LastSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) - 1
if err := c.processProposerDuties(ctx); err != nil {
return err
}
if err := c.processAttesterDuties(ctx); err != nil {
return err
}
return c.processSyncCommitteeDuties(ctx)
}
func (c *command) processProposerDuties(ctx context.Context) error {
duties, err := c.proposerDutiesProvider.ProposerDuties(ctx, c.summary.Epoch, nil)
if err != nil {
return errors.Wrap(err, "failed to obtain proposer duties")
}
if duties == nil {
return errors.New("empty proposer duties")
}
for _, duty := range duties {
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", duty.Slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", duty.Slot))
}
present := block != nil
c.summary.Proposals = append(c.summary.Proposals, &epochProposal{
Slot: duty.Slot,
Proposer: duty.ValidatorIndex,
Block: present,
})
}
return nil
}
func (c *command) activeValidators(ctx context.Context) (map[phase0.ValidatorIndex]*apiv1.Validator, error) {
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), nil)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validators for epoch")
}
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
for _, validator := range validators {
if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch {
activeValidators[validator.Index] = validator
}
}
return activeValidators, nil
}
func (c *command) processAttesterDuties(ctx context.Context) error {
activeValidators, err := c.activeValidators(ctx)
if err != nil {
return err
}
c.summary.ActiveValidators = len(activeValidators)
// Obtain number of validators that voted for blocks in the epoch.
// These votes can be included anywhere from the second slot of
// the epoch to the first slot of the next-but-one epoch.
firstSlot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) + 1
lastSlot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch + 2)
if lastSlot > c.chainTime.CurrentSlot() {
lastSlot = c.chainTime.CurrentSlot()
}
var votes map[phase0.ValidatorIndex]struct{}
var participations map[phase0.ValidatorIndex]*nonParticipatingValidator
c.summary.ParticipatingValidators, c.summary.HeadCorrectValidators, c.summary.HeadTimelyValidators, c.summary.SourceTimelyValidators, c.summary.TargetCorrectValidators, c.summary.TargetTimelyValidators, votes, participations, err = c.processSlots(ctx, firstSlot, lastSlot)
if err != nil {
return err
}
c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0, len(activeValidators)-len(votes))
for activeValidatorIndex := range activeValidators {
if _, exists := votes[activeValidatorIndex]; !exists {
if _, exists := participations[activeValidatorIndex]; exists {
c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, participations[activeValidatorIndex])
}
}
}
sort.Slice(c.summary.NonParticipatingValidators, func(i int, j int) bool {
if c.summary.NonParticipatingValidators[i].Slot != c.summary.NonParticipatingValidators[j].Slot {
return c.summary.NonParticipatingValidators[i].Slot < c.summary.NonParticipatingValidators[j].Slot
}
if c.summary.NonParticipatingValidators[i].Committee != c.summary.NonParticipatingValidators[j].Committee {
return c.summary.NonParticipatingValidators[i].Committee < c.summary.NonParticipatingValidators[j].Committee
}
return c.summary.NonParticipatingValidators[i].Validator < c.summary.NonParticipatingValidators[j].Validator
})
return nil
}
func (c *command) processSlots(ctx context.Context,
firstSlot phase0.Slot,
lastSlot phase0.Slot,
) (
int,
int,
int,
int,
int,
int,
map[phase0.ValidatorIndex]struct{},
map[phase0.ValidatorIndex]*nonParticipatingValidator,
error,
) {
votes := make(map[phase0.ValidatorIndex]struct{})
headCorrects := make(map[phase0.ValidatorIndex]struct{})
headTimelys := make(map[phase0.ValidatorIndex]struct{})
sourceTimelys := make(map[phase0.ValidatorIndex]struct{})
targetCorrects := make(map[phase0.ValidatorIndex]struct{})
targetTimelys := make(map[phase0.ValidatorIndex]struct{})
allCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
participations := make(map[phase0.ValidatorIndex]*nonParticipatingValidator)
// Need a cache of beacon block headers to reduce lookup times.
headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider)
for slot := firstSlot; slot <= lastSlot; slot++ {
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
}
if block == nil {
// No block at this slot; that's fine.
continue
}
slot, err := block.Slot()
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
attestations, err := block.Attestations()
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
for _, attestation := range attestations {
if attestation.Data.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestation.Data.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
// Outside of this epoch's range.
continue
}
slotCommittees, exists := allCommittees[attestation.Data.Slot]
if !exists {
beaconCommittees, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", attestation.Data.Slot))
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
}
for _, beaconCommittee := range beaconCommittees {
if _, exists := allCommittees[beaconCommittee.Slot]; !exists {
allCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
}
allCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
for _, index := range beaconCommittee.Validators {
participations[index] = &nonParticipatingValidator{
Validator: index,
Slot: beaconCommittee.Slot,
Committee: beaconCommittee.Index,
}
}
}
slotCommittees = allCommittees[attestation.Data.Slot]
}
committee := slotCommittees[attestation.Data.Index]
inclusionDistance := slot - attestation.Data.Slot
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
if err != nil {
return 0, 0, 0, 0, 0, 0, nil, nil, err
}
for i := uint64(0); i < attestation.AggregationBits.Len(); i++ {
if attestation.AggregationBits.BitAt(i) {
votes[committee[int(i)]] = struct{}{}
if _, exists := headCorrects[committee[int(i)]]; !exists && headCorrect {
headCorrects[committee[int(i)]] = struct{}{}
}
if _, exists := headTimelys[committee[int(i)]]; !exists && headCorrect && inclusionDistance == 1 {
headTimelys[committee[int(i)]] = struct{}{}
}
if _, exists := sourceTimelys[committee[int(i)]]; !exists && inclusionDistance <= 5 {
sourceTimelys[committee[int(i)]] = struct{}{}
}
if _, exists := targetCorrects[committee[int(i)]]; !exists && targetCorrect {
targetCorrects[committee[int(i)]] = struct{}{}
}
if _, exists := targetTimelys[committee[int(i)]]; !exists && targetCorrect && inclusionDistance <= 32 {
targetTimelys[committee[int(i)]] = struct{}{}
}
}
}
}
}
return len(votes),
len(headCorrects),
len(headTimelys),
len(sourceTimelys),
len(targetCorrects),
len(targetTimelys),
votes,
participations,
nil
}
func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
if c.summary.Epoch < c.chainTime.AltairInitialEpoch() {
// The epoch is pre-Altair. No info but no error.
return nil
}
committee, err := c.syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", c.summary.FirstSlot))
if err != nil {
return errors.Wrap(err, "failed to obtain sync committee")
}
if len(committee.Validators) == 0 {
return errors.Wrap(err, "empty sync committee")
}
missed := make(map[phase0.ValidatorIndex]int)
for _, index := range committee.Validators {
missed[index] = 0
}
for slot := c.summary.FirstSlot; slot <= c.summary.LastSlot; slot++ {
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
}
if block == nil {
// If the block is missed we don't count the sync aggregate miss.
continue
}
var aggregate *altair.SyncAggregate
switch block.Version {
case spec.DataVersionPhase0:
// No sync committees in this fork.
return nil
case spec.DataVersionAltair:
aggregate = block.Altair.Message.Body.SyncAggregate
case spec.DataVersionBellatrix:
aggregate = block.Bellatrix.Message.Body.SyncAggregate
case spec.DataVersionCapella:
aggregate = block.Capella.Message.Body.SyncAggregate
case spec.DataVersionDeneb:
aggregate = block.Deneb.Message.Body.SyncAggregate
default:
return fmt.Errorf("unhandled block version %v", block.Version)
}
for i := uint64(0); i < aggregate.SyncCommitteeBits.Len(); i++ {
if !aggregate.SyncCommitteeBits.BitAt(i) {
missed[committee.Validators[int(i)]]++
}
}
}
c.summary.SyncCommittee = make([]*epochSyncCommittee, 0, len(missed))
for index, count := range missed {
if count > 0 {
c.summary.SyncCommittee = append(c.summary.SyncCommittee, &epochSyncCommittee{
Index: index,
Missed: count,
})
}
}
sort.Slice(c.summary.SyncCommittee, func(i int, j int) bool {
missedDiff := c.summary.SyncCommittee[i].Missed - c.summary.SyncCommittee[j].Missed
if missedDiff != 0 {
// Actually want to order by missed descending, so invert the expected condition.
return missedDiff > 0
}
// Then order by validator index.
return c.summary.SyncCommittee[i].Index < c.summary.SyncCommittee[j].Index
})
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, &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.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
var isProvider bool
c.proposerDutiesProvider, isProvider = c.eth2Client.(eth2client.ProposerDutiesProvider)
if !isProvider {
return errors.New("connection does not provide proposer duties")
}
c.blocksProvider, isProvider = c.eth2Client.(eth2client.SignedBeaconBlockProvider)
if !isProvider {
return errors.New("connection does not provide signed beacon blocks")
}
c.syncCommitteesProvider, isProvider = c.eth2Client.(eth2client.SyncCommitteesProvider)
if !isProvider {
return errors.New("connection does not provide sync committee duties")
}
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
if !isProvider {
return errors.New("connection does not provide validators")
}
c.beaconCommitteesProvider, isProvider = c.eth2Client.(eth2client.BeaconCommitteesProvider)
if !isProvider {
return errors.New("connection does not provide beacon committees")
}
c.beaconBlockHeadersProvider, isProvider = c.eth2Client.(eth2client.BeaconBlockHeadersProvider)
if !isProvider {
return errors.New("connection does not provide beacon block headers")
}
return nil
}

View File

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

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

@@ -0,0 +1,50 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package epochsummary
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

54
cmd/epochsummary.go Normal file
View File

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

View File

@@ -16,12 +16,14 @@ package cmd
import (
"fmt"
"os"
"github.com/spf13/viper"
)
// errCheck checks for an error and quits if it is present
// errCheck checks for an error and quits if it is present.
func errCheck(err error, msg string) {
if err != nil {
if !quiet {
if !viper.GetBool("quiet") {
if msg == "" {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
} else {
@@ -48,16 +50,16 @@ func errCheck(err error, msg string) {
// }
// }
// assert checks a condition and quits if it is false
// assert checks a condition and quits if it is false.
func assert(condition bool, msg string) {
if !condition {
die(msg)
}
}
// die prints an error and quits
// die prints an error and quits.
func die(msg string) {
if msg != "" && !quiet {
if msg != "" && !viper.GetBool("quiet") {
fmt.Fprintf(os.Stderr, "%s\n", msg)
}
os.Exit(_exitFailure)

View File

@@ -17,7 +17,7 @@ import (
"github.com/spf13/cobra"
)
// exitCmd represents the exit command
// exitCmd represents the exit command.
var exitCmd = &cobra.Command{
Use: "exit",
Short: "Manage Ethereum 2 voluntary exits",
@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(exitCmd)
}
func exitFlags(cmd *cobra.Command) {
func exitFlags(_ *cobra.Command) {
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -14,81 +14,97 @@
package cmd
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
consensusclient "github.com/attestantio/go-eth2-client"
v1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
var exitVerifyPubKey string
var exitVerifyCmd = &cobra.Command{
Use: "verify",
Short: "Verify exit data is valid",
Long: `Verify that exit data generated by "ethdo validator exit" is correct for a given account. For example:
ethdo exit verify --data=exitdata.json --account=primary/current
ethdo exit verify --signed-operation=exitdata.json --validator=primary/current
In quiet mode this will return 0 if the the exit is verified correctly, otherwise 1.`,
In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
assert(viper.GetString("account") != "" || exitVerifyPubKey != "", "account or public key is required")
account, err := exitVerifyAccount(ctx)
errCheck(err, "Failed to obtain account")
assert(viper.GetString("signed-operation") != "", "signed-operation is required")
signedOp, err := obtainSignedOperation(viper.GetString("signed-operation"))
errCheck(err, "Failed to obtain signed operation")
assert(viper.GetString("exit") != "", "exit is required")
data, err := obtainExitData(viper.GetString("exit"))
errCheck(err, "Failed to obtain exit data")
// Confirm signature is good.
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !viper.GetBool("quiet"),
})
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
validator, err := util.ParseValidator(ctx, eth2Client.(consensusclient.ValidatorsProvider), fmt.Sprintf("%d", signedOp.Message.ValidatorIndex), "head")
errCheck(err, "failed to obtain validator")
pubkey, err := validator.PubKey(ctx)
errCheck(err, "failed to obtain validator public key")
account, err := util.ParseAccount(ctx, pubkey.String(), nil, false)
errCheck(err, "failed to obtain account")
// Ensure the validator is in a suitable state.
assert(validator.Status == v1.ValidatorStateActiveOngoing, "validator not in a suitable state to exit")
// Obtain the hash tree root of the message to check the signature.
opRoot, err := signedOp.Message.HashTreeRoot()
errCheck(err, "Failed to obtain exit hash tree root")
genesis, err := eth2Client.(consensusclient.GenesisProvider).Genesis(ctx)
errCheck(err, "Failed to obtain beacon chain genesis")
domain := e2types.Domain(e2types.DomainVoluntaryExit, data.ForkVersion[:], genesis.GenesisValidatorsRoot[:])
var exitDomain spec.Domain
copy(exitDomain[:], domain)
exit := &spec.VoluntaryExit{
Epoch: data.Exit.Message.Epoch,
ValidatorIndex: data.Exit.Message.ValidatorIndex,
}
exitRoot, err := exit.HashTreeRoot()
errCheck(err, "Failed to obtain exit hash tree root")
fork, err := eth2Client.(consensusclient.ForkProvider).Fork(ctx, "head")
errCheck(err, "Failed to obtain fork information")
// Check against current and prior fork versions.
signatureBytes := make([]byte, 96)
copy(signatureBytes, data.Exit.Signature[:])
copy(signatureBytes, signedOp.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(signatureBytes)
errCheck(err, "Invalid signature")
verified, err := util.VerifyRoot(account, exitRoot, exitDomain, sig)
verified := false
// Try with the current fork.
domain := phase0.Domain{}
currentExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork.CurrentVersion[:], genesis.GenesisValidatorsRoot[:])
errCheck(err, "Failed to compute domain")
copy(domain[:], currentExitDomain)
verified, err = util.VerifyRoot(account, opRoot, domain, sig)
errCheck(err, "Failed to verify voluntary exit")
assert(verified, "Voluntary exit failed to verify")
if !verified {
// Try again with the previous fork.
previousExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork.PreviousVersion[:], genesis.GenesisValidatorsRoot[:])
copy(domain[:], previousExitDomain)
errCheck(err, "Failed to compute domain")
verified, err = util.VerifyRoot(account, opRoot, domain, sig)
errCheck(err, "Failed to verify voluntary exit")
}
assert(verified, "Voluntary exit failed to verify against current and previous fork versions")
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
errCheck(err, "Failed to obtain current fork")
assert(bytes.Equal(data.ForkVersion[:], fork.CurrentVersion[:]) || bytes.Equal(data.ForkVersion[:], fork.PreviousVersion[:]), "Exit is for an old fork version and is no longer valid")
outputIf(verbose, "Verified")
outputIf(viper.GetBool("verbose"), "Verified")
os.Exit(_exitSuccess)
},
}
// obtainExitData obtains exit data from an input, could be JSON itself or a path to JSON.
func obtainExitData(input string) (*util.ValidatorExitData, error) {
// obtainSignedOperation obtains exit data from an input, could be JSON itself or a path to JSON.
func obtainSignedOperation(input string) (*phase0.SignedVoluntaryExit, error) {
var err error
var data []byte
// Input could be JSON or a path to JSON
@@ -97,51 +113,28 @@ func obtainExitData(input string) (*util.ValidatorExitData, error) {
data = []byte(input)
} else {
// Assume it's a path to JSON
data, err = ioutil.ReadFile(input)
data, err = os.ReadFile(input)
if err != nil {
return nil, errors.Wrap(err, "failed to find deposit data file")
}
}
exitData := &util.ValidatorExitData{}
err = json.Unmarshal(data, exitData)
signedOp := &phase0.SignedVoluntaryExit{}
err = json.Unmarshal(data, signedOp)
if err != nil {
return nil, errors.Wrap(err, "data is not valid JSON")
}
return exitData, nil
}
// exitVerifyAccount obtains the account for the exitVerify command.
func exitVerifyAccount(ctx context.Context) (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
if viper.GetString("account") != "" {
_, account, err = walletAndAccountFromPath(ctx, viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(exitVerifyPubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", exitVerifyPubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", exitVerifyPubKey))
}
}
return account, nil
return signedOp, nil
}
func init() {
exitCmd.AddCommand(exitVerifyCmd)
exitFlags(exitVerifyCmd)
exitVerifyCmd.Flags().String("exit", "", "JSON data, or path to JSON data")
exitVerifyCmd.Flags().StringVar(&exitVerifyPubKey, "pubkey", "", "Public key for which to verify exit")
exitVerifyCmd.Flags().String("signed-operation", "", "JSON data, or path to JSON data")
}
func exitVerifyBindings() {
if err := viper.BindPFlag("exit", exitVerifyCmd.Flags().Lookup("exit")); err != nil {
func exitVerifyBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("signed-operation", cmd.Flags().Lookup("signed-operation")); err != nil {
panic(err)
}
}

View File

@@ -17,7 +17,7 @@ import (
"github.com/spf13/cobra"
)
// nodeCmd represents the node command
// nodeCmd represents the node command.
var nodeCmd = &cobra.Command{
Use: "node",
Short: "Obtain information about an Ethereum 2 node",
@@ -28,5 +28,5 @@ func init() {
RootCmd.AddCommand(nodeCmd)
}
func nodeFlags(cmd *cobra.Command) {
func nodeFlags(_ *cobra.Command) {
}

View File

@@ -50,7 +50,12 @@ func input(ctx context.Context) (*dataIn, error) {
data.topics = viper.GetStringSlice("topics")
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, err
}

Some files were not shown because too many files have changed in this diff Show More